more organized backend and better mobile control columns

This commit is contained in:
legop3
2026-01-31 20:49:14 -05:00
parent d64e99b674
commit 133b433255
18 changed files with 763 additions and 668 deletions
+146 -123
View File
@@ -4,20 +4,14 @@ import { clampUnit } from '../controls/controlMath.js';
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
import NightVisionControl from './NightVisionControl.jsx';
import HornControl from './HornControl.jsx';
import CameraTiltControl from './CameraTiltControl.jsx';
const SOURCE = 'mobile-joystick';
const JOYSTICK_RADIUS = 80;
const JOYSTICK_SMOOTHING = 0.15;
const AUX_BUTTONS = [
{ id: 'main-forward', label: 'Main Brush +', values: { main: 127 }, hold: true, color: 'bg-emerald-600' },
{ id: 'main-reverse', label: 'Main Brush -', values: { main: -127 }, hold: true, color: 'bg-emerald-800' },
{ id: 'all-forward', label: 'All Motors +', values: { main: 127, side: 127, vacuum: 127 }, hold: true, color: 'bg-fuchsia-600' },
{ id: 'side-forward', label: 'Side Brush +', values: { side: 127 }, hold: true, color: 'bg-cyan-600' },
{ id: 'side-reverse', label: 'Side Brush -', values: { side: -70 }, hold: true, color: 'bg-cyan-800' },
{ id: 'vacuum-fast', label: 'Vacuum Max', values: { vacuum: 127 }, hold: true, color: 'bg-amber-500 text-amber-950' },
{ id: 'vacuum-slow', label: 'Vacuum Low', values: { vacuum: 50 }, hold: true, color: 'bg-amber-700' },
{ id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false, color: 'bg-slate-900' },
];
const AUX_ZERO = { main: 0, side: 0, vacuum: 0 };
const AUX_ALL_FORWARD = { main: 127, side: 127, vacuum: 127 };
const AUX_ALL_BACKWARD = { main: -127, side: -127, vacuum: -127 };
function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
const containerRef = useRef(null);
@@ -134,27 +128,12 @@ function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
function MobileJoystickPanel({ layout }) {
const {
state: { roverId, camera, horn },
pipeline,
actions: { setDriveVector, registerInputState, setServoAngle, setNightVision, startHorn, stopHorn },
state: { roverId },
actions: { setDriveVector, registerInputState },
} = useControlSystem();
const driveDockState = useDriveDockState(roverId);
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
const disabled = !roverId;
const cameraConfig = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornBlocked = horn?.overheated;
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
const cameraValue =
typeof camera?.angle === 'number'
? camera.angle
: typeof cameraConfig?.homeAngle === 'number'
? cameraConfig.homeAngle
: (cameraMin + cameraMax) / 2;
const joystickRadius = JOYSTICK_RADIUS;
const smoothing = JOYSTICK_SMOOTHING;
const smoothedVectorRef = useRef({ x: 0, y: 0, boost: false });
@@ -196,62 +175,20 @@ function MobileJoystickPanel({ layout }) {
registerInputState(SOURCE, { vector: zero, lastEvent: 'stop' });
}, [disabled, registerInputState, setDriveVector]);
const handleCameraSlider = (event) => {
if (!cameraEnabled) return;
const next = Number(event.target.value);
if (Number.isNaN(next)) return;
setServoAngle(next);
};
const handleNightVisionToggle = useCallback(
(nextOn) => {
if (!nightVisionAvailable) return;
setNightVision(nextOn);
},
[nightVisionAvailable, setNightVision],
);
const fillClass = dockedNotDriving ? 'max-h-screen self-start' : '';
const containerClass = `flex h-full flex-col gap-0.5 text-slate-100 ${fillClass}`;
return (
<div className={containerClass}>
<DriveDockAction layout="mobile" expand={dockedNotDriving} driveDockState={driveDockState} />
<DriveDockAction
layout="mobile"
expand={dockedNotDriving}
driveDockState={driveDockState}
compactHeightClass="min-h-[5rem]"
/>
{!dockedNotDriving ? (
<>
{nightVisionAvailable && (
<NightVisionControl
nightVisionOn={nightVisionState?.nightVisionOn}
disabled={disabled}
onToggle={handleNightVisionToggle}
/>
)}
{hornAvailable && (
<HornControl
disabled={disabled || hornBlocked}
onStart={startHorn}
onStop={stopHorn}
active={horn?.active}
heat={horn?.heat}
/>
)}
{cameraEnabled && (
<div className="bg-zinc-950 p-0.5 text-xs">
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
<span>Camera Tilt</span>
<span className="font-mono text-slate-200">{cameraValue.toFixed(1)}°</span>
</div>
<input
type="range"
min={cameraMin}
max={cameraMax}
step={0.5}
value={cameraValue}
onChange={handleCameraSlider}
className="mt-0 w-full accent-cyan-400"
/>
</div>
)}
<FloatingJoystick
disabled={disabled}
layout={layout}
@@ -267,90 +204,176 @@ function MobileJoystickPanel({ layout }) {
);
}
function AuxMotorPanel({ orientation }) {
function MobileAuxButton({ id, label, values, color, disabled, onPress, onRelease }) {
return (
<button
type="button"
disabled={disabled}
onPointerDown={(event) => {
event.preventDefault();
onPress(id, values);
}}
onPointerUp={() => onRelease(id)}
onPointerLeave={() => onRelease(id)}
onPointerCancel={() => onRelease(id)}
onContextMenu={(event) => event.preventDefault()}
className={`flex h-full w-full items-center justify-center rounded-xl border-2 px-1 py-0.75 text-center text-sm font-semibold text-white transition select-none no-touch-select ${color} hover:brightness-110 active:brightness-125 active:scale-[0.99] disabled:opacity-30`}
>
{label}
</button>
);
}
function MobileLeftColumnContent({ layout }) {
const {
state: { roverId },
actions: { setAuxMotors },
state: { roverId, camera, horn },
pipeline,
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn },
} = useControlSystem();
const activeRef = useRef(null);
const disabled = !roverId;
const activeRef = useRef(null);
const cameraConfig = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornBlocked = horn?.overheated;
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
const cameraValue =
typeof camera?.angle === 'number'
? camera.angle
: typeof cameraConfig?.homeAngle === 'number'
? cameraConfig.homeAngle
: (cameraMin + cameraMax) / 2;
const handlePress = useCallback(
(button) => {
const handleNightVisionToggle = useCallback(
(nextOn) => {
if (!nightVisionAvailable) return;
setNightVision(nextOn);
},
[nightVisionAvailable, setNightVision],
);
const handleAuxPress = useCallback(
(id, values) => {
if (disabled) return;
if (button.hold === false) {
setAuxMotors(button.values);
activeRef.current = null;
return;
}
activeRef.current = button.id;
setAuxMotors(button.values);
activeRef.current = id;
setAuxMotors(values);
},
[disabled, setAuxMotors],
);
const handleRelease = useCallback(
(button) => {
if (disabled || button.hold === false) return;
if (activeRef.current === button.id) {
const handleAuxRelease = useCallback(
(id) => {
if (disabled) return;
if (activeRef.current === id) {
activeRef.current = null;
setAuxMotors({ main: 0, side: 0, vacuum: 0 });
setAuxMotors(AUX_ZERO);
}
},
[disabled, setAuxMotors],
);
const gridCols = orientation === 'landscape' ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2';
return (
<div className="flex h-full flex-col gap-0.5 text-slate-100">
<p className="text-xs text-slate-400">Aux controls</p>
<div className={`grid ${gridCols} gap-0.5`}>
{AUX_BUTTONS.map((button) => (
<button
key={button.id}
type="button"
disabled={disabled}
onPointerDown={(event) => {
event.preventDefault();
handlePress(button);
}}
onPointerUp={() => handleRelease(button)}
onPointerLeave={() => handleRelease(button)}
onPointerCancel={() => handleRelease(button)}
onContextMenu={(event) => event.preventDefault()}
className={`px-0.5 py-0.5 text-left text-sm font-semibold text-white transition select-none no-touch-select ${button.color} hover:brightness-110 disabled:opacity-30 h-10`}
<div className="grid h-full min-h-0 w-full grid-rows-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] gap-0.5 text-slate-100">
<div className="flex min-h-0 items-stretch gap-0.5">
{cameraEnabled ? (
<div
className={`flex-1 min-h-0 rounded bg-zinc-950 p-0.25 ${
layout === 'landscape' ? 'pt-3' : ''
}`.trim()}
>
{button.label}
</button>
))}
<CameraTiltControl
value={cameraValue}
min={cameraMin}
max={cameraMax}
step={0.5}
onChange={setServoAngle}
orientation="vertical"
label="Camera Tilt"
labelClass="text-sm font-semibold text-white [writing-mode:vertical-rl] rotate-180"
labelRowClass="text-[0.7rem] text-slate-300"
valueClass="font-mono text-slate-200"
className="h-full gap-0"
sliderClass="h-full w-7"
accentClass="accent-cyan-400"
showEndpoints={false}
showValue={false}
/>
</div>
) : null}
{nightVisionAvailable ? (
<NightVisionControl
nightVisionOn={nightVisionState?.nightVisionOn}
disabled={disabled}
onToggle={handleNightVisionToggle}
heightClass="h-full"
/>
) : null}
</div>
<div className="grid min-h-0 grid-rows-2 gap-0.5">
<MobileAuxButton
id="aux-vac-forward"
label="Vacuum Forward"
values={AUX_ALL_FORWARD}
color="bg-fuchsia-600"
disabled={disabled}
onPress={handleAuxPress}
onRelease={handleAuxRelease}
/>
<MobileAuxButton
id="aux-vac-backward"
label="Vacuum Backward"
values={AUX_ALL_BACKWARD}
color="bg-fuchsia-800"
disabled={disabled}
onPress={handleAuxPress}
onRelease={handleAuxRelease}
/>
</div>
<div className="min-h-0">
{hornAvailable ? (
<HornControl
disabled={disabled || hornBlocked}
onStart={startHorn}
onStop={stopHorn}
active={horn?.active}
heat={horn?.heat}
defaultShowSettings={false}
showSettingsToggle
compactSettings
className="h-full"
/>
) : null}
</div>
</div>
);
}
export function MobileLandscapeAuxColumn() {
export function MobileLeftColumn({ layout, className = '' }) {
return (
<div className="flex h-full flex-col gap-0.5">
<AuxMotorPanel orientation="landscape" />
<div className={`flex flex-col gap-0.5 ${className}`.trim()}>
<MobileLeftColumnContent layout={layout} />
</div>
);
}
export function MobileLandscapeControlColumn() {
export function MobileRightColumn({ layout, className = '' }) {
return (
<div className="flex h-full flex-col gap-0.5">
<MobileJoystickPanel layout="landscape" />
<div className={`flex flex-col gap-0.5 ${className}`.trim()}>
<MobileJoystickPanel layout={layout === 'landscape' ? 'landscape' : 'portrait'} />
</div>
);
}
export default function MobilePortraitControls() {
const columnHeight = 'h-[min(60svh,24rem)]';
return (
<section className="panel">
<div className="grid grid-cols-2 gap-0.5 items-stretch">
<AuxMotorPanel orientation="portrait" />
<MobileJoystickPanel layout="portrait" />
<MobileLeftColumn layout="portrait" className={columnHeight} />
<MobileRightColumn layout="portrait" className={columnHeight} />
</div>
</section>
);