import React, { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Slider } from '@/components/ui/slider'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Bot,
Play,
Square,
Move,
Activity,
Battery,
Wifi,
WifiOff,
Loader2,
RefreshCw
} from 'lucide-react'
const GAZEBO_ROBOT_ID = 'gazebo_01'
const API_BASE = '/api/v1'
async function robotControl(robotId: string, action: string, params?: Record<string, unknown>) {
const res = await fetch(`${API_BASE}/robots/${robotId}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, robot_id: robotId, ...params }),
})
return res.json()
}
async function robotStatus(robotId: string) {
const res = await fetch(`${API_BASE}/robots/${robotId}/status`)
return res.json()
}
export default function GazeboControlPage() {
const [isConnected, setIsConnected] = useState(false)
const [robotData, setRobotData] = useState<Record<string, unknown> | null>(null)
const [linearVelocity, setLinearVelocity] = useState(0.3)
const [angularVelocity, setAngularVelocity] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastCommand, setLastCommand] = useState<string | null>(null)
const loadStatus = async () => {
setIsLoading(true)
setError(null)
try {
const data = await robotStatus(GAZEBO_ROBOT_ID)
const connected = !data.error && (data.connected ?? data.mock ?? true)
setRobotData(data)
setIsConnected(connected)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setIsConnected(false)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadStatus()
}, [])
const handleMove = async () => {
setLastCommand('move')
try {
await robotControl(GAZEBO_ROBOT_ID, 'move', {
linear: linearVelocity,
angular: angularVelocity,
duration: 1.0,
})
loadStatus()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
}
}
const handleStop = async () => {
setLastCommand('stop')
try {
await robotControl(GAZEBO_ROBOT_ID, 'stop')
loadStatus()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
}
}
const handleGetStatus = async () => {
setLastCommand('get_status')
await loadStatus()
}
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="h-12 w-12 animate-spin text-blue-500" />
</div>
)
}
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Gazebo Simulation Control</h1>
<p className="text-gray-600">
Control simulated robots (TurtleBot3, differential drive) via rosbridge WebSocket
</p>
</div>
{error && (
<Alert variant="destructive" className="mb-6">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bot className="h-6 w-6" />
Robot Status
</CardTitle>
<CardDescription>
{isConnected ? (
<span className="flex items-center gap-1 text-green-600">
<Wifi className="h-4 w-4" />
Connected
</span>
) : (
<span className="flex items-center gap-1 text-red-600">
<WifiOff className="h-4 w-4" />
Disconnected
</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Robot ID</span>
<span>{GAZEBO_ROBOT_ID}</span>
</div>
<div className="flex justify-between">
<span>Platform</span>
<Badge variant="outline">Gazebo</Badge>
</div>
{robotData?.battery != null && (
<div className="flex justify-between items-center">
<span>Battery</span>
<span className="flex items-center gap-1">
<Battery className="h-4 w-4" />
{String(robotData.battery)}%
</span>
</div>
)}
{robotData?.mock && (
<p className="text-xs text-amber-600 pt-2">Mock mode - Gazebo not connected</p>
)}
</div>
<Button
variant="outline"
className="w-full mt-4"
onClick={handleGetStatus}
disabled={!isConnected && !robotData?.mock}
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh Status
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Move className="h-6 w-6" />
Movement Controls
</CardTitle>
<CardDescription>
Linear and angular velocity via /cmd_vel
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Linear: {linearVelocity.toFixed(1)} m/s
</label>
<Slider
value={[linearVelocity]}
onValueChange={([v]) => setLinearVelocity(v)}
min={-1}
max={1}
step={0.1}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Angular: {angularVelocity.toFixed(1)} rad/s
</label>
<Slider
value={[angularVelocity]}
onValueChange={([v]) => setAngularVelocity(v)}
min={-2}
max={2}
step={0.1}
/>
</div>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleMove}
disabled={!isConnected && !robotData?.mock}
>
<Play className="h-4 w-4 mr-2" />
Move
</Button>
<Button
variant="destructive"
className="flex-1"
onClick={handleStop}
disabled={!isConnected && !robotData?.mock}
>
<Square className="h-4 w-4 mr-2" />
Stop
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
<Card className="mt-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-6 w-6" />
Setup
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 mb-2">
Enable Gazebo in <code className="bg-gray-100 px-1 rounded">~/.robotics-mcp/config.yaml</code> and
run rosbridge + Gazebo. See <code className="bg-gray-100 px-1 rounded">docs/GAZEBO_INTEGRATION.md</code>.
</p>
<pre className="text-xs bg-gray-100 p-4 rounded overflow-x-auto">
{`robotics:
gazebo:
enabled: true
robot_id: "gazebo_01"
host: "localhost"
port: 9090
mock_mode: false`}
</pre>
</CardContent>
</Card>
</div>
)
}