feat(hid): remove HID-RPC related code and improve keyboard handling logic

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
luckfox-eng29
2026-05-15 18:43:04 +08:00
parent 18f7d8425f
commit b1090c9493
13 changed files with 170 additions and 519 deletions

View File

@@ -270,6 +270,7 @@ function KeyboardWrapper() {
setIsCapsLockActive(false);
}
sendKeyboardEvent([keys["CapsLock"]], []);
setTimeout(resetKeyboardState, 100);
return;
}
}

View File

@@ -1,70 +0,0 @@
export const MessageType = {
Handshake: 0x01,
KeyboardReport: 0x02,
PointerReport: 0x03,
WheelReport: 0x04,
KeypressReport: 0x05,
MouseReport: 0x06,
KeyboardMacroReport: 0x07,
CancelKeyboardMacro: 0x08,
KeypressKeepAlive: 0x09,
KeyboardLedState: 0x32,
KeysDownState: 0x33,
KeyboardMacroState: 0x34,
} as const;
export function marshalKeypressReport(key: number, press: boolean): Uint8Array {
return new Uint8Array([MessageType.KeypressReport, key, press ? 1 : 0]);
}
export function marshalKeyboardReport(modifier: number, keys: number[]): Uint8Array {
const data = new Uint8Array(8);
data[0] = MessageType.KeyboardReport;
data[1] = modifier;
for (let i = 0; i < Math.min(keys.length, 6); i++) {
data[2 + i] = keys[i];
}
return data;
}
export function marshalKeypressKeepAlive(): Uint8Array {
return new Uint8Array([MessageType.KeypressKeepAlive]);
}
export function marshalHandshake(version: number): Uint8Array {
return new Uint8Array([MessageType.Handshake, version]);
}
export interface HidRpcMessage {
type: number;
data: Uint8Array;
}
export function unmarshalMessage(data: ArrayBuffer): HidRpcMessage {
const view = new Uint8Array(data);
return {
type: view[0],
data: view.slice(1),
};
}
export interface KeyboardLedState {
num_lock: boolean;
caps_lock: boolean;
scroll_lock: boolean;
compose: boolean;
kana: boolean;
shift: boolean;
}
export function parseKeyboardLedState(data: Uint8Array): KeyboardLedState {
const raw = data[0] || 0;
return {
num_lock: !!(raw & (1 << 0)),
caps_lock: !!(raw & (1 << 1)),
scroll_lock: !!(raw & (1 << 2)),
compose: !!(raw & (1 << 3)),
kana: !!(raw & (1 << 4)),
shift: !!(raw & (1 << 6)),
};
}

View File

@@ -1,109 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
import { useRTCStore, useHidStore } from "./stores";
import {
marshalKeypressReport,
marshalKeyboardReport,
marshalKeypressKeepAlive,
marshalHandshake,
unmarshalMessage,
MessageType,
parseKeyboardLedState,
} from "./hidRpc";
export function useHidRpc() {
const hidChannel = useRTCStore(state => state.hidChannel);
const setRpcHidReady = useHidStore(state => state.setRpcHidReady);
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
const setKeysDownState = useHidStore(state => state.setKeysDownState);
const rpcHidReadyRef = useRef(false);
// Send keypress event
const reportKeypressEvent = useCallback(
(key: number, press: boolean) => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeypressReport(key, press);
hidChannel.send(data);
return true;
},
[hidChannel]
);
// Send keyboard report (for legacy compatibility)
const reportKeyboardEvent = useCallback(
(modifier: number, keys: number[]) => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeyboardReport(modifier, keys);
hidChannel.send(data);
return true;
},
[hidChannel]
);
// Send keepalive
const reportKeypressKeepAlive = useCallback(() => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeypressKeepAlive();
hidChannel.send(data);
return true;
}, [hidChannel]);
// Handle incoming HID-RPC messages
useEffect(() => {
if (!hidChannel) return;
const messageHandler = (event: MessageEvent) => {
const msg = unmarshalMessage(event.data);
switch (msg.type) {
case MessageType.Handshake:
if (msg.data[0] === 1) {
rpcHidReadyRef.current = true;
setRpcHidReady(true);
}
break;
case MessageType.KeyboardLedState:
setKeyboardLedState(parseKeyboardLedState(msg.data));
break;
case MessageType.KeysDownState:
// Parse modifier + 6 keys
if (msg.data.length >= 7) {
setKeysDownState({
modifier: msg.data[0],
keys: Array.from(msg.data.slice(1, 7)),
});
}
break;
}
};
hidChannel.addEventListener("message", messageHandler);
// Send handshake
if (hidChannel.readyState === "open") {
hidChannel.send(marshalHandshake(1));
} else {
hidChannel.addEventListener("open", () => {
hidChannel.send(marshalHandshake(1));
}, { once: true });
}
return () => {
hidChannel.removeEventListener("message", messageHandler);
rpcHidReadyRef.current = false;
setRpcHidReady(false);
};
}, [hidChannel, setRpcHidReady, setKeyboardLedState, setKeysDownState]);
return {
rpcHidReady: rpcHidReadyRef.current,
reportKeypressEvent,
reportKeyboardEvent,
reportKeypressKeepAlive,
};
}

View File

@@ -3,12 +3,10 @@ import { useCallback, useEffect, useRef } from "react";
import notifications from "@/notifications";
import { useHidStore, useRTCStore, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidRpc } from "@/hooks/useHidRpc";
import { keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() {
const [send] = useJsonRpc();
const { rpcHidReady, reportKeypressEvent, reportKeyboardEvent, reportKeypressKeepAlive } = useHidRpc();
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const forceHttp = useSettingsStore(state => state.forceHttp);
@@ -30,80 +28,45 @@ export default function useKeyboard() {
if (usbState !== "configured") return;
const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
// Try HID-RPC first
if (rpcHidReady && !forceHttp) {
reportKeyboardEvent(accModifier, keys);
} else {
// Fallback to JSON-RPC
send("keyboardReport", { keys, modifier: accModifier }, resp => {
if ("error" in resp) {
const msg = (resp.error.data as string) || resp.error.message || "";
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
}
// Fallback to JSON-RPC
send("keyboardReport", { keys, modifier: accModifier }, resp => {
if ("error" in resp) {
const msg = (resp.error.data as string) || resp.error.message || "";
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
}
});
}
}
});
// We do this for the info bar to display the currently pressed keys for the user
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers });
},
[forceHttp, rpcDataChannel?.readyState, rpcHidReady, reportKeyboardEvent, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
[forceHttp, rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
);
// Send per-key press/release (new HID-RPC method)
// Send per-key press/release
const sendKeypress = useCallback(
(key: number, press: boolean) => {
if (isReinitializingGadget || usbState !== "configured") return;
if (rpcHidReady && !forceHttp) {
reportKeypressEvent(key, press);
// Track held keys for keepalive
if (press) {
heldKeysRef.current.add(key);
// Start keepalive interval if not already running
if (!keepaliveIntervalRef.current) {
keepaliveIntervalRef.current = setInterval(() => {
if (heldKeysRef.current.size > 0) {
reportKeypressKeepAlive();
}
}, 50);
}
} else {
heldKeysRef.current.delete(key);
if (heldKeysRef.current.size === 0 && keepaliveIntervalRef.current) {
clearInterval(keepaliveIntervalRef.current);
keepaliveIntervalRef.current = null;
}
}
} else {
// Legacy: simulate device-side key handling
// This maintains the 6-key buffer on the frontend for legacy compatibility
// ... (existing logic would go here, but for now use sendKeyboardEvent)
// For simplicity in migration, we fall back to full state reports
const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
sendKeyboardEvent(press ? [key] : [], [modifier]);
}
// Legacy: simulate device-side key handling
// This maintains the 6-key buffer on the frontend for legacy compatibility
// For simplicity in migration, we fall back to full state reports
const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
sendKeyboardEvent(press ? [key] : [], [modifier]);
},
[rpcHidReady, forceHttp, reportKeypressEvent, reportKeypressKeepAlive, isReinitializingGadget, usbState, sendKeyboardEvent]
[isReinitializingGadget, usbState, sendKeyboardEvent]
);
const resetKeyboardState = useCallback(() => {
// Release all held keys
if (rpcHidReady && !forceHttp) {
heldKeysRef.current.forEach(key => {
reportKeypressEvent(key, false);
});
} else {
sendKeyboardEvent([], []);
}
sendKeyboardEvent([], []);
heldKeysRef.current.clear();
if (keepaliveIntervalRef.current) {
clearInterval(keepaliveIntervalRef.current);
keepaliveIntervalRef.current = null;
}
}, [rpcHidReady, forceHttp, reportKeypressEvent, sendKeyboardEvent]);
}, [sendKeyboardEvent]);
// Cleanup on unmount
useEffect(() => {

View File

@@ -10,7 +10,7 @@ export const useKeyboardEvents = (
pasteCaptureRef?: React.RefObject<HTMLTextAreaElement>,
isReinitializingGadget?: boolean
) => {
const { sendKeyboardEvent, sendKeypress, resetKeyboardState } = useKeyboard();
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore();
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
@@ -65,6 +65,11 @@ export const useKeyboardEvents = (
}
if (isReinitializingGadget) return;
if (e.repeat) {
e.preventDefault();
return;
}
e.preventDefault();
const prev = useHidStore.getState();
let code = e.code;
@@ -94,15 +99,8 @@ export const useKeyboardEvents = (
}, 10);
}
// Send per-key press event
const hidKey = keys[code];
if (hidKey !== undefined) {
sendKeypress(hidKey, true);
}
// Still update the full state for legacy compatibility and UI display
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, [handleModifierKeys, remapCode, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, pasteShortcutEnabled, pasteShortcut, pasteCaptureRef, isReinitializingGadget, isOcrMode]);
}, [handleModifierKeys, remapCode, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, pasteShortcutEnabled, pasteShortcut, pasteCaptureRef, isReinitializingGadget, isOcrMode]);
const keyUpHandler = useCallback((e: KeyboardEvent) => {
if (isOcrMode) return;
@@ -124,15 +122,8 @@ export const useKeyboardEvents = (
prev.activeModifiers.filter(k => k !== modifiers[code]),
);
// Send per-key release event
const hidKey = keys[code];
if (hidKey !== undefined) {
sendKeypress(hidKey, false);
}
// Still update the full state for legacy compatibility and UI display
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, [handleModifierKeys, remapCode, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, isOcrMode, isReinitializingGadget]);
}, [handleModifierKeys, remapCode, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, isOcrMode, isReinitializingGadget]);
const setupKeyboardEvents = useCallback(() => {
const abortController = new AbortController();

View File

@@ -34,7 +34,9 @@ export const useMouseEvents = (
if (!force && settings.mouseMode !== "relative") return;
// Don't send mouse events while reinitializing gadget
if (isReinitializingGadget) return;
send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
const dx = calcDelta(x);
const dy = calcDelta(y);
send("relMouseReport", { dx, dy, buttons });
setMouseMove({ x, y, buttons });
},
[send, setMouseMove, settings.mouseMode, isReinitializingGadget],

View File

@@ -462,11 +462,6 @@ export default function PCHome() {
setDiskChannel(diskDataChannel);
};
const hidDataChannel = pc.createDataChannel("hid");
hidDataChannel.onopen = () => {
useRTCStore.getState().setHidChannel(hidDataChannel);
};
setPeerConnection(pc);
}, [
forceHttp,