UWB-webapp/components/PositioningVisualizer.tsx
martin fa75faa69d Initial commit: Next.js UWB positioning webapp
- Complete Next.js 14 app with TypeScript and Tailwind CSS
- ESP32 serial communication via API routes
- Real-time UWB positioning visualization
- Interactive 2D warehouse mapping with Canvas
- Device connection interface with auto-detection
- AT command parsing for UWBHelper library integration
- Clean project structure with comprehensive documentation
2025-08-20 15:14:34 +02:00

233 lines
No EOL
6.8 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import { AnchorPosition, TagPosition, DeviceData, RangeResult } from '@/lib/uwb-types'
import DeviceConnection from './DeviceConnection'
interface VisualizerProps {
width?: number
height?: number
}
export default function PositioningVisualizer({
width = 800,
height = 600
}: VisualizerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [anchors, setAnchors] = useState<AnchorPosition[]>([])
const [tagPosition, setTagPosition] = useState<TagPosition | null>(null)
const [connectedDevices, setConnectedDevices] = useState<DeviceData[]>([])
const [isConnected, setIsConnected] = useState(false)
const [lastDataUpdate, setLastDataUpdate] = useState<number>(0)
// Mock data for development
useEffect(() => {
const mockAnchors: AnchorPosition[] = [
{ anchorId: 0, x: 0, y: 0, confidence: 0.9, valid: true },
{ anchorId: 1, x: 10, y: 0, confidence: 0.8, valid: true },
{ anchorId: 2, x: 10, y: 8, confidence: 0.9, valid: true },
{ anchorId: 3, x: 0, y: 8, confidence: 0.7, valid: true },
{ anchorId: 4, x: 5, y: 4, confidence: 0.8, valid: true },
]
const mockTag: TagPosition = {
x: 3.5,
y: 2.1,
timestamp: Date.now(),
confidence: 0.85
}
setAnchors(mockAnchors)
setTagPosition(mockTag)
}, [])
// Canvas rendering
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// Clear canvas
ctx.clearRect(0, 0, width, height)
// Set up coordinate system (flip Y axis for standard cartesian)
ctx.save()
ctx.scale(1, -1)
ctx.translate(0, -height)
// Scale to fit data (assuming 12m x 10m warehouse area)
const scale = Math.min(width / 12, height / 10)
ctx.scale(scale, scale)
ctx.translate(1, 1) // Small offset from edges
// Draw grid
ctx.strokeStyle = '#e5e7eb'
ctx.lineWidth = 1 / scale
for (let i = 0; i <= 12; i += 2) {
ctx.beginPath()
ctx.moveTo(i, 0)
ctx.lineTo(i, 10)
ctx.stroke()
}
for (let i = 0; i <= 10; i += 2) {
ctx.beginPath()
ctx.moveTo(0, i)
ctx.lineTo(12, i)
ctx.stroke()
}
// Draw anchors
anchors.forEach(anchor => {
if (!anchor.valid) return
ctx.fillStyle = anchor.confidence > 0.8 ? '#10b981' : '#f59e0b'
ctx.beginPath()
ctx.arc(anchor.x, anchor.y, 0.3, 0, 2 * Math.PI)
ctx.fill()
// Anchor ID label
ctx.save()
ctx.scale(1, -1) // Flip text back
ctx.fillStyle = '#374151'
ctx.font = `${0.4}px Arial`
ctx.textAlign = 'center'
ctx.fillText(
anchor.anchorId.toString(),
anchor.x,
-anchor.y + 0.15
)
ctx.restore()
})
// Draw tag position
if (tagPosition) {
ctx.fillStyle = '#3b82f6'
ctx.beginPath()
ctx.arc(tagPosition.x, tagPosition.y, 0.2, 0, 2 * Math.PI)
ctx.fill()
// Tag confidence indicator
ctx.strokeStyle = `rgba(59, 130, 246, ${tagPosition.confidence})`
ctx.lineWidth = 3 / scale
ctx.beginPath()
ctx.arc(tagPosition.x, tagPosition.y, 0.5, 0, 2 * Math.PI)
ctx.stroke()
}
ctx.restore()
}, [anchors, tagPosition, width, height])
const connectToDevice = async () => {
try {
// This will be implemented with actual serial connection
setIsConnected(true)
console.log('Connecting to UWB device...')
} catch (error) {
console.error('Failed to connect:', error)
}
}
// Handle real-time data updates
const handleDataReceived = useCallback((data: any) => {
setLastDataUpdate(Date.now())
if (data.rangeData) {
// Process range data for positioning
const rangeData: RangeResult = data.rangeData
console.log('Range data received:', rangeData)
// Update device data from range results
const deviceUpdates: DeviceData[] = []
for (let i = 0; i < 8; i++) {
if (rangeData.ranges[i] > 0 && rangeData.anchorIds[i] > 0) {
deviceUpdates.push({
deviceId: rangeData.anchorIds[i],
distance: rangeData.ranges[i],
rssi: rangeData.rssi[i],
lastUpdate: Date.now(),
active: true
})
}
}
setConnectedDevices(deviceUpdates)
}
}, [])
const handleConnectionChange = useCallback((connected: boolean) => {
setIsConnected(connected)
if (!connected) {
setConnectedDevices([])
setLastDataUpdate(0)
}
}, [])
return (
<div className="space-y-6">
{/* Device Connection */}
<DeviceConnection
onConnectionChange={handleConnectionChange}
onDataReceived={handleDataReceived}
/>
{/* Status Panel */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">System Status</h2>
{/* Device Status */}
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600">Active Anchors:</span>
<span className="ml-2">{anchors.filter(a => a.valid).length}</span>
</div>
<div>
<span className="font-medium text-gray-600">Tag Position:</span>
<span className="ml-2">
{tagPosition ?
`(${tagPosition.x.toFixed(1)}, ${tagPosition.y.toFixed(1)})` :
'No data'
}
</span>
</div>
<div>
<span className="font-medium text-gray-600">Confidence:</span>
<span className="ml-2">
{tagPosition ?
`${(tagPosition.confidence * 100).toFixed(0)}%` :
'N/A'
}
</span>
</div>
</div>
</div>
{/* Visualization Canvas */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4">Warehouse Map</h3>
<div className="border rounded-lg overflow-hidden">
<canvas
ref={canvasRef}
width={width}
height={height}
className="block"
/>
</div>
<div className="mt-4 flex items-center gap-6 text-sm text-gray-600">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded-full" />
<span>High Confidence Anchor</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
<span>Low Confidence Anchor</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-full" />
<span>Mobile Tag</span>
</div>
</div>
</div>
</div>
)
}