mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 17:11:20 +02:00
feat(hid): remove HID-RPC related code and improve keyboard handling logic
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
package hidrpc
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Handler interface {
|
||||
HandleHandshake(version byte) error
|
||||
HandleKeyboardReport(modifier byte, keys []byte) error
|
||||
HandleKeypressReport(key byte, press bool) error
|
||||
HandleKeypressKeepAlive() error
|
||||
HandleKeyboardMacroReport(data []byte) error
|
||||
HandleCancelKeyboardMacro() error
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func NewServer(handler Handler) *Server {
|
||||
return &Server{handler: handler}
|
||||
}
|
||||
|
||||
func (s *Server) HandleMessage(data []byte) error {
|
||||
msg, err := UnmarshalMessage(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case MessageTypeHandshake:
|
||||
if len(msg.Data) < 1 {
|
||||
return fmt.Errorf("invalid handshake length: %d", len(msg.Data))
|
||||
}
|
||||
return s.handler.HandleHandshake(msg.Data[0])
|
||||
case MessageTypeKeyboardReport:
|
||||
if len(msg.Data) < 7 {
|
||||
return fmt.Errorf("invalid keyboard report length: %d", len(msg.Data))
|
||||
}
|
||||
return s.handler.HandleKeyboardReport(msg.Data[0], msg.Data[1:7])
|
||||
case MessageTypeKeypressReport:
|
||||
if len(msg.Data) < 2 {
|
||||
return fmt.Errorf("invalid keypress report length: %d", len(msg.Data))
|
||||
}
|
||||
return s.handler.HandleKeypressReport(msg.Data[0], msg.Data[1] != 0)
|
||||
case MessageTypeKeypressKeepAlive:
|
||||
return s.handler.HandleKeypressKeepAlive()
|
||||
case MessageTypeKeyboardMacroReport:
|
||||
return s.handler.HandleKeyboardMacroReport(msg.Data)
|
||||
case MessageTypeCancelKeyboardMacro:
|
||||
return s.handler.HandleCancelKeyboardMacro()
|
||||
default:
|
||||
return fmt.Errorf("unknown message type: 0x%02x", msg.Type)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package hidrpc
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
MessageTypeHandshake = 0x01
|
||||
MessageTypeKeyboardReport = 0x02
|
||||
MessageTypePointerReport = 0x03
|
||||
MessageTypeWheelReport = 0x04
|
||||
MessageTypeKeypressReport = 0x05
|
||||
MessageTypeMouseReport = 0x06
|
||||
MessageTypeKeyboardMacroReport = 0x07
|
||||
MessageTypeCancelKeyboardMacro = 0x08
|
||||
MessageTypeKeypressKeepAlive = 0x09
|
||||
MessageTypeKeyboardLedState = 0x32
|
||||
MessageTypeKeysDownState = 0x33
|
||||
MessageTypeKeyboardMacroState = 0x34
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Type byte
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func MarshalKeyboardReport(modifier byte, keys []byte) []byte {
|
||||
data := make([]byte, 8)
|
||||
data[0] = MessageTypeKeyboardReport
|
||||
data[1] = modifier
|
||||
copy(data[2:], keys)
|
||||
return data
|
||||
}
|
||||
|
||||
func MarshalHandshake(version byte) []byte {
|
||||
return []byte{MessageTypeHandshake, version}
|
||||
}
|
||||
|
||||
func MarshalKeypressReport(key byte, press bool) []byte {
|
||||
data := make([]byte, 3)
|
||||
data[0] = MessageTypeKeypressReport
|
||||
data[1] = key
|
||||
if press {
|
||||
data[2] = 1
|
||||
} else {
|
||||
data[2] = 0
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func MarshalKeypressKeepAlive() []byte {
|
||||
return []byte{MessageTypeKeypressKeepAlive}
|
||||
}
|
||||
|
||||
func MarshalKeyboardLedState(state byte) []byte {
|
||||
return []byte{MessageTypeKeyboardLedState, state}
|
||||
}
|
||||
|
||||
func MarshalKeysDownState(modifier byte, keys []byte) []byte {
|
||||
data := make([]byte, 8)
|
||||
data[0] = MessageTypeKeysDownState
|
||||
data[1] = modifier
|
||||
copy(data[2:], keys)
|
||||
return data
|
||||
}
|
||||
|
||||
func UnmarshalMessage(data []byte) (Message, error) {
|
||||
if len(data) < 1 {
|
||||
return Message{}, fmt.Errorf("empty message")
|
||||
}
|
||||
return Message{Type: data[0], Data: data[1:]}, nil
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -132,10 +131,10 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||
return u.keyboardState
|
||||
}
|
||||
|
||||
func (u *UsbGadget) listenKeyboardEvents() {
|
||||
func (u *UsbGadget) listenKeyboardEvents(ctx context.Context, file *os.File) {
|
||||
var path string
|
||||
if u.keyboardHidFile != nil {
|
||||
path = u.keyboardHidFile.Name()
|
||||
if file != nil {
|
||||
path = file.Name()
|
||||
}
|
||||
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
|
||||
l.Trace().Msg("starting")
|
||||
@@ -144,12 +143,12 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||
buf := make([]byte, hidReadBufferSize)
|
||||
for {
|
||||
select {
|
||||
case <-u.keyboardStateCtx.Done():
|
||||
case <-ctx.Done():
|
||||
l.Info().Msg("context done")
|
||||
return
|
||||
default:
|
||||
l.Trace().Msg("reading from keyboard")
|
||||
if u.keyboardHidFile == nil {
|
||||
if file == nil {
|
||||
u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||
// show the error every 100 times to avoid spamming the logs
|
||||
time.Sleep(time.Second)
|
||||
@@ -158,16 +157,26 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||
// reset the counter
|
||||
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||
|
||||
n, err := u.keyboardHidFile.Read(buf)
|
||||
n, err := file.Read(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
l.Info().Msg("context canceled while reading keyboard HID file")
|
||||
return
|
||||
}
|
||||
|
||||
u.logWithSupression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||
continue
|
||||
if reopenErr := u.reopenKeyboardHidFile(); reopenErr != nil {
|
||||
u.logWithSupression("keyboardHidFileReopen", 100, &l, reopenErr, "failed to reopen keyboard HID file")
|
||||
} else {
|
||||
u.resetLogSuppressionCounter("keyboardHidFileReopen")
|
||||
}
|
||||
return
|
||||
}
|
||||
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||
|
||||
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||
if n != 1 {
|
||||
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||
if n < 1 {
|
||||
l.Info().Int("n", n).Msg("expected at least 1 byte, got 0")
|
||||
continue
|
||||
}
|
||||
u.updateKeyboardState(buf[0])
|
||||
@@ -176,13 +185,52 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||
func openWithTimeout(name string, flag int, perm os.FileMode, timeout time.Duration) (*os.File, error) {
|
||||
type result struct {
|
||||
file *os.File
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
f, err := os.OpenFile(name, flag, perm)
|
||||
ch <- result{f, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
return r.file, r.err
|
||||
case <-time.After(timeout):
|
||||
// Drain the channel in the background to close the leaked fd if the
|
||||
// open eventually succeeds.
|
||||
go func() {
|
||||
if r := <-ch; r.file != nil {
|
||||
r.file.Close()
|
||||
}
|
||||
}()
|
||||
return nil, fmt.Errorf("open %s: timed out after %s", name, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) closeKeyboardHidFileLocked() {
|
||||
if u.keyboardStateCancel != nil {
|
||||
u.keyboardStateCancel()
|
||||
u.keyboardStateCancel = nil
|
||||
}
|
||||
|
||||
if u.keyboardHidFile != nil {
|
||||
u.keyboardHidFile.Close()
|
||||
u.keyboardHidFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) openKeyboardHidFileLocked(forceReopen bool) error {
|
||||
if forceReopen {
|
||||
u.closeKeyboardHidFileLocked()
|
||||
} else if u.keyboardHidFile != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||
file, err := openWithTimeout("/dev/hidg0", os.O_RDWR, 0666, 3*time.Second)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "no such device") {
|
||||
u.log.Error().
|
||||
@@ -197,34 +245,62 @@ func (u *UsbGadget) openKeyboardHidFile() error {
|
||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||
}
|
||||
|
||||
if u.keyboardStateCancel != nil {
|
||||
u.keyboardStateCancel()
|
||||
}
|
||||
|
||||
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||
u.listenKeyboardEvents()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
u.keyboardHidFile = file
|
||||
u.keyboardStateCtx = ctx
|
||||
u.keyboardStateCancel = cancel
|
||||
u.listenKeyboardEvents(ctx, file)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
return u.openKeyboardHidFileLocked(false)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) reopenKeyboardHidFile() error {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
return u.openKeyboardHidFileLocked(true)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||
return u.openKeyboardHidFile()
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||
var parts []string
|
||||
for _, b := range data {
|
||||
parts = append(parts, fmt.Sprintf("\\x%02x", b))
|
||||
}
|
||||
hexString := strings.Join(parts, "")
|
||||
func (u *UsbGadget) ReopenKeyboardHidFile() error {
|
||||
return u.reopenKeyboardHidFile()
|
||||
}
|
||||
|
||||
cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -n -e '%s' > /dev/hidg0", hexString))
|
||||
err := cmd.Run()
|
||||
func (u *UsbGadget) keyboardWriteHidFileLocked(modifier byte, keys []byte) error {
|
||||
if len(keys) > 6 {
|
||||
keys = keys[:6]
|
||||
}
|
||||
if len(keys) < 6 {
|
||||
keys = append(keys, make([]byte, 6-len(keys))...)
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]}
|
||||
|
||||
if u.keyboardHidFile == nil {
|
||||
if err := u.openKeyboardHidFileLocked(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := u.writeWithTimeout(u.keyboardHidFile, data)
|
||||
if err != nil {
|
||||
u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||
u.closeKeyboardHidFileLocked()
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||
|
||||
u.resetUserInputTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -369,23 +445,6 @@ func (u *UsbGadget) KeypressKeepAlive() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFileLocked(modifier byte, keys []byte) error {
|
||||
if len(keys) > 6 {
|
||||
keys = keys[:6]
|
||||
}
|
||||
if len(keys) < 6 {
|
||||
keys = append(keys, make([]byte, 6-len(keys))...)
|
||||
}
|
||||
|
||||
err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.resetUserInputTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
@@ -2,10 +2,13 @@ package usbgadget
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -107,3 +110,36 @@ func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
}
|
||||
}
|
||||
|
||||
const hidWriteTimeout = 50 * time.Millisecond
|
||||
|
||||
func (u *UsbGadget) writeWithTimeout(file *os.File, data []byte) (n int, err error) {
|
||||
if err := file.SetWriteDeadline(time.Now().Add(hidWriteTimeout)); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
n, err = file.Write(data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.log.Trace().
|
||||
Str("file", file.Name()).
|
||||
Bytes("data", data).
|
||||
Err(err).
|
||||
Msg("write failed")
|
||||
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
u.logWithSupression(
|
||||
fmt.Sprintf("writeWithTimeout_%s", file.Name()),
|
||||
1000,
|
||||
u.log,
|
||||
err,
|
||||
"write timed out: %s",
|
||||
file.Name(),
|
||||
)
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -270,6 +270,7 @@ function KeyboardWrapper() {
|
||||
setIsCapsLockActive(false);
|
||||
}
|
||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
||||
setTimeout(resetKeyboardState, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
export const MessageType = {
|
||||
Handshake: 0x01,
|
||||
KeyboardReport: 0x02,
|
||||
PointerReport: 0x03,
|
||||
WheelReport: 0x04,
|
||||
KeypressReport: 0x05,
|
||||
MouseReport: 0x06,
|
||||
KeyboardMacroReport: 0x07,
|
||||
CancelKeyboardMacro: 0x08,
|
||||
KeypressKeepAlive: 0x09,
|
||||
KeyboardLedState: 0x32,
|
||||
KeysDownState: 0x33,
|
||||
KeyboardMacroState: 0x34,
|
||||
} as const;
|
||||
|
||||
export function marshalKeypressReport(key: number, press: boolean): Uint8Array {
|
||||
return new Uint8Array([MessageType.KeypressReport, key, press ? 1 : 0]);
|
||||
}
|
||||
|
||||
export function marshalKeyboardReport(modifier: number, keys: number[]): Uint8Array {
|
||||
const data = new Uint8Array(8);
|
||||
data[0] = MessageType.KeyboardReport;
|
||||
data[1] = modifier;
|
||||
for (let i = 0; i < Math.min(keys.length, 6); i++) {
|
||||
data[2 + i] = keys[i];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function marshalKeypressKeepAlive(): Uint8Array {
|
||||
return new Uint8Array([MessageType.KeypressKeepAlive]);
|
||||
}
|
||||
|
||||
export function marshalHandshake(version: number): Uint8Array {
|
||||
return new Uint8Array([MessageType.Handshake, version]);
|
||||
}
|
||||
|
||||
export interface HidRpcMessage {
|
||||
type: number;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
export function unmarshalMessage(data: ArrayBuffer): HidRpcMessage {
|
||||
const view = new Uint8Array(data);
|
||||
return {
|
||||
type: view[0],
|
||||
data: view.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
export interface KeyboardLedState {
|
||||
num_lock: boolean;
|
||||
caps_lock: boolean;
|
||||
scroll_lock: boolean;
|
||||
compose: boolean;
|
||||
kana: boolean;
|
||||
shift: boolean;
|
||||
}
|
||||
|
||||
export function parseKeyboardLedState(data: Uint8Array): KeyboardLedState {
|
||||
const raw = data[0] || 0;
|
||||
return {
|
||||
num_lock: !!(raw & (1 << 0)),
|
||||
caps_lock: !!(raw & (1 << 1)),
|
||||
scroll_lock: !!(raw & (1 << 2)),
|
||||
compose: !!(raw & (1 << 3)),
|
||||
kana: !!(raw & (1 << 4)),
|
||||
shift: !!(raw & (1 << 6)),
|
||||
};
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useRTCStore, useHidStore } from "./stores";
|
||||
import {
|
||||
marshalKeypressReport,
|
||||
marshalKeyboardReport,
|
||||
marshalKeypressKeepAlive,
|
||||
marshalHandshake,
|
||||
unmarshalMessage,
|
||||
MessageType,
|
||||
parseKeyboardLedState,
|
||||
} from "./hidRpc";
|
||||
|
||||
export function useHidRpc() {
|
||||
const hidChannel = useRTCStore(state => state.hidChannel);
|
||||
const setRpcHidReady = useHidStore(state => state.setRpcHidReady);
|
||||
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
|
||||
const setKeysDownState = useHidStore(state => state.setKeysDownState);
|
||||
const rpcHidReadyRef = useRef(false);
|
||||
|
||||
// Send keypress event
|
||||
const reportKeypressEvent = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
|
||||
return false;
|
||||
}
|
||||
const data = marshalKeypressReport(key, press);
|
||||
hidChannel.send(data);
|
||||
return true;
|
||||
},
|
||||
[hidChannel]
|
||||
);
|
||||
|
||||
// Send keyboard report (for legacy compatibility)
|
||||
const reportKeyboardEvent = useCallback(
|
||||
(modifier: number, keys: number[]) => {
|
||||
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
|
||||
return false;
|
||||
}
|
||||
const data = marshalKeyboardReport(modifier, keys);
|
||||
hidChannel.send(data);
|
||||
return true;
|
||||
},
|
||||
[hidChannel]
|
||||
);
|
||||
|
||||
// Send keepalive
|
||||
const reportKeypressKeepAlive = useCallback(() => {
|
||||
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
|
||||
return false;
|
||||
}
|
||||
const data = marshalKeypressKeepAlive();
|
||||
hidChannel.send(data);
|
||||
return true;
|
||||
}, [hidChannel]);
|
||||
|
||||
// Handle incoming HID-RPC messages
|
||||
useEffect(() => {
|
||||
if (!hidChannel) return;
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
const msg = unmarshalMessage(event.data);
|
||||
|
||||
switch (msg.type) {
|
||||
case MessageType.Handshake:
|
||||
if (msg.data[0] === 1) {
|
||||
rpcHidReadyRef.current = true;
|
||||
setRpcHidReady(true);
|
||||
}
|
||||
break;
|
||||
case MessageType.KeyboardLedState:
|
||||
setKeyboardLedState(parseKeyboardLedState(msg.data));
|
||||
break;
|
||||
case MessageType.KeysDownState:
|
||||
// Parse modifier + 6 keys
|
||||
if (msg.data.length >= 7) {
|
||||
setKeysDownState({
|
||||
modifier: msg.data[0],
|
||||
keys: Array.from(msg.data.slice(1, 7)),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
hidChannel.addEventListener("message", messageHandler);
|
||||
|
||||
// Send handshake
|
||||
if (hidChannel.readyState === "open") {
|
||||
hidChannel.send(marshalHandshake(1));
|
||||
} else {
|
||||
hidChannel.addEventListener("open", () => {
|
||||
hidChannel.send(marshalHandshake(1));
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
hidChannel.removeEventListener("message", messageHandler);
|
||||
rpcHidReadyRef.current = false;
|
||||
setRpcHidReady(false);
|
||||
};
|
||||
}, [hidChannel, setRpcHidReady, setKeyboardLedState, setKeysDownState]);
|
||||
|
||||
return {
|
||||
rpcHidReady: rpcHidReadyRef.current,
|
||||
reportKeypressEvent,
|
||||
reportKeyboardEvent,
|
||||
reportKeypressKeepAlive,
|
||||
};
|
||||
}
|
||||
@@ -3,12 +3,10 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import notifications from "@/notifications";
|
||||
import { useHidStore, useRTCStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export default function useKeyboard() {
|
||||
const [send] = useJsonRpc();
|
||||
const { rpcHidReady, reportKeypressEvent, reportKeyboardEvent, reportKeypressKeepAlive } = useHidRpc();
|
||||
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
const forceHttp = useSettingsStore(state => state.forceHttp);
|
||||
@@ -30,80 +28,45 @@ export default function useKeyboard() {
|
||||
if (usbState !== "configured") return;
|
||||
const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// Try HID-RPC first
|
||||
if (rpcHidReady && !forceHttp) {
|
||||
reportKeyboardEvent(accModifier, keys);
|
||||
} else {
|
||||
// Fallback to JSON-RPC
|
||||
send("keyboardReport", { keys, modifier: accModifier }, resp => {
|
||||
if ("error" in resp) {
|
||||
const msg = (resp.error.data as string) || resp.error.message || "";
|
||||
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
|
||||
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
|
||||
}
|
||||
// Fallback to JSON-RPC
|
||||
send("keyboardReport", { keys, modifier: accModifier }, resp => {
|
||||
if ("error" in resp) {
|
||||
const msg = (resp.error.data as string) || resp.error.message || "";
|
||||
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
|
||||
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We do this for the info bar to display the currently pressed keys for the user
|
||||
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers });
|
||||
},
|
||||
[forceHttp, rpcDataChannel?.readyState, rpcHidReady, reportKeyboardEvent, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
|
||||
[forceHttp, rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
|
||||
);
|
||||
|
||||
// Send per-key press/release (new HID-RPC method)
|
||||
// Send per-key press/release
|
||||
const sendKeypress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (isReinitializingGadget || usbState !== "configured") return;
|
||||
|
||||
if (rpcHidReady && !forceHttp) {
|
||||
reportKeypressEvent(key, press);
|
||||
|
||||
// Track held keys for keepalive
|
||||
if (press) {
|
||||
heldKeysRef.current.add(key);
|
||||
// Start keepalive interval if not already running
|
||||
if (!keepaliveIntervalRef.current) {
|
||||
keepaliveIntervalRef.current = setInterval(() => {
|
||||
if (heldKeysRef.current.size > 0) {
|
||||
reportKeypressKeepAlive();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
} else {
|
||||
heldKeysRef.current.delete(key);
|
||||
if (heldKeysRef.current.size === 0 && keepaliveIntervalRef.current) {
|
||||
clearInterval(keepaliveIntervalRef.current);
|
||||
keepaliveIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy: simulate device-side key handling
|
||||
// This maintains the 6-key buffer on the frontend for legacy compatibility
|
||||
// ... (existing logic would go here, but for now use sendKeyboardEvent)
|
||||
// For simplicity in migration, we fall back to full state reports
|
||||
const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
|
||||
sendKeyboardEvent(press ? [key] : [], [modifier]);
|
||||
}
|
||||
// Legacy: simulate device-side key handling
|
||||
// This maintains the 6-key buffer on the frontend for legacy compatibility
|
||||
// For simplicity in migration, we fall back to full state reports
|
||||
const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
|
||||
sendKeyboardEvent(press ? [key] : [], [modifier]);
|
||||
},
|
||||
[rpcHidReady, forceHttp, reportKeypressEvent, reportKeypressKeepAlive, isReinitializingGadget, usbState, sendKeyboardEvent]
|
||||
[isReinitializingGadget, usbState, sendKeyboardEvent]
|
||||
);
|
||||
|
||||
const resetKeyboardState = useCallback(() => {
|
||||
// Release all held keys
|
||||
if (rpcHidReady && !forceHttp) {
|
||||
heldKeysRef.current.forEach(key => {
|
||||
reportKeypressEvent(key, false);
|
||||
});
|
||||
} else {
|
||||
sendKeyboardEvent([], []);
|
||||
}
|
||||
sendKeyboardEvent([], []);
|
||||
heldKeysRef.current.clear();
|
||||
if (keepaliveIntervalRef.current) {
|
||||
clearInterval(keepaliveIntervalRef.current);
|
||||
keepaliveIntervalRef.current = null;
|
||||
}
|
||||
}, [rpcHidReady, forceHttp, reportKeypressEvent, sendKeyboardEvent]);
|
||||
}, [sendKeyboardEvent]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useKeyboardEvents = (
|
||||
pasteCaptureRef?: React.RefObject<HTMLTextAreaElement>,
|
||||
isReinitializingGadget?: boolean
|
||||
) => {
|
||||
const { sendKeyboardEvent, sendKeypress, resetKeyboardState } = useKeyboard();
|
||||
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
||||
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore();
|
||||
|
||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||
@@ -65,6 +65,11 @@ export const useKeyboardEvents = (
|
||||
}
|
||||
if (isReinitializingGadget) return;
|
||||
|
||||
if (e.repeat) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const prev = useHidStore.getState();
|
||||
let code = e.code;
|
||||
@@ -94,15 +99,8 @@ export const useKeyboardEvents = (
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Send per-key press event
|
||||
const hidKey = keys[code];
|
||||
if (hidKey !== undefined) {
|
||||
sendKeypress(hidKey, true);
|
||||
}
|
||||
|
||||
// Still update the full state for legacy compatibility and UI display
|
||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||
}, [handleModifierKeys, remapCode, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, pasteShortcutEnabled, pasteShortcut, pasteCaptureRef, isReinitializingGadget, isOcrMode]);
|
||||
}, [handleModifierKeys, remapCode, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, pasteShortcutEnabled, pasteShortcut, pasteCaptureRef, isReinitializingGadget, isOcrMode]);
|
||||
|
||||
const keyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||
if (isOcrMode) return;
|
||||
@@ -124,15 +122,8 @@ export const useKeyboardEvents = (
|
||||
prev.activeModifiers.filter(k => k !== modifiers[code]),
|
||||
);
|
||||
|
||||
// Send per-key release event
|
||||
const hidKey = keys[code];
|
||||
if (hidKey !== undefined) {
|
||||
sendKeypress(hidKey, false);
|
||||
}
|
||||
|
||||
// Still update the full state for legacy compatibility and UI display
|
||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||
}, [handleModifierKeys, remapCode, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, isOcrMode, isReinitializingGadget]);
|
||||
}, [handleModifierKeys, remapCode, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, isOcrMode, isReinitializingGadget]);
|
||||
|
||||
const setupKeyboardEvents = useCallback(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -34,7 +34,9 @@ export const useMouseEvents = (
|
||||
if (!force && settings.mouseMode !== "relative") return;
|
||||
// Don't send mouse events while reinitializing gadget
|
||||
if (isReinitializingGadget) return;
|
||||
send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
|
||||
const dx = calcDelta(x);
|
||||
const dy = calcDelta(y);
|
||||
send("relMouseReport", { dx, dy, buttons });
|
||||
setMouseMove({ x, y, buttons });
|
||||
},
|
||||
[send, setMouseMove, settings.mouseMode, isReinitializingGadget],
|
||||
|
||||
@@ -462,11 +462,6 @@ export default function PCHome() {
|
||||
setDiskChannel(diskDataChannel);
|
||||
};
|
||||
|
||||
const hidDataChannel = pc.createDataChannel("hid");
|
||||
hidDataChannel.onopen = () => {
|
||||
useRTCStore.getState().setHidChannel(hidDataChannel);
|
||||
};
|
||||
|
||||
setPeerConnection(pc);
|
||||
}, [
|
||||
forceHttp,
|
||||
|
||||
92
usb.go
92
usb.go
@@ -6,9 +6,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"kvm/internal/hidrpc"
|
||||
"kvm/internal/usbgadget"
|
||||
)
|
||||
|
||||
@@ -40,35 +37,13 @@ func initUsbGadget() {
|
||||
go func() {
|
||||
for {
|
||||
checkUSBState()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
time.Sleep(2500 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||
|
||||
// Send HID-RPC LED state message
|
||||
if currentSession.HidChannel != nil {
|
||||
var raw byte
|
||||
if state.NumLock {
|
||||
raw |= usbgadget.KeyboardLedMaskNumLock
|
||||
}
|
||||
if state.CapsLock {
|
||||
raw |= usbgadget.KeyboardLedMaskCapsLock
|
||||
}
|
||||
if state.ScrollLock {
|
||||
raw |= usbgadget.KeyboardLedMaskScrollLock
|
||||
}
|
||||
if state.Compose {
|
||||
raw |= usbgadget.KeyboardLedMaskCompose
|
||||
}
|
||||
if state.Kana {
|
||||
raw |= usbgadget.KeyboardLedMaskKana
|
||||
}
|
||||
ledData := hidrpc.MarshalKeyboardLedState(raw)
|
||||
currentSession.HidChannel.Send(ledData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -144,49 +119,6 @@ func rpcKeypressKeepAlive() error {
|
||||
return gadget.KeypressKeepAlive()
|
||||
}
|
||||
|
||||
func handleHidChannel(d *webrtc.DataChannel, session *Session) {
|
||||
hidServer := hidrpc.NewServer(&hidRpcHandler{session: session})
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err := hidServer.HandleMessage(msg.Data); err != nil {
|
||||
usbLogger.Warn().Err(err).Msg("HID-RPC message handling error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type hidRpcHandler struct {
|
||||
session *Session
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleHandshake(version byte) error {
|
||||
if h.session.HidChannel != nil {
|
||||
handshakeData := hidrpc.MarshalHandshake(version)
|
||||
return h.session.HidChannel.Send(handshakeData)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleKeyboardReport(modifier byte, keys []byte) error {
|
||||
return rpcKeyboardReport(modifier, keys)
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleKeypressReport(key byte, press bool) error {
|
||||
return rpcKeypressReport(key, press)
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleKeypressKeepAlive() error {
|
||||
return rpcKeypressKeepAlive()
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleKeyboardMacroReport(data []byte) error {
|
||||
// TODO: Implement macro handling
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hidRpcHandler) HandleCancelKeyboardMacro() error {
|
||||
// TODO: Implement macro cancellation
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcAbsMouseReport(x, y int, buttons uint8) error {
|
||||
return gadget.AbsMouseReport(x, y, buttons)
|
||||
}
|
||||
@@ -282,28 +214,6 @@ func rpcReinitializeUsbGadget() error {
|
||||
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||
|
||||
// Send HID-RPC LED state message
|
||||
if currentSession.HidChannel != nil {
|
||||
var raw byte
|
||||
if state.NumLock {
|
||||
raw |= usbgadget.KeyboardLedMaskNumLock
|
||||
}
|
||||
if state.CapsLock {
|
||||
raw |= usbgadget.KeyboardLedMaskCapsLock
|
||||
}
|
||||
if state.ScrollLock {
|
||||
raw |= usbgadget.KeyboardLedMaskScrollLock
|
||||
}
|
||||
if state.Compose {
|
||||
raw |= usbgadget.KeyboardLedMaskCompose
|
||||
}
|
||||
if state.Kana {
|
||||
raw |= usbgadget.KeyboardLedMaskKana
|
||||
}
|
||||
ledData := hidrpc.MarshalKeyboardLedState(raw)
|
||||
currentSession.HidChannel.Send(ledData)
|
||||
}
|
||||
}
|
||||
})
|
||||
gadget.SetOnHidDeviceMissing(func(device string, err error) {
|
||||
|
||||
@@ -22,7 +22,6 @@ type Session struct {
|
||||
//AudioTrack *webrtc.TrackLocalStaticSample
|
||||
ControlChannel *webrtc.DataChannel
|
||||
RPCChannel *webrtc.DataChannel
|
||||
HidChannel *webrtc.DataChannel
|
||||
DiskChannel *webrtc.DataChannel
|
||||
shouldUmountVirtualMedia bool
|
||||
}
|
||||
@@ -142,9 +141,6 @@ func newSession(sessionConfig SessionConfig) (*Session, error) {
|
||||
handleTerminalChannel(d)
|
||||
case "serial":
|
||||
handleSerialChannel(d)
|
||||
case "hid":
|
||||
session.HidChannel = d
|
||||
go handleHidChannel(d, session)
|
||||
default:
|
||||
if strings.HasPrefix(d.Label(), uploadIdPrefix) {
|
||||
go handleUploadChannel(d)
|
||||
|
||||
Reference in New Issue
Block a user