Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/panels/ArmControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand Down
199 changes: 199 additions & 0 deletions src/components/panels/DriveThrottlePanel.tsx
Original file line number Diff line number Diff line change
@@ -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<ROSLIB.Topic | null>(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 (
<div className="throttle-panel">
<div className="slider-container">
<label>Drive Throttle</label>

<div className="slider-wrapper">
<input
type="range"
min="0"
max="100"
value={throttleValue}
onChange={(e) => setThrottleValue(Number(e.target.value))}
className="vertical-slider"
disabled={!ros || !cookiesLoaded}
/>
</div>

<span>
{throttleValue}% (
{throttlePercentToFloat(throttleValue).toFixed(2)})
</span>
</div>

<button
className="off-button"
onClick={throttleOff}
disabled={!ros || !cookiesLoaded}
>
Throttle Off
</button>

<style jsx>{`
.throttle-panel {
background: #1e1e1e;
color: #f1f1f1;
padding: 1rem;
border-radius: 8px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.slider-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
}

.slider-wrapper {
height: 220px;
width: 50px;
display: flex;
align-items: center;
justify-content: center;
}

.vertical-slider {
width: 220px;
height: 40px;
transform: rotate(-90deg);
cursor: pointer;
}

.slider-container span {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, 'Liberation Mono', 'Courier New', monospace;
color: #ccc;
}

.off-button {
width: 100%;
padding: 0.75rem;
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
}

.off-button:hover:enabled {
background: #b52a37;
}

.off-button:disabled {
background: #333;
cursor: not-allowed;
opacity: 0.8;
}

.vertical-slider:disabled {
cursor: not-allowed;
opacity: 0.5;
}
`}</style>
</div>
);
};

export default DriveThrottlePanel;
11 changes: 11 additions & 0 deletions src/components/panels/MosaicDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -44,6 +45,7 @@ type TileType =
| 'goalSetter'
| 'networkHealthMonitor'
| 'motorStatusPanel'
| 'driveThrottlePanel'
| 'nodeStatusPanel'
| 'antennaControlPanel'
| 'scienceControlPanel'
Expand All @@ -65,6 +67,7 @@ const TILE_DISPLAY_NAMES: Record<TileType, string> = {
waypointList: 'Waypoint List',
videoControls: 'Video Stream',
rtpStats: 'RTP Statistics',
driveThrottlePanel: 'Drive Throttle',
gasSensor: 'Science',
orientationDisplay: 'Rover Orientation',
goalSetter: 'Nav2',
Expand All @@ -90,6 +93,7 @@ const ALL_TILE_TYPES: TileType[] = [
'orientationDisplay',
'videoControls',
'rtpStats',
'driveThrottlePanel',
'waypointList',
'gasSensor',
'goalSetter',
Expand Down Expand Up @@ -385,6 +389,13 @@ const MosaicDashboard: React.FC = () => {
</MosaicWindow>
);

case 'driveThrottlePanel':
return (
<MosaicWindow {...windowProps}>
<DriveThrottlePanel />
</MosaicWindow>
);

case 'rosMonitor':
return (
<MosaicWindow {...windowProps}>
Expand Down
Loading