From edf4290a5acde10c96dcc13c8a85ec72af653553 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Sun, 2 Aug 2026 16:35:28 -0400 Subject: [PATCH 1/2] feat: drive throttle panel --- src/components/panels/DriveThrottlePanel.tsx | 199 +++++++++++++++++++ src/components/panels/MosaicDashboard.tsx | 11 + 2 files changed, 210 insertions(+) create mode 100644 src/components/panels/DriveThrottlePanel.tsx diff --git a/src/components/panels/DriveThrottlePanel.tsx b/src/components/panels/DriveThrottlePanel.tsx new file mode 100644 index 0000000..5ebd43f --- /dev/null +++ b/src/components/panels/DriveThrottlePanel.tsx @@ -0,0 +1,199 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; +import ROSLIB from 'roslib'; +import { useROS } from '@/ros/ROSContext'; + +const DRIVE_THROTTLE_COOKIE = 'drive_throttle'; +const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24; // 1 day + +const clampThrottle = (value: number) => { + return Math.min(100, Math.max(0, value)); +}; + +const getCookieNumber = (name: string, fallback: number) => { + const cookie = document.cookie + .split('; ') + .find((entry) => entry.startsWith(`${name}=`)); + + if (!cookie) return fallback; + + const value = Number(decodeURIComponent(cookie.split('=')[1])); + + if (!Number.isFinite(value)) return fallback; + + return clampThrottle(value); +}; + +const setCookieNumber = (name: string, value: number) => { + document.cookie = + `${name}=${encodeURIComponent(value)}; ` + + `max-age=${COOKIE_MAX_AGE_SECONDS}; path=/; SameSite=Lax`; +}; + +const throttlePercentToFloat = (throttle: number) => { + return clampThrottle(throttle) / 100; +}; + +const DriveThrottlePanel: React.FC = () => { + const { ros } = useROS(); + + const [throttleValue, setThrottleValue] = useState(100); + const [cookiesLoaded, setCookiesLoaded] = useState(false); + + const throttleTopicRef = useRef(null); + + useEffect(() => { + setThrottleValue(getCookieNumber(DRIVE_THROTTLE_COOKIE, 100)); + setCookiesLoaded(true); + }, []); + + useEffect(() => { + if (!cookiesLoaded) return; + + setCookieNumber(DRIVE_THROTTLE_COOKIE, throttleValue); + }, [cookiesLoaded, throttleValue]); + + useEffect(() => { + if (!ros) { + throttleTopicRef.current = null; + return; + } + + throttleTopicRef.current = new ROSLIB.Topic({ + ros, + name: '/drive_throttle', + messageType: 'std_msgs/Float32', + }); + + return () => { + try { + throttleTopicRef.current?.unadvertise(); + } catch { + // ignore cleanup errors + } + + throttleTopicRef.current = null; + }; + }, [ros]); + + useEffect(() => { + if (!ros || !cookiesLoaded) return; + + throttleTopicRef.current?.publish( + new ROSLIB.Message({ + data: throttlePercentToFloat(throttleValue), + }) + ); + }, [ros, cookiesLoaded, throttleValue]); + + const throttleOff = () => { + setThrottleValue(0); + }; + + return ( +
+
+ + +
+ setThrottleValue(Number(e.target.value))} + className="vertical-slider" + disabled={!ros || !cookiesLoaded} + /> +
+ + + {throttleValue}% ( + {throttlePercentToFloat(throttleValue).toFixed(2)}) + +
+ + + + +
+ ); +}; + +export default DriveThrottlePanel; \ No newline at end of file diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index c68405b..176d44d 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -30,6 +30,7 @@ import HeadlightControlPanel from './HeadlightControlPanel'; import MorseTransmissionPanel from './MorseTransmissionPanel'; import RtpStats from './RtpStats'; import TopicEchoPanel from './TopicEchoPanel'; +import DriveThrottlePanel from './DriveThrottlePanel'; import { ROVER_IP } from '@/constants'; @@ -44,6 +45,7 @@ type TileType = | 'goalSetter' | 'networkHealthMonitor' | 'motorStatusPanel' + | 'driveThrottlePanel' | 'nodeStatusPanel' | 'antennaControlPanel' | 'scienceControlPanel' @@ -65,6 +67,7 @@ const TILE_DISPLAY_NAMES: Record = { waypointList: 'Waypoint List', videoControls: 'Video Stream', rtpStats: 'RTP Statistics', + driveThrottlePanel: 'Drive Throttle', gasSensor: 'Science', orientationDisplay: 'Rover Orientation', goalSetter: 'Nav2', @@ -90,6 +93,7 @@ const ALL_TILE_TYPES: TileType[] = [ 'orientationDisplay', 'videoControls', 'rtpStats', + 'driveThrottlePanel', 'waypointList', 'gasSensor', 'goalSetter', @@ -385,6 +389,13 @@ const MosaicDashboard: React.FC = () => { ); + case 'driveThrottlePanel': + return ( + + + + ); + case 'rosMonitor': return ( From fe42b21f2db8788ad0eac98d3bef438bd87aaad9 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Sun, 2 Aug 2026 16:47:32 -0400 Subject: [PATCH 2/2] fix: arm stop button --- src/components/panels/ArmControlPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/panels/ArmControlPanel.tsx b/src/components/panels/ArmControlPanel.tsx index 50e866d..a56bc7d 100644 --- a/src/components/panels/ArmControlPanel.tsx +++ b/src/components/panels/ArmControlPanel.tsx @@ -290,7 +290,7 @@ const ArmControlPanel: React.FC = () => { const service = new ROSLIB.Service({ ros, - name: '/stop_motion', + name: '/move_group_interface/stop', serviceType: 'std_srvs/srv/Trigger', });