chore: simplify handshake of hid rpc

This commit is contained in:
Siyuan Miao 2025-08-29 19:51:59 +02:00
parent 58b72add90
commit eacc2a6621
6 changed files with 91 additions and 40 deletions

View File

@ -10,11 +10,13 @@ import {
VideoState
} from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { useHidRpc } from "@/hooks/useHidRpc";
export default function InfoBar() {
const { keysDownState } = useHidStore();
const { mouseX, mouseY, mouseMove } = useMouseStore();
const { rpcHidReady } = useHidRpc();
const videoClientSize = useVideoStore(
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
);
@ -100,6 +102,12 @@ export default function InfoBar() {
<span className="text-xs">{hdmiState}</span>
</div>
)}
{debugMode && (
<div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">HidRPC State:</span>
<span className="text-xs">{rpcHidReady ? "Ready" : "Not Ready"}</span>
</div>
)}
{showPressedKeys && (
<div className="flex items-center gap-x-1">

View File

@ -61,7 +61,7 @@ export default function WebRTCVideo() {
// Misc states and hooks
const { send } = useJsonRpc();
const { reportAbsMouseEvent, reportRelMouseEvent, handshakeCompleted } = useHidRpc();
const { reportAbsMouseEvent, reportRelMouseEvent, rpcHidReady } = useHidRpc();
// Video-related
const handleResize = useCallback(
@ -226,7 +226,7 @@ export default function WebRTCVideo() {
// if (x === 0 && y === 0 && buttons === 0) return;
const dx = calcDelta(x);
const dy = calcDelta(y);
if (handshakeCompleted) {
if (rpcHidReady) {
reportRelMouseEvent(dx, dy, buttons);
} else {
send("relMouseReport", { dx, dy, buttons });
@ -238,7 +238,7 @@ export default function WebRTCVideo() {
reportRelMouseEvent,
setMouseMove,
settings.mouseMode,
handshakeCompleted,
rpcHidReady,
],
);
@ -257,7 +257,7 @@ export default function WebRTCVideo() {
const sendAbsMouseMovement = useCallback(
(x: number, y: number, buttons: number) => {
if (settings.mouseMode !== "absolute") return;
if (handshakeCompleted) {
if (rpcHidReady) {
reportAbsMouseEvent(x, y, buttons);
} else {
send("absMouseReport", { x, y, buttons });
@ -270,7 +270,7 @@ export default function WebRTCVideo() {
reportAbsMouseEvent,
setMousePosition,
settings.mouseMode,
handshakeCompleted,
rpcHidReady,
],
);

View File

@ -105,6 +105,9 @@ export interface RTCState {
setRpcDataChannel: (channel: RTCDataChannel) => void;
rpcDataChannel: RTCDataChannel | null;
rpcHidProtocolVersion: number | null;
setRpcHidProtocolVersion: (version: number) => void;
setRpcHidChannel: (channel: RTCDataChannel) => void;
rpcHidChannel: RTCDataChannel | null;
@ -154,6 +157,9 @@ export const useRTCStore = create<RTCState>(set => ({
rpcDataChannel: null,
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
rpcHidProtocolVersion: null,
setRpcHidProtocolVersion: (version: number) => set({ rpcHidProtocolVersion: version }),
rpcHidChannel: null,
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { KeysDownState, useRTCStore } from "@/hooks/stores";
import { KeyboardLedState, KeysDownState, useRTCStore } from "@/hooks/stores";
export const HID_RPC_MESSAGE_TYPES = {
Handshake: 0x01,
@ -40,6 +40,30 @@ const fromInt8ToUint8 = (n: number) => {
return (n >> 0) & 0xFF;
};
const keyboardLedStateMasks = {
num_lock: 1 << 0,
caps_lock: 1 << 1,
scroll_lock: 1 << 2,
compose: 1 << 3,
kana: 1 << 4,
shift: 1 << 6,
}
export const toKeyboardLedState = (s: number): KeyboardLedState => {
if (!withinUint8Range(s)) {
throw new Error(`State ${s} is not within the uint8 range`);
}
return {
num_lock: (s & keyboardLedStateMasks.num_lock) !== 0,
caps_lock: (s & keyboardLedStateMasks.caps_lock) !== 0,
scroll_lock: (s & keyboardLedStateMasks.scroll_lock) !== 0,
compose: (s & keyboardLedStateMasks.compose) !== 0, // TODO: check if this is correct
kana: (s & keyboardLedStateMasks.kana) !== 0,
shift: (s & keyboardLedStateMasks.shift) !== 0,
} as KeyboardLedState;
};
const toPointerReportEvent = (x: number, y: number, buttons: number) => {
if (!withinUint8Range(buttons)) {
throw new Error(`Buttons ${buttons} is not within the uint8 range`);
@ -130,47 +154,49 @@ const unmarshalHidRpcMessage = (data: Uint8Array): HidRpcMessage | undefined =>
};
export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
const { rpcHidChannel } = useRTCStore();
const [handshakeCompleted, setHandshakeCompleted] = useState(false);
const { rpcHidChannel, setRpcHidProtocolVersion, rpcHidProtocolVersion } = useRTCStore();
const rpcHidReady = useMemo(() => {
return rpcHidChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
}, [rpcHidChannel, rpcHidProtocolVersion]);
const reportKeyboardEvent = useCallback(
(keys: number[], modifier: number) => {
if (rpcHidChannel?.readyState !== "open") return;
rpcHidChannel.send(toKeyboardReportEvent(keys, modifier));
if (!rpcHidReady) return;
rpcHidChannel?.send(toKeyboardReportEvent(keys, modifier));
},
[rpcHidChannel],
[rpcHidChannel, rpcHidReady],
);
const reportKeypressEvent = useCallback(
(key: number, press: boolean) => {
if (rpcHidChannel?.readyState !== "open") return;
rpcHidChannel.send(toKeypressReportEvent(key, press));
if (!rpcHidReady) return;
rpcHidChannel?.send(toKeypressReportEvent(key, press));
},
[rpcHidChannel],
[rpcHidChannel, rpcHidReady],
);
const reportAbsMouseEvent = useCallback(
(x: number, y: number, buttons: number) => {
if (rpcHidChannel?.readyState !== "open") return;
rpcHidChannel.send(toPointerReportEvent(x, y, buttons));
if (!rpcHidReady) return;
rpcHidChannel?.send(toPointerReportEvent(x, y, buttons));
},
[rpcHidChannel],
[rpcHidChannel, rpcHidReady],
);
const reportRelMouseEvent = useCallback(
(dx: number, dy: number, buttons: number) => {
if (rpcHidChannel?.readyState !== "open") return;
rpcHidChannel.send(toMouseReportEvent(dx, dy, buttons));
if (!rpcHidReady) return;
rpcHidChannel?.send(toMouseReportEvent(dx, dy, buttons));
},
[rpcHidChannel],
[rpcHidChannel, rpcHidReady],
);
const doHandshake = useCallback(() => {
if (handshakeCompleted) return;
if (rpcHidProtocolVersion) return;
if (!rpcHidChannel) return;
rpcHidChannel.send(toHandshakeMessage());
}, [rpcHidChannel, handshakeCompleted]);
rpcHidChannel?.send(toHandshakeMessage());
}, [rpcHidChannel, rpcHidProtocolVersion]);
useEffect(() => {
if (!rpcHidChannel) return;
@ -184,7 +210,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
return;
}
console.log("Received HID RPC message", e.data);
console.debug("Received HID RPC message", e.data);
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
if (!message) {
@ -193,7 +219,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
}
if (message.type === HID_RPC_MESSAGE_TYPES.Handshake) {
setHandshakeCompleted(true);
setRpcHidProtocolVersion(1);
}
onHidRpcMessage?.(message);
@ -205,13 +231,21 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
rpcHidChannel.removeEventListener("message", messageHandler);
};
},
[rpcHidChannel, onHidRpcMessage, setHandshakeCompleted, doHandshake]);
[
rpcHidChannel,
onHidRpcMessage,
setRpcHidProtocolVersion,
doHandshake,
rpcHidReady,
],
);
return {
reportKeyboardEvent,
reportKeypressEvent,
reportAbsMouseEvent,
reportRelMouseEvent,
handshakeCompleted,
rpcHidProtocolVersion,
rpcHidReady,
};
}

View File

@ -7,7 +7,7 @@ import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() {
const { send } = useJsonRpc();
const { rpcDataChannel, rpcHidChannel } = useRTCStore();
const { rpcDataChannel } = useRTCStore();
const { keysDownState, setKeysDownState } = useHidStore();
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
@ -23,7 +23,7 @@ export default function useKeyboard() {
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable } = useHidStore();
// HidRPC is a binary format for exchanging keyboard and mouse events
const { reportKeyboardEvent, reportKeypressEvent } = useHidRpc((message) => {
const { reportKeyboardEvent, reportKeypressEvent, rpcHidReady } = useHidRpc((message) => {
if (message.type === HID_RPC_MESSAGE_TYPES.KeysDownState) {
if (!message.keysDownState) {
return;
@ -40,11 +40,11 @@ export default function useKeyboard() {
// or just accept the state if it does not support (returning no result)
const sendKeyboardEvent = useCallback(
async (state: KeysDownState) => {
if (rpcDataChannel?.readyState !== "open" && rpcHidChannel?.readyState !== "open") return;
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
if (rpcHidChannel?.readyState === "open") {
if (rpcHidReady) {
console.debug("Sending keyboard report via HidRPC");
reportKeyboardEvent(state.keys, state.modifier);
return;
@ -72,7 +72,7 @@ export default function useKeyboard() {
},
[
rpcDataChannel?.readyState,
rpcHidChannel?.readyState,
rpcHidReady,
send,
reportKeyboardEvent,
setKeysDownState,
@ -88,11 +88,11 @@ export default function useKeyboard() {
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
const sendKeypressEvent = useCallback(
async (key: number, press: boolean) => {
if (rpcDataChannel?.readyState !== "open" && rpcHidChannel?.readyState !== "open") return;
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
if (rpcHidChannel?.readyState === "open") {
if (rpcHidReady) {
console.debug("Sending keypress event via HidRPC");
reportKeypressEvent(key, press);
return;
@ -119,7 +119,7 @@ export default function useKeyboard() {
},
[
rpcDataChannel?.readyState,
rpcHidChannel?.readyState,
rpcHidReady,
send,
setkeyPressReportApiAvailable,
setKeysDownState,
@ -176,10 +176,10 @@ export default function useKeyboard() {
// It then sends the full keyboard state to the device.
const handleKeyPress = useCallback(
async (key: number, press: boolean) => {
if (rpcDataChannel?.readyState !== "open" && rpcHidChannel?.readyState !== "open") return;
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
if (rpcHidChannel?.readyState === "open") {
if (rpcHidReady) {
console.debug("Sending keypress event via HidRPC");
reportKeypressEvent(key, press);
return;
@ -204,7 +204,7 @@ export default function useKeyboard() {
keysDownState,
resetKeyboardState,
rpcDataChannel?.readyState,
rpcHidChannel?.readyState,
rpcHidReady,
sendKeyboardEvent,
sendKeypressEvent,
reportKeypressEvent,

View File

@ -28,6 +28,9 @@ export default defineConfig(({ mode, command }) => {
return {
plugins,
esbuild: {
pure: ["console.debug"],
},
build: { outDir: isCloud ? "dist" : "../static" },
server: {
host: "0.0.0.0",