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, ], ); }