mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
guhpad
This commit is contained in:
@@ -3,6 +3,7 @@ import { useControlSystem } from '../controls/index.js';
|
||||
import AuthPanel from './AuthPanel.jsx';
|
||||
import AdminPanel from './AdminPanel.jsx';
|
||||
import KeymapSettings from './KeymapSettings.jsx';
|
||||
import InputSettings from './InputSettings.jsx';
|
||||
|
||||
const manualTabs = [
|
||||
{ key: 'start', label: 'Start OI' },
|
||||
@@ -31,10 +32,10 @@ export default function AdvancedSettings() {
|
||||
if (!roverId) return;
|
||||
setSensorStream(enable);
|
||||
};
|
||||
// why is this broken
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<KeymapSettings />
|
||||
<InputSettings />
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<p className="text-xs text-slate-400">Manual OI commands</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { clampUnit } from '../controls/controlMath.js';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
|
||||
import DriveModeToggle from './controls/DriveModeToggle.jsx';
|
||||
|
||||
const SOURCE = 'mobile-joystick';
|
||||
const JOYSTICK_RADIUS = 80;
|
||||
const AUX_BUTTONS = [
|
||||
{ id: 'main-forward', label: 'Main +', values: { main: 127 }, hold: true, color: 'bg-emerald-600' },
|
||||
{ id: 'main-reverse', label: 'Main -', values: { main: -127 }, hold: true, color: 'bg-emerald-800' },
|
||||
@@ -16,7 +17,7 @@ const AUX_BUTTONS = [
|
||||
{ id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false, color: 'bg-slate-900' },
|
||||
];
|
||||
|
||||
function FloatingJoystick({ disabled, layout, onMove, onStop }) {
|
||||
function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
|
||||
const containerRef = useRef(null);
|
||||
const pointerIdRef = useRef(null);
|
||||
const baseRef = useRef({ x: 0, y: 0 });
|
||||
@@ -57,13 +58,13 @@ function FloatingJoystick({ disabled, layout, onMove, onStop }) {
|
||||
const currentY = event.clientY - rect.top;
|
||||
const dx = currentX - baseRef.current.x;
|
||||
const dy = currentY - baseRef.current.y;
|
||||
const distance = Math.min(Math.hypot(dx, dy), JOYSTICK_RADIUS);
|
||||
const distance = Math.min(Math.hypot(dx, dy), radius);
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const knobX = Math.cos(angle) * distance;
|
||||
const knobY = Math.sin(angle) * distance;
|
||||
const vector = {
|
||||
x: clampUnit(knobX / JOYSTICK_RADIUS),
|
||||
y: clampUnit(-knobY / JOYSTICK_RADIUS),
|
||||
x: clampUnit(knobX / radius),
|
||||
y: clampUnit(-knobY / radius),
|
||||
boost: false,
|
||||
};
|
||||
setVisual((prev) => ({ ...prev, knob: { x: knobX, y: knobY } }));
|
||||
@@ -134,6 +135,8 @@ function MobileJoystickPanel({ layout }) {
|
||||
state: { roverId, camera },
|
||||
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle },
|
||||
} = useControlSystem();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const mobileSettings = inputSettings.mobile ?? INPUT_SETTINGS_DEFAULTS.mobile;
|
||||
const disabled = !roverId;
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
@@ -142,9 +145,18 @@ function MobileJoystickPanel({ layout }) {
|
||||
const cameraValue =
|
||||
typeof camera?.angle === 'number'
|
||||
? camera.angle
|
||||
: typeof cameraConfig?.homeAngle === 'number'
|
||||
? cameraConfig.homeAngle
|
||||
: (cameraMin + cameraMax) / 2;
|
||||
: typeof cameraConfig?.homeAngle === 'number'
|
||||
? cameraConfig.homeAngle
|
||||
: (cameraMin + cameraMax) / 2;
|
||||
const joystickRadius = Math.max(40, mobileSettings.joystickRadius ?? INPUT_SETTINGS_DEFAULTS.mobile.joystickRadius);
|
||||
const smoothing = Math.min(Math.max(mobileSettings.joystickSmoothing ?? 0, 0), 0.85);
|
||||
const smoothedVectorRef = useRef({ x: 0, y: 0, boost: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
smoothedVectorRef.current = { x: 0, y: 0, boost: false };
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(vector = {}) => {
|
||||
@@ -154,15 +166,25 @@ function MobileJoystickPanel({ layout }) {
|
||||
y: clampUnit(vector.y ?? 0),
|
||||
boost: Boolean(vector.boost),
|
||||
};
|
||||
setDriveVector(next, { source: SOURCE });
|
||||
registerInputState(SOURCE, { vector: next, lastEvent: 'move' });
|
||||
const applied =
|
||||
smoothing > 0
|
||||
? {
|
||||
x: smoothedVectorRef.current.x + (next.x - smoothedVectorRef.current.x) * (1 - smoothing),
|
||||
y: smoothedVectorRef.current.y + (next.y - smoothedVectorRef.current.y) * (1 - smoothing),
|
||||
boost: next.boost,
|
||||
}
|
||||
: next;
|
||||
smoothedVectorRef.current = applied;
|
||||
setDriveVector(applied, { source: SOURCE });
|
||||
registerInputState(SOURCE, { vector: applied, lastEvent: 'move' });
|
||||
},
|
||||
[disabled, registerInputState, setDriveVector],
|
||||
[disabled, registerInputState, setDriveVector, smoothing],
|
||||
);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (disabled) return;
|
||||
const zero = { x: 0, y: 0, boost: false };
|
||||
smoothedVectorRef.current = zero;
|
||||
setDriveVector(zero, { source: SOURCE });
|
||||
registerInputState(SOURCE, { vector: zero, lastEvent: 'stop' });
|
||||
}, [disabled, registerInputState, setDriveVector]);
|
||||
@@ -177,7 +199,13 @@ function MobileJoystickPanel({ layout }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 text-slate-100">
|
||||
<DriveModeToggle size="compact" />
|
||||
<FloatingJoystick disabled={disabled} layout={layout} onMove={handleMove} onStop={handleStop} />
|
||||
<FloatingJoystick
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
radius={joystickRadius}
|
||||
onMove={handleMove}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopAllMotion}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useControlSystem } from '../ControlContext.jsx';
|
||||
import { clampUnit } from '../controlMath.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
|
||||
const SOURCE = 'gamepad';
|
||||
const DEADZONE = 0.2;
|
||||
|
||||
function applyDeadzone(value) {
|
||||
return Math.abs(value) < DEADZONE ? 0 : value;
|
||||
function applyDeadzone(value, amount) {
|
||||
return Math.abs(value) < amount ? 0 : value;
|
||||
}
|
||||
|
||||
function computeTriggerSpeed(button, reverseButton, max = 127, reverseScale = 1) {
|
||||
@@ -42,6 +43,12 @@ export default function GamepadInputManager() {
|
||||
registerInputState,
|
||||
},
|
||||
} = useControlSystem();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const gamepadSettings = inputSettings.gamepad ?? INPUT_SETTINGS_DEFAULTS.gamepad;
|
||||
const driveDeadzone = Math.min(Math.max(gamepadSettings.driveDeadzone ?? 0.2, 0), 0.8);
|
||||
const cameraDeadzone = Math.min(Math.max(gamepadSettings.cameraDeadzone ?? 0.25, 0), 0.9);
|
||||
const servoStep = gamepadSettings.servoStep ?? INPUT_SETTINGS_DEFAULTS.gamepad.servoStep;
|
||||
const auxReverseScale = gamepadSettings.auxReverseScale ?? INPUT_SETTINGS_DEFAULTS.gamepad.auxReverseScale;
|
||||
const rafRef = useRef(null);
|
||||
const lastVectorRef = useRef({ x: 0, y: 0, boost: false });
|
||||
const lastAuxRef = useRef({ main: 0, side: 0, vacuum: 0 });
|
||||
@@ -76,8 +83,8 @@ export default function GamepadInputManager() {
|
||||
return;
|
||||
}
|
||||
|
||||
const axisLX = clampUnit(applyDeadzone(pad.axes?.[0] ?? 0));
|
||||
const axisLY = clampUnit(applyDeadzone(-(pad.axes?.[1] ?? 0)));
|
||||
const axisLX = clampUnit(applyDeadzone(pad.axes?.[0] ?? 0, driveDeadzone));
|
||||
const axisLY = clampUnit(applyDeadzone(-(pad.axes?.[1] ?? 0), driveDeadzone));
|
||||
const vector = { x: axisLX, y: axisLY, boost: false };
|
||||
if (!vectorsEqual(vector, lastVectorRef.current)) {
|
||||
lastVectorRef.current = vector;
|
||||
@@ -85,7 +92,7 @@ export default function GamepadInputManager() {
|
||||
}
|
||||
|
||||
const main = computeTriggerSpeed(pad.buttons?.[7], pad.buttons?.[5]);
|
||||
const side = computeTriggerSpeed(pad.buttons?.[6], pad.buttons?.[4], 127, 0.55);
|
||||
const side = computeTriggerSpeed(pad.buttons?.[6], pad.buttons?.[4], 127, auxReverseScale);
|
||||
const vacuum = pad.buttons?.[1]?.pressed ? 127 : 0;
|
||||
let aux = { main, side, vacuum };
|
||||
if (pad.buttons?.[0]?.pressed) {
|
||||
@@ -96,11 +103,11 @@ export default function GamepadInputManager() {
|
||||
setAuxMotors(aux);
|
||||
}
|
||||
|
||||
const cameraAxis = clampUnit(applyDeadzone(-(pad.axes?.[3] ?? 0)));
|
||||
const cameraAxis = clampUnit(applyDeadzone(-(pad.axes?.[3] ?? 0), cameraDeadzone));
|
||||
const now = performance.now();
|
||||
if (Math.abs(cameraAxis) > 0.25 && now - servoThrottleRef.current > 120) {
|
||||
servoThrottleRef.current = now;
|
||||
nudgeServo(cameraAxis > 0 ? 2 : -2);
|
||||
nudgeServo(cameraAxis > 0 ? servoStep : -servoStep);
|
||||
}
|
||||
|
||||
if (handleButtonEdge(14, pad.buttons?.[14]?.pressed)) {
|
||||
@@ -124,7 +131,19 @@ export default function GamepadInputManager() {
|
||||
rb: pad.buttons?.[5]?.pressed || false,
|
||||
},
|
||||
});
|
||||
}, [handleButtonEdge, nudgeServo, registerInputState, runMacro, setAuxMotors, setDriveVector, setMode]);
|
||||
}, [
|
||||
auxReverseScale,
|
||||
cameraDeadzone,
|
||||
driveDeadzone,
|
||||
handleButtonEdge,
|
||||
nudgeServo,
|
||||
registerInputState,
|
||||
runMacro,
|
||||
servoStep,
|
||||
setAuxMotors,
|
||||
setDriveVector,
|
||||
setMode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
function loop() {
|
||||
|
||||
Reference in New Issue
Block a user