mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
guh
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import AuthPanel from './AuthPanel.jsx';
|
||||
import AdminPanel from './AdminPanel.jsx';
|
||||
|
||||
@@ -13,8 +12,10 @@ const manualTabs = [
|
||||
];
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
const { roverId, sendOiCommand } = useDriveControl();
|
||||
const socket = useSocket();
|
||||
const {
|
||||
state: { roverId },
|
||||
actions: { sendOiCommand, setSensorStream },
|
||||
} = useControlSystem();
|
||||
const canControl = Boolean(roverId);
|
||||
|
||||
const sensorButtons = useMemo(
|
||||
@@ -27,11 +28,7 @@ export default function AdvancedSettings() {
|
||||
|
||||
const handleSensorToggle = (enable) => {
|
||||
if (!roverId) return;
|
||||
socket.emit('command', {
|
||||
roverId,
|
||||
type: 'sensorStream',
|
||||
data: { sensorStream: { enable } },
|
||||
});
|
||||
setSensorStream(enable);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
|
||||
const SLIDER_THROTTLE_MS = 150;
|
||||
|
||||
@@ -9,14 +9,17 @@ function formatDegrees(value) {
|
||||
}
|
||||
|
||||
export default function CameraServoPanel() {
|
||||
const { roverId, servo } = useDriveControl();
|
||||
const config = servo?.config;
|
||||
const enabled = Boolean(roverId && servo?.enabled && config);
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
actions: { setServoAngle, nudgeServo, goServoHome },
|
||||
} = useControlSystem();
|
||||
const config = camera?.config;
|
||||
const enabled = Boolean(roverId && camera?.enabled && config);
|
||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||
const value =
|
||||
typeof servo?.angle === 'number'
|
||||
? servo.angle
|
||||
typeof camera?.angle === 'number'
|
||||
? camera.angle
|
||||
: typeof config?.homeAngle === 'number'
|
||||
? config.homeAngle
|
||||
: (min + max) / 2;
|
||||
@@ -52,7 +55,7 @@ export default function CameraServoPanel() {
|
||||
clearTimeout(throttleRef.current);
|
||||
}
|
||||
throttleRef.current = setTimeout(() => {
|
||||
servo.setAngle(next);
|
||||
setServoAngle(next);
|
||||
}, SLIDER_THROTTLE_MS);
|
||||
};
|
||||
|
||||
@@ -69,12 +72,12 @@ export default function CameraServoPanel() {
|
||||
if (throttleRef.current) {
|
||||
clearTimeout(throttleRef.current);
|
||||
}
|
||||
servo.setAngle(pendingAngle);
|
||||
setServoAngle(pendingAngle);
|
||||
};
|
||||
|
||||
const handleNudge = (direction) => {
|
||||
const delta = step * direction;
|
||||
servo.nudge(delta);
|
||||
nudgeServo(delta);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -112,7 +115,7 @@ export default function CameraServoPanel() {
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 rounded bg-slate-700 px-1 py-0.5 text-slate-100 hover:bg-slate-600"
|
||||
onClick={() => servo.goHome()}
|
||||
onClick={() => goServoHome()}
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
|
||||
export default function DrivePanel() {
|
||||
const { roverId, runStartDockFull, seekDock } = useDriveControl();
|
||||
const {
|
||||
state: { roverId },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const sensors = frame?.sensors || {};
|
||||
const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full';
|
||||
@@ -11,6 +14,18 @@ export default function DrivePanel() {
|
||||
sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging',
|
||||
);
|
||||
|
||||
const handleStartDrive = () => {
|
||||
if (!roverId) return;
|
||||
actions.setMode('drive');
|
||||
actions.runMacro('drive-sequence');
|
||||
};
|
||||
|
||||
const handleDock = () => {
|
||||
if (!roverId) return;
|
||||
actions.setMode('dock');
|
||||
actions.runMacro('seek-dock');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
|
||||
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||
@@ -23,7 +38,7 @@ export default function DrivePanel() {
|
||||
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
|
||||
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
|
||||
tone="emerald"
|
||||
onClick={runStartDockFull}
|
||||
onClick={handleStartDrive}
|
||||
disabled={!roverId}
|
||||
/>
|
||||
<ActionCard
|
||||
@@ -34,7 +49,7 @@ export default function DrivePanel() {
|
||||
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
|
||||
]}
|
||||
tone="indigo"
|
||||
onClick={seekDock}
|
||||
onClick={handleDock}
|
||||
disabled={!roverId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,114 +1,132 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Joystick } from 'react-joystick-component';
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { clampUnit } from '../controls/controlMath.js';
|
||||
import DriveModeToggle from './controls/DriveModeToggle.jsx';
|
||||
|
||||
function clampUnit(value = 0) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return 0;
|
||||
return Math.max(-1, Math.min(1, value));
|
||||
}
|
||||
const SOURCE = 'mobile-joystick';
|
||||
const AUX_BUTTONS = [
|
||||
{ id: 'main-forward', label: 'Main +', values: { main: 127 }, hold: true },
|
||||
{ id: 'main-reverse', label: 'Main -', values: { main: -127 }, hold: true },
|
||||
{ id: 'side-forward', label: 'Side +', values: { side: 127 }, hold: true },
|
||||
{ id: 'side-reverse', label: 'Side -', values: { side: -70 }, hold: true },
|
||||
{ id: 'vacuum-fast', label: 'Vacuum Max', values: { vacuum: 127 }, hold: true },
|
||||
{ id: 'vacuum-slow', label: 'Vacuum Low', values: { vacuum: 50 }, hold: true },
|
||||
{ id: 'all-forward', label: 'All +', values: { main: 127, side: 127, vacuum: 127 }, hold: true },
|
||||
{ id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false },
|
||||
];
|
||||
|
||||
function MobileJoystick() {
|
||||
const { roverId, driveWithVector, stopMotors } = useDriveControl();
|
||||
function MobileJoystickPanel({ layout }) {
|
||||
const {
|
||||
state: { roverId },
|
||||
actions: { setDriveVector, registerInputState, stopAllMotion },
|
||||
} = useControlSystem();
|
||||
const disabled = !roverId;
|
||||
|
||||
const handleMove = useCallback(
|
||||
(event = {}) => {
|
||||
if (disabled) return;
|
||||
const x = clampUnit(event.x ?? 0);
|
||||
const y = clampUnit(event.y ?? 0);
|
||||
driveWithVector({ x, y });
|
||||
const vector = {
|
||||
x: clampUnit(event.x ?? 0),
|
||||
y: clampUnit(event.y ?? 0),
|
||||
boost: Boolean(event.shiftKey),
|
||||
};
|
||||
setDriveVector(vector, { source: SOURCE });
|
||||
registerInputState(SOURCE, { vector, lastEvent: 'move' });
|
||||
},
|
||||
[disabled, driveWithVector],
|
||||
[disabled, registerInputState, setDriveVector],
|
||||
);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (disabled) return;
|
||||
driveWithVector({ x: 0, y: 0 });
|
||||
}, [disabled, driveWithVector]);
|
||||
const zero = { x: 0, y: 0, boost: false };
|
||||
setDriveVector(zero, { source: SOURCE });
|
||||
registerInputState(SOURCE, { vector: zero, lastEvent: 'stop' });
|
||||
}, [disabled, registerInputState, setDriveVector]);
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<div className="text-xs text-slate-400">Joystick</div>
|
||||
<div className="mt-1 flex items-center justify-center">
|
||||
<Joystick
|
||||
size={120}
|
||||
baseColor="#0f172a"
|
||||
stickColor="#38bdf8"
|
||||
throttle={75}
|
||||
move={handleMove}
|
||||
stop={handleStop}
|
||||
<DriveModeToggle size="compact" />
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<div className="flex items-center justify-center">
|
||||
<Joystick
|
||||
size={layout === 'landscape' ? 140 : 120}
|
||||
baseColor="#0f172a"
|
||||
stickColor="#38bdf8"
|
||||
throttle={60}
|
||||
move={handleMove}
|
||||
stop={handleStop}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[0.7rem] text-slate-400">Drag to steer · Release to stop sending drive commands.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopAllMotion}
|
||||
disabled={disabled}
|
||||
/>
|
||||
className="rounded-sm bg-red-600 px-1 py-1 text-xs font-semibold uppercase tracking-wide text-red-100 disabled:opacity-40"
|
||||
>
|
||||
Panic Stop
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
Drag to drive. Release to stop sending drive commands.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopMotors}
|
||||
disabled={disabled}
|
||||
className="mt-1 w-full rounded-sm bg-black/40 px-1 py-1 text-xs text-slate-200 disabled:opacity-40"
|
||||
>
|
||||
Panic Stop
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AuxMotorControls() {
|
||||
const { roverId, setAuxMotors } = useDriveControl();
|
||||
function AuxMotorPanel({ orientation }) {
|
||||
const {
|
||||
state: { roverId },
|
||||
actions: { setAuxMotors },
|
||||
} = useControlSystem();
|
||||
const activeRef = useRef(null);
|
||||
const disabled = !roverId;
|
||||
|
||||
const runAllForward = () => {
|
||||
if (disabled) return;
|
||||
setAuxMotors({ main: 127, side: 127, vacuum: 127 });
|
||||
};
|
||||
const handlePress = useCallback(
|
||||
(button) => {
|
||||
if (disabled) return;
|
||||
if (button.hold === false) {
|
||||
setAuxMotors(button.values);
|
||||
activeRef.current = null;
|
||||
return;
|
||||
}
|
||||
activeRef.current = button.id;
|
||||
setAuxMotors(button.values);
|
||||
},
|
||||
[disabled, setAuxMotors],
|
||||
);
|
||||
|
||||
const stopAll = () => {
|
||||
if (disabled) return;
|
||||
setAuxMotors({ main: 0, side: 0, vacuum: 0 });
|
||||
};
|
||||
const handleRelease = useCallback(
|
||||
(button) => {
|
||||
if (disabled || button.hold === false) return;
|
||||
if (activeRef.current === button.id) {
|
||||
activeRef.current = null;
|
||||
setAuxMotors({ main: 0, side: 0, vacuum: 0 });
|
||||
}
|
||||
},
|
||||
[disabled, setAuxMotors],
|
||||
);
|
||||
|
||||
const auxButtons = [
|
||||
{ label: 'Main +', values: { main: 127 } },
|
||||
{ label: 'Main -', values: { main: -127 } },
|
||||
{ label: 'Side +', values: { side: 127 } },
|
||||
{ label: 'Side -', values: { side: -127 } },
|
||||
{ label: 'Vacuum Max', values: { vacuum: 127 } },
|
||||
{ label: 'Vacuum Off', values: { vacuum: 0 } },
|
||||
];
|
||||
const buttonClasses = orientation === 'landscape' ? 'text-xs' : 'text-[0.75rem]';
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<div className="text-xs text-slate-400">Aux motors</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={runAllForward}
|
||||
disabled={disabled}
|
||||
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-xs text-slate-200 disabled:opacity-40"
|
||||
>
|
||||
All forward
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopAll}
|
||||
disabled={disabled}
|
||||
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-xs text-slate-200 disabled:opacity-40"
|
||||
>
|
||||
Stop all
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-1 text-[0.75rem]">
|
||||
{auxButtons.map((btn) => (
|
||||
<p className="text-xs text-slate-400">Aux motors · hold to run</p>
|
||||
<div className="mt-1 grid grid-cols-2 gap-1">
|
||||
{AUX_BUTTONS.map((button) => (
|
||||
<button
|
||||
key={btn.label}
|
||||
key={button.id}
|
||||
type="button"
|
||||
onClick={() => !disabled && setAuxMotors(btn.values)}
|
||||
disabled={disabled}
|
||||
className="rounded-sm bg-black/30 px-1 py-1 text-slate-200 disabled:opacity-30"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
handlePress(button);
|
||||
}}
|
||||
onPointerUp={() => handleRelease(button)}
|
||||
onPointerLeave={() => handleRelease(button)}
|
||||
onPointerCancel={() => handleRelease(button)}
|
||||
className={`rounded-sm bg-black/40 px-1 py-1 text-left text-slate-100 disabled:opacity-30 ${buttonClasses}`}
|
||||
>
|
||||
{btn.label}
|
||||
{button.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -116,17 +134,29 @@ function AuxMotorControls() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function MobileControlsStack() {
|
||||
export function MobileLandscapeAuxColumn() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 sm:flex-row">
|
||||
<div className="flex-1">
|
||||
<MobileJoystick />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<AuxMotorControls />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AuxMotorPanel orientation="landscape" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MobileJoystick, AuxMotorControls };
|
||||
export function MobileLandscapeControlColumn() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<MobileJoystickPanel layout="landscape" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MobilePortraitControls() {
|
||||
return (
|
||||
<section className="rounded-sm bg-transparent">
|
||||
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2">
|
||||
<AuxMotorPanel orientation="portrait" />
|
||||
<MobileJoystickPanel layout="portrait" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
|
||||
export default function DriveModeToggle({ size = 'default' }) {
|
||||
const {
|
||||
state: { roverId, mode },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const [pending, setPending] = useState(null);
|
||||
const disabled = !roverId || pending !== null;
|
||||
|
||||
const handleDrive = async () => {
|
||||
if (!roverId) return;
|
||||
setPending('drive');
|
||||
try {
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDock = async () => {
|
||||
if (!roverId) return;
|
||||
setPending('dock');
|
||||
try {
|
||||
actions.setMode('dock');
|
||||
await actions.runMacro('seek-dock');
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
};
|
||||
|
||||
const pillClass =
|
||||
size === 'compact'
|
||||
? 'text-xs px-1 py-0.5'
|
||||
: 'text-sm px-1.5 py-0.5';
|
||||
const currentLabel = mode === 'dock' ? 'Dock mode' : 'Drive mode';
|
||||
|
||||
return (
|
||||
<div className="rounded-sm bg-black/30 p-1 text-slate-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`rounded-full bg-slate-800 ${pillClass}`}>{currentLabel}</span>
|
||||
<span className="text-[0.65rem] text-slate-400">Rover {roverId ?? '—'}</span>
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDrive}
|
||||
disabled={disabled}
|
||||
className={`rounded-sm bg-emerald-600 px-1 py-1 font-semibold uppercase tracking-wide text-emerald-50 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
|
||||
>
|
||||
Drive
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDock}
|
||||
disabled={disabled}
|
||||
className={`rounded-sm bg-indigo-600 px-1 py-1 font-semibold uppercase tracking-wide text-indigo-50 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
|
||||
>
|
||||
Dock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user