mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
256 lines
7.0 KiB
JavaScript
256 lines
7.0 KiB
JavaScript
import { useCallback, useMemo } from 'react';
|
|
import { useSocket } from '../context/SocketContext.jsx';
|
|
import { useSessionSelector } from '../context/SessionContext.jsx';
|
|
import {
|
|
AUX_LIMITS,
|
|
COMMAND_DELAY_MS,
|
|
OI_COMMANDS,
|
|
SONG_DEFAULT_DURATION,
|
|
SONG_DEFAULT_NOTE,
|
|
SONG_NOTE_RANGE,
|
|
} from './constants.js';
|
|
import { bytesToBase64, clampRange, sleep } from './controlMath.js';
|
|
|
|
export function useCommandPipeline(options = {}) {
|
|
const { driveTransform, auxTransform } = options;
|
|
const socket = useSocket();
|
|
const session = useSessionSelector((state) => state.session);
|
|
const roverId = session?.assignment?.roverId;
|
|
|
|
const rosterEntry = useMemo(() => {
|
|
if (!roverId || !Array.isArray(session?.roster)) return null;
|
|
return session.roster.find((entry) => String(entry.id) === String(roverId)) || null;
|
|
}, [roverId, session?.roster]);
|
|
|
|
const servoConfig = useMemo(() => {
|
|
if (!rosterEntry?.cameraServo || !rosterEntry.cameraServo.enabled) return null;
|
|
return rosterEntry.cameraServo;
|
|
}, [rosterEntry]);
|
|
|
|
const nightVision = useMemo(() => {
|
|
if (!rosterEntry?.nightVision || !rosterEntry.nightVision.enabled) return null;
|
|
return rosterEntry.nightVision;
|
|
}, [rosterEntry]);
|
|
|
|
const horn = useMemo(() => {
|
|
if (!rosterEntry?.horn || !rosterEntry.horn.enabled) return null;
|
|
return rosterEntry.horn;
|
|
}, [rosterEntry]);
|
|
|
|
const nightVisionState = useMemo(() => rosterEntry?.nightVision?.state ?? null, [rosterEntry]);
|
|
|
|
const emitCommand = useCallback(
|
|
(payload, cb) => {
|
|
if (!roverId) return;
|
|
socket.emit('command', { roverId, ...payload }, cb);
|
|
},
|
|
[socket, roverId],
|
|
);
|
|
|
|
const enableSensorStream = useCallback(() => {
|
|
if (!roverId) return;
|
|
emitCommand({
|
|
type: 'sensorStream',
|
|
data: { sensorStream: { enable: true } },
|
|
});
|
|
}, [emitCommand, roverId]);
|
|
|
|
const sendDriveDirect = useCallback(
|
|
(speeds) => {
|
|
if (!roverId) return null;
|
|
const rawPayload = {
|
|
left: clampRange(speeds?.left ?? 0, [-500, 500]),
|
|
right: clampRange(speeds?.right ?? 0, [-500, 500]),
|
|
};
|
|
const transformed = driveTransform ? driveTransform(rawPayload) : rawPayload;
|
|
const payload = {
|
|
left: clampRange(transformed?.left ?? 0, [-500, 500]),
|
|
right: clampRange(transformed?.right ?? 0, [-500, 500]),
|
|
};
|
|
emitCommand({
|
|
type: 'drive',
|
|
data: { driveDirect: payload },
|
|
});
|
|
return payload;
|
|
},
|
|
[driveTransform, emitCommand, roverId],
|
|
);
|
|
|
|
const sendAuxMotors = useCallback(
|
|
({ main = 0, side = 0, vacuum = 0 } = {}) => {
|
|
if (!roverId) return null;
|
|
const rawPayload = {
|
|
main: clampRange(main, AUX_LIMITS.main),
|
|
side: clampRange(side, AUX_LIMITS.side),
|
|
vacuum: clampRange(vacuum, AUX_LIMITS.vacuum),
|
|
};
|
|
const transformed = auxTransform ? auxTransform(rawPayload) : rawPayload;
|
|
const payload = {
|
|
main: clampRange(transformed?.main ?? 0, AUX_LIMITS.main),
|
|
side: clampRange(transformed?.side ?? 0, AUX_LIMITS.side),
|
|
vacuum: clampRange(transformed?.vacuum ?? 0, AUX_LIMITS.vacuum),
|
|
};
|
|
emitCommand({
|
|
type: 'motors',
|
|
data: { motorPwm: payload },
|
|
});
|
|
return payload;
|
|
},
|
|
[auxTransform, emitCommand, roverId],
|
|
);
|
|
|
|
const sendServoAngle = useCallback(
|
|
(angle) => {
|
|
if (!roverId || !servoConfig) return null;
|
|
emitCommand({
|
|
type: 'servo',
|
|
data: { servo: { angle } },
|
|
});
|
|
return angle;
|
|
},
|
|
[emitCommand, roverId, servoConfig],
|
|
);
|
|
|
|
const sendOiCommand = useCallback(
|
|
(keyOrBytes) => {
|
|
if (!roverId) return false;
|
|
const bytes = Array.isArray(keyOrBytes)
|
|
? keyOrBytes
|
|
: typeof keyOrBytes === 'string'
|
|
? OI_COMMANDS[keyOrBytes]
|
|
: null;
|
|
if (!bytes) return false;
|
|
emitCommand({
|
|
type: 'raw',
|
|
data: { raw: bytesToBase64(bytes) },
|
|
});
|
|
enableSensorStream();
|
|
return true;
|
|
},
|
|
[emitCommand, enableSensorStream, roverId],
|
|
);
|
|
|
|
const runMacroSteps = useCallback(
|
|
async (macro) => {
|
|
if (!macro || !Array.isArray(macro.steps) || !roverId) return;
|
|
for (const step of macro.steps) {
|
|
if (!roverId) break;
|
|
switch (step.type) {
|
|
case 'oi':
|
|
sendOiCommand(step.command);
|
|
break;
|
|
case 'drive':
|
|
sendDriveDirect(step.speeds ?? { left: 0, right: 0 });
|
|
break;
|
|
case 'motors':
|
|
sendAuxMotors(step.values ?? {});
|
|
break;
|
|
case 'servo':
|
|
sendServoAngle(step.angle);
|
|
break;
|
|
case 'pause':
|
|
await sleep(step.duration ?? COMMAND_DELAY_MS); // eslint-disable-line no-await-in-loop
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (step.delay || step.delayMs) {
|
|
const delay = step.delayMs ?? step.delay;
|
|
if (typeof delay === 'number' && delay > 0) {
|
|
await sleep(delay); // eslint-disable-line no-await-in-loop
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[roverId, sendOiCommand, sendDriveDirect, sendAuxMotors, sendServoAngle],
|
|
);
|
|
|
|
const sendNightVision = useCallback(
|
|
(action = 'toggle') => {
|
|
if (!roverId || !nightVision) return null;
|
|
emitCommand({
|
|
type: 'nightVision',
|
|
data: { nightVision: { action } },
|
|
});
|
|
return action;
|
|
},
|
|
[emitCommand, nightVision, roverId],
|
|
);
|
|
|
|
const sendHorn = useCallback(
|
|
(payload) => {
|
|
if (!roverId) return null;
|
|
emitCommand({
|
|
type: 'horn',
|
|
data: { horn: payload },
|
|
});
|
|
return payload;
|
|
},
|
|
[emitCommand, roverId],
|
|
);
|
|
|
|
const sendSong = useCallback(
|
|
(notes = [], options = {}) => {
|
|
if (!roverId) return null;
|
|
const prepared =
|
|
Array.isArray(notes) && notes.length > 0
|
|
? notes
|
|
: [{ note: SONG_DEFAULT_NOTE, duration: SONG_DEFAULT_DURATION }];
|
|
const payloadNotes = prepared.slice(0, 16).map((entry) => ({
|
|
note: clampRange(Math.round(entry?.note ?? SONG_DEFAULT_NOTE), SONG_NOTE_RANGE),
|
|
duration: clampRange(Math.round(entry?.duration ?? SONG_DEFAULT_DURATION), [1, 255]),
|
|
}));
|
|
emitCommand({
|
|
type: 'song',
|
|
data: {
|
|
song: {
|
|
notes: payloadNotes,
|
|
slot: options.slot,
|
|
loop: options.loop,
|
|
},
|
|
},
|
|
});
|
|
return payloadNotes;
|
|
},
|
|
[emitCommand, roverId],
|
|
);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
roverId,
|
|
rosterEntry,
|
|
servoConfig,
|
|
nightVision,
|
|
nightVisionState,
|
|
horn,
|
|
emitCommand,
|
|
enableSensorStream,
|
|
sendDriveDirect,
|
|
sendAuxMotors,
|
|
sendServoAngle,
|
|
sendOiCommand,
|
|
sendNightVision,
|
|
sendHorn,
|
|
sendSong,
|
|
runMacroSteps,
|
|
}),
|
|
[
|
|
roverId,
|
|
rosterEntry,
|
|
servoConfig,
|
|
nightVision,
|
|
nightVisionState,
|
|
horn,
|
|
emitCommand,
|
|
enableSensorStream,
|
|
sendDriveDirect,
|
|
sendAuxMotors,
|
|
sendServoAngle,
|
|
sendOiCommand,
|
|
sendNightVision,
|
|
sendHorn,
|
|
runMacroSteps,
|
|
],
|
|
);
|
|
}
|