/**
* 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) && (
)}
{/* 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
);
}