"use client"
import { useState, useEffect, useCallback, useRef } from 'react'
import { Bot, Play, Pause, RotateCcw, Settings, Eye, Activity, Zap, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Wifi, WifiOff, Map } from 'lucide-react'
import { MapVisualization } from '@/components/MapVisualization'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { toast } from 'sonner'
// WebSocket connection management
class RoboticsWebSocket {
private ws: WebSocket | null = null
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 3000
private onMessage: (data: any) => void
private onConnectionChange: (connected: boolean) => void
constructor(onMessage: (data: any) => void, onConnectionChange: (connected: boolean) => void) {
this.onMessage = onMessage
this.onConnectionChange = onConnectionChange
this.connect()
}
private connect() {
try {
this.ws = new WebSocket('ws://localhost:8354/ws/robotics')
this.ws.onopen = () => {
console.log('WebSocket connected')
this.reconnectAttempts = 0
this.onConnectionChange(true)
toast.success('Connected to robotics server')
}
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.onMessage(data)
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
}
}
this.ws.onclose = () => {
console.log('WebSocket disconnected')
this.onConnectionChange(false)
this.attemptReconnect()
}
this.ws.onerror = (error) => {
console.error('WebSocket error:', error)
this.onConnectionChange(false)
}
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
this.attemptReconnect()
}
}
private attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
setTimeout(() => {
this.connect()
}, this.reconnectInterval)
} else {
toast.error('Failed to connect to robotics server after multiple attempts')
}
}
public sendCommand(robotId: string, command: string, parameters: any = {}) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = {
type: 'command',
robot_id: robotId,
command: command,
parameters: parameters,
timestamp: new Date().toISOString()
}
this.ws.send(JSON.stringify(message))
return true
} else {
toast.error('Not connected to robotics server')
return false
}
}
public disconnect() {
if (this.ws) {
this.ws.close()
}
}
}
// Real-time robot data state
interface RobotData {
id: string
name: string
type: string
status: string
position: { x: number; y: number; z: number }
rotation: { roll: number; pitch: number; yaw: number }
velocity: { linear: number; angular: number }
battery: number
sensors: {
imu: { ax: number; ay: number; az: number; gx: number; gy: number; gz: number }
odometry: { distance: number; speed: number; heading: number }
camera: { streaming: boolean; fps: number; resolution: string }
}
last_update: string
uptime: number
}
export default function VBotControlPage() {
const [isConnected, setIsConnected] = useState(false)
const [isRunning, setIsRunning] = useState(false)
const [linearVelocity, setLinearVelocity] = useState([0])
const [angularVelocity, setAngularVelocity] = useState([0])
const [selectedRobot, setSelectedRobot] = useState('vbot_scout_mini')
const [autoMode, setAutoMode] = useState(false)
const [pressedKeys, setPressedKeys] = useState(new Set<string>())
const [robotData, setRobotData] = useState<RobotData | null>(null)
const [allRobots, setAllRobots] = useState<RobotData[]>([])
const [mapData, setMapData] = useState<any>(null)
const wsRef = useRef<RoboticsWebSocket | null>(null)
// Load initial robot and map data
const loadInitialData = async () => {
try {
// Load robots
const response = await fetch('/api/robots')
if (response.ok) {
const data = await response.json()
setAllRobots(data.robots || [])
}
// Load map data from Dreame robot if available
const dreameRobot = allRobots.find(r => r.type === 'dreame' && r.status === 'online')
if (dreameRobot) {
try {
const mapResponse = await fetch(`/api/robots/${dreameRobot.id}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_map' })
})
if (mapResponse.ok) {
const result = await mapResponse.json()
if (result.success && result.data) {
setMapData(result.data.map_data || result.data.map)
}
}
} catch (mapError) {
console.warn('Failed to load map data:', mapError)
}
}
} catch (error) {
console.error('Failed to load initial data:', error)
}
}
// Initialize WebSocket connection
useEffect(() => {
const handleWebSocketMessage = (data: any) => {
if (data.type === 'robot_update') {
// Update specific robot data
setAllRobots(prev => {
const updated = prev.map(robot =>
robot.id === data.robot_id ? { ...robot, ...data.data } : robot
)
return updated
})
// Update selected robot data if it matches
if (data.robot_id === selectedRobot) {
setRobotData(data.data)
}
} else if (data.type === 'command_ack') {
toast.success(`Command executed: ${data.command}`)
} else if (data.type === 'error') {
toast.error(data.message)
}
}
const handleConnectionChange = (connected: boolean) => {
setIsConnected(connected)
}
wsRef.current = new RoboticsWebSocket(handleWebSocketMessage, handleConnectionChange)
// Load initial data
loadInitialData()
return () => {
wsRef.current?.disconnect()
}
}, [selectedRobot])
// Load data when component mounts
useEffect(() => {
loadInitialData()
}, [])
// Keyboard controls
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Prevent default behavior for game controls
if (['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(event.key.toLowerCase())) {
event.preventDefault()
}
const newKeys = new Set(pressedKeys)
newKeys.add(event.key.toLowerCase())
setPressedKeys(newKeys)
// Update velocities based on keys
updateVelocitiesFromKeys(newKeys)
}
const handleKeyUp = (event: KeyboardEvent) => {
const newKeys = new Set(pressedKeys)
newKeys.delete(event.key.toLowerCase())
setPressedKeys(newKeys)
// Reset velocities when keys are released
if (newKeys.size === 0) {
setLinearVelocity([0])
setAngularVelocity([0])
sendCommand('stop')
} else {
updateVelocitiesFromKeys(newKeys)
}
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [pressedKeys])
const updateVelocitiesFromKeys = useCallback((keys: Set<string>) => {
let linear = 0
let angular = 0
const speed = 0.5 // m/s
if (keys.has('w') || keys.has('arrowup')) linear += speed
if (keys.has('s') || keys.has('arrowdown')) linear -= speed
if (keys.has('a') || keys.has('arrowleft')) angular += speed
if (keys.has('d') || keys.has('arrowright')) angular -= speed
setLinearVelocity([linear])
setAngularVelocity([angular])
// Send movement command
if (linear !== 0 || angular !== 0) {
sendCommand('move', { linear, angular })
}
}, [])
const sendCommand = useCallback((command: string, params: any = {}) => {
if (wsRef.current && selectedRobot) {
wsRef.current.sendCommand(selectedRobot, command, params)
} else {
toast.error('No connection to robotics server')
}
}, [selectedRobot])
const handleMovement = useCallback((direction: string) => {
const speed = 0.3
switch (direction) {
case 'forward':
sendCommand('move', { linear: speed, angular: 0 })
break
case 'backward':
sendCommand('move', { linear: -speed, angular: 0 })
break
case 'left':
sendCommand('move', { linear: 0, angular: speed })
break
case 'right':
sendCommand('move', { linear: 0, angular: -speed })
break
}
}, [sendCommand])
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto p-6 space-y-6">
{/* Header */}
<div className="text-center space-y-4 py-8">
<div className="flex items-center justify-center space-x-4">
<Bot className="h-12 w-12 text-primary animate-pulse" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary via-blue-600 to-purple-600 bg-clip-text text-transparent">
VBot Control Center
</h1>
<Bot className="h-12 w-12 text-primary animate-pulse" />
</div>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Real-time control interface for virtual robots. Experience direct robot control with live sensor feedback,
camera streaming, and autonomous navigation capabilities.
</p>
<div className="flex items-center justify-center space-x-4 text-sm">
<Badge variant={isConnected ? "default" : "destructive"} className="flex items-center space-x-1">
{isConnected ? <Wifi className="w-3 h-3" /> : <WifiOff className="w-3 h-3" />}
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
</Badge>
<Badge variant={isRunning ? "default" : "secondary"} className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${isRunning ? 'bg-green-500' : 'bg-yellow-500'}`} />
<span>{isRunning ? 'Running' : 'Stopped'}</span>
</Badge>
{robotData && (
<Badge variant="outline" className="flex items-center space-x-1">
<Activity className="w-3 h-3" />
<span>Battery: {robotData.battery}%</span>
</Badge>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Control Panel */}
<div className="lg:col-span-2 space-y-6">
{/* Robot Selection & Status */}
<Card>
<CardHeader>
<CardTitle>Robot Control</CardTitle>
<CardDescription>Select robot and monitor real-time status</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<select
className="px-3 py-2 border rounded-md bg-background"
value={selectedRobot}
onChange={(e) => setSelectedRobot(e.target.value)}
>
<option value="vbot_scout_mini">VBot Scout Mini</option>
<option value="vbot_go2">VBot Go2 Quadruped</option>
<option value="vbot_g1">VBot G1 Humanoid</option>
</select>
<div className="flex items-center space-x-2">
<Label htmlFor="auto-mode">Auto Mode</Label>
<Switch
id="auto-mode"
checked={autoMode}
onCheckedChange={setAutoMode}
/>
</div>
</div>
{/* Control Buttons */}
<div className="flex justify-center">
<div className="grid grid-cols-3 gap-2 max-w-xs">
<div></div>
<Button
variant="outline"
size="lg"
className="aspect-square"
onMouseDown={() => handleMovement('forward')}
onMouseUp={() => sendCommand('stop')}
onMouseLeave={() => sendCommand('stop')}
>
<ChevronUp className="h-6 w-6" />
</Button>
<div></div>
<Button
variant="outline"
size="lg"
className="aspect-square"
onMouseDown={() => handleMovement('left')}
onMouseUp={() => sendCommand('stop')}
onMouseLeave={() => sendCommand('stop')}
>
<ChevronLeft className="h-6 w-6" />
</Button>
<Button
variant="outline"
size="lg"
className="aspect-square"
onClick={() => sendCommand('stop')}
>
⏹️
</Button>
<Button
variant="outline"
size="lg"
className="aspect-square"
onMouseDown={() => handleMovement('right')}
onMouseUp={() => sendCommand('stop')}
onMouseLeave={() => sendCommand('stop')}
>
<ChevronRight className="h-6 w-6" />
</Button>
<div></div>
<Button
variant="outline"
size="lg"
className="aspect-square"
onMouseDown={() => handleMovement('backward')}
onMouseUp={() => sendCommand('stop')}
onMouseLeave={() => sendCommand('stop')}
>
<ChevronDown className="h-6 w-6" />
</Button>
<div></div>
</div>
</div>
{/* Velocity Controls */}
<div className="space-y-4">
<div>
<Label>Linear Velocity: {linearVelocity[0].toFixed(2)} m/s</Label>
<Slider
value={linearVelocity}
onValueChange={setLinearVelocity}
max={1}
min={-1}
step={0.1}
className="mt-2"
/>
</div>
<div>
<Label>Angular Velocity: {angularVelocity[0].toFixed(2)} rad/s</Label>
<Slider
value={angularVelocity}
onValueChange={setAngularVelocity}
max={2}
min={-2}
step={0.1}
className="mt-2"
/>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-center space-x-4">
<Button
variant={isRunning ? "destructive" : "default"}
onClick={() => {
setIsRunning(!isRunning)
sendCommand(isRunning ? 'stop' : 'start')
}}
>
{isRunning ? <Pause className="mr-2 h-4 w-4" /> : <Play className="mr-2 h-4 w-4" />}
{isRunning ? 'Stop Robot' : 'Start Robot'}
</Button>
<Button variant="outline" onClick={() => sendCommand('reset')}>
<RotateCcw className="mr-2 h-4 w-4" />
Reset Position
</Button>
<Button variant="outline" onClick={() => sendCommand('emergency_stop')}>
<Zap className="mr-2 h-4 w-4" />
Emergency Stop
</Button>
</div>
</CardContent>
</Card>
{/* Live Camera Feed */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Eye className="h-5 w-5" />
<span>Live Camera Feed</span>
</CardTitle>
<CardDescription>Real-time video stream from robot camera</CardDescription>
</CardHeader>
<CardContent>
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center relative">
<div className="text-center">
<Eye className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
<p className="text-lg font-medium mb-2">Camera Feed</p>
{robotData?.sensors?.camera ? (
<>
<p className="text-sm text-muted-foreground mb-4">
{robotData.sensors.camera.streaming ? 'Live video stream active' : 'Camera feed offline'}
</p>
<div className="flex justify-center space-x-4 text-xs text-muted-foreground">
<span>📹 {robotData.sensors.camera.resolution}</span>
<span>🎬 {robotData.sensors.camera.fps} FPS</span>
<span>📡 {robotData.sensors.camera.streaming ? 'Streaming' : 'Offline'}</span>
</div>
</>
) : (
<p className="text-sm text-muted-foreground mb-4">
Connect to robotics server for live camera feed
</p>
)}
</div>
{/* Camera controls overlay */}
{robotData?.sensors?.camera?.streaming && (
<div className="absolute top-4 right-4 flex space-x-2">
<Button size="sm" variant="secondary">
<Settings className="h-3 w-3" />
</Button>
</div>
)}
{/* Connection indicator */}
<div className="absolute top-4 left-4">
<Badge variant={robotData?.sensors?.camera?.streaming ? "default" : "secondary"} className="text-xs">
{robotData?.sensors?.camera?.streaming ? 'LIVE' : 'OFFLINE'}
</Badge>
</div>
</div>
<div className="flex justify-between mt-4 text-sm text-muted-foreground">
<span>Stream: {robotData?.sensors?.camera?.streaming ? 'Active' : 'Inactive'}</span>
<span>Server: {isConnected ? 'Connected' : 'Disconnected'}</span>
<span>Robot: {selectedRobot}</span>
</div>
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Robot Status */}
<Card>
<CardHeader>
<CardTitle>Robot Status</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{robotData ? (
<>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Position:</span>
<div className="font-mono">
X: {robotData.position.x.toFixed(2)}<br/>
Y: {robotData.position.y.toFixed(2)}<br/>
Z: {robotData.position.z.toFixed(2)}
</div>
</div>
<div>
<span className="text-muted-foreground">Rotation:</span>
<div className="font-mono">
Yaw: {robotData.rotation.yaw.toFixed(1)}°
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Battery</span>
<span>{robotData.battery}%</span>
</div>
<Progress value={robotData.battery} className="h-2" />
</div>
<div className="text-xs text-muted-foreground space-y-1">
<div>Status: <Badge variant="default" className="text-xs">{robotData.status}</Badge></div>
<div>Uptime: {Math.floor(robotData.uptime / 3600)}h {Math.floor((robotData.uptime % 3600) / 60)}m</div>
<div>Type: {robotData.type}</div>
<div>Last Update: {new Date(robotData.last_update).toLocaleTimeString()}</div>
</div>
</>
) : (
<div className="text-center text-muted-foreground py-8">
<Bot className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No robot data available</p>
<p className="text-xs">Connect to robotics server to see live data</p>
</div>
)}
</CardContent>
</Card>
{/* Map Visualization */}
<MapVisualization
robots={allRobots.map(robot => ({
robot_id: robot.id,
robot_type: robot.type,
name: robot.name,
position: robot.position,
rotation: robot.rotation,
connected: robot.status === 'online' || robot.status === 'active',
status: robot.status,
battery: robot.battery
}))}
mapData={mapData}
onRobotCommand={(robotId, command) => {
const ws = wsRef.current
if (ws) {
ws.sendCommand(robotId, command.action, command)
}
}}
className="w-full"
/>
{/* Sensor Dashboard */}
<Card>
<CardHeader>
<CardTitle>Sensor Data</CardTitle>
<CardDescription>Real-time sensor readings</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="imu" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="imu">IMU</TabsTrigger>
<TabsTrigger value="odom">Odometry</TabsTrigger>
<TabsTrigger value="camera">Camera</TabsTrigger>
</TabsList>
<TabsContent value="imu" className="space-y-3">
{robotData?.sensors?.imu ? (
<div className="text-xs font-mono space-y-1">
<div>Accel: {robotData.sensors.imu.ax.toFixed(3)}, {robotData.sensors.imu.ay.toFixed(3)}, {robotData.sensors.imu.az.toFixed(3)}</div>
<div>Gyro: {robotData.sensors.imu.gx.toFixed(3)}, {robotData.sensors.imu.gy.toFixed(3)}, {robotData.sensors.imu.gz.toFixed(3)}</div>
<div>Status: <Badge variant="default" className="text-xs">Active</Badge></div>
</div>
) : (
<div className="text-muted-foreground text-xs">IMU data not available</div>
)}
</TabsContent>
<TabsContent value="odom" className="space-y-3">
{robotData?.sensors?.odometry ? (
<div className="text-xs font-mono space-y-1">
<div>Distance: {robotData.sensors.odometry.distance.toFixed(2)}m</div>
<div>Speed: {robotData.sensors.odometry.speed.toFixed(2)}m/s</div>
<div>Heading: {robotData.sensors.odometry.heading.toFixed(1)}°</div>
</div>
) : (
<div className="text-muted-foreground text-xs">Odometry data not available</div>
)}
</TabsContent>
<TabsContent value="camera" className="space-y-3">
{robotData?.sensors?.camera ? (
<div className="text-xs space-y-1">
<div>Resolution: {robotData.sensors.camera.resolution}</div>
<div>Frame Rate: {robotData.sensors.camera.fps} FPS</div>
<div>Status: <Badge variant="default" className="text-xs">{robotData.sensors.camera.streaming ? 'Streaming' : 'Offline'}</Badge></div>
</div>
) : (
<div className="text-muted-foreground text-xs">Camera data not available</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Control Instructions */}
<Card>
<CardHeader>
<CardTitle>Control Instructions</CardTitle>
</CardHeader>
<CardContent className="text-sm space-y-2">
<div><strong>WASD Keys:</strong> Direct movement control</div>
<div><strong>Mouse:</strong> Click and hold buttons for continuous movement</div>
<div><strong>Sliders:</strong> Fine-tune velocity control</div>
<div><strong>Emergency Stop:</strong> Immediately halts all robot movement</div>
<div className="pt-2 text-xs text-muted-foreground">
Real-time control with 50Hz update rate and <50ms latency
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
)
}