mirror of https://github.com/jetkvm/kvm.git
Compare commits
5 Commits
fefbc7611f
...
af8fff7cee
Author | SHA1 | Date |
---|---|---|
|
af8fff7cee | |
|
7389467c2f | |
|
d61ea2195b | |
|
c459929a91 | |
|
3dd8645295 |
34
hidrpc.go
34
hidrpc.go
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
)
|
||||
|
||||
func handleHidRpcMessage(message hidrpc.Message, session *Session) {
|
||||
func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||
var rpcErr error
|
||||
|
||||
switch message.Type() {
|
||||
|
@ -22,11 +22,11 @@ func handleHidRpcMessage(message hidrpc.Message, session *Session) {
|
|||
logger.Warn().Err(err).Msg("failed to send handshake message")
|
||||
return
|
||||
}
|
||||
session.hidRpcAvailable = true
|
||||
session.hidRPCAvailable = true
|
||||
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||
keysDownState, err := handleHidRpcKeyboardInput(message)
|
||||
keysDownState, err := handleHidRPCKeyboardInput(message)
|
||||
if keysDownState != nil {
|
||||
reportHidRpcKeysDownState(*keysDownState, session)
|
||||
session.reportHidRPCKeysDownState(*keysDownState)
|
||||
}
|
||||
rpcErr = err
|
||||
case hidrpc.TypePointerReport:
|
||||
|
@ -53,7 +53,7 @@ func handleHidRpcMessage(message hidrpc.Message, session *Session) {
|
|||
}
|
||||
|
||||
func onHidMessage(data []byte, session *Session) {
|
||||
scopedLogger := hidRpcLogger.With().Bytes("data", data).Logger()
|
||||
scopedLogger := hidRPCLogger.With().Bytes("data", data).Logger()
|
||||
scopedLogger.Debug().Msg("HID RPC message received")
|
||||
|
||||
if len(data) < 1 {
|
||||
|
@ -74,7 +74,7 @@ func onHidMessage(data []byte, session *Session) {
|
|||
|
||||
r := make(chan interface{})
|
||||
go func() {
|
||||
handleHidRpcMessage(message, session)
|
||||
handleHidRPCMessage(message, session)
|
||||
r <- nil
|
||||
}()
|
||||
select {
|
||||
|
@ -85,7 +85,7 @@ func onHidMessage(data []byte, session *Session) {
|
|||
}
|
||||
}
|
||||
|
||||
func handleHidRpcKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
|
||||
func handleHidRPCKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
|
||||
switch message.Type() {
|
||||
case hidrpc.TypeKeypressReport:
|
||||
keypressReport, err := message.KeypressReport()
|
||||
|
@ -108,7 +108,7 @@ func handleHidRpcKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState
|
|||
return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
||||
}
|
||||
|
||||
func reportHidRpc(params any, session *Session) {
|
||||
func reportHidRPC(params any, session *Session) {
|
||||
var (
|
||||
message []byte
|
||||
err error
|
||||
|
@ -118,6 +118,8 @@ func reportHidRpc(params any, session *Session) {
|
|||
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
|
||||
case usbgadget.KeysDownState:
|
||||
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
||||
default:
|
||||
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -135,16 +137,16 @@ func reportHidRpc(params any, session *Session) {
|
|||
}
|
||||
}
|
||||
|
||||
func reportHidRpcKeyboardLedState(state usbgadget.KeyboardState, session *Session) {
|
||||
if !session.hidRpcAvailable {
|
||||
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||
func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
|
||||
if !s.hidRPCAvailable {
|
||||
writeJSONRPCEvent("keyboardLedState", state, s)
|
||||
}
|
||||
reportHidRpc(state, session)
|
||||
reportHidRPC(state, s)
|
||||
}
|
||||
|
||||
func reportHidRpcKeysDownState(state usbgadget.KeysDownState, session *Session) {
|
||||
if !session.hidRpcAvailable {
|
||||
writeJSONRPCEvent("keysDownState", state, currentSession)
|
||||
func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
|
||||
if !s.hidRPCAvailable {
|
||||
writeJSONRPCEvent("keysDownState", state, s)
|
||||
}
|
||||
reportHidRpc(state, session)
|
||||
reportHidRPC(state, s)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,8 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
)
|
||||
|
||||
// HID RPC is a variable-length packet format that is used to exchange keyboard and mouse events between the client and the server.
|
||||
// The packet format is as follows:
|
||||
// 1 byte: Event Type
|
||||
|
||||
// MessageType is the type of the HID RPC message
|
||||
type MessageType uint8
|
||||
type MessageType byte
|
||||
|
||||
const (
|
||||
TypeHandshake MessageType = 0x01
|
||||
|
@ -24,9 +20,22 @@ const (
|
|||
TypeKeydownState MessageType = 0x33
|
||||
)
|
||||
|
||||
// ShouldUseEnqueue returns true if the message type should be enqueued to the HID queue.
|
||||
func ShouldUseEnqueue(messageType MessageType) bool {
|
||||
return messageType == TypeMouseReport
|
||||
const (
|
||||
Version byte = 0x01 // Version of the HID RPC protocol
|
||||
)
|
||||
|
||||
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
||||
func GetQueueIndex(messageType MessageType) int {
|
||||
switch messageType {
|
||||
case TypeHandshake:
|
||||
return 0
|
||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState:
|
||||
return 1
|
||||
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the HID RPC message from the data.
|
||||
|
@ -58,7 +67,7 @@ func Marshal(message *Message) ([]byte, error) {
|
|||
func NewHandshakeMessage() *Message {
|
||||
return &Message{
|
||||
t: TypeHandshake,
|
||||
d: []byte{},
|
||||
d: []byte{Version},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,13 +24,25 @@ func (m *Message) String() string {
|
|||
case TypeHandshake:
|
||||
return "Handshake"
|
||||
case TypeKeypressReport:
|
||||
if len(m.d) < 2 {
|
||||
return fmt.Sprintf("KeypressReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeypressReport{Key: %d, Press: %v}", m.d[0], m.d[1] == uint8(1))
|
||||
case TypeKeyboardReport:
|
||||
if len(m.d) < 2 {
|
||||
return fmt.Sprintf("KeyboardReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardReport{Modifier: %d, Keys: %v}", m.d[0], m.d[1:])
|
||||
case TypePointerReport:
|
||||
if len(m.d) < 9 {
|
||||
return fmt.Sprintf("PointerReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("PointerReport{X: %d, Y: %d, Button: %d}", m.d[0:4], m.d[4:8], m.d[8])
|
||||
case TypeMouseReport:
|
||||
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0:2], m.d[2:4], m.d[4])
|
||||
if len(m.d) < 3 {
|
||||
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
||||
default:
|
||||
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ func getKeyboardState(b byte) KeyboardState {
|
|||
Compose: b&KeyboardLedMaskCompose != 0,
|
||||
Kana: b&KeyboardLedMaskKana != 0,
|
||||
Shift: b&KeyboardLedMaskShift != 0,
|
||||
raw: b,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ func writeJSONRPCEvent(event string, params any, session *Session) {
|
|||
Str("data", requestString).
|
||||
Logger()
|
||||
|
||||
scopedLogger.Info().Msg("sending JSONRPC event")
|
||||
scopedLogger.Trace().Msg("sending JSONRPC event")
|
||||
|
||||
err = session.RPCChannel.SendText(requestString)
|
||||
if err != nil {
|
||||
|
|
2
log.go
2
log.go
|
@ -19,7 +19,7 @@ var (
|
|||
nbdLogger = logging.GetSubsystemLogger("nbd")
|
||||
timesyncLogger = logging.GetSubsystemLogger("timesync")
|
||||
jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc")
|
||||
hidRpcLogger = logging.GetSubsystemLogger("hidrpc")
|
||||
hidRPCLogger = logging.GetSubsystemLogger("hidrpc")
|
||||
watchdogLogger = logging.GetSubsystemLogger("watchdog")
|
||||
websecureLogger = logging.GetSubsystemLogger("websecure")
|
||||
otaLogger = logging.GetSubsystemLogger("ota")
|
||||
|
|
|
@ -15,8 +15,8 @@ import { useHidRpc } from "@/hooks/useHidRpc";
|
|||
export default function InfoBar() {
|
||||
const { keysDownState } = useHidStore();
|
||||
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
||||
const { rpcHidReady } = useHidRpc();
|
||||
|
||||
const { rpcHidStatus } = useHidRpc();
|
||||
|
||||
const videoClientSize = useVideoStore(
|
||||
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ export default function InfoBar() {
|
|||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||
|
||||
return [...modifierNames,...keyNames].join(", ");
|
||||
return [...modifierNames, ...keyNames].join(", ");
|
||||
}, [keysDownState, showPressedKeys]);
|
||||
|
||||
return (
|
||||
|
@ -105,7 +105,7 @@ export default function InfoBar() {
|
|||
{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>
|
||||
<span className="text-xs">{rpcHidStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -0,0 +1,303 @@
|
|||
import { KeyboardLedState, KeysDownState } from "./stores";
|
||||
|
||||
export const HID_RPC_MESSAGE_TYPES = {
|
||||
Handshake: 0x01,
|
||||
KeyboardReport: 0x02,
|
||||
PointerReport: 0x03,
|
||||
WheelReport: 0x04,
|
||||
KeypressReport: 0x05,
|
||||
MouseReport: 0x06,
|
||||
KeyboardLedState: 0x32,
|
||||
KeysDownState: 0x33,
|
||||
}
|
||||
|
||||
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
||||
|
||||
export const HID_RPC_VERSION = 0x01;
|
||||
|
||||
const withinUint8Range = (value: number) => {
|
||||
return value >= 0 && value <= 255;
|
||||
};
|
||||
|
||||
const fromInt32toUint8 = (n: number) => {
|
||||
if (n !== n >> 0) {
|
||||
throw new Error(`Number ${n} is not within the int32 range`);
|
||||
}
|
||||
|
||||
return new Uint8Array([
|
||||
(n >> 24) & 0xFF,
|
||||
(n >> 16) & 0xFF,
|
||||
(n >> 8) & 0xFF,
|
||||
(n >> 0) & 0xFF,
|
||||
]);
|
||||
};
|
||||
|
||||
const fromInt8ToUint8 = (n: number) => {
|
||||
if (n < -128 || n > 127) {
|
||||
throw new Error(`Number ${n} is not within the int8 range`);
|
||||
}
|
||||
|
||||
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 class RpcMessage {
|
||||
messageType: HidRpcMessageType;
|
||||
|
||||
constructor(messageType: HidRpcMessageType) {
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
// @ts-expect-error: this is a base class, so we don't need to implement it
|
||||
public static unmarshal(data: Uint8Array): RpcMessage | undefined {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
export class HandshakeMessage extends RpcMessage {
|
||||
version: number;
|
||||
|
||||
constructor(version: number) {
|
||||
super(HID_RPC_MESSAGE_TYPES.Handshake);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([this.messageType, this.version]);
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): HandshakeMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid handshake message length: ${data.length}`);
|
||||
}
|
||||
|
||||
return new HandshakeMessage(data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeypressReportMessage extends RpcMessage {
|
||||
private _key = 0;
|
||||
private _press = false;
|
||||
|
||||
get key(): number {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
set key(value: number) {
|
||||
if (!withinUint8Range(value)) {
|
||||
throw new Error(`Key ${value} is not within the uint8 range`);
|
||||
}
|
||||
|
||||
this._key = value;
|
||||
}
|
||||
|
||||
get press(): boolean {
|
||||
return this._press;
|
||||
}
|
||||
|
||||
set press(value: boolean) {
|
||||
this._press = value;
|
||||
}
|
||||
|
||||
constructor(key: number, press: boolean) {
|
||||
super(HID_RPC_MESSAGE_TYPES.KeypressReport);
|
||||
this.key = key;
|
||||
this.press = press;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([
|
||||
this.messageType,
|
||||
this.key,
|
||||
this.press ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): KeypressReportMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid keypress report message length: ${data.length}`);
|
||||
}
|
||||
|
||||
return new KeypressReportMessage(data[0], data[1] === 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardReportMessage extends RpcMessage {
|
||||
private _keys: number[] = [];
|
||||
private _modifier = 0;
|
||||
|
||||
get keys(): number[] {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
set keys(value: number[]) {
|
||||
value.forEach((k) => {
|
||||
if (!withinUint8Range(k)) {
|
||||
throw new Error(`Key ${k} is not within the uint8 range`);
|
||||
}
|
||||
});
|
||||
|
||||
this._keys = value;
|
||||
}
|
||||
|
||||
get modifier(): number {
|
||||
return this._modifier;
|
||||
}
|
||||
|
||||
set modifier(value: number) {
|
||||
if (!withinUint8Range(value)) {
|
||||
throw new Error(`Modifier ${value} is not within the uint8 range`);
|
||||
}
|
||||
|
||||
this._modifier = value;
|
||||
}
|
||||
|
||||
constructor(keys: number[], modifier: number) {
|
||||
super(HID_RPC_MESSAGE_TYPES.KeyboardReport);
|
||||
this.keys = keys;
|
||||
this.modifier = modifier;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([
|
||||
this.messageType,
|
||||
this.modifier,
|
||||
...this.keys,
|
||||
]);
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): KeyboardReportMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid keyboard report message length: ${data.length}`);
|
||||
}
|
||||
|
||||
return new KeyboardReportMessage(Array.from(data.slice(1)), data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardLedStateMessage extends RpcMessage {
|
||||
keyboardLedState: KeyboardLedState;
|
||||
|
||||
constructor(keyboardLedState: KeyboardLedState) {
|
||||
super(HID_RPC_MESSAGE_TYPES.KeyboardLedState);
|
||||
this.keyboardLedState = keyboardLedState;
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): KeyboardLedStateMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid keyboard led state message length: ${data.length}`);
|
||||
}
|
||||
|
||||
const s = data[0];
|
||||
|
||||
const state = {
|
||||
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,
|
||||
kana: (s & keyboardLedStateMasks.kana) !== 0,
|
||||
shift: (s & keyboardLedStateMasks.shift) !== 0,
|
||||
} as KeyboardLedState;
|
||||
|
||||
return new KeyboardLedStateMessage(state);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeysDownStateMessage extends RpcMessage {
|
||||
keysDownState: KeysDownState;
|
||||
|
||||
constructor(keysDownState: KeysDownState) {
|
||||
super(HID_RPC_MESSAGE_TYPES.KeysDownState);
|
||||
this.keysDownState = keysDownState;
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): KeysDownStateMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid keys down state message length: ${data.length}`);
|
||||
}
|
||||
|
||||
return new KeysDownStateMessage({
|
||||
modifier: data[0],
|
||||
keys: Array.from(data.slice(1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PointerReportMessage extends RpcMessage {
|
||||
x: number;
|
||||
y: number;
|
||||
buttons: number;
|
||||
|
||||
constructor(x: number, y: number, buttons: number) {
|
||||
super(HID_RPC_MESSAGE_TYPES.PointerReport);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.buttons = buttons;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([
|
||||
this.messageType,
|
||||
...fromInt32toUint8(this.x),
|
||||
...fromInt32toUint8(this.y),
|
||||
this.buttons,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseReportMessage extends RpcMessage {
|
||||
dx: number;
|
||||
dy: number;
|
||||
buttons: number;
|
||||
|
||||
constructor(dx: number, dy: number, buttons: number) {
|
||||
super(HID_RPC_MESSAGE_TYPES.MouseReport);
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.buttons = buttons;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([
|
||||
this.messageType,
|
||||
fromInt8ToUint8(this.dx),
|
||||
fromInt8ToUint8(this.dy),
|
||||
this.buttons,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export const messageRegistry = {
|
||||
[HID_RPC_MESSAGE_TYPES.Handshake]: HandshakeMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeysDownState]: KeysDownStateMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeyboardLedState]: KeyboardLedStateMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeyboardReport]: KeyboardReportMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeypressReport]: KeypressReportMessage,
|
||||
}
|
||||
|
||||
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid HID RPC message length: ${data.length}`);
|
||||
}
|
||||
|
||||
const payload = data.slice(1);
|
||||
|
||||
const messageType = data[0];
|
||||
if (!(messageType in messageRegistry)) {
|
||||
throw new Error(`Unknown HID RPC message type: ${messageType}`);
|
||||
}
|
||||
|
||||
return messageRegistry[messageType].unmarshal(payload);
|
||||
};
|
|
@ -108,8 +108,8 @@ export interface RTCState {
|
|||
rpcHidProtocolVersion: number | null;
|
||||
setRpcHidProtocolVersion: (version: number) => void;
|
||||
|
||||
setRpcHidChannel: (channel: RTCDataChannel) => void;
|
||||
rpcHidChannel: RTCDataChannel | null;
|
||||
setRpcHidChannel: (channel: RTCDataChannel) => void;
|
||||
|
||||
peerConnectionState: RTCPeerConnectionState | null;
|
||||
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
||||
|
|
|
@ -1,208 +1,99 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { KeyboardLedState, KeysDownState, useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
|
||||
export const HID_RPC_MESSAGE_TYPES = {
|
||||
Handshake: 0x01,
|
||||
KeyboardReport: 0x02,
|
||||
PointerReport: 0x03,
|
||||
WheelReport: 0x04,
|
||||
KeypressReport: 0x05,
|
||||
MouseReport: 0x06,
|
||||
KeyboardLedState: 0x32,
|
||||
KeysDownState: 0x33,
|
||||
}
|
||||
import {
|
||||
HID_RPC_VERSION,
|
||||
HandshakeMessage,
|
||||
KeyboardReportMessage,
|
||||
KeypressReportMessage,
|
||||
MouseReportMessage,
|
||||
PointerReportMessage,
|
||||
RpcMessage,
|
||||
unmarshalHidRpcMessage,
|
||||
} from "./hidRpc";
|
||||
|
||||
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
||||
|
||||
const withinUint8Range = (value: number) => {
|
||||
return value >= 0 && value <= 255;
|
||||
};
|
||||
|
||||
const fromInt32toUint8 = (n: number) => {
|
||||
if (n !== n >> 0) {
|
||||
throw new Error(`Number ${n} is not within the int32 range`);
|
||||
}
|
||||
|
||||
return new Uint8Array([
|
||||
(n >> 24) & 0xFF,
|
||||
(n >> 16) & 0xFF,
|
||||
(n >> 8) & 0xFF,
|
||||
(n >> 0) & 0xFF,
|
||||
]);
|
||||
};
|
||||
|
||||
const fromInt8ToUint8 = (n: number) => {
|
||||
if (n < -128 || n > 127) {
|
||||
throw new Error(`Number ${n} is not within the int8 range`);
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
return new Uint8Array([
|
||||
HID_RPC_MESSAGE_TYPES.PointerReport,
|
||||
...fromInt32toUint8(x),
|
||||
...fromInt32toUint8(y),
|
||||
buttons,
|
||||
]);
|
||||
};
|
||||
|
||||
const toMouseReportEvent = (dx: number, dy: number, buttons: number) => {
|
||||
if (!withinUint8Range(buttons)) {
|
||||
throw new Error(`Buttons ${buttons} is not within the uint8 range`);
|
||||
}
|
||||
return new Uint8Array([
|
||||
HID_RPC_MESSAGE_TYPES.MouseReport,
|
||||
fromInt8ToUint8(dx),
|
||||
fromInt8ToUint8(dy),
|
||||
buttons,
|
||||
]);
|
||||
};
|
||||
|
||||
const toKeyboardReportEvent = (keys: number[], modifier: number) => {
|
||||
if (!withinUint8Range(modifier)) {
|
||||
throw new Error(`Modifier ${modifier} is not within the uint8 range`);
|
||||
}
|
||||
|
||||
keys.forEach((k) => {
|
||||
if (!withinUint8Range(k)) {
|
||||
throw new Error(`Key ${k} is not within the uint8 range`);
|
||||
}
|
||||
});
|
||||
|
||||
return new Uint8Array([
|
||||
HID_RPC_MESSAGE_TYPES.KeyboardReport,
|
||||
modifier,
|
||||
...keys,
|
||||
]);
|
||||
};
|
||||
|
||||
const toKeypressReportEvent = (key: number, press: boolean) => {
|
||||
if (!withinUint8Range(key)) {
|
||||
throw new Error(`Key ${key} is not within the uint8 range`);
|
||||
}
|
||||
|
||||
return new Uint8Array([
|
||||
HID_RPC_MESSAGE_TYPES.KeypressReport,
|
||||
key,
|
||||
press ? 1 : 0,
|
||||
]);
|
||||
};
|
||||
|
||||
const toHandshakeMessage = () => {
|
||||
return new Uint8Array([HID_RPC_MESSAGE_TYPES.Handshake]);
|
||||
};
|
||||
|
||||
export interface HidRpcMessage {
|
||||
type: HidRpcMessageType;
|
||||
keysDownState?: KeysDownState;
|
||||
}
|
||||
|
||||
const unmarshalHidRpcMessage = (data: Uint8Array): HidRpcMessage | undefined => {
|
||||
if (data.length < 1) {
|
||||
throw new Error(`Invalid HID RPC message length: ${data.length}`);
|
||||
}
|
||||
|
||||
const payload = data.slice(1);
|
||||
|
||||
switch (data[0]) {
|
||||
case HID_RPC_MESSAGE_TYPES.Handshake:
|
||||
return {
|
||||
type: HID_RPC_MESSAGE_TYPES.Handshake,
|
||||
};
|
||||
case HID_RPC_MESSAGE_TYPES.KeysDownState:
|
||||
return {
|
||||
type: HID_RPC_MESSAGE_TYPES.KeysDownState,
|
||||
keysDownState: {
|
||||
modifier: payload[0],
|
||||
keys: Array.from(payload.slice(1))
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown HID RPC message type: ${data[0]}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
||||
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||
const { rpcHidChannel, setRpcHidProtocolVersion, rpcHidProtocolVersion } = useRTCStore();
|
||||
const rpcHidReady = useMemo(() => {
|
||||
return rpcHidChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
||||
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||
|
||||
const rpcHidStatus = useMemo(() => {
|
||||
if (!rpcHidChannel) return "N/A";
|
||||
if (rpcHidChannel.readyState !== "open") return rpcHidChannel.readyState;
|
||||
if (!rpcHidProtocolVersion) return "handshaking";
|
||||
return `ready (v${rpcHidProtocolVersion})`;
|
||||
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||
|
||||
const sendMessage = useCallback((message: RpcMessage, ignoreHandshakeState = false) => {
|
||||
if (rpcHidChannel?.readyState !== "open") return;
|
||||
if (!rpcHidReady && !ignoreHandshakeState) return;
|
||||
|
||||
let data: Uint8Array | undefined;
|
||||
try {
|
||||
data = message.marshal();
|
||||
} catch (e) {
|
||||
console.error("Failed to send HID RPC message", e);
|
||||
}
|
||||
if (!data) return;
|
||||
|
||||
rpcHidChannel?.send(data as unknown as ArrayBuffer);
|
||||
}, [rpcHidChannel, rpcHidReady]);
|
||||
|
||||
const reportKeyboardEvent = useCallback(
|
||||
(keys: number[], modifier: number) => {
|
||||
if (!rpcHidReady) return;
|
||||
rpcHidChannel?.send(toKeyboardReportEvent(keys, modifier));
|
||||
},
|
||||
[rpcHidChannel, rpcHidReady],
|
||||
sendMessage(new KeyboardReportMessage(keys, modifier));
|
||||
}, [sendMessage],
|
||||
);
|
||||
|
||||
const reportKeypressEvent = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (!rpcHidReady) return;
|
||||
rpcHidChannel?.send(toKeypressReportEvent(key, press));
|
||||
sendMessage(new KeypressReportMessage(key, press));
|
||||
},
|
||||
[rpcHidChannel, rpcHidReady],
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
const reportAbsMouseEvent = useCallback(
|
||||
(x: number, y: number, buttons: number) => {
|
||||
if (!rpcHidReady) return;
|
||||
rpcHidChannel?.send(toPointerReportEvent(x, y, buttons));
|
||||
sendMessage(new PointerReportMessage(x, y, buttons));
|
||||
},
|
||||
[rpcHidChannel, rpcHidReady],
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
const reportRelMouseEvent = useCallback(
|
||||
(dx: number, dy: number, buttons: number) => {
|
||||
if (!rpcHidReady) return;
|
||||
rpcHidChannel?.send(toMouseReportEvent(dx, dy, buttons));
|
||||
sendMessage(new MouseReportMessage(dx, dy, buttons));
|
||||
},
|
||||
[rpcHidChannel, rpcHidReady],
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
const doHandshake = useCallback(() => {
|
||||
const sendHandshake = useCallback(() => {
|
||||
if (rpcHidProtocolVersion) return;
|
||||
if (!rpcHidChannel) return;
|
||||
|
||||
rpcHidChannel?.send(toHandshakeMessage());
|
||||
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||
sendMessage(new HandshakeMessage(HID_RPC_VERSION), true);
|
||||
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage]);
|
||||
|
||||
const handleHandshake = useCallback((message: HandshakeMessage) => {
|
||||
if (!message.version) {
|
||||
console.error("Received handshake message without version", message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.version < HID_RPC_VERSION) {
|
||||
console.error("Server is using an older HID RPC version than the client", message);
|
||||
return;
|
||||
}
|
||||
|
||||
setRpcHidProtocolVersion(message.version);
|
||||
}, [setRpcHidProtocolVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcHidChannel) return;
|
||||
|
||||
// send handshake message
|
||||
doHandshake();
|
||||
sendHandshake();
|
||||
|
||||
const messageHandler = (e: MessageEvent) => {
|
||||
if (typeof e.data === "string") {
|
||||
|
@ -210,16 +101,20 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
|||
return;
|
||||
}
|
||||
|
||||
console.debug("Received HID RPC message", e.data);
|
||||
|
||||
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
||||
if (!message) {
|
||||
console.warn("Received invalid HID RPC message", e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === HID_RPC_MESSAGE_TYPES.Handshake) {
|
||||
setRpcHidProtocolVersion(1);
|
||||
console.debug("Received HID RPC message", message);
|
||||
switch (message.constructor) {
|
||||
case HandshakeMessage:
|
||||
handleHandshake(message as HandshakeMessage);
|
||||
break;
|
||||
default:
|
||||
// not all events are handled here, the rest are handled by the onHidRpcMessage callback
|
||||
break;
|
||||
}
|
||||
|
||||
onHidRpcMessage?.(message);
|
||||
|
@ -235,8 +130,8 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
|||
rpcHidChannel,
|
||||
onHidRpcMessage,
|
||||
setRpcHidProtocolVersion,
|
||||
doHandshake,
|
||||
rpcHidReady,
|
||||
sendHandshake,
|
||||
handleHandshake,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -247,5 +142,6 @@ export function useHidRpc(onHidRpcMessage?: (payload: HidRpcMessage) => void) {
|
|||
reportRelMouseEvent,
|
||||
rpcHidProtocolVersion,
|
||||
rpcHidReady,
|
||||
rpcHidStatus,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import { useCallback } from "react";
|
|||
|
||||
import { hidErrorRollOver, hidKeyBufferSize, KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { HID_RPC_MESSAGE_TYPES, useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc";
|
||||
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export default function useKeyboard() {
|
||||
const { send } = useJsonRpc();
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { keysDownState, setKeysDownState } = useHidStore();
|
||||
const { keysDownState, setKeysDownState, setKeyboardLedState } = useHidStore();
|
||||
|
||||
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
||||
// being tracked on the browser/client-side. When adding the keyPressReport API to the
|
||||
|
@ -24,13 +25,16 @@ export default function useKeyboard() {
|
|||
|
||||
// HidRPC is a binary format for exchanging keyboard and mouse events
|
||||
const { reportKeyboardEvent, reportKeypressEvent, rpcHidReady } = useHidRpc((message) => {
|
||||
if (message.type === HID_RPC_MESSAGE_TYPES.KeysDownState) {
|
||||
if (!message.keysDownState) {
|
||||
return;
|
||||
}
|
||||
|
||||
setKeysDownState(message.keysDownState);
|
||||
setkeyPressReportApiAvailable(true);
|
||||
switch (message.constructor) {
|
||||
case KeysDownStateMessage:
|
||||
setKeysDownState((message as KeysDownStateMessage).keysDownState);
|
||||
setkeyPressReportApiAvailable(true);
|
||||
break;
|
||||
case KeyboardLedStateMessage:
|
||||
setKeyboardLedState((message as KeyboardLedStateMessage).keyboardLedState);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -95,7 +99,7 @@ export default function useKeyboard() {
|
|||
if (rpcHidReady) {
|
||||
console.debug("Sending keypress event via HidRPC");
|
||||
reportKeypressEvent(key, press);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
send("keypressReport", { key, press }, (resp: JsonRpcResponse) => {
|
||||
|
|
4
usb.go
4
usb.go
|
@ -27,13 +27,13 @@ func initUsbGadget() {
|
|||
|
||||
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||
if currentSession != nil {
|
||||
reportHidRpcKeyboardLedState(state, currentSession)
|
||||
currentSession.reportHidRPCKeyboardLedState(state)
|
||||
}
|
||||
})
|
||||
|
||||
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
||||
if currentSession != nil {
|
||||
reportHidRpcKeysDownState(state, currentSession)
|
||||
currentSession.reportHidRPCKeysDownState(state)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
69
webrtc.go
69
webrtc.go
|
@ -6,10 +6,12 @@ import (
|
|||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
|
@ -23,9 +25,11 @@ type Session struct {
|
|||
HidChannel *webrtc.DataChannel
|
||||
shouldUmountVirtualMedia bool
|
||||
|
||||
hidRpcAvailable bool
|
||||
hidQueue chan webrtc.DataChannelMessage
|
||||
rpcQueue chan webrtc.DataChannelMessage
|
||||
rpcQueue chan webrtc.DataChannelMessage
|
||||
|
||||
hidRPCAvailable bool
|
||||
hidQueueLock sync.Mutex
|
||||
hidQueue []chan webrtc.DataChannelMessage
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
|
@ -70,6 +74,23 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
|||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||
}
|
||||
|
||||
func (s *Session) initQueues() {
|
||||
s.hidQueueLock.Lock()
|
||||
defer s.hidQueueLock.Unlock()
|
||||
|
||||
s.hidQueue = make([]chan webrtc.DataChannelMessage, 0)
|
||||
for i := 0; i < 4; i++ {
|
||||
q := make(chan webrtc.DataChannelMessage, 256)
|
||||
s.hidQueue = append(s.hidQueue, q)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) handleQueues(index int) {
|
||||
for msg := range s.hidQueue[index] {
|
||||
onHidMessage(msg.Data, s)
|
||||
}
|
||||
}
|
||||
|
||||
func newSession(config SessionConfig) (*Session, error) {
|
||||
webrtcSettingEngine := webrtc.SettingEngine{
|
||||
LoggerFactory: logging.GetPionDefaultLoggerFactory(),
|
||||
|
@ -111,7 +132,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
|
||||
session := &Session{peerConnection: peerConnection}
|
||||
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
||||
session.hidQueue = make(chan webrtc.DataChannelMessage, 1024)
|
||||
session.initQueues()
|
||||
|
||||
go func() {
|
||||
for msg := range session.rpcQueue {
|
||||
|
@ -120,38 +141,51 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for msg := range session.hidQueue {
|
||||
onHidMessage(msg.Data, session)
|
||||
}
|
||||
}()
|
||||
for i := 0; i < len(session.hidQueue); i++ {
|
||||
go session.handleQueues(i)
|
||||
}
|
||||
|
||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
scopedLogger.Warn().Interface("error", r).Msg("Recovered from panic in DataChannel handler")
|
||||
scopedLogger.Error().Interface("error", r).Msg("Recovered from panic in DataChannel handler")
|
||||
}
|
||||
}()
|
||||
|
||||
scopedLogger.Info().Str("label", d.Label()).Uint16("id", *d.ID()).Msg("New DataChannel")
|
||||
|
||||
switch d.Label() {
|
||||
case "hidrpc":
|
||||
session.HidChannel = d
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
l := scopedLogger.With().Str("data", string(msg.Data)).Int("length", len(msg.Data)).Logger()
|
||||
|
||||
if msg.IsString {
|
||||
scopedLogger.Warn().Str("data", string(msg.Data)).Msg("received string data in HID RPC message handler")
|
||||
l.Warn().Msg("received string data in HID RPC message handler")
|
||||
return
|
||||
}
|
||||
|
||||
if len(msg.Data) < 1 {
|
||||
scopedLogger.Warn().Int("length", len(msg.Data)).Msg("received empty data in HID RPC message handler")
|
||||
l.Warn().Msg("received empty data in HID RPC message handler")
|
||||
return
|
||||
}
|
||||
|
||||
scopedLogger.Debug().Str("data", string(msg.Data)).Msg("received data in HID RPC message handler")
|
||||
l.Trace().Msg("received data in HID RPC message handler")
|
||||
|
||||
// Enqueue to ensure ordered processing
|
||||
session.hidQueue <- msg
|
||||
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
|
||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
|
||||
queueIndex = 3
|
||||
}
|
||||
|
||||
queue := session.hidQueue[queueIndex]
|
||||
if queue != nil {
|
||||
queue <- msg
|
||||
} else {
|
||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
|
||||
return
|
||||
}
|
||||
})
|
||||
case "rpc":
|
||||
session.RPCChannel = d
|
||||
|
@ -235,6 +269,13 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
close(session.rpcQueue)
|
||||
session.rpcQueue = nil
|
||||
}
|
||||
|
||||
// Stop HID RPC processor
|
||||
for i := 0; i < len(session.hidQueue); i++ {
|
||||
close(session.hidQueue[i])
|
||||
session.hidQueue[i] = nil
|
||||
}
|
||||
|
||||
if session.shouldUmountVirtualMedia {
|
||||
if err := rpcUnmountImage(); err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
|
||||
|
|
Loading…
Reference in New Issue