refactor: send keypress as early as possible

This commit is contained in:
Adam Shiervani 2025-09-16 12:34:06 +02:00
parent 11b3e8935f
commit 421666ccdc
1 changed files with 47 additions and 25 deletions

View File

@ -1,6 +1,12 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { hidErrorRollOver, hidKeyBufferSize, KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores"; import {
hidErrorRollOver,
hidKeyBufferSize,
KeysDownState,
useHidStore,
useRTCStore,
} from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidRpc } from "@/hooks/useHidRpc"; import { useHidRpc } from "@/hooks/useHidRpc";
import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc"; import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc";
@ -20,9 +26,9 @@ export default function useKeyboard() {
// is running on the cloud against a device that has not been updated yet and thus does not // is running on the cloud against a device that has not been updated yet and thus does not
// support the keyPressReport API. In that case, we need to handle the key presses locally // support the keyPressReport API. In that case, we need to handle the key presses locally
// and send the full state to the device, so it can behave like a real USB HID keyboard. // and send the full state to the device, so it can behave like a real USB HID keyboard.
// This flag indicates whether the keyPressReport API is available on the device which is // This flag indicates whether the keyPressReport API is available on the device which is
// dynamically set when the device responds to the first key press event or reports its // dynamically set when the device responds to the first key press event or reports its
// keysDownState when queried since the keyPressReport was introduced together with the // keysDownState when queried since the keyPressReport was introduced together with the
// getKeysDownState API. // getKeysDownState API.
// HidRPC is a binary format for exchanging keyboard and mouse events // HidRPC is a binary format for exchanging keyboard and mouse events
@ -31,7 +37,7 @@ export default function useKeyboard() {
reportKeypressEvent: sendKeypressEventHidRpc, reportKeypressEvent: sendKeypressEventHidRpc,
reportKeypressKeepAlive: sendKeypressKeepAliveHidRpc, reportKeypressKeepAlive: sendKeypressKeepAliveHidRpc,
rpcHidReady, rpcHidReady,
} = useHidRpc((message) => { } = useHidRpc(message => {
switch (message.constructor) { switch (message.constructor) {
case KeysDownStateMessage: case KeysDownStateMessage:
setKeysDownState((message as KeysDownStateMessage).keysDownState); setKeysDownState((message as KeysDownStateMessage).keysDownState);
@ -52,7 +58,9 @@ export default function useKeyboard() {
async (state: KeysDownState) => { async (state: KeysDownState) => {
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return; if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`); console.debug(
`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`,
);
if (rpcHidReady) { if (rpcHidReady) {
console.debug("Sending keyboard report via HidRPC"); console.debug("Sending keyboard report via HidRPC");
@ -60,31 +68,33 @@ export default function useKeyboard() {
return; return;
} }
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => { send(
if ("error" in resp) { "keyboardReport",
console.error(`Failed to send keyboard report ${state}`, resp.error); { keys: state.keys, modifier: state.modifier },
} (resp: JsonRpcResponse) => {
}); if ("error" in resp) {
console.error(`Failed to send keyboard report ${state}`, resp.error);
}
},
);
}, },
[ [rpcDataChannel?.readyState, rpcHidReady, send, sendKeyboardEventHidRpc],
rpcDataChannel?.readyState,
rpcHidReady,
send,
sendKeyboardEventHidRpc,
],
); );
// executeMacro is used to execute a macro consisting of multiple steps. // executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay. // Each step can have multiple keys, multiple modifiers and a delay.
// The keys and modifiers are pressed together and held for the delay duration. // The keys and modifiers are pressed together and held for the delay duration.
// After the delay, the keys and modifiers are released and the next step is executed. // After the delay, the keys and modifiers are released and the next step is executed.
// If a step has no keys or modifiers, it is treated as a delay-only step. // If a step has no keys or modifiers, it is treated as a delay-only step.
// A small pause is added between steps to ensure that the device can process the events. // A small pause is added between steps to ensure that the device can process the events.
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => { const executeMacro = async (
steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[],
) => {
for (const [index, step] of steps.entries()) { for (const [index, step] of steps.entries()) {
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
const modifierMask: number = (step.modifiers || []).map(mod => modifiers[mod]).reduce((acc, val) => acc + val, 0); const modifierMask: number = (step.modifiers || [])
.map(mod => modifiers[mod])
.reduce((acc, val) => acc + val, 0);
// If the step has keys and/or modifiers, press them and hold for the delay // If the step has keys and/or modifiers, press them and hold for the delay
if (keyValues.length > 0 || modifierMask > 0) { if (keyValues.length > 0 || modifierMask > 0) {
@ -119,6 +129,8 @@ export default function useKeyboard() {
clearInterval(keepAliveTimerRef.current); clearInterval(keepAliveTimerRef.current);
} }
sendKeypressKeepAliveHidRpc();
// Create new interval timer // Create new interval timer
keepAliveTimerRef.current = setInterval(() => { keepAliveTimerRef.current = setInterval(() => {
sendKeypressKeepAliveHidRpc(); sendKeypressKeepAliveHidRpc();
@ -174,7 +186,11 @@ export default function useKeyboard() {
sendKeypress(key, press); sendKeypress(key, press);
} else { } else {
// if the keyPress api is not available, we need to handle the key locally // if the keyPress api is not available, we need to handle the key locally
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(keysDownState, key, press); const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
keysDownState,
key,
press,
);
sendKeyboardEvent(downState); // then we send the full state sendKeyboardEvent(downState); // then we send the full state
// if we just sent ErrorRollOver, reset to empty state // if we just sent ErrorRollOver, reset to empty state
@ -194,7 +210,11 @@ export default function useKeyboard() {
); );
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists // IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
function simulateDeviceSideKeyHandlingForLegacyDevices(state: KeysDownState, key: number, press: boolean): KeysDownState { function simulateDeviceSideKeyHandlingForLegacyDevices(
state: KeysDownState,
key: number,
press: boolean,
): KeysDownState {
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver // IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
// for handling key presses and releases. It ensures that the USB gadget // for handling key presses and releases. It ensures that the USB gadget
// behaves similarly to a real USB HID keyboard. This logic is paralleled // behaves similarly to a real USB HID keyboard. This logic is paralleled
@ -206,7 +226,7 @@ export default function useKeyboard() {
if (modifierMask !== 0) { if (modifierMask !== 0) {
// If the key is a modifier key, we update the keyboardModifier state // If the key is a modifier key, we update the keyboardModifier state
// by setting or clearing the corresponding bit in the modifier byte. // by setting or clearing the corresponding bit in the modifier byte.
// This allows us to track the state of dynamic modifier keys like // This allows us to track the state of dynamic modifier keys like
// Shift, Control, Alt, and Super. // Shift, Control, Alt, and Super.
if (press) { if (press) {
modifiers |= modifierMask; modifiers |= modifierMask;
@ -223,7 +243,7 @@ export default function useKeyboard() {
// and if we find a zero byte, we can place the key there (if press is true) // and if we find a zero byte, we can place the key there (if press is true)
if (keys[i] === key || keys[i] === 0) { if (keys[i] === key || keys[i] === 0) {
if (press) { if (press) {
keys[i] = key // overwrites the zero byte or the same key if already pressed keys[i] = key; // overwrites the zero byte or the same key if already pressed
} else { } else {
// we are releasing the key, remove it from the buffer // we are releasing the key, remove it from the buffer
if (keys[i] !== 0) { if (keys[i] !== 0) {
@ -239,13 +259,15 @@ export default function useKeyboard() {
// If we reach here it means we didn't find an empty slot or the key in the buffer // If we reach here it means we didn't find an empty slot or the key in the buffer
if (overrun) { if (overrun) {
if (press) { if (press) {
console.warn(`keyboard buffer overflow current keys ${keys}, key: ${key} not added`); console.warn(
`keyboard buffer overflow current keys ${keys}, key: ${key} not added`,
);
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow // Fill all key slots with ErrorRollOver (0x01) to indicate overflow
keys.length = hidKeyBufferSize; keys.length = hidKeyBufferSize;
keys.fill(hidErrorRollOver); keys.fill(hidErrorRollOver);
} else { } else {
// If we are releasing a key, and we didn't find it in a slot, who cares? // If we are releasing a key, and we didn't find it in a slot, who cares?
console.debug(`key ${key} not found in buffer, nothing to release`) console.debug(`key ${key} not found in buffer, nothing to release`);
} }
} }
} }