/**
* 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
ERROR
{balState.pid_error_deg}°
INTEGRAL
{balState.integral}
)}
{/* 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}
);
})}
)}
);
}