sl-webui f71dad5344 feat(arch): migrate all STM32/Mamba/BlackPill refs to ESP32 BALANCE/IO + fix roslib@1.4.0
Architecture change (2026-04-03): Mamba F722S (STM32F722) and BlackPill
replaced by ESP32 BALANCE (PID loop) and ESP32 IO (motors/sensors/comms).

- Update CLAUDE.md, docs, chassis BOM/ASSEMBLY, pinout, power-budget,
  wiring-diagram, TEAM.md, AUTONOMOUS_ARMING.md, docker-compose
- Update all ROS2 package comments, config labels, launch args
  (stm32_port→esp32_port, /dev/stm32-bridge→/dev/esp32-bridge)
- Update WebUI: stm32Mode→esp32Mode, stm32Version→esp32Version,
  "STM32 State/Mode" labels → "ESP32 State/Mode" (ControlMode, SettingsPanel)
- Add TODO(esp32-migration) markers on stm32_protocol.py and mamba_protocol.py
  binary frame layouts — pending ESP32 protocol spec from max
- Fix roslib CDN 1.3.0→1.4.0 in all 11 HTML panels (fixes ROS2 Humble
  rosbridge "Received a message without an op" incompatibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00

199 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 (
<div className={`flex items-center gap-2 rounded px-3 py-2 border ${
isOk ? 'bg-green-950 border-green-800' : 'bg-red-950 border-red-800'
}`}>
<div className={`w-2 h-2 rounded-full ${isOk ? 'bg-green-400' : 'bg-red-400 animate-pulse'}`} />
<span className="text-xs">
<span className={isOk ? 'text-green-300' : 'text-red-300'}>{label}</span>
</span>
<span className={`ml-auto text-xs font-bold ${isOk ? 'text-green-400' : 'text-red-400'}`}>
{isOk ? 'OK' : 'FAULT'}
</span>
</div>
);
}
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 (
<div className="space-y-4">
{/* Main mode badge */}
<div className={`${cfg.bg} border ${cfg.border} rounded-xl p-5`}>
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-gray-500 text-xs font-bold tracking-widest mb-1">CONTROL MODE</div>
<div className={`text-3xl font-bold tracking-widest ${cfg.color}`}>{cfg.label}</div>
<div className="text-gray-500 text-xs mt-1">{cfg.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="text-gray-600 text-xs">Blend α</div>
<div className={`text-2xl font-bold ${alpha > 0.5 ? 'text-green-400' : 'text-blue-400'}`}>
{alpha.toFixed(2)}
</div>
{stale && <div className="text-red-500 text-xs mt-1">STALE</div>}
</div>
</div>
{/* Blend alpha bar */}
{(mode === 'RAMP_TO_AUTO' || mode === 'RAMP_TO_RC' || alpha > 0) && (
<div className="mt-3">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>RC</span>
<span>AUTO</span>
</div>
<div className="w-full h-3 bg-gray-950 rounded overflow-hidden border border-gray-800">
<div
className="h-full transition-all duration-200 rounded"
style={{
width: `${alpha * 100}%`,
background: `linear-gradient(to right, #2563eb, #16a34a)`,
}}
/>
</div>
</div>
)}
</div>
{/* Safety flags */}
<div className="bg-gray-950 rounded-lg border border-cyan-950 p-4">
<div className="text-cyan-700 text-xs font-bold tracking-widest mb-3">SAFETY INTERLOCKS</div>
<div className="space-y-2">
<SafetyFlag label="SLAM Fix" ok={ctrlMode?.slam_ok ?? false} />
<SafetyFlag label="RC Link" ok={ctrlMode?.rc_link_ok ?? false} />
<SafetyFlag label="Stick Override" ok={ctrlMode?.override_active ?? false} invert />
</div>
{!ctrlMode && (
<div className="text-gray-600 text-xs mt-2">
Waiting for /saltybot/control_mode
</div>
)}
</div>
{/* Balance state detail */}
{balState && (
<div className="bg-gray-950 rounded-lg border border-cyan-950 p-4">
<div className="text-cyan-700 text-xs font-bold tracking-widest mb-3">BALANCE STATE</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-gray-600">ESP32 State: </span>
<span className={`font-bold ${
balState.state === 'ARMED' ? 'text-green-400' :
balState.state === 'TILT FAULT' ? 'text-red-400' : 'text-gray-400'
}`}>{balState.state}</span>
</div>
<div>
<span className="text-gray-600">ESP32 Mode: </span>
<span className="text-cyan-400">{balState.mode}</span>
</div>
<div>
<span className="text-gray-600">Pitch: </span>
<span className="text-amber-400">{balState.pitch_deg}°</span>
</div>
<div>
<span className="text-gray-600">Motor CMD: </span>
<span className="text-orange-400">{balState.motor_cmd}</span>
</div>
</div>
</div>
)}
{/* Mode transition guide */}
<div className="bg-gray-950 rounded-lg border border-cyan-950 p-4">
<div className="text-cyan-700 text-xs font-bold tracking-widest mb-2">STATE MACHINE</div>
<div className="flex items-center gap-1 text-xs flex-wrap">
{['RC', 'RAMP_TO_AUTO', 'AUTO', 'RAMP_TO_RC'].map((m, i) => (
<div key={m} className="flex items-center gap-1">
<span className={`px-2 py-0.5 rounded border ${
mode === m
? `${MODE_CONFIG[m].bg} ${MODE_CONFIG[m].border} ${MODE_CONFIG[m].color} font-bold`
: 'border-gray-800 text-gray-600'
}`}>{m}</span>
{i < 3 && <span className="text-gray-700"></span>}
</div>
))}
</div>
<div className="mt-2 text-gray-700 text-xs space-y-0.5">
<div>AUX2 switch RCAUTO | Stick &gt; 10% while AUTO instant RC</div>
<div>SLAM fix lost while AUTO RAMP_TO_RC | RC link lost instant RC</div>
</div>
</div>
</div>
);
}