wip: send macro using hidRPC channel

This commit is contained in:
Siyuan Miao 2025-09-10 02:42:36 +02:00
parent f58e5476bf
commit 024cbb8fb1
7 changed files with 310 additions and 174 deletions

View File

@ -1,6 +1,7 @@
package kvm package kvm
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -31,6 +32,13 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
session.reportHidRPCKeysDownState(*keysDownState) session.reportHidRPCKeysDownState(*keysDownState)
} }
rpcErr = err rpcErr = err
case hidrpc.TypeKeyboardMacroReport:
keyboardMacroReport, err := message.KeyboardMacroReport()
if err != nil {
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
return
}
_, rpcErr = rpcKeyboardReportMulti(context.Background(), keyboardMacroReport.Macro)
case hidrpc.TypePointerReport: case hidrpc.TypePointerReport:
pointerReport, err := message.PointerReport() pointerReport, err := message.PointerReport()
if err != nil { if err != nil {

View File

@ -10,14 +10,17 @@ import (
type MessageType byte type MessageType byte
const ( const (
TypeHandshake MessageType = 0x01 TypeHandshake MessageType = 0x01
TypeKeyboardReport MessageType = 0x02 TypeKeyboardReport MessageType = 0x02
TypePointerReport MessageType = 0x03 TypePointerReport MessageType = 0x03
TypeWheelReport MessageType = 0x04 TypeWheelReport MessageType = 0x04
TypeKeypressReport MessageType = 0x05 TypeKeypressReport MessageType = 0x05
TypeMouseReport MessageType = 0x06 TypeMouseReport MessageType = 0x06
TypeKeyboardLedState MessageType = 0x32 TypeKeyboardMacroReport MessageType = 0x07
TypeKeydownState MessageType = 0x33 TypeCancelKeyboardMacroReport MessageType = 0x08
TypeKeyboardLedState MessageType = 0x32
TypeKeydownState MessageType = 0x33
TypeKeyboardMacroStateReport MessageType = 0x34
) )
const ( const (
@ -29,7 +32,7 @@ func GetQueueIndex(messageType MessageType) int {
switch messageType { switch messageType {
case TypeHandshake: case TypeHandshake:
return 0 return 0
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState: case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroStateReport:
return 1 return 1
case TypePointerReport, TypeMouseReport, TypeWheelReport: case TypePointerReport, TypeMouseReport, TypeWheelReport:
return 2 return 2

View File

@ -1,6 +1,7 @@
package hidrpc package hidrpc
import ( import (
"encoding/binary"
"fmt" "fmt"
) )
@ -43,6 +44,11 @@ func (m *Message) String() string {
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d) 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]) return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
case TypeKeyboardMacroReport:
if len(m.d) < 5 {
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
}
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
default: default:
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d) return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
} }
@ -84,6 +90,51 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
}, nil }, nil
} }
// Macro ..
type KeyboardMacro struct {
Modifier byte // 1 byte
Keys []byte // 6 bytes, to make things easier, the keys length is fixed to 6
Delay uint16 // 2 bytes
}
type KeyboardMacroReport struct {
IsPaste bool
Length uint32
Macro []KeyboardMacro
}
// KeyboardMacroReport returns the keyboard macro report from the message.
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
if m.t != TypeKeyboardMacroReport {
return KeyboardMacroReport{}, fmt.Errorf("invalid message type: %d", m.t)
}
isPaste := m.d[0] == uint8(1)
length := binary.BigEndian.Uint32(m.d[1:5])
// check total length
expectedLength := int(length)*9 + 5
if len(m.d) != expectedLength {
return KeyboardMacroReport{}, fmt.Errorf("invalid length: %d, expected: %d", len(m.d), expectedLength)
}
macro := make([]KeyboardMacro, 0, int(length))
for i := 0; i < int(length); i++ {
offset := 5 + i*9
macro = append(macro, KeyboardMacro{
Modifier: m.d[offset],
Keys: m.d[offset+1 : offset+7],
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
})
}
return KeyboardMacroReport{
IsPaste: isPaste,
Macro: macro,
Length: length,
}, nil
}
// PointerReport .. // PointerReport ..
type PointerReport struct { type PointerReport struct {
X int X int

View File

@ -10,13 +10,13 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.bug.st/serial" "go.bug.st/serial"
"github.com/jetkvm/kvm/internal/hidrpc"
"github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/usbgadget"
) )
@ -1050,52 +1050,56 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
return nil return nil
} }
// cancelKeyboardReportMulti cancels any ongoing keyboard report multi execution
func cancelKeyboardReportMulti() { func cancelKeyboardReportMulti() {
keyboardReportMultiLock.Lock()
defer keyboardReportMultiLock.Unlock()
if keyboardReportMultiCancel != nil {
keyboardReportMultiCancel()
logger.Info().Msg("canceled keyboard report multi")
keyboardReportMultiCancel = nil
}
} }
func setKeyboardReportMultiCancel(cancel context.CancelFunc) { // // cancelKeyboardReportMulti cancels any ongoing keyboard report multi execution
keyboardReportMultiLock.Lock() // func cancelKeyboardReportMulti() {
defer keyboardReportMultiLock.Unlock() // keyboardReportMultiLock.Lock()
// defer keyboardReportMultiLock.Unlock()
keyboardReportMultiCancel = cancel // if keyboardReportMultiCancel != nil {
} // keyboardReportMultiCancel()
// logger.Info().Msg("canceled keyboard report multi")
// keyboardReportMultiCancel = nil
// }
// }
func rpcKeyboardReportMultiWrapper(macro []map[string]any) (usbgadget.KeysDownState, error) { // func setKeyboardReportMultiCancel(cancel context.CancelFunc) {
cancelKeyboardReportMulti() // keyboardReportMultiLock.Lock()
// defer keyboardReportMultiLock.Unlock()
ctx, cancel := context.WithCancel(context.Background()) // keyboardReportMultiCancel = cancel
setKeyboardReportMultiCancel(cancel) // }
writeJSONRPCEvent("keyboardReportMultiState", true, currentSession) // func rpcKeyboardReportMultiWrapper(macro []map[string]any) (usbgadget.KeysDownState, error) {
// // cancelKeyboardReportMulti()
result, err := rpcKeyboardReportMulti(ctx, macro) // // ctx, cancel := context.WithCancel(context.Background())
// // setKeyboardReportMultiCancel(cancel)
setKeyboardReportMultiCancel(nil) // // writeJSONRPCEvent("keyboardReportMultiState", true, currentSession)
writeJSONRPCEvent("keyboardReportMultiState", false, currentSession) // // result, err := rpcKeyboardReportMulti(ctx, macro)
return result, err // // setKeyboardReportMultiCancel(nil)
}
var ( // // writeJSONRPCEvent("keyboardReportMultiState", false, currentSession)
keyboardReportMultiCancel context.CancelFunc
keyboardReportMultiLock sync.Mutex
)
func rpcCancelKeyboardReportMulti() { // // return result, err
cancelKeyboardReportMulti() // }
}
func rpcKeyboardReportMulti(ctx context.Context, macro []map[string]any) (usbgadget.KeysDownState, error) { // var (
// keyboardReportMultiCancel context.CancelFunc
// keyboardReportMultiLock sync.Mutex
// )
// func rpcCancelKeyboardReportMulti() {
// cancelKeyboardReportMulti()
// }
func rpcKeyboardReportMulti(ctx context.Context, macro []hidrpc.KeyboardMacro) (usbgadget.KeysDownState, error) {
var last usbgadget.KeysDownState var last usbgadget.KeysDownState
var err error var err error
@ -1110,134 +1114,112 @@ func rpcKeyboardReportMulti(ctx context.Context, macro []map[string]any) (usbgad
default: default:
} }
var modifier byte delay := time.Duration(step.Delay) * time.Millisecond
if m, ok := step["modifier"].(float64); ok { logger.Info().Int("step", i).Uint16("delay", step.Delay).Msg("Keyboard report multi delay")
modifier = byte(int(m))
} else if mi, ok := step["modifier"].(int); ok {
modifier = byte(mi)
} else if mb, ok := step["modifier"].(uint8); ok {
modifier = mb
}
var keys []byte last, err = rpcKeyboardReport(step.Modifier, step.Keys)
if arr, ok := step["keys"].([]any); ok { if err != nil {
keys = make([]byte, 0, len(arr)) logger.Warn().Err(err).Msg("failed to execute keyboard report multi")
for _, v := range arr { return last, err
if f, ok := v.(float64); ok {
keys = append(keys, byte(int(f)))
} else if i, ok := v.(int); ok {
keys = append(keys, byte(i))
} else if b, ok := v.(uint8); ok {
keys = append(keys, b)
}
}
} else if bs, ok := step["keys"].([]byte); ok {
keys = bs
} }
// Use context-aware sleep that can be cancelled // Use context-aware sleep that can be cancelled
select { select {
case <-time.After(100 * time.Millisecond): case <-time.After(delay):
// Sleep completed normally // Sleep completed normally
case <-ctx.Done(): case <-ctx.Done():
logger.Debug().Int("step", i).Msg("Keyboard report multi cancelled during sleep") logger.Debug().Int("step", i).Msg("Keyboard report multi cancelled during sleep")
return last, ctx.Err() return last, ctx.Err()
} }
last, err = rpcKeyboardReport(modifier, keys)
if err != nil {
logger.Warn().Err(err).Msg("failed to execute keyboard report multi")
return last, err
}
} }
return last, nil return last, nil
} }
var rpcHandlers = map[string]RPCHandler{ var rpcHandlers = map[string]RPCHandler{
"ping": {Func: rpcPing}, "ping": {Func: rpcPing},
"reboot": {Func: rpcReboot, Params: []string{"force"}}, "reboot": {Func: rpcReboot, Params: []string{"force"}},
"getDeviceID": {Func: rpcGetDeviceID}, "getDeviceID": {Func: rpcGetDeviceID},
"deregisterDevice": {Func: rpcDeregisterDevice}, "deregisterDevice": {Func: rpcDeregisterDevice},
"getCloudState": {Func: rpcGetCloudState}, "getCloudState": {Func: rpcGetCloudState},
"getNetworkState": {Func: rpcGetNetworkState}, "getNetworkState": {Func: rpcGetNetworkState},
"getNetworkSettings": {Func: rpcGetNetworkSettings}, "getNetworkSettings": {Func: rpcGetNetworkSettings},
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
"renewDHCPLease": {Func: rpcRenewDHCPLease}, "renewDHCPLease": {Func: rpcRenewDHCPLease},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"keyboardReportMulti": {Func: rpcKeyboardReportMultiWrapper, Params: []string{"macro"}}, // "keyboardReportMulti": {Func: rpcKeyboardReportMultiWrapper, Params: []string{"macro"}},
"cancelKeyboardReportMulti": {Func: rpcCancelKeyboardReportMulti}, // "cancelKeyboardReportMulti": {Func: rpcCancelKeyboardReportMulti},
"getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState},
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
"getKeyDownState": {Func: rpcGetKeysDownState}, "getKeyDownState": {Func: rpcGetKeysDownState},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
"getVideoState": {Func: rpcGetVideoState}, "getVideoState": {Func: rpcGetVideoState},
"getUSBState": {Func: rpcGetUSBState}, "getUSBState": {Func: rpcGetUSBState},
"unmountImage": {Func: rpcUnmountImage}, "unmountImage": {Func: rpcUnmountImage},
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
"getJigglerState": {Func: rpcGetJigglerState}, "getJigglerState": {Func: rpcGetJigglerState},
"setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}}, "setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}},
"getJigglerConfig": {Func: rpcGetJigglerConfig}, "getJigglerConfig": {Func: rpcGetJigglerConfig},
"getTimezones": {Func: rpcGetTimezones}, "getTimezones": {Func: rpcGetTimezones},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
"getAutoUpdateState": {Func: rpcGetAutoUpdateState}, "getAutoUpdateState": {Func: rpcGetAutoUpdateState},
"setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
"getEDID": {Func: rpcGetEDID}, "getEDID": {Func: rpcGetEDID},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getDevChannelState": {Func: rpcGetDevChannelState}, "getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getUpdateStatus": {Func: rpcGetUpdateStatus}, "getUpdateStatus": {Func: rpcGetUpdateStatus},
"tryUpdate": {Func: rpcTryUpdate}, "tryUpdate": {Func: rpcTryUpdate},
"getDevModeState": {Func: rpcGetDevModeState}, "getDevModeState": {Func: rpcGetDevModeState},
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
"getSSHKeyState": {Func: rpcGetSSHKeyState}, "getSSHKeyState": {Func: rpcGetSSHKeyState},
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
"getTLSState": {Func: rpcGetTLSState}, "getTLSState": {Func: rpcGetTLSState},
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
"getMassStorageMode": {Func: rpcGetMassStorageMode}, "getMassStorageMode": {Func: rpcGetMassStorageMode},
"isUpdatePending": {Func: rpcIsUpdatePending}, "isUpdatePending": {Func: rpcIsUpdatePending},
"getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState},
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
"getUsbConfig": {Func: rpcGetUsbConfig}, "getUsbConfig": {Func: rpcGetUsbConfig},
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
"getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "getVirtualMediaState": {Func: rpcGetVirtualMediaState},
"getStorageSpace": {Func: rpcGetStorageSpace}, "getStorageSpace": {Func: rpcGetStorageSpace},
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
"listStorageFiles": {Func: rpcListStorageFiles}, "listStorageFiles": {Func: rpcListStorageFiles},
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
"startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}},
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
"resetConfig": {Func: rpcResetConfig}, "resetConfig": {Func: rpcResetConfig},
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
"getDisplayRotation": {Func: rpcGetDisplayRotation}, "getDisplayRotation": {Func: rpcGetDisplayRotation},
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
"getBacklightSettings": {Func: rpcGetBacklightSettings}, "getBacklightSettings": {Func: rpcGetBacklightSettings},
"getDCPowerState": {Func: rpcGetDCPowerState}, "getDCPowerState": {Func: rpcGetDCPowerState},
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}}, "setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
"getActiveExtension": {Func: rpcGetActiveExtension}, "getActiveExtension": {Func: rpcGetActiveExtension},
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
"getATXState": {Func: rpcGetATXState}, "getATXState": {Func: rpcGetATXState},
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
"getSerialSettings": {Func: rpcGetSerialSettings}, "getSerialSettings": {Func: rpcGetSerialSettings},
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
"getUsbDevices": {Func: rpcGetUsbDevices}, "getUsbDevices": {Func: rpcGetUsbDevices},
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
"getKeyboardLayout": {Func: rpcGetKeyboardLayout}, "getKeyboardLayout": {Func: rpcGetKeyboardLayout},
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
"getKeyboardMacros": {Func: getKeyboardMacros}, "getKeyboardMacros": {Func: getKeyboardMacros},
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
} }

View File

@ -7,6 +7,7 @@ export const HID_RPC_MESSAGE_TYPES = {
WheelReport: 0x04, WheelReport: 0x04,
KeypressReport: 0x05, KeypressReport: 0x05,
MouseReport: 0x06, MouseReport: 0x06,
KeyboardMacroReport: 0x07,
KeyboardLedState: 0x32, KeyboardLedState: 0x32,
KeysDownState: 0x33, KeysDownState: 0x33,
} }
@ -32,6 +33,30 @@ const fromInt32toUint8 = (n: number) => {
]); ]);
}; };
const fromUint16toUint8 = (n: number) => {
if (n > 65535 || n < 0) {
throw new Error(`Number ${n} is not within the uint16 range`);
}
return new Uint8Array([
(n >> 8) & 0xFF,
(n >> 0) & 0xFF,
]);
};
const fromUint32toUint8 = (n: number) => {
if (n > 4294967295 || n < 0) {
throw new Error(`Number ${n} is not within the uint32 range`);
}
return new Uint8Array([
(n >> 24) & 0xFF,
(n >> 16) & 0xFF,
(n >> 8) & 0xFF,
(n >> 0) & 0xFF,
]);
};
const fromInt8ToUint8 = (n: number) => { const fromInt8ToUint8 = (n: number) => {
if (n < -128 || n > 127) { if (n < -128 || n > 127) {
throw new Error(`Number ${n} is not within the int8 range`); throw new Error(`Number ${n} is not within the int8 range`);
@ -186,6 +211,64 @@ export class KeyboardReportMessage extends RpcMessage {
} }
} }
export interface KeyboardMacro extends KeysDownState {
delay: number;
}
export class KeyboardMacroReportMessage extends RpcMessage {
isPaste: boolean;
length: number;
macro: KeyboardMacro[];
KEYS_LENGTH = 6;
constructor(isPaste: boolean, length: number, macro: KeyboardMacro[]) {
super(HID_RPC_MESSAGE_TYPES.KeyboardMacroReport);
this.isPaste = isPaste;
this.length = length;
this.macro = macro;
}
marshal(): Uint8Array {
const dataHeader = new Uint8Array([
this.messageType,
this.isPaste ? 1 : 0,
...fromUint32toUint8(this.length),
]);
let dataBody = new Uint8Array();
for (const step of this.macro) {
if (!withinUint8Range(step.modifier)) {
throw new Error(`Modifier ${step.modifier} is not within the uint8 range`);
}
// Ensure the keys are within the KEYS_LENGTH range
const keys = step.keys;
if (keys.length > this.KEYS_LENGTH) {
throw new Error(`Keys ${keys} is not within the hidKeyBufferSize range`);
} else if (keys.length < this.KEYS_LENGTH) {
keys.push(...Array(this.KEYS_LENGTH - keys.length).fill(0));
}
for (const key of keys) {
if (!withinUint8Range(key)) {
throw new Error(`Key ${key} is not within the uint8 range`);
}
}
const macroBinary = new Uint8Array([
step.modifier,
...keys,
...fromUint16toUint8(step.delay),
]);
dataBody = new Uint8Array([...dataBody, ...macroBinary]);
}
return new Uint8Array([...dataHeader, ...dataBody]);
}
}
export class KeyboardLedStateMessage extends RpcMessage { export class KeyboardLedStateMessage extends RpcMessage {
keyboardLedState: KeyboardLedState; keyboardLedState: KeyboardLedState;

View File

@ -5,6 +5,8 @@ import { useRTCStore } from "@/hooks/stores";
import { import {
HID_RPC_VERSION, HID_RPC_VERSION,
HandshakeMessage, HandshakeMessage,
KeyboardMacro,
KeyboardMacroReportMessage,
KeyboardReportMessage, KeyboardReportMessage,
KeypressReportMessage, KeypressReportMessage,
MouseReportMessage, MouseReportMessage,
@ -68,6 +70,15 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
[sendMessage], [sendMessage],
); );
const reportKeyboardMacroEvent = useCallback(
(macro: KeyboardMacro[]) => {
const d = new KeyboardMacroReportMessage(false, macro.length, macro);
sendMessage(d);
console.log("Sent keyboard macro report", d, d.marshal());
},
[sendMessage],
);
const sendHandshake = useCallback(() => { const sendHandshake = useCallback(() => {
if (rpcHidProtocolVersion) return; if (rpcHidProtocolVersion) return;
if (!rpcHidChannel) return; if (!rpcHidChannel) return;
@ -143,6 +154,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
reportKeypressEvent, reportKeypressEvent,
reportAbsMouseEvent, reportAbsMouseEvent,
reportRelMouseEvent, reportRelMouseEvent,
reportKeyboardMacroEvent,
rpcHidProtocolVersion, rpcHidProtocolVersion,
rpcHidReady, rpcHidReady,
rpcHidStatus, rpcHidStatus,

View File

@ -1,4 +1,4 @@
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
import { import {
hidErrorRollOver, hidErrorRollOver,
@ -9,7 +9,7 @@ import {
} from "@/hooks/stores"; } 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, KeyboardMacro, KeysDownStateMessage } from "@/hooks/hidRpc";
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings"; import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() { export default function useKeyboard() {
@ -32,6 +32,7 @@ export default function useKeyboard() {
const { const {
reportKeyboardEvent: sendKeyboardEventHidRpc, reportKeyboardEvent: sendKeyboardEventHidRpc,
reportKeypressEvent: sendKeypressEventHidRpc, reportKeypressEvent: sendKeypressEventHidRpc,
reportKeyboardMacroEvent: sendKeyboardMacroEventHidRpc,
rpcHidReady, rpcHidReady,
} = useHidRpc(message => { } = useHidRpc(message => {
switch (message.constructor) { switch (message.constructor) {
@ -77,16 +78,19 @@ export default function useKeyboard() {
[rpcDataChannel?.readyState, rpcHidReady, send, sendKeyboardEventHidRpc], [rpcDataChannel?.readyState, rpcHidReady, send, sendKeyboardEventHidRpc],
); );
const MACRO_RESET_KEYBOARD_STATE = useMemo(() => ({
keys: new Array(hidKeyBufferSize).fill(0),
modifier: 0,
delay: 0,
}), []);
// resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers. // resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers.
// This is useful for macros and when the browser loses focus to ensure that the keyboard state // This is useful for macros and when the browser loses focus to ensure that the keyboard state
// is clean. // is clean.
const resetKeyboardState = useCallback(async () => { const resetKeyboardState = useCallback(async () => {
// Reset the keys buffer to zeros and the modifier state to zero // Reset the keys buffer to zeros and the modifier state to zero
keysDownState.keys.length = hidKeyBufferSize; sendKeyboardEvent(MACRO_RESET_KEYBOARD_STATE);
keysDownState.keys.fill(0); }, [sendKeyboardEvent, MACRO_RESET_KEYBOARD_STATE]);
keysDownState.modifier = 0;
sendKeyboardEvent(keysDownState);
}, [keysDownState, sendKeyboardEvent]);
// 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.
@ -97,7 +101,7 @@ export default function useKeyboard() {
const executeMacro = async ( const executeMacro = async (
steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[], steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[],
) => { ) => {
const macro: KeysDownState[] = []; const macro: KeyboardMacro[] = [];
for (const [_, step] of steps.entries()) { for (const [_, step] of steps.entries()) {
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
@ -107,19 +111,12 @@ export default function useKeyboard() {
// 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) {
macro.push({ keys: keyValues, modifier: modifierMask }); macro.push({ keys: keyValues, modifier: modifierMask, delay: 50 });
keysDownState.keys.length = hidKeyBufferSize; macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: 200 });
keysDownState.keys.fill(0);
keysDownState.modifier = 0;
macro.push(keysDownState);
} }
} }
// KeyboardReportMessage
send("keyboardReportMulti", { macro }, (resp: JsonRpcResponse) => { sendKeyboardMacroEventHidRpc(macro);
if ("error" in resp) {
console.error(`Failed to send keyboard report ${macro}`, resp.error);
}
});
}; };
const cancelExecuteMacro = useCallback(async () => { const cancelExecuteMacro = useCallback(async () => {