/** * SystemHealth.jsx — System resource monitoring and ROS2 node status. * * Topics: * /diagnostics (diagnostic_msgs/DiagnosticArray) * Each DiagnosticStatus has: name, level (0=OK,1=WARN,2=ERROR,3=STALE) * message, hardware_id, values: [{key,value}] * Expected KeyValue sources (from tegrastats bridge / custom nodes): * cpu_temp_c, gpu_temp_c, ram_used_mb, ram_total_mb, * disk_used_gb, disk_total_gb, gpu_used_mb, gpu_total_mb */ import { useEffect, useState } from 'react'; const LEVEL_CONFIG = { 0: { label: 'OK', color: 'text-green-400', bg: 'bg-green-950', border: 'border-green-800', dot: 'bg-green-400' }, 1: { label: 'WARN', color: 'text-amber-400', bg: 'bg-amber-950', border: 'border-amber-800', dot: 'bg-amber-400 animate-pulse' }, 2: { label: 'ERROR', color: 'text-red-400', bg: 'bg-red-950', border: 'border-red-800', dot: 'bg-red-400 animate-pulse' }, 3: { label: 'STALE', color: 'text-gray-500', bg: 'bg-gray-900', border: 'border-gray-700', dot: 'bg-gray-500' }, }; function ResourceBar({ label, used, total, unit, warnPct = 80 }) { if (total == null || total === 0) return null; const pct = Math.round((used / total) * 100); const warn = pct >= warnPct; const crit = pct >= 95; const color = crit ? '#ef4444' : warn ? '#f59e0b' : '#06b6d4'; return (
{label} {typeof used === 'number' ? used.toFixed(1) : used}/{typeof total === 'number' ? total.toFixed(1) : total} {unit} ({pct}%)
); } function TempGauge({ label, tempC }) { if (tempC == null) return null; const warn = tempC > 75; const crit = tempC > 90; const pct = Math.min(100, (tempC / 100) * 100); const color = crit ? '#ef4444' : warn ? '#f59e0b' : '#22c55e'; return (
{label}
{tempC.toFixed(0)}°C
); } function NodeRow({ status }) { const cfg = LEVEL_CONFIG[status.level] ?? LEVEL_CONFIG[3]; return (
{status.name} {status.message || cfg.label}
); } export function SystemHealth({ subscribe }) { const [resources, setResources] = useState({ cpuTemp: null, gpuTemp: null, ramUsed: null, ramTotal: null, diskUsed: null, diskTotal: null, gpuUsed: null, gpuTotal: null, }); const [nodes, setNodes] = useState([]); const [lastTs, setLastTs] = useState(null); useEffect(() => { const unsub = subscribe('/diagnostics', 'diagnostic_msgs/DiagnosticArray', (msg) => { setLastTs(Date.now()); // Parse each DiagnosticStatus const nodeList = []; for (const status of msg.status ?? []) { const kv = {}; for (const pair of status.values ?? []) kv[pair.key] = pair.value; // Resource metrics setResources(prev => { const next = { ...prev }; if (kv.cpu_temp_c !== undefined) next.cpuTemp = parseFloat(kv.cpu_temp_c); if (kv.gpu_temp_c !== undefined) next.gpuTemp = parseFloat(kv.gpu_temp_c); if (kv.ram_used_mb !== undefined) next.ramUsed = parseFloat(kv.ram_used_mb) / 1024; if (kv.ram_total_mb !== undefined) next.ramTotal = parseFloat(kv.ram_total_mb) / 1024; if (kv.disk_used_gb !== undefined) next.diskUsed = parseFloat(kv.disk_used_gb); if (kv.disk_total_gb!== undefined) next.diskTotal = parseFloat(kv.disk_total_gb); if (kv.gpu_used_mb !== undefined) next.gpuUsed = parseFloat(kv.gpu_used_mb); if (kv.gpu_total_mb !== undefined) next.gpuTotal = parseFloat(kv.gpu_total_mb); return next; }); // Collect all statuses as node rows nodeList.push({ name: status.name, level: status.level, message: status.message }); } if (nodeList.length > 0) { setNodes(nodeList); } }); return unsub; }, [subscribe]); const stale = lastTs && Date.now() - lastTs > 10000; const errorCount = nodes.filter(n => n.level === 2).length; const warnCount = nodes.filter(n => n.level === 1).length; return (
{/* Summary banner */} {nodes.length > 0 && (
0 ? 'bg-red-950 border-red-800' : warnCount > 0 ? 'bg-amber-950 border-amber-800' : 'bg-green-950 border-green-800' }`}>
0 ? 'bg-red-400 animate-pulse' : warnCount > 0 ? 'bg-amber-400 animate-pulse' : 'bg-green-400' }`} />
{errorCount > 0 ? `${errorCount} ERROR${errorCount > 1 ? 'S' : ''}` : warnCount > 0 ? `${warnCount} WARNING${warnCount > 1 ? 'S' : ''}` : 'All systems nominal'}
{nodes.length} nodes
{stale &&
STALE
}
)} {/* Temperatures */} {(resources.cpuTemp != null || resources.gpuTemp != null) && (
)} {/* Resource bars */}
SYSTEM RESOURCES
{resources.ramUsed == null && resources.gpuUsed == null && resources.diskUsed == null && (
Waiting for resource metrics from /diagnostics…
Expected keys: cpu_temp_c, gpu_temp_c, ram_used_mb, disk_used_gb
)}
{/* Node list */}
ROS2 NODE HEALTH
{nodes.length} statuses
{nodes.length === 0 ? (
Waiting for /diagnostics…
) : (
{/* Sort: errors first, then warns, then OK */} {[...nodes] .sort((a, b) => (b.level ?? 0) - (a.level ?? 0)) .map((n, i) => ) }
)}
); }