'use client'
import React, { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Activity,
Cpu,
HardDrive,
Wifi,
Thermometer,
Zap,
Navigation,
Eye,
Settings,
AlertTriangle,
CheckCircle,
XCircle,
Loader2
} from 'lucide-react'
interface ROS2Status {
master_connected: boolean
master_uri: string
nodes: string[]
topics: Array<{
name: string
type: string
publishers: number
subscribers: number
}>
services: Array<{
name: string
type: string
}>
parameters: Record<string, any>
}
interface SystemMetrics {
cpu_usage: number
memory_usage: number
disk_usage: number
temperature: number
network_latency: number
uptime: number
}
interface SensorData {
imu: {
accelerometer: { x: number; y: number; z: number }
gyroscope: { x: number; y: number; z: number }
magnetometer: { x: number; y: number; z: number }
} | null
lidar: {
ranges: number[]
angle_min: number
angle_max: number
range_min: number
range_max: number
} | null
ultrasonic: number[] | null
camera: {
resolution: string
fps: number
is_streaming: boolean
} | null
}
interface YahboomMonitoringDashboardProps {
robotId: string
className?: string
}
export function YahboomMonitoringDashboard({ robotId, className }: YahboomMonitoringDashboardProps) {
const [rosStatus, setRosStatus] = useState<ROS2Status | null>(null)
const [systemMetrics, setSystemMetrics] = useState<SystemMetrics | null>(null)
const [sensorData, setSensorData] = useState<SensorData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchMonitoringData = async () => {
try {
const [rosRes, metricsRes, sensorsRes] = await Promise.all([
fetch(`http://localhost:8081/api/robots/${robotId}/ros/status`),
fetch(`http://localhost:8081/api/robots/${robotId}/system/metrics`),
fetch(`http://localhost:8081/api/robots/${robotId}/sensors/data`)
])
if (rosRes.ok) setRosStatus(await rosRes.json())
if (metricsRes.ok) setSystemMetrics(await metricsRes.json())
if (sensorsRes.ok) setSensorData(await sensorsRes.json())
setError(null)
} catch (err) {
setError(`Failed to fetch monitoring data: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchMonitoringData()
const interval = setInterval(fetchMonitoringData, 2000) // Update every 2 seconds
return () => clearInterval(interval)
}, [robotId])
if (loading) {
return (
<Card className={className}>
<CardContent className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin mr-2" />
Loading monitoring data...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card className={className}>
<CardContent className="py-4">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
</CardContent>
</Card>
)
}
return (
<div className={`space-y-4 ${className}`}>
{/* ROS 2 Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-500" />
ROS 2 Status
</CardTitle>
</CardHeader>
<CardContent>
{rosStatus ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Badge variant={rosStatus.master_connected ? "default" : "destructive"}>
{rosStatus.master_connected ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
Master
</Badge>
</div>
<div className="text-sm">
<span className="text-gray-600">Nodes:</span>
<span className="ml-2 font-mono">{rosStatus.nodes.length}</span>
</div>
<div className="text-sm">
<span className="text-gray-600">Topics:</span>
<span className="ml-2 font-mono">{rosStatus.topics.length}</span>
</div>
<div className="text-sm">
<span className="text-gray-600">Services:</span>
<span className="ml-2 font-mono">{rosStatus.services.length}</span>
</div>
</div>
) : (
<p className="text-gray-500">No ROS 2 data available</p>
)}
</CardContent>
</Card>
{/* System Metrics */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="w-5 h-5 text-green-500" />
System Metrics
</CardTitle>
</CardHeader>
<CardContent>
{systemMetrics ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center gap-2 mb-2">
<Cpu className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium">CPU</span>
</div>
<Progress value={systemMetrics.cpu_usage} className="mb-1" />
<p className="text-xs text-gray-600">{systemMetrics.cpu_usage.toFixed(1)}%</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<HardDrive className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium">Memory</span>
</div>
<Progress value={systemMetrics.memory_usage} className="mb-1" />
<p className="text-xs text-gray-600">{systemMetrics.memory_usage.toFixed(1)}%</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Thermometer className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium">Temperature</span>
</div>
<Progress value={(systemMetrics.temperature / 80) * 100} className="mb-1" />
<p className="text-xs text-gray-600">{systemMetrics.temperature.toFixed(1)}°C</p>
</div>
<div className="md:col-span-3">
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-600">Disk Usage:</span>
<span className="ml-2 font-mono">{systemMetrics.disk_usage.toFixed(1)}%</span>
</div>
<div>
<span className="text-gray-600">Network:</span>
<span className="ml-2 font-mono">{systemMetrics.network_latency}ms</span>
</div>
<div>
<span className="text-gray-600">Uptime:</span>
<span className="ml-2 font-mono">{Math.floor(systemMetrics.uptime / 3600)}h {Math.floor((systemMetrics.uptime % 3600) / 60)}m</span>
</div>
</div>
</div>
</div>
) : (
<p className="text-gray-500">No system metrics available</p>
)}
</CardContent>
</Card>
{/* Sensor Data */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Eye className="w-5 h-5 text-indigo-500" />
Sensor Data
</CardTitle>
</CardHeader>
<CardContent>
{sensorData ? (
<div className="space-y-4">
{/* IMU Data */}
{sensorData.imu && (
<div>
<h4 className="text-sm font-medium mb-2 flex items-center gap-2">
<Navigation className="w-4 h-4" />
IMU (Inertial Measurement Unit)
</h4>
<div className="grid grid-cols-3 gap-4 text-xs">
<div>
<p className="text-gray-600 mb-1">Accelerometer (m/s²)</p>
<div className="font-mono">
<p>X: {sensorData.imu.accelerometer.x.toFixed(3)}</p>
<p>Y: {sensorData.imu.accelerometer.y.toFixed(3)}</p>
<p>Z: {sensorData.imu.accelerometer.z.toFixed(3)}</p>
</div>
</div>
<div>
<p className="text-gray-600 mb-1">Gyroscope (°/s)</p>
<div className="font-mono">
<p>X: {sensorData.imu.gyroscope.x.toFixed(3)}</p>
<p>Y: {sensorData.imu.gyroscope.y.toFixed(3)}</p>
<p>Z: {sensorData.imu.gyroscope.z.toFixed(3)}</p>
</div>
</div>
<div>
<p className="text-gray-600 mb-1">Magnetometer (µT)</p>
<div className="font-mono">
<p>X: {sensorData.imu.magnetometer.x.toFixed(3)}</p>
<p>Y: {sensorData.imu.magnetometer.y.toFixed(3)}</p>
<p>Z: {sensorData.imu.magnetometer.z.toFixed(3)}</p>
</div>
</div>
</div>
</div>
)}
{/* LiDAR Data */}
{sensorData.lidar && (
<div>
<h4 className="text-sm font-medium mb-2">LiDAR</h4>
<div className="text-xs">
<p>Ranges: {sensorData.lidar.ranges.length} points</p>
<p>Angle: {sensorData.lidar.angle_min.toFixed(1)}° to {sensorData.lidar.angle_max.toFixed(1)}°</p>
<p>Range: {sensorData.lidar.range_min.toFixed(2)}m to {sensorData.lidar.range_max.toFixed(2)}m</p>
</div>
</div>
)}
{/* Ultrasonic Data */}
{sensorData.ultrasonic && (
<div>
<h4 className="text-sm font-medium mb-2">Ultrasonic Sensors</h4>
<div className="text-xs font-mono">
{sensorData.ultrasonic.map((distance, index) => (
<span key={index} className="mr-4">
S{index + 1}: {distance.toFixed(2)}m
</span>
))}
</div>
</div>
)}
{/* Camera Data */}
{sensorData.camera && (
<div>
<h4 className="text-sm font-medium mb-2 flex items-center gap-2">
<Eye className="w-4 h-4" />
Camera
</h4>
<div className="flex items-center gap-4 text-xs">
<Badge variant={sensorData.camera.is_streaming ? "default" : "secondary"}>
{sensorData.camera.is_streaming ? "Streaming" : "Idle"}
</Badge>
<span>Resolution: {sensorData.camera.resolution}</span>
<span>FPS: {sensorData.camera.fps}</span>
</div>
</div>
)}
</div>
) : (
<p className="text-gray-500">No sensor data available</p>
)}
</CardContent>
</Card>
{/* ROS 2 Topics Table */}
{rosStatus && rosStatus.topics.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wifi className="w-5 h-5 text-cyan-500" />
Active ROS 2 Topics
</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-48 overflow-y-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b">
<th className="text-left py-1">Topic</th>
<th className="text-left py-1">Type</th>
<th className="text-center py-1">Pub</th>
<th className="text-center py-1">Sub</th>
</tr>
</thead>
<tbody>
{rosStatus.topics.slice(0, 10).map((topic, index) => (
<tr key={index} className="border-b border-gray-100">
<td className="py-1 font-mono">{topic.name}</td>
<td className="py-1 text-gray-600">{topic.type.split('/').pop()}</td>
<td className="py-1 text-center">{topic.publishers}</td>
<td className="py-1 text-center">{topic.subscribers}</td>
</tr>
))}
</tbody>
</table>
{rosStatus.topics.length > 10 && (
<p className="text-xs text-gray-500 mt-2">
Showing 10 of {rosStatus.topics.length} topics
</p>
)}
</div>
</CardContent>
</Card>
)}
</div>
)
}