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
This commit is contained in:
commit
fa75faa69d
20 changed files with 7600 additions and 0 deletions
91
pages/api/serial/connect.ts
Normal file
91
pages/api/serial/connect.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { SerialPort } from 'serialport'
|
||||
import { ReadlineParser } from '@serialport/parser-readline'
|
||||
|
||||
// Global connection state
|
||||
let serialConnection: SerialPort | null = null
|
||||
let parser: ReadlineParser | null = null
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method === 'POST') {
|
||||
const { port, baudRate = 115200 } = req.body
|
||||
|
||||
if (!port) {
|
||||
return res.status(400).json({ message: 'Port is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
// Close existing connection
|
||||
if (serialConnection?.isOpen) {
|
||||
serialConnection.close()
|
||||
}
|
||||
|
||||
// Create new connection
|
||||
serialConnection = new SerialPort({
|
||||
path: port,
|
||||
baudRate: baudRate,
|
||||
autoOpen: false
|
||||
})
|
||||
|
||||
// Set up parser for line-based data
|
||||
parser = serialConnection.pipe(new ReadlineParser({ delimiter: '\r\n' }))
|
||||
|
||||
// Open connection
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
serialConnection!.open((err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Connected successfully',
|
||||
port,
|
||||
baudRate,
|
||||
isOpen: serialConnection.isOpen
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Serial connection error:', error)
|
||||
res.status(500).json({
|
||||
message: 'Failed to connect',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else if (req.method === 'DELETE') {
|
||||
// Disconnect
|
||||
try {
|
||||
if (serialConnection?.isOpen) {
|
||||
serialConnection.close()
|
||||
serialConnection = null
|
||||
parser = null
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Disconnected successfully' })
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
message: 'Failed to disconnect',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else if (req.method === 'GET') {
|
||||
// Get connection status
|
||||
res.status(200).json({
|
||||
connected: serialConnection?.isOpen || false,
|
||||
port: serialConnection?.path || null
|
||||
})
|
||||
}
|
||||
|
||||
else {
|
||||
res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
}
|
||||
|
||||
export { serialConnection, parser }
|
||||
37
pages/api/serial/ports.ts
Normal file
37
pages/api/serial/ports.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { SerialPort } from 'serialport'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
|
||||
try {
|
||||
const ports = await SerialPort.list()
|
||||
|
||||
// Filter for ESP32 devices (common VID/PID patterns)
|
||||
const esp32Ports = ports.filter(port =>
|
||||
port.vendorId === '10C4' || // Silicon Labs CP210x
|
||||
port.vendorId === '1A86' || // QinHeng Electronics CH340
|
||||
port.vendorId === '0403' || // FTDI
|
||||
port.manufacturer?.includes('Silicon Labs') ||
|
||||
port.manufacturer?.includes('FTDI') ||
|
||||
port.productId === 'EA60' // CP2102
|
||||
)
|
||||
|
||||
res.status(200).json({
|
||||
allPorts: ports,
|
||||
esp32Ports,
|
||||
count: esp32Ports.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error listing serial ports:', error)
|
||||
res.status(500).json({
|
||||
message: 'Failed to list serial ports',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
112
pages/api/uwb/data.ts
Normal file
112
pages/api/uwb/data.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { serialConnection, parser } from '../serial/connect'
|
||||
import { RangeResult, DeviceData } from '@/lib/uwb-types'
|
||||
|
||||
// Data storage for real-time updates
|
||||
let latestRangeData: RangeResult | null = null
|
||||
let latestDeviceData: DeviceData[] = []
|
||||
|
||||
// Parse UWB range data (from ESP32 UWBHelper parseRangeData format)
|
||||
function parseUWBRangeData(data: string): RangeResult | DeviceData[] | null {
|
||||
if (!data.startsWith("AT+RANGE=")) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse tid (Tag ID)
|
||||
const tidMatch = data.match(/tid:(\d+)/)
|
||||
if (!tidMatch) return null
|
||||
|
||||
const tagId = parseInt(tidMatch[1])
|
||||
|
||||
// Parse detailed range data format
|
||||
const maskMatch = data.match(/mask:(\d+)/)
|
||||
const seqMatch = data.match(/seq:(\d+)/)
|
||||
const timerMatch = data.match(/timer:(\d+)/)
|
||||
const timerSysMatch = data.match(/timerSys:(\d+)/)
|
||||
|
||||
// Parse range data
|
||||
const rangeMatch = data.match(/range:\(([^)]+)\)/)
|
||||
const rssiMatch = data.match(/rssi:\(([^)]+)\)/)
|
||||
const ancidMatch = data.match(/ancid:\(([^)]+)\)/)
|
||||
|
||||
if (rangeMatch && rssiMatch) {
|
||||
const ranges = rangeMatch[1].split(',').map(r => parseFloat(r) / 100) // Convert cm to meters
|
||||
const rssi = rssiMatch[1].split(',').map(r => parseFloat(r))
|
||||
const anchorIds = ancidMatch ? ancidMatch[1].split(',').map(a => parseInt(a)) : Array(8).fill(0)
|
||||
|
||||
const result: RangeResult = {
|
||||
tagId,
|
||||
mask: maskMatch ? parseInt(maskMatch[1]) : 0,
|
||||
sequence: seqMatch ? parseInt(seqMatch[1]) : 0,
|
||||
ranges: ranges.slice(0, 8).concat(Array(8 - ranges.length).fill(0)),
|
||||
rssi: rssi.slice(0, 8).concat(Array(8 - rssi.length).fill(0)),
|
||||
anchorIds: anchorIds.slice(0, 8).concat(Array(8 - anchorIds.length).fill(0)),
|
||||
timer: timerMatch ? parseInt(timerMatch[1]) : 0,
|
||||
timerSys: timerSysMatch ? parseInt(timerSysMatch[1]) : 0
|
||||
}
|
||||
|
||||
latestRangeData = result
|
||||
return result
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Set up data listener when connection is established
|
||||
if (parser) {
|
||||
parser.on('data', (data: string) => {
|
||||
const parsed = parseUWBRangeData(data.trim())
|
||||
if (parsed) {
|
||||
console.log('UWB Data received:', parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method === 'GET') {
|
||||
if (!serialConnection?.isOpen) {
|
||||
return res.status(400).json({
|
||||
message: 'Serial connection not established'
|
||||
})
|
||||
}
|
||||
|
||||
// Return latest data
|
||||
res.status(200).json({
|
||||
rangeData: latestRangeData,
|
||||
deviceData: latestDeviceData,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
else if (req.method === 'POST') {
|
||||
// Send command to UWB device
|
||||
const { command } = req.body
|
||||
|
||||
if (!serialConnection?.isOpen) {
|
||||
return res.status(400).json({
|
||||
message: 'Serial connection not established'
|
||||
})
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
return res.status(400).json({ message: 'Command is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
serialConnection.write(command + '\r\n')
|
||||
res.status(200).json({ message: 'Command sent', command })
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
message: 'Failed to send command',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue