set default tab to help. add beep keys.

This commit is contained in:
legop3
2025-12-08 13:57:27 -05:00
parent 161929e280
commit 6580de08d4
20 changed files with 254 additions and 21 deletions
+21 -1
View File
@@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useReducer,
import { controlReducer, initialControlState } from './controlReducer.js';
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
import { useCommandPipeline } from './commandPipeline.js';
import { DEFAULT_KEYMAP, DEFAULT_MACROS } from './constants.js';
import { DEFAULT_KEYMAP, DEFAULT_MACROS, SONG_DEFAULT_NOTE } from './constants.js';
import { canonicalizeKeyInput } from './keymapUtils.js';
import { useSettingsNamespace } from '../settings/index.js';
import { useSession } from '../context/SessionContext.jsx';
@@ -258,6 +258,22 @@ export function ControlSystemProvider({ children }) {
pipeline.sendNightVision('toggle');
}, [pipeline]);
const setSongNote = useCallback(
(note) => {
const next = typeof note === 'number' ? note : SONG_DEFAULT_NOTE;
dispatch({ type: 'control/set-song-note', payload: next });
return next;
},
[],
);
const sendSong = useCallback(
(notes, options) => {
return pipeline.sendSong(notes, options);
},
[pipeline],
);
const registerInputState = useCallback((source, data) => {
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
}, []);
@@ -282,6 +298,8 @@ export function ControlSystemProvider({ children }) {
updateKeyBinding,
resetKeyBindings,
registerInputState,
setSongNote,
sendSong,
},
}),
[
@@ -301,6 +319,8 @@ export function ControlSystemProvider({ children }) {
updateKeyBinding,
resetKeyBindings,
registerInputState,
setSongNote,
sendSong,
],
);
+35 -1
View File
@@ -1,7 +1,14 @@
import { useCallback, useMemo } from 'react';
import { useSocket } from '../context/SocketContext.jsx';
import { useSession } from '../context/SessionContext.jsx';
import { AUX_LIMITS, COMMAND_DELAY_MS, OI_COMMANDS } from './constants.js';
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() {
@@ -151,6 +158,32 @@ export function useCommandPipeline() {
[emitCommand, nightVision, 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,
@@ -164,6 +197,7 @@ export function useCommandPipeline() {
sendServoAngle,
sendOiCommand,
sendNightVision,
sendSong,
runMacroSteps,
}),
[
+8 -1
View File
@@ -10,7 +10,12 @@ export const DRIVE_LIMITS = {
boostSpeed: 400,
};
export const COMMAND_DELAY_MS = 200;
export const COMMAND_DELAY_MS = 100;
export const SONG_NOTE_RANGE = [31, 127];
export const SONG_DEFAULT_NOTE = 60;
export const SONG_DEFAULT_DURATION = 32;
export const SONG_REPEAT_MS = 150;
export const OI_COMMANDS = {
start: [128],
@@ -40,6 +45,8 @@ export const DEFAULT_KEYMAP = {
driveMacro: ['f'],
dockMacro: ['g'],
chatFocus: ['enter'],
songNoteUp: ['ArrowUp'],
songNoteDown: ['ArrowDown'],
};
export const DEFAULT_MACROS = [
+18 -1
View File
@@ -1,4 +1,4 @@
import { DEFAULT_KEYMAP, DEFAULT_MACROS } from './constants.js';
import { DEFAULT_KEYMAP, DEFAULT_MACROS, SONG_DEFAULT_NOTE } from './constants.js';
function createDriveState() {
return {
@@ -21,12 +21,19 @@ function createCameraState() {
};
}
function createSongState() {
return {
note: SONG_DEFAULT_NOTE,
};
}
export const initialControlState = {
roverId: null,
mode: 'drive',
drive: createDriveState(),
aux: createAuxState(),
camera: createCameraState(),
song: createSongState(),
macros: DEFAULT_MACROS,
keymap: DEFAULT_KEYMAP,
inputs: {},
@@ -40,6 +47,7 @@ export function controlReducer(state, action) {
roverId: action.payload ?? null,
drive: action.payload ? state.drive : createDriveState(),
aux: action.payload ? state.aux : createAuxState(),
song: action.payload ? state.song : createSongState(),
};
case 'control/set-mode':
return state.mode === action.payload
@@ -114,6 +122,15 @@ export function controlReducer(state, action) {
...state,
drive: createDriveState(),
aux: createAuxState(),
song: createSongState(),
};
case 'control/set-song-note':
return {
...state,
song: {
...(state.song || createSongState()),
note: action.payload ?? SONG_DEFAULT_NOTE,
},
};
default:
return state;
@@ -4,11 +4,19 @@ import { useChat } from '../../context/ChatContext.jsx';
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import {
SONG_DEFAULT_DURATION,
SONG_DEFAULT_NOTE,
SONG_NOTE_RANGE,
SONG_REPEAT_MS,
} from '../constants.js';
const SOURCE = 'keyboard';
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
const ZERO_AUX = { main: 0, side: 0, vacuum: 0 };
const SERVO_REPEAT_MS = 110;
const NOTE_MIN = SONG_NOTE_RANGE[0];
const NOTE_MAX = SONG_NOTE_RANGE[1];
function clampSpeed(value, fallback) {
const num = Number(value);
@@ -95,6 +103,8 @@ export default function KeyboardInputManager() {
stopAllMotion,
registerInputState,
toggleNightVision,
setSongNote,
sendSong,
},
} = useControlSystem();
const { focusChat, blurChat, isChatFocused } = useChat();
@@ -126,6 +136,7 @@ export default function KeyboardInputManager() {
const lastVectorRef = useRef(ZERO_VECTOR);
const lastAuxRef = useRef(ZERO_AUX);
const servoIntervalRef = useRef(null);
const songIntervalRef = useRef(null);
const driveFromKeys = useCallback(() => {
const tokensSnapshot = new Set(activeTokensRef.current);
@@ -194,14 +205,66 @@ export default function KeyboardInputManager() {
servoIntervalRef.current = setTimeout(tick, 0);
}, [computeServoDirection, nudgeServo, servoStep, stopServoLoop]);
const stopSongLoop = useCallback(() => {
if (songIntervalRef.current) {
clearTimeout(songIntervalRef.current);
songIntervalRef.current = null;
}
}, []);
const computeSongDirection = useCallback(() => {
const tokensSnapshot = new Set(activeTokensRef.current);
const up = bindingActive(keymap.songNoteUp, tokensSnapshot);
const down = bindingActive(keymap.songNoteDown, tokensSnapshot);
return (up ? 1 : 0) - (down ? 1 : 0);
}, [keymap]);
const triggerSongChange = useCallback(
(direction) => {
if (direction === 0) return;
const current = typeof state.song?.note === 'number' ? state.song.note : SONG_DEFAULT_NOTE;
let next = current + direction;
if (next > NOTE_MAX) {
next = NOTE_MIN;
} else if (next < NOTE_MIN) {
next = NOTE_MAX;
}
const finalNote = setSongNote(next);
sendSong([{ note: finalNote, duration: SONG_DEFAULT_DURATION }], { slot: 0 });
},
[sendSong, setSongNote, state.song?.note],
);
const ensureSongLoop = useCallback(() => {
const direction = computeSongDirection();
if (direction === 0) {
stopSongLoop();
return;
}
if (songIntervalRef.current) {
return;
}
const tick = () => {
const nextDirection = computeSongDirection();
if (nextDirection === 0) {
stopSongLoop();
return;
}
triggerSongChange(nextDirection);
songIntervalRef.current = setTimeout(tick, SONG_REPEAT_MS);
};
songIntervalRef.current = setTimeout(tick, 0);
}, [computeSongDirection, stopSongLoop, triggerSongChange]);
const resetAll = useCallback(() => {
activeTokensRef.current.clear();
lastVectorRef.current = ZERO_VECTOR;
lastAuxRef.current = ZERO_AUX;
stopServoLoop();
stopSongLoop();
stopAllMotion();
registerInputState(SOURCE, { keys: [], vector: ZERO_VECTOR, aux: ZERO_AUX });
}, [registerInputState, stopAllMotion, stopServoLoop]);
}, [registerInputState, stopAllMotion, stopServoLoop, stopSongLoop]);
useEffect(() => {
function handleKeyDown(event) {
@@ -238,6 +301,7 @@ export default function KeyboardInputManager() {
}
ensureServoLoop();
ensureSongLoop();
driveFromKeys();
}
@@ -245,6 +309,7 @@ export default function KeyboardInputManager() {
const tokens = tokensForEvent(event);
tokens.forEach((token) => activeTokensRef.current.delete(token));
ensureServoLoop();
ensureSongLoop();
driveFromKeys();
}
@@ -265,6 +330,7 @@ export default function KeyboardInputManager() {
blurChat,
driveFromKeys,
ensureServoLoop,
ensureSongLoop,
focusChat,
isChatFocused,
keymap.chatFocus,
@@ -274,6 +340,7 @@ export default function KeyboardInputManager() {
runMacro,
setMode,
stopAllMotion,
stopSongLoop,
]);
const latestResetAllRef = useRef(resetAll);
@@ -288,8 +355,9 @@ export default function KeyboardInputManager() {
useEffect(
() => () => {
stopServoLoop();
stopSongLoop();
},
[stopServoLoop],
[stopServoLoop, stopSongLoop],
);
return null;
+4
View File
@@ -92,5 +92,9 @@ export function formatKeyLabel(value) {
if (canonical === '/') return '/ or ?';
if (canonical === '-') return '- or _';
if (canonical === '=') return '= or +';
if (canonical === 'arrowup') return 'Arrow Up';
if (canonical === 'arrowdown') return 'Arrow Down';
if (canonical === 'arrowleft') return 'Arrow Left';
if (canonical === 'arrowright') return 'Arrow Right';
return canonical.length === 1 ? canonical.toUpperCase() : canonical;
}