mirror of https://github.com/jetkvm/kvm.git
chore: simplify handshake of hid rpc
This commit is contained in:
parent
58b72add90
commit
eacc2a6621
|
@ -10,11 +10,13 @@ import {
|
||||||
VideoState
|
VideoState
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||||
|
|
||||||
export default function InfoBar() {
|
export default function InfoBar() {
|
||||||
const { keysDownState } = useHidStore();
|
const { keysDownState } = useHidStore();
|
||||||
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
||||||
|
const { rpcHidReady } = useHidRpc();
|
||||||
|
|
||||||
const videoClientSize = useVideoStore(
|
const videoClientSize = useVideoStore(
|
||||||
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
(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>
|
<span className="text-xs">{hdmiState}</span>
|
||||||
</div>
|
</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 && (
|
{showPressedKeys && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default function WebRTCVideo() {
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { reportAbsMouseEvent, reportRelMouseEvent, handshakeCompleted } = useHidRpc();
|
const { reportAbsMouseEvent, reportRelMouseEvent, rpcHidReady } = useHidRpc();
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
const handleResize = useCallback(
|
const handleResize = useCallback(
|
||||||
|
@ -226,7 +226,7 @@ export default function WebRTCVideo() {
|
||||||
// if (x === 0 && y === 0 && buttons === 0) return;
|
// if (x === 0 && y === 0 && buttons === 0) return;
|
||||||
const dx = calcDelta(x);
|
const dx = calcDelta(x);
|
||||||
const dy = calcDelta(y);
|
const dy = calcDelta(y);
|
||||||
if (handshakeCompleted) {
|
if (rpcHidReady) {
|
||||||
reportRelMouseEvent(dx, dy, buttons);
|
reportRelMouseEvent(dx, dy, buttons);
|
||||||
} else {
|
} else {
|
||||||
send("relMouseReport", { dx, dy, buttons });
|
send("relMouseReport", { dx, dy, buttons });
|
||||||
|
@ -238,7 +238,7 @@ export default function WebRTCVideo() {
|
||||||
reportRelMouseEvent,
|
reportRelMouseEvent,
|
||||||
setMouseMove,
|
setMouseMove,
|
||||||
settings.mouseMode,
|
settings.mouseMode,
|
||||||
handshakeCompleted,
|
rpcHidReady,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ export default function WebRTCVideo() {
|
||||||
const sendAbsMouseMovement = useCallback(
|
const sendAbsMouseMovement = useCallback(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number) => {
|
||||||
if (settings.mouseMode !== "absolute") return;
|
if (settings.mouseMode !== "absolute") return;
|
||||||
if (handshakeCompleted) {
|
if (rpcHidReady) {
|
||||||
reportAbsMouseEvent(x, y, buttons);
|
reportAbsMouseEvent(x, y, buttons);
|
||||||
} else {
|
} else {
|
||||||
send("absMouseReport", { x, y, buttons });
|
send("absMouseReport", { x, y, buttons });
|
||||||
|
@ -270,7 +270,7 @@ export default function WebRTCVideo() {
|
||||||
reportAbsMouseEvent,
|
reportAbsMouseEvent,
|
||||||
setMousePosition,
|
setMousePosition,
|
||||||
settings.mouseMode,
|
settings.mouseMode,
|
||||||
handshakeCompleted,
|
rpcHidReady,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,9 @@ export interface RTCState {
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
|
rpcHidProtocolVersion: number | null;
|
||||||
|
setRpcHidProtocolVersion: (version: number) => void;
|
||||||
|
|
||||||
setRpcHidChannel: (channel: RTCDataChannel) => void;
|
setRpcHidChannel: (channel: RTCDataChannel) => void;
|
||||||
rpcHidChannel: RTCDataChannel | null;
|
rpcHidChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
|
@ -154,6 +157,9 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
rpcDataChannel: null,
|
rpcDataChannel: null,
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
||||||
|
|
||||||
|
rpcHidProtocolVersion: null,
|
||||||
|
setRpcHidProtocolVersion: (version: number) => set({ rpcHidProtocolVersion: version }),
|
||||||
|
|
||||||
rpcHidChannel: null,
|
rpcHidChannel: null,
|
||||||
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
export const HID_RPC_MESSAGE_TYPES = {
|
||||||
Handshake: 0x01,
|
Handshake: 0x01,
|
||||||
|
@ -40,6 +40,30 @@ const fromInt8ToUint8 = (n: number) => {
|
||||||
return (n >> 0) & 0xFF;
|
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) => {
|
const toPointerReportEvent = (x: number, y: number, buttons: number) => {
|
||||||
if (!withinUint8Range(buttons)) {
|
if (!withinUint8Range(buttons)) {
|
||||||
throw new Error(`Buttons ${buttons} is not within the uint8 range`);
|
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) {
|
export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
||||||
const { rpcHidChannel } = useRTCStore();
|
const { rpcHidChannel, setRpcHidProtocolVersion, rpcHidProtocolVersion } = useRTCStore();
|
||||||
const [handshakeCompleted, setHandshakeCompleted] = useState(false);
|
const rpcHidReady = useMemo(() => {
|
||||||
|
return rpcHidChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
||||||
|
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||||
|
|
||||||
const reportKeyboardEvent = useCallback(
|
const reportKeyboardEvent = useCallback(
|
||||||
(keys: number[], modifier: number) => {
|
(keys: number[], modifier: number) => {
|
||||||
if (rpcHidChannel?.readyState !== "open") return;
|
if (!rpcHidReady) return;
|
||||||
rpcHidChannel.send(toKeyboardReportEvent(keys, modifier));
|
rpcHidChannel?.send(toKeyboardReportEvent(keys, modifier));
|
||||||
},
|
},
|
||||||
[rpcHidChannel],
|
[rpcHidChannel, rpcHidReady],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reportKeypressEvent = useCallback(
|
const reportKeypressEvent = useCallback(
|
||||||
(key: number, press: boolean) => {
|
(key: number, press: boolean) => {
|
||||||
if (rpcHidChannel?.readyState !== "open") return;
|
if (!rpcHidReady) return;
|
||||||
rpcHidChannel.send(toKeypressReportEvent(key, press));
|
rpcHidChannel?.send(toKeypressReportEvent(key, press));
|
||||||
},
|
},
|
||||||
[rpcHidChannel],
|
[rpcHidChannel, rpcHidReady],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reportAbsMouseEvent = useCallback(
|
const reportAbsMouseEvent = useCallback(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number) => {
|
||||||
if (rpcHidChannel?.readyState !== "open") return;
|
if (!rpcHidReady) return;
|
||||||
rpcHidChannel.send(toPointerReportEvent(x, y, buttons));
|
rpcHidChannel?.send(toPointerReportEvent(x, y, buttons));
|
||||||
},
|
},
|
||||||
[rpcHidChannel],
|
[rpcHidChannel, rpcHidReady],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reportRelMouseEvent = useCallback(
|
const reportRelMouseEvent = useCallback(
|
||||||
(dx: number, dy: number, buttons: number) => {
|
(dx: number, dy: number, buttons: number) => {
|
||||||
if (rpcHidChannel?.readyState !== "open") return;
|
if (!rpcHidReady) return;
|
||||||
rpcHidChannel.send(toMouseReportEvent(dx, dy, buttons));
|
rpcHidChannel?.send(toMouseReportEvent(dx, dy, buttons));
|
||||||
},
|
},
|
||||||
[rpcHidChannel],
|
[rpcHidChannel, rpcHidReady],
|
||||||
);
|
);
|
||||||
|
|
||||||
const doHandshake = useCallback(() => {
|
const doHandshake = useCallback(() => {
|
||||||
if (handshakeCompleted) return;
|
if (rpcHidProtocolVersion) return;
|
||||||
if (!rpcHidChannel) return;
|
if (!rpcHidChannel) return;
|
||||||
|
|
||||||
rpcHidChannel.send(toHandshakeMessage());
|
rpcHidChannel?.send(toHandshakeMessage());
|
||||||
}, [rpcHidChannel, handshakeCompleted]);
|
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rpcHidChannel) return;
|
if (!rpcHidChannel) return;
|
||||||
|
@ -184,7 +210,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Received HID RPC message", e.data);
|
console.debug("Received HID RPC message", e.data);
|
||||||
|
|
||||||
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -193,7 +219,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === HID_RPC_MESSAGE_TYPES.Handshake) {
|
if (message.type === HID_RPC_MESSAGE_TYPES.Handshake) {
|
||||||
setHandshakeCompleted(true);
|
setRpcHidProtocolVersion(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
onHidRpcMessage?.(message);
|
onHidRpcMessage?.(message);
|
||||||
|
@ -205,13 +231,21 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
||||||
rpcHidChannel.removeEventListener("message", messageHandler);
|
rpcHidChannel.removeEventListener("message", messageHandler);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[rpcHidChannel, onHidRpcMessage, setHandshakeCompleted, doHandshake]);
|
[
|
||||||
|
rpcHidChannel,
|
||||||
|
onHidRpcMessage,
|
||||||
|
setRpcHidProtocolVersion,
|
||||||
|
doHandshake,
|
||||||
|
rpcHidReady,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reportKeyboardEvent,
|
reportKeyboardEvent,
|
||||||
reportKeypressEvent,
|
reportKeypressEvent,
|
||||||
reportAbsMouseEvent,
|
reportAbsMouseEvent,
|
||||||
reportRelMouseEvent,
|
reportRelMouseEvent,
|
||||||
handshakeCompleted,
|
rpcHidProtocolVersion,
|
||||||
|
rpcHidReady,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function useKeyboard() {
|
export default function useKeyboard() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { rpcDataChannel, rpcHidChannel } = useRTCStore();
|
const { rpcDataChannel } = useRTCStore();
|
||||||
const { keysDownState, setKeysDownState } = useHidStore();
|
const { keysDownState, setKeysDownState } = useHidStore();
|
||||||
|
|
||||||
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
// 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();
|
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable } = useHidStore();
|
||||||
|
|
||||||
// HidRPC is a binary format for exchanging keyboard and mouse events
|
// 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.type === HID_RPC_MESSAGE_TYPES.KeysDownState) {
|
||||||
if (!message.keysDownState) {
|
if (!message.keysDownState) {
|
||||||
return;
|
return;
|
||||||
|
@ -40,11 +40,11 @@ export default function useKeyboard() {
|
||||||
// or just accept the state if it does not support (returning no result)
|
// or just accept the state if it does not support (returning no result)
|
||||||
const sendKeyboardEvent = useCallback(
|
const sendKeyboardEvent = useCallback(
|
||||||
async (state: KeysDownState) => {
|
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}`);
|
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
|
||||||
|
|
||||||
if (rpcHidChannel?.readyState === "open") {
|
if (rpcHidReady) {
|
||||||
console.debug("Sending keyboard report via HidRPC");
|
console.debug("Sending keyboard report via HidRPC");
|
||||||
reportKeyboardEvent(state.keys, state.modifier);
|
reportKeyboardEvent(state.keys, state.modifier);
|
||||||
return;
|
return;
|
||||||
|
@ -72,7 +72,7 @@ export default function useKeyboard() {
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
rpcDataChannel?.readyState,
|
rpcDataChannel?.readyState,
|
||||||
rpcHidChannel?.readyState,
|
rpcHidReady,
|
||||||
send,
|
send,
|
||||||
reportKeyboardEvent,
|
reportKeyboardEvent,
|
||||||
setKeysDownState,
|
setKeysDownState,
|
||||||
|
@ -88,11 +88,11 @@ export default function useKeyboard() {
|
||||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||||
const sendKeypressEvent = useCallback(
|
const sendKeypressEvent = useCallback(
|
||||||
async (key: number, press: boolean) => {
|
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}`);
|
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
|
||||||
|
|
||||||
if (rpcHidChannel?.readyState === "open") {
|
if (rpcHidReady) {
|
||||||
console.debug("Sending keypress event via HidRPC");
|
console.debug("Sending keypress event via HidRPC");
|
||||||
reportKeypressEvent(key, press);
|
reportKeypressEvent(key, press);
|
||||||
return;
|
return;
|
||||||
|
@ -119,7 +119,7 @@ export default function useKeyboard() {
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
rpcDataChannel?.readyState,
|
rpcDataChannel?.readyState,
|
||||||
rpcHidChannel?.readyState,
|
rpcHidReady,
|
||||||
send,
|
send,
|
||||||
setkeyPressReportApiAvailable,
|
setkeyPressReportApiAvailable,
|
||||||
setKeysDownState,
|
setKeysDownState,
|
||||||
|
@ -176,10 +176,10 @@ export default function useKeyboard() {
|
||||||
// It then sends the full keyboard state to the device.
|
// It then sends the full keyboard state to the device.
|
||||||
const handleKeyPress = useCallback(
|
const handleKeyPress = useCallback(
|
||||||
async (key: number, press: boolean) => {
|
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 ((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");
|
console.debug("Sending keypress event via HidRPC");
|
||||||
reportKeypressEvent(key, press);
|
reportKeypressEvent(key, press);
|
||||||
return;
|
return;
|
||||||
|
@ -204,7 +204,7 @@ export default function useKeyboard() {
|
||||||
keysDownState,
|
keysDownState,
|
||||||
resetKeyboardState,
|
resetKeyboardState,
|
||||||
rpcDataChannel?.readyState,
|
rpcDataChannel?.readyState,
|
||||||
rpcHidChannel?.readyState,
|
rpcHidReady,
|
||||||
sendKeyboardEvent,
|
sendKeyboardEvent,
|
||||||
sendKeypressEvent,
|
sendKeypressEvent,
|
||||||
reportKeypressEvent,
|
reportKeypressEvent,
|
||||||
|
|
|
@ -28,6 +28,9 @@ export default defineConfig(({ mode, command }) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
|
esbuild: {
|
||||||
|
pure: ["console.debug"],
|
||||||
|
},
|
||||||
build: { outDir: isCloud ? "dist" : "../static" },
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
|
|
Loading…
Reference in New Issue