/** * ControlMode.jsx — RC / Autonomous control mode display. * * Topics: * /saltybot/control_mode (std_msgs/String JSON) * { * mode: "RC" | "RAMP_TO_AUTO" | "AUTO" | "RAMP_TO_RC", * blend_alpha: 0.0..1.0, * slam_ok: bool, * rc_link_ok: bool, * override_active: bool * } * /saltybot/balance_state (std_msgs/String JSON) — robot state, mode label */ import { useEffect, useState } from 'react'; const MODE_CONFIG = { RC: { label: 'RC MANUAL', color: 'text-blue-300', bg: 'bg-blue-950', border: 'border-blue-600', description: 'Pilot has full control via CRSF/ELRS RC link', }, RAMP_TO_AUTO: { label: 'RAMP → AUTO', color: 'text-amber-300', bg: 'bg-amber-950', border: 'border-amber-600', description: 'Transitioning from RC to autonomous (500 ms blend)', }, AUTO: { label: 'AUTONOMOUS', color: 'text-green-300', bg: 'bg-green-950', border: 'border-green-600', description: 'Jetson AI/Nav2 in full control', }, RAMP_TO_RC: { label: 'RAMP → RC', color: 'text-orange-300', bg: 'bg-orange-950', border: 'border-orange-600', description: 'Returning control to pilot (500 ms blend)', }, }; function SafetyFlag({ label, ok, invert }) { const isOk = invert ? !ok : ok; return (
{label} {isOk ? 'OK' : 'FAULT'}
); } export function ControlMode({ subscribe }) { const [ctrlMode, setCtrlMode] = useState(null); const [balState, setBalState] = useState(null); const [lastTs, setLastTs] = useState(null); useEffect(() => { const unsub = subscribe('/saltybot/control_mode', 'std_msgs/String', (msg) => { try { setCtrlMode(JSON.parse(msg.data)); setLastTs(Date.now()); } catch { /* ignore */ } }); return unsub; }, [subscribe]); useEffect(() => { const unsub = subscribe('/saltybot/balance_state', 'std_msgs/String', (msg) => { try { setBalState(JSON.parse(msg.data)); } catch { /* ignore */ } }); return unsub; }, [subscribe]); const mode = ctrlMode?.mode ?? 'RC'; const cfg = MODE_CONFIG[mode] ?? MODE_CONFIG.RC; const alpha = ctrlMode?.blend_alpha ?? 0; const stale = lastTs && Date.now() - lastTs > 5000; return (
{/* Main mode badge */}
CONTROL MODE
{cfg.label}
{cfg.description}
Blend α
0.5 ? 'text-green-400' : 'text-blue-400'}`}> {alpha.toFixed(2)}
{stale &&
STALE
}
{/* Blend alpha bar */} {(mode === 'RAMP_TO_AUTO' || mode === 'RAMP_TO_RC' || alpha > 0) && (
RC AUTO
)}
{/* Safety flags */}
SAFETY INTERLOCKS
{!ctrlMode && (
Waiting for /saltybot/control_mode…
)}
{/* Balance state detail */} {balState && (
BALANCE STATE
ESP32 State: {balState.state}
ESP32 Mode: {balState.mode}
Pitch: {balState.pitch_deg}°
Motor CMD: {balState.motor_cmd}
)} {/* Mode transition guide */}
STATE MACHINE
{['RC', 'RAMP_TO_AUTO', 'AUTO', 'RAMP_TO_RC'].map((m, i) => (
{m} {i < 3 && }
))}
AUX2 switch → RC⇄AUTO | Stick > 10% while AUTO → instant RC
SLAM fix lost while AUTO → RAMP_TO_RC | RC link lost → instant RC
); }