/** * MotorPanel.jsx — Motor telemetry display. * * Balance bot: /saltybot/balance_state (std_msgs/String JSON) * motor_cmd [-1000..1000], bridge_speed, bridge_steer * * Rover mode: /saltybot/rover_pwm (std_msgs/String JSON) * ch1_us, ch2_us, ch3_us, ch4_us [1000..2000] * * Temperatures: /diagnostics (diagnostic_msgs/DiagnosticArray) * motor_temp_l_c, motor_temp_r_c * * Displays PWM-bar for each motor, RPM proxy, temperature. */ import { useEffect, useState } from 'react'; const PWM_MIN = 1000; const PWM_MID = 1500; const PWM_MAX = 2000; /** Map [-1..1] normalised value to bar width and color. */ function dutyBar(norm) { const pct = Math.abs(norm) * 50; const color = norm > 0 ? '#f97316' : '#3b82f6'; const left = norm >= 0 ? '50%' : `${50 - pct}%`; return { pct, color, left }; } function MotorGauge({ label, norm, pwmUs, tempC }) { const { pct, color, left } = dutyBar(norm); const tempWarn = tempC != null && tempC > 70; return (
{label} {tempC != null && ( {tempC.toFixed(0)}°C )}
{/* PWM value */}
{pwmUs != null ? `${pwmUs} µs` : `${Math.round(norm * 1000)}`}
{/* Bidirectional duty bar */}
REV {(norm * 100).toFixed(0)}% FWD
); } export function MotorPanel({ subscribe }) { const [balState, setBalState] = useState(null); const [roverPwm, setRoverPwm] = useState(null); const [temps, setTemps] = useState({ l: null, r: null }); // Balance state (2-wheel robot) useEffect(() => { const unsub = subscribe('/saltybot/balance_state', 'std_msgs/String', (msg) => { try { setBalState(JSON.parse(msg.data)); } catch { /* ignore */ } }); return unsub; }, [subscribe]); // Rover PWM (4-wheel rover) useEffect(() => { const unsub = subscribe('/saltybot/rover_pwm', 'std_msgs/String', (msg) => { try { setRoverPwm(JSON.parse(msg.data)); } catch { /* ignore */ } }); return unsub; }, [subscribe]); // Motor temperatures from diagnostics useEffect(() => { const unsub = subscribe('/diagnostics', 'diagnostic_msgs/DiagnosticArray', (msg) => { for (const status of msg.status ?? []) { const kv = {}; for (const pair of status.values ?? []) kv[pair.key] = pair.value; if (kv.motor_temp_l_c !== undefined || kv.motor_temp_r_c !== undefined) { setTemps({ l: kv.motor_temp_l_c != null ? parseFloat(kv.motor_temp_l_c) : null, r: kv.motor_temp_r_c != null ? parseFloat(kv.motor_temp_r_c) : null, }); break; } } }); return unsub; }, [subscribe]); // Determine display mode: rover if rover_pwm active, else balance const isRover = roverPwm != null; let leftNorm = 0, rightNorm = 0; let leftPwm = null, rightPwm = null; if (isRover) { // ch1=left-front, ch2=right-front, ch3=left-rear, ch4=right-rear leftPwm = Math.round((roverPwm.ch1_us + (roverPwm.ch3_us ?? roverPwm.ch1_us)) / 2); rightPwm = Math.round((roverPwm.ch2_us + (roverPwm.ch4_us ?? roverPwm.ch2_us)) / 2); leftNorm = (leftPwm - PWM_MID) / (PWM_MAX - PWM_MID); rightNorm = (rightPwm - PWM_MID) / (PWM_MAX - PWM_MID); } else if (balState) { const cmd = (balState.motor_cmd ?? 0) / 1000; const steer = (balState.bridge_steer ?? 0) / 1000; leftNorm = cmd + steer; rightNorm = cmd - steer; } // Clip to ±1 leftNorm = Math.max(-1, Math.min(1, leftNorm)); rightNorm = Math.max(-1, Math.min(1, rightNorm)); return (
{/* Mode indicator */}
{isRover ? 'ROVER (4WD)' : 'BALANCE (2WD)'}
{!balState && !roverPwm && ( No motor data )}
{/* Motor gauges */}
{/* Balance-specific details */} {balState && !isRover && (
PID DETAILS
CMD
{balState.motor_cmd}
ERROR
{balState.pid_error_deg}°
INTEGRAL
{balState.integral}
FRAMES
{balState.frames}
)} {/* Rover PWM details */} {roverPwm && (
CHANNEL PWM (µs)
{[1,2,3,4].map(ch => { const us = roverPwm[`ch${ch}_us`] ?? PWM_MID; const norm = (us - PWM_MID) / (PWM_MAX - PWM_MID); return (
CH{ch}
0.05 ? '#f97316' : norm < -0.05 ? '#3b82f6' : '#6b7280' }}> {us}
); })}
)}
); }