mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
drive
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>webui</title>
|
<title>webui</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D9Hjnx7-.js"></script>
|
<script type="module" crossorigin src="/assets/index-BrexHw5K.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-dfoT05bK.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Cyy-WeOF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useSession } from './context/SessionContext.jsx';
|
import { useSession } from './context/SessionContext.jsx';
|
||||||
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
||||||
import VideoTile from './components/VideoTile.jsx';
|
import VideoTile from './components/VideoTile.jsx';
|
||||||
|
import DrivePanel from './components/DrivePanel.jsx';
|
||||||
import { useVideoRequests } from './hooks/useVideoRequests.js';
|
import { useVideoRequests } from './hooks/useVideoRequests.js';
|
||||||
|
|
||||||
function StatusBadge({ connected, role, mode }) {
|
function StatusBadge({ connected, role, mode }) {
|
||||||
@@ -263,6 +264,7 @@ function App() {
|
|||||||
<section className="grid gap-6 lg:grid-cols-2">
|
<section className="grid gap-6 lg:grid-cols-2">
|
||||||
<RosterPanel />
|
<RosterPanel />
|
||||||
<AssignmentCard />
|
<AssignmentCard />
|
||||||
|
<DrivePanel />
|
||||||
<TelemetryPanel />
|
<TelemetryPanel />
|
||||||
<AuthPanel />
|
<AuthPanel />
|
||||||
<SessionInspector />
|
<SessionInspector />
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useDriveControls } from '../hooks/useDriveControls.js';
|
||||||
|
|
||||||
|
function SpeedMeter({ left, right }) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-4 rounded-2xl border border-slate-800/60 bg-slate-950/40 p-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">Left</p>
|
||||||
|
<p className="text-2xl font-semibold text-white">{left}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">Right</p>
|
||||||
|
<p className="text-2xl font-semibold text-white">{right}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DrivePanel() {
|
||||||
|
const { roverId, speeds, sensorEnabled, toggleSensorStream, stopMotors } = useDriveControls();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||||
|
<header className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">Drive Controls</p>
|
||||||
|
<h2 className="text-2xl font-semibold text-white">
|
||||||
|
{roverId ? `Driving rover ${roverId}` : 'No rover assigned'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-400">
|
||||||
|
<span>W/A/S/D: move</span>
|
||||||
|
<span>Shift: boost</span>
|
||||||
|
<span>Space: browser handles scroll</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<SpeedMeter left={speeds.left} right={speeds.right} />
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={stopMotors}
|
||||||
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white disabled:opacity-50"
|
||||||
|
disabled={!roverId}
|
||||||
|
>
|
||||||
|
Stop Motors
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleSensorStream}
|
||||||
|
className="rounded-lg border border-slate-700 px-4 py-2 text-sm font-semibold text-slate-200 disabled:opacity-50"
|
||||||
|
disabled={!roverId}
|
||||||
|
>
|
||||||
|
{sensorEnabled ? 'Disable Sensor Stream' : 'Enable Sensor Stream'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-4 text-xs text-slate-500">
|
||||||
|
Keep this tab focused to drive. Keyboard input is ignored while typing in form fields.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSocket } from '../context/SocketContext.jsx';
|
||||||
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSpeeds(keys) {
|
||||||
|
const forward = keys.has('w');
|
||||||
|
const backward = keys.has('s');
|
||||||
|
const left = keys.has('a');
|
||||||
|
const right = keys.has('d');
|
||||||
|
const fast = keys.has('shift');
|
||||||
|
const base = fast ? 300 : 150;
|
||||||
|
let leftSpeed = 0;
|
||||||
|
let rightSpeed = 0;
|
||||||
|
|
||||||
|
if (forward && !backward) {
|
||||||
|
leftSpeed += base;
|
||||||
|
rightSpeed += base;
|
||||||
|
} else if (backward && !forward) {
|
||||||
|
leftSpeed -= base;
|
||||||
|
rightSpeed -= base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left && !right) {
|
||||||
|
leftSpeed -= base;
|
||||||
|
rightSpeed += base;
|
||||||
|
} else if (right && !left) {
|
||||||
|
leftSpeed += base;
|
||||||
|
rightSpeed -= base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forward && !backward && (left || right)) {
|
||||||
|
leftSpeed = left ? -base : base;
|
||||||
|
rightSpeed = left ? base : -base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: clamp(leftSpeed, -500, 500),
|
||||||
|
right: clamp(rightSpeed, -500, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreEvent(event) {
|
||||||
|
const target = event.target;
|
||||||
|
if (!target) return false;
|
||||||
|
const tag = target.tagName;
|
||||||
|
return (
|
||||||
|
tag === 'INPUT' ||
|
||||||
|
tag === 'TEXTAREA' ||
|
||||||
|
target.isContentEditable ||
|
||||||
|
(tag === 'SELECT' && ['w', 'a', 's', 'd', 'Shift'].includes(event.key))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDriveControls() {
|
||||||
|
const socket = useSocket();
|
||||||
|
const { session } = useSession();
|
||||||
|
const roverId = session?.assignment?.roverId;
|
||||||
|
const keysRef = useRef(new Set());
|
||||||
|
const lastSpeedsRef = useRef({ left: 0, right: 0 });
|
||||||
|
const [currentSpeeds, setCurrentSpeeds] = useState(lastSpeedsRef.current);
|
||||||
|
const [sensorEnabled, setSensorEnabled] = useState(false);
|
||||||
|
|
||||||
|
const emitCommand = useCallback(
|
||||||
|
(payload, cb) => {
|
||||||
|
if (!roverId) return;
|
||||||
|
socket.emit('command', { roverId, ...payload }, cb);
|
||||||
|
},
|
||||||
|
[socket, roverId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendDriveUpdate = useCallback(() => {
|
||||||
|
if (!roverId) return;
|
||||||
|
const speeds = computeSpeeds(keysRef.current);
|
||||||
|
const last = lastSpeedsRef.current;
|
||||||
|
if (speeds.left === last.left && speeds.right === last.right) return;
|
||||||
|
lastSpeedsRef.current = speeds;
|
||||||
|
setCurrentSpeeds(speeds);
|
||||||
|
emitCommand({
|
||||||
|
type: 'drive',
|
||||||
|
data: { driveDirect: speeds },
|
||||||
|
});
|
||||||
|
}, [emitCommand, roverId]);
|
||||||
|
|
||||||
|
const stopMotors = useCallback(() => {
|
||||||
|
if (!roverId) return;
|
||||||
|
keysRef.current.clear();
|
||||||
|
lastSpeedsRef.current = { left: 0, right: 0 };
|
||||||
|
setCurrentSpeeds(lastSpeedsRef.current);
|
||||||
|
emitCommand({
|
||||||
|
type: 'motors',
|
||||||
|
data: { motorPwm: { main: 0, side: 0, vacuum: 0 } },
|
||||||
|
});
|
||||||
|
}, [emitCommand, roverId]);
|
||||||
|
|
||||||
|
const toggleSensorStream = useCallback(() => {
|
||||||
|
if (!roverId) return;
|
||||||
|
setSensorEnabled((prev) => {
|
||||||
|
const next = !prev;
|
||||||
|
emitCommand({
|
||||||
|
type: 'sensorStream',
|
||||||
|
data: { sensorStream: { enable: next } },
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [emitCommand, roverId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!roverId) {
|
||||||
|
keysRef.current.clear();
|
||||||
|
lastSpeedsRef.current = { left: 0, right: 0 };
|
||||||
|
setCurrentSpeeds(lastSpeedsRef.current);
|
||||||
|
setSensorEnabled(false);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(event) {
|
||||||
|
if (shouldIgnoreEvent(event)) return;
|
||||||
|
const key = event.key.toLowerCase();
|
||||||
|
if (!['w', 'a', 's', 'd', 'shift'].includes(key)) return;
|
||||||
|
if (keysRef.current.has(key)) return;
|
||||||
|
keysRef.current.add(key);
|
||||||
|
sendDriveUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(event) {
|
||||||
|
const key = event.key.toLowerCase();
|
||||||
|
if (!keysRef.current.has(key)) return;
|
||||||
|
keysRef.current.delete(key);
|
||||||
|
sendDriveUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
};
|
||||||
|
}, [roverId, sendDriveUpdate]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
roverId,
|
||||||
|
speeds: currentSpeeds,
|
||||||
|
sensorEnabled,
|
||||||
|
toggleSensorStream,
|
||||||
|
stopMotors,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user