precision camera stuff

This commit is contained in:
legop3
2026-06-29 21:06:21 -04:00
parent 79be3e1583
commit 0cc40b7608
12 changed files with 129 additions and 28 deletions
+2 -2
View File
@@ -196,9 +196,9 @@ function MobileFeatureTabs({
{/* activities tab */}
<TabPanel id="activities">
<div className={`flex flex-col ${themeGapClass}`}>
<BarcodeGamesPanel />
<LiftCard />
<NeatoCard />
<LiftCard />
<BarcodeGamesPanel />
<OdometerPanel />
<ButtonBoxPanel />
<KinectPanel />
@@ -12,6 +12,9 @@ import VerticalCameraTilt from './VerticalCameraTilt.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { useSessionSelector } from '../../context/SessionContext.jsx';
const CAMERA_TILT_STEP_DEGREES = 0.5;
const CAMERA_TILT_PRECISION_STEP_DEGREES = 0.1;
function AuxColumnContent() {
const roverId = useControlSelector((control) => control.state.roverId);
const roomLightsLockedOn = useSessionSelector((state) => Boolean(state.session?.homeAssistant?.lightPolicy?.lockedOn));
@@ -41,6 +44,14 @@ function AuxColumnContent() {
? cameraConfig.homeAngle
: (cameraMin + cameraMax) / 2;
const cameraDisabled = Boolean(disabled || dockAssist.cameraLocked);
/*
The mobile tilt track shares the same precision flag as desktop tilt. This
keeps the servo fine-step behavior tied to the selected movement mode rather
than inventing a separate mobile-only camera setting.
*/
const cameraTiltStep = camera?.precisionMode
? CAMERA_TILT_PRECISION_STEP_DEGREES
: CAMERA_TILT_STEP_DEGREES;
const handleHeadlightToggle = useCallback(
(nextOn) => {
@@ -101,7 +112,7 @@ function AuxColumnContent() {
value={cameraValue}
min={cameraMin}
max={cameraMax}
step={0.5}
step={cameraTiltStep}
disabled={cameraDisabled}
onChange={setServoAngle}
/>
@@ -30,7 +30,7 @@ function getSpeedModeConfig(speedMode) {
export default function ControlPadPanel({ disabled = false }) {
const rawKeymap = useControlSelector((control) => control.state.keymap);
const { setDriveVector, registerInputState } = useControlActions();
const { setCameraPrecisionMode, setDriveVector, registerInputState } = useControlActions();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const [speedMode, setSpeedMode] = useState('normal');
const [activeInputLabel, setActiveInputLabel] = useState('stop');
@@ -71,10 +71,18 @@ export default function ControlPadPanel({ disabled = false }) {
const tokens = buildVirtualKeyTokens(cell, modeId);
const vector = computeKeyboardDriveVector(tokens, keymap);
const speedOptions = getKeyboardDriveSpeedOptions(tokens, keymap, keyboardSpeeds);
const precisionActive = modeId === 'precision';
// Mobile deliberately routes through the keyboard vector/speed helpers. The
// thumb pad only chooses which virtual keys are down, so changes to keyboard
// drive behavior automatically stay matched here.
/*
The selected mobile speed mode is persistent, unlike the keyboard's held
Shift modifier. Publishing camera precision here makes the servo tilt
controls follow the mobile movement mode even before the driver starts
dragging on the pad.
*/
setCameraPrecisionMode(precisionActive);
setDriveVector(vector, { source: SOURCE, speedOptions });
registerInputState(SOURCE, {
keys: Array.from(tokens),
@@ -90,6 +98,7 @@ export default function ControlPadPanel({ disabled = false }) {
keyboardSpeeds,
keymap,
registerInputState,
setCameraPrecisionMode,
setDriveVector,
],
);
@@ -99,7 +108,7 @@ export default function ControlPadPanel({ disabled = false }) {
clearRepeatTimer();
activeCellRef.current = null;
setActiveInputLabel('stop');
sendDriveCell({ id: 'stop', actions: [] }, lastEvent, 'normal');
sendDriveCell({ id: 'stop', actions: [] }, lastEvent, speedModeRef.current);
},
[clearRepeatTimer, sendDriveCell],
);
@@ -127,21 +136,31 @@ export default function ControlPadPanel({ disabled = false }) {
(nextMode) => {
setSpeedMode(nextMode);
speedModeRef.current = nextMode;
setCameraPrecisionMode(nextMode === 'precision');
if (activeCellRef.current) {
sendDriveCell(activeCellRef.current, 'speed', nextMode);
}
},
[sendDriveCell],
[sendDriveCell, setCameraPrecisionMode],
);
useEffect(() => {
return () => clearRepeatTimer();
}, [clearRepeatTimer]);
return () => {
clearRepeatTimer();
/*
Mobile controls can unmount when layouts change or the driver leaves the
control surface. Clear the shared flag so a stale mobile precision choice
cannot leave desktop/keyboard camera tilt in fine-step mode.
*/
setCameraPrecisionMode(false);
};
}, [clearRepeatTimer, setCameraPrecisionMode]);
useEffect(() => {
if (!disabled) return;
stopDrivePad('disabled');
}, [disabled, stopDrivePad]);
setCameraPrecisionMode(false);
}, [disabled, setCameraPrecisionMode, stopDrivePad]);
return (
<div className="mobile-touch-control flex flex-1 min-h-0 flex-col overflow-hidden rounded-xl border-2 border-slate-700 bg-slate-900 text-slate-100 shadow-md">
+13 -2
View File
@@ -39,6 +39,8 @@ const CHAT_DOCK_INITIAL_HEIGHT = 224;
const CHAT_DOCK_MIN_HEIGHT = 144;
const CHAT_DOCK_MAX_HEIGHT = 300;
const CHAT_DOCK_BOTTOM_INSET = 8;
const CAMERA_TILT_STEP_DEGREES = 0.5;
const CAMERA_TILT_PRECISION_STEP_DEGREES = 0.1;
function TopDownMapPanel() {
const roverId = useControlSelector((control) => control.state.roverId);
@@ -90,6 +92,14 @@ function DriveDockPanel() {
const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]);
const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]);
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
/*
Precision movement mode also tightens the servo slider step. The command
path still sends ordinary angle targets; only the UI increment changes while
precision mode is active.
*/
const cameraTiltStep = camera?.precisionMode
? CAMERA_TILT_PRECISION_STEP_DEGREES
: CAMERA_TILT_STEP_DEGREES;
const trackedControls = useMemo(
() => ({
setHeadlight: (nextOn) => {
@@ -156,6 +166,7 @@ function DriveDockPanel() {
value={value}
min={min}
max={max}
step={cameraTiltStep}
label="Camera tilt"
disabled={cameraDisabled}
onChange={setServoAngle}
@@ -388,9 +399,9 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
{/* activities tab */}
<TabPanel id="activities">
<div className={`flex flex-col ${themeGapClass}`}>
<BarcodeGamesPanel />
<LiftCard />
<NeatoCard />
<LiftCard />
<BarcodeGamesPanel />
<OdometerPanel />
<ButtonBoxPanel />
<KinectPanel />
+12
View File
@@ -49,6 +49,7 @@ const CONTROL_ACTION_NAMES = [
'setServoAngle',
'nudgeServo',
'goServoHome',
'setCameraPrecisionMode',
'runMacro',
'stopAllMotion',
'sendOiCommand',
@@ -411,6 +412,15 @@ export function ControlSystemProvider({ children }) {
setServoAngle(target);
}, [pipeline.servoConfig, setServoAngle]);
const setCameraPrecisionMode = useCallback((active) => {
/*
This action only changes how the browser chooses servo increments. The Pi
already accepts decimal angle targets, so no server or rover command shape
changes are needed for precision camera mode.
*/
dispatch({ type: 'control/set-camera-precision-mode', payload: Boolean(active) });
}, []);
const runMacro = useCallback(
async (macroId) => {
const macro = state.macros.find((item) => item.id === macroId) || null;
@@ -675,6 +685,7 @@ export function ControlSystemProvider({ children }) {
setServoAngle,
nudgeServo,
goServoHome,
setCameraPrecisionMode,
runMacro,
stopAllMotion,
sendOiCommand,
@@ -701,6 +712,7 @@ export function ControlSystemProvider({ children }) {
setServoAngle,
nudgeServo,
goServoHome,
setCameraPrecisionMode,
runMacro,
stopAllMotion,
sendOiCommand,
+23
View File
@@ -20,6 +20,12 @@ function createCameraState() {
enabled: false,
angle: null,
config: null,
/*
Servo precision is UI/control state, not rover hardware state. Keeping it
beside the camera angle lets every camera-tilt surface use the same
precision setting while still sending the normal decimal angle commands.
*/
precisionMode: false,
};
}
@@ -110,6 +116,7 @@ export function controlReducer(state, action) {
...state.camera,
enabled: Boolean(action.payload?.config),
config: action.payload?.config ?? null,
precisionMode: action.payload?.config ? state.camera.precisionMode : false,
angle:
typeof action.payload?.angle === 'number'
? action.payload.angle
@@ -123,6 +130,22 @@ export function controlReducer(state, action) {
...state,
camera: { ...state.camera, angle: action.payload },
};
case 'control/set-camera-precision-mode':
if (state.camera.precisionMode === Boolean(action.payload)) {
return state;
}
return {
...state,
camera: {
...state.camera,
/*
Movement inputs own this flag because precision camera mode is meant
to follow movement precision mode automatically instead of becoming a
separate toggle the driver has to remember to reset.
*/
precisionMode: Boolean(action.payload),
},
};
case 'control/register-input-state': {
const sourceKey = action.payload?.source || 'unknown';
return {
@@ -21,6 +21,7 @@ import {
computeKeyboardAuxMotors,
computeKeyboardDriveVector,
getKeyboardDriveSpeedOptions,
isPrecisionDriveActive,
resolveKeyboardSpeeds,
} from './driveIntent.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
@@ -34,6 +35,7 @@ const TILT_INTERVAL_MIN = 5;
const TILT_INTERVAL_MAX = 500;
const TILT_SPEED_MIN = 1;
const TILT_SPEED_MAX = 100;
const PRECISION_SERVO_NUDGE_DEGREES = 0.25;
const VIDEO_FILTER_SEQUENCE = ['none', 'grayscale', 'greenscale'];
function normalizeVideoFilter(value) {
@@ -91,6 +93,7 @@ export default function KeyboardInputManager() {
runMacro,
stopAllMotion,
registerInputState,
setCameraPrecisionMode,
toggleHeadlight,
toggleLaser,
startHorn,
@@ -157,9 +160,16 @@ export default function KeyboardInputManager() {
const latest = latestRef.current;
if (!latest) return;
const tokensSnapshot = new Set(activeTokensRef.current);
const precisionActive = isPrecisionDriveActive(tokensSnapshot, latest.keymap);
const speedOptions = getKeyboardDriveSpeedOptions(tokensSnapshot, latest.keymap, latest.keyboardSpeeds);
const vector = computeKeyboardDriveVector(tokensSnapshot, latest.keymap);
const aux = computeKeyboardAuxMotors(tokensSnapshot, latest.keymap);
/*
Keyboard precision is a held modifier, so publish the camera precision
state from the same token snapshot that drives movement. This keeps the
servo UI in lockstep with the actual precision-driving intent.
*/
latest.setCameraPrecisionMode(precisionActive);
if (
vector.x !== lastVectorRef.current.x ||
vector.y !== lastVectorRef.current.y ||
@@ -219,7 +229,10 @@ export default function KeyboardInputManager() {
stopServoLoop();
return;
}
latest.nudgeServo(nextDirection * latest.servoStep);
const tokensSnapshot = new Set(activeTokensRef.current);
const precisionActive = isPrecisionDriveActive(tokensSnapshot, latest.keymap);
const servoStep = precisionActive ? PRECISION_SERVO_NUDGE_DEGREES : latest.servoStep;
latest.nudgeServo(nextDirection * servoStep);
servoIntervalRef.current = setTimeout(tick, latest.servoRepeatMs);
};
servoIntervalRef.current = setTimeout(tick, 0);
@@ -288,6 +301,7 @@ export default function KeyboardInputManager() {
latest?.stopHorn();
}
latest?.setMicPttActive(false);
latest?.setCameraPrecisionMode(false);
latest?.stopAllMotion();
latest?.registerInputState(SOURCE, { keys: [], vector: ZERO_VECTOR, aux: ZERO_AUX });
}, [stopServoLoop, stopSongLoop]);
@@ -369,6 +383,7 @@ export default function KeyboardInputManager() {
servoRepeatMs,
servoStep,
setAuxMotors,
setCameraPrecisionMode,
setDriveVector,
setMicPttActive,
setMode,
+11 -1
View File
@@ -54,6 +54,16 @@ export function computeKeyboardDriveVector(keys, keymap) {
};
}
export function isPrecisionDriveActive(keys, keymap) {
/*
The slow modifier is the canonical precision-drive signal shared by physical
keyboard input and the mobile pad's virtual-key path. Centralizing the check
keeps camera precision coupled to the same driver intent instead of copying
modifier-specific knowledge into each caller.
*/
return bindingActive(keymap?.slowModifier, keys);
}
export function computeKeyboardAuxMotors(keys, keymap) {
const allForward = bindingActive(keymap.auxAllForward, keys);
if (allForward) {
@@ -78,7 +88,7 @@ export function computeKeyboardAuxMotors(keys, keymap) {
}
export function getKeyboardDriveSpeedOptions(keys, keymap, keyboardSpeeds) {
const slowActive = bindingActive(keymap.slowModifier, keys);
const slowActive = isPrecisionDriveActive(keys, keymap);
return slowActive
? { baseSpeed: keyboardSpeeds.precisionSpeed, boostSpeed: keyboardSpeeds.precisionSpeed }
: { baseSpeed: keyboardSpeeds.baseSpeed, boostSpeed: keyboardSpeeds.turboSpeed };