This commit is contained in:
legop3
2026-07-11 02:22:13 -04:00
parent 3af74870a5
commit 0e5f76fb2f
9 changed files with 434 additions and 269 deletions
+43 -20
View File
@@ -35,6 +35,7 @@ import {
applyDriveOvercurrentScale,
useOvercurrentLimiter,
} from './overcurrentLimiter.js';
import { usePtzControlAdapter } from './ptzControlAdapter.js';
const ControlSystemContext = createContext(null);
@@ -179,6 +180,7 @@ export function ControlSystemProvider({ children }) {
[overcurrentLimiter.adminImmune, overcurrentLimiter.scales],
);
const pipeline = useCommandPipeline({ driveTransform, auxTransform });
const ptzControls = usePtzControlAdapter();
const turnOnAllLights = useCallback(() => {
const entities = homeAssistantEntities || [];
@@ -327,9 +329,10 @@ export function ControlSystemProvider({ children }) {
payload: { ...computed, source: meta.source ?? null },
});
recordControlIntent();
if (ptzControls.applyDriveVector(vector, meta)) return;
pipeline.sendDriveDirect(computed.speeds);
},
[pipeline, recordControlIntent, state.manualDockAssist?.active],
[pipeline, ptzControls, recordControlIntent, state.manualDockAssist?.active],
);
const setAuxMotors = useCallback(
@@ -372,30 +375,37 @@ export function ControlSystemProvider({ children }) {
const setServoAngle = useCallback(
(value, options = {}) => {
if (!pipeline.servoConfig && !pipeline.isPtzOperator) return;
if (ptzControls.isActive) {
/*
Servo-capable rover controls converge here from keyboard, gamepad,
desktop, and mobile. When the active control target is the PTZ camera,
route the intent through the PTZ adapter instead of making the rover
command pipeline understand camera zoom semantics.
*/
const baseline = typeof servoAngleRef.current === 'number' ? servoAngleRef.current : 0;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return;
ptzControls.pulseZoom(numeric - baseline);
servoAngleRef.current = numeric;
recordControlIntent();
return;
}
if (!pipeline.servoConfig) return;
const force = Boolean(options?.force);
if (state.manualDockAssist?.active && !force) return;
/*
Rover cameras need hardware min/max clamping from their servo config.
PTZ zoom reuses the same browser camera controls after the rover has
been released, so there may be no rover servo config at all; in that
case the raw numeric target is only used as a direction signal by the
command pipeline and does not represent a physical angle.
*/
const clamped = pipeline.servoConfig ? clampServoAngle(pipeline.servoConfig, value) : Number(value);
if (!Number.isFinite(clamped)) return;
const clamped = clampServoAngle(pipeline.servoConfig, value);
dispatch({ type: 'control/set-camera-angle', payload: clamped });
pipeline.sendServoAngle(clamped);
servoAngleRef.current = clamped;
recordControlIntent();
},
[pipeline, recordControlIntent, state.manualDockAssist?.active],
[pipeline, ptzControls, recordControlIntent, state.manualDockAssist?.active],
);
const nudgeServo = useCallback(
(delta = 0) => {
const config = pipeline.servoConfig;
if (!config && !pipeline.isPtzOperator) return;
if (!config && !ptzControls.isActive) return;
const step = typeof delta === 'number' && delta !== 0 ? delta : config?.nudgeDegrees || 1;
const baseline =
typeof servoAngleRef.current === 'number'
@@ -405,7 +415,7 @@ export function ControlSystemProvider({ children }) {
: 0;
setServoAngle(baseline + step);
},
[pipeline.isPtzOperator, pipeline.servoConfig, setServoAngle],
[pipeline.servoConfig, ptzControls.isActive, setServoAngle],
);
const goServoHome = useCallback(() => {
@@ -468,9 +478,13 @@ export function ControlSystemProvider({ children }) {
source: 'system-stop',
},
});
if (ptzControls.isActive) {
ptzControls.stopMotion();
return;
}
pipeline.sendDriveDirect({ left: 0, right: 0 });
pipeline.sendAuxMotors({ main: 0, side: 0, vacuum: 0 });
}, [pipeline]);
}, [pipeline, ptzControls]);
const sendOiCommand = useCallback(
(command) => {
@@ -492,7 +506,11 @@ export function ControlSystemProvider({ children }) {
const setHeadlight = useCallback(
(headlightOn) => {
if (!pipeline.headlight && !pipeline.isPtzOperator) return;
if (ptzControls.setSpotlight(headlightOn)) {
recordControlIntent();
return;
}
if (!pipeline.headlight) return;
// Web controls now speak in logical device state. Any electrical
// inversion needed by the actual GPIO driver is handled by roverd's
// activeLow config, so this command stays readable and direct.
@@ -500,7 +518,7 @@ export function ControlSystemProvider({ children }) {
pipeline.sendHeadlight(action);
recordControlIntent();
},
[pipeline, recordControlIntent],
[pipeline, ptzControls, recordControlIntent],
);
const toggleHeadlight = useCallback(() => {
@@ -509,7 +527,11 @@ export function ControlSystemProvider({ children }) {
const setLaser = useCallback(
(laserOn) => {
if (!pipeline.laser && !pipeline.isPtzOperator) return;
if (ptzControls.setIr(laserOn)) {
recordControlIntent();
return;
}
if (!pipeline.laser) return;
if (roomLightsLockedOn && laserOn !== false) return;
// The laser shares the same logical toggle contract as the headlight; it
// is separate only because it has its own GPIO pin, UI control, and keybind.
@@ -517,7 +539,7 @@ export function ControlSystemProvider({ children }) {
pipeline.sendLaser(action);
recordControlIntent();
},
[pipeline, recordControlIntent, roomLightsLockedOn],
[pipeline, ptzControls, recordControlIntent, roomLightsLockedOn],
);
const toggleLaser = useCallback(() => {
@@ -747,10 +769,11 @@ export function ControlSystemProvider({ children }) {
state,
dispatch,
pipeline,
ptzControls,
overcurrentLimiter,
actions: stableActions,
}),
[state, pipeline, overcurrentLimiter, stableActions],
[state, pipeline, ptzControls, overcurrentLimiter, stableActions],
);
if (snapshotRef.current == null) {