- 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
193 lines
No EOL
5.7 KiB
TypeScript
193 lines
No EOL
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { SerialPortInfo } from '@/lib/serial-utils'
|
|
|
|
interface DeviceConnectionProps {
|
|
onConnectionChange: (connected: boolean) => void
|
|
onDataReceived: (data: any) => void
|
|
}
|
|
|
|
export default function DeviceConnection({
|
|
onConnectionChange,
|
|
onDataReceived
|
|
}: DeviceConnectionProps) {
|
|
const [availablePorts, setAvailablePorts] = useState<SerialPortInfo[]>([])
|
|
const [selectedPort, setSelectedPort] = useState<string>('')
|
|
const [isConnected, setIsConnected] = useState(false)
|
|
const [isConnecting, setIsConnecting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Load available serial ports
|
|
useEffect(() => {
|
|
loadPorts()
|
|
}, [])
|
|
|
|
// Poll for data when connected
|
|
useEffect(() => {
|
|
if (!isConnected) return
|
|
|
|
const pollInterval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch('/api/uwb/data')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
if (data.rangeData || data.deviceData?.length > 0) {
|
|
onDataReceived(data)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Data polling error:', error)
|
|
}
|
|
}, 1000) // Poll every second
|
|
|
|
return () => clearInterval(pollInterval)
|
|
}, [isConnected, onDataReceived])
|
|
|
|
const loadPorts = async () => {
|
|
try {
|
|
const response = await fetch('/api/serial/ports')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setAvailablePorts(data.esp32Ports || [])
|
|
|
|
// Auto-select first ESP32 port if available
|
|
if (data.esp32Ports?.length > 0 && !selectedPort) {
|
|
setSelectedPort(data.esp32Ports[0].path)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load ports:', error)
|
|
setError('Failed to load available ports')
|
|
}
|
|
}
|
|
|
|
const connect = async () => {
|
|
if (!selectedPort) return
|
|
|
|
setIsConnecting(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const response = await fetch('/api/serial/connect', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
port: selectedPort,
|
|
baudRate: 115200
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
setIsConnected(true)
|
|
onConnectionChange(true)
|
|
|
|
// Send initial test command
|
|
await sendCommand('AT?')
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || 'Connection failed')
|
|
}
|
|
} catch (error) {
|
|
setError(`Connection error: ${error}`)
|
|
} finally {
|
|
setIsConnecting(false)
|
|
}
|
|
}
|
|
|
|
const disconnect = async () => {
|
|
try {
|
|
await fetch('/api/serial/connect', { method: 'DELETE' })
|
|
setIsConnected(false)
|
|
onConnectionChange(false)
|
|
} catch (error) {
|
|
console.error('Disconnect error:', error)
|
|
}
|
|
}
|
|
|
|
const sendCommand = async (command: string) => {
|
|
try {
|
|
await fetch('/api/uwb/data', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command })
|
|
})
|
|
} catch (error) {
|
|
console.error('Command send error:', error)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Device Connection</h2>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Select ESP32 Device
|
|
</label>
|
|
<select
|
|
value={selectedPort}
|
|
onChange={(e) => setSelectedPort(e.target.value)}
|
|
disabled={isConnected}
|
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
|
>
|
|
<option value="">Select a port...</option>
|
|
{availablePorts.map(port => (
|
|
<option key={port.path} value={port.path}>
|
|
{port.path} - {port.manufacturer || 'Unknown'}
|
|
{port.productId && ` (${port.productId})`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
{!isConnected ? (
|
|
<button
|
|
onClick={connect}
|
|
disabled={!selectedPort || isConnecting}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
{isConnecting ? 'Connecting...' : 'Connect'}
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={disconnect}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
|
>
|
|
Disconnect
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={loadPorts}
|
|
disabled={isConnecting}
|
|
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:bg-gray-400"
|
|
>
|
|
Refresh Ports
|
|
</button>
|
|
|
|
<div className={`flex items-center gap-2 ${isConnected ? 'text-green-600' : 'text-gray-400'}`}>
|
|
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
<span className="text-sm font-medium">
|
|
{isConnected ? 'Connected' : 'Disconnected'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{availablePorts.length === 0 && (
|
|
<p className="text-sm text-gray-500">
|
|
No ESP32 devices detected. Make sure your device is connected and drivers are installed.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |