mirror of https://github.com/jetkvm/kvm.git
wip: send macro using hidRPC channel
This commit is contained in:
parent
f58e5476bf
commit
024cbb8fb1
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
274
jsonrpc.go
274
jsonrpc.go
|
@ -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"}},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
Loading…
Reference in New Issue