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:
luckfox-eng29
2026-05-15 18:43:04 +08:00
parent 18f7d8425f
commit b1090c9493
13 changed files with 170 additions and 519 deletions

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"reflect" "reflect"
"strings" "strings"
"time" "time"
@@ -132,10 +131,10 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
return u.keyboardState return u.keyboardState
} }
func (u *UsbGadget) listenKeyboardEvents() { func (u *UsbGadget) listenKeyboardEvents(ctx context.Context, file *os.File) {
var path string var path string
if u.keyboardHidFile != nil { if file != nil {
path = u.keyboardHidFile.Name() path = file.Name()
} }
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger() l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
l.Trace().Msg("starting") l.Trace().Msg("starting")
@@ -144,12 +143,12 @@ func (u *UsbGadget) listenKeyboardEvents() {
buf := make([]byte, hidReadBufferSize) buf := make([]byte, hidReadBufferSize)
for { for {
select { select {
case <-u.keyboardStateCtx.Done(): case <-ctx.Done():
l.Info().Msg("context done") l.Info().Msg("context done")
return return
default: default:
l.Trace().Msg("reading from keyboard") l.Trace().Msg("reading from keyboard")
if u.keyboardHidFile == nil { if file == nil {
u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil") u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
// show the error every 100 times to avoid spamming the logs // show the error every 100 times to avoid spamming the logs
time.Sleep(time.Second) time.Sleep(time.Second)
@@ -158,16 +157,26 @@ func (u *UsbGadget) listenKeyboardEvents() {
// reset the counter // reset the counter
u.resetLogSuppressionCounter("keyboardHidFileNil") u.resetLogSuppressionCounter("keyboardHidFileNil")
n, err := u.keyboardHidFile.Read(buf) n, err := file.Read(buf)
if err != nil { 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") 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") u.resetLogSuppressionCounter("keyboardHidFileRead")
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
if n != 1 { if n < 1 {
l.Trace().Int("n", n).Msg("expected 1 byte, got") l.Info().Int("n", n).Msg("expected at least 1 byte, got 0")
continue continue
} }
u.updateKeyboardState(buf[0]) 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 { 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 return nil
} }
var err error file, err := openWithTimeout("/dev/hidg0", os.O_RDWR, 0666, 3*time.Second)
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
if err != nil { 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") { 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(). u.log.Error().
@@ -197,34 +245,62 @@ func (u *UsbGadget) openKeyboardHidFile() error {
return fmt.Errorf("failed to open hidg0: %w", err) return fmt.Errorf("failed to open hidg0: %w", err)
} }
if u.keyboardStateCancel != nil { ctx, cancel := context.WithCancel(context.Background())
u.keyboardStateCancel() u.keyboardHidFile = file
} u.keyboardStateCtx = ctx
u.keyboardStateCancel = cancel
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background()) u.listenKeyboardEvents(ctx, file)
u.listenKeyboardEvents()
return nil 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 { func (u *UsbGadget) OpenKeyboardHidFile() error {
return u.openKeyboardHidFile() return u.openKeyboardHidFile()
} }
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { func (u *UsbGadget) ReopenKeyboardHidFile() error {
var parts []string return u.reopenKeyboardHidFile()
for _, b := range data { }
parts = append(parts, fmt.Sprintf("\\x%02x", b))
}
hexString := strings.Join(parts, "")
cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -n -e '%s' > /dev/hidg0", hexString)) func (u *UsbGadget) keyboardWriteHidFileLocked(modifier byte, keys []byte) error {
err := cmd.Run() 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 { if err != nil {
u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
u.closeKeyboardHidFileLocked()
return err return err
} }
u.resetLogSuppressionCounter("keyboardWriteHidFile") u.resetLogSuppressionCounter("keyboardWriteHidFile")
u.resetUserInputTime()
return nil return nil
} }
@@ -369,23 +445,6 @@ func (u *UsbGadget) KeypressKeepAlive() error {
return nil 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 { func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
u.keyboardLock.Lock() u.keyboardLock.Lock()
defer u.keyboardLock.Unlock() defer u.keyboardLock.Unlock()

View File

@@ -2,10 +2,13 @@ package usbgadget
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -107,3 +110,36 @@ func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
u.logSuppressionCounter[counterName] = 0 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
}

View File

@@ -270,6 +270,7 @@ function KeyboardWrapper() {
setIsCapsLockActive(false); setIsCapsLockActive(false);
} }
sendKeyboardEvent([keys["CapsLock"]], []); sendKeyboardEvent([keys["CapsLock"]], []);
setTimeout(resetKeyboardState, 100);
return; return;
} }
} }

View File

@@ -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)),
};
}

View File

@@ -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,
};
}

View File

@@ -3,12 +3,10 @@ import { useCallback, useEffect, useRef } from "react";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { useHidStore, useRTCStore, useSettingsStore } from "@/hooks/stores"; import { useHidStore, useRTCStore, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidRpc } from "@/hooks/useHidRpc";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() { export default function useKeyboard() {
const [send] = useJsonRpc(); const [send] = useJsonRpc();
const { rpcHidReady, reportKeypressEvent, reportKeyboardEvent, reportKeypressKeepAlive } = useHidRpc();
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const forceHttp = useSettingsStore(state => state.forceHttp); const forceHttp = useSettingsStore(state => state.forceHttp);
@@ -30,80 +28,45 @@ export default function useKeyboard() {
if (usbState !== "configured") return; if (usbState !== "configured") return;
const accModifier = modifiers.reduce((acc, val) => acc + val, 0); const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
// Try HID-RPC first // Fallback to JSON-RPC
if (rpcHidReady && !forceHttp) { send("keyboardReport", { keys, modifier: accModifier }, resp => {
reportKeyboardEvent(accModifier, keys); if ("error" in resp) {
} else { const msg = (resp.error.data as string) || resp.error.message || "";
// Fallback to JSON-RPC if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
send("keyboardReport", { keys, modifier: accModifier }, resp => { notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
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 // We do this for the info bar to display the currently pressed keys for the user
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers }); 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( const sendKeypress = useCallback(
(key: number, press: boolean) => { (key: number, press: boolean) => {
if (isReinitializingGadget || usbState !== "configured") return; if (isReinitializingGadget || usbState !== "configured") return;
if (rpcHidReady && !forceHttp) { // Legacy: simulate device-side key handling
reportKeypressEvent(key, press); // This maintains the 6-key buffer on the frontend for legacy compatibility
// For simplicity in migration, we fall back to full state reports
// Track held keys for keepalive const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
if (press) { sendKeyboardEvent(press ? [key] : [], [modifier]);
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]);
}
}, },
[rpcHidReady, forceHttp, reportKeypressEvent, reportKeypressKeepAlive, isReinitializingGadget, usbState, sendKeyboardEvent] [isReinitializingGadget, usbState, sendKeyboardEvent]
); );
const resetKeyboardState = useCallback(() => { const resetKeyboardState = useCallback(() => {
// Release all held keys // Release all held keys
if (rpcHidReady && !forceHttp) { sendKeyboardEvent([], []);
heldKeysRef.current.forEach(key => {
reportKeypressEvent(key, false);
});
} else {
sendKeyboardEvent([], []);
}
heldKeysRef.current.clear(); heldKeysRef.current.clear();
if (keepaliveIntervalRef.current) { if (keepaliveIntervalRef.current) {
clearInterval(keepaliveIntervalRef.current); clearInterval(keepaliveIntervalRef.current);
keepaliveIntervalRef.current = null; keepaliveIntervalRef.current = null;
} }
}, [rpcHidReady, forceHttp, reportKeypressEvent, sendKeyboardEvent]); }, [sendKeyboardEvent]);
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {

View File

@@ -10,7 +10,7 @@ export const useKeyboardEvents = (
pasteCaptureRef?: React.RefObject<HTMLTextAreaElement>, pasteCaptureRef?: React.RefObject<HTMLTextAreaElement>,
isReinitializingGadget?: boolean isReinitializingGadget?: boolean
) => { ) => {
const { sendKeyboardEvent, sendKeypress, resetKeyboardState } = useKeyboard(); const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore(); const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore();
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
@@ -65,6 +65,11 @@ export const useKeyboardEvents = (
} }
if (isReinitializingGadget) return; if (isReinitializingGadget) return;
if (e.repeat) {
e.preventDefault();
return;
}
e.preventDefault(); e.preventDefault();
const prev = useHidStore.getState(); const prev = useHidStore.getState();
let code = e.code; let code = e.code;
@@ -94,15 +99,8 @@ export const useKeyboardEvents = (
}, 10); }, 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)]); 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) => { const keyUpHandler = useCallback((e: KeyboardEvent) => {
if (isOcrMode) return; if (isOcrMode) return;
@@ -124,15 +122,8 @@ export const useKeyboardEvents = (
prev.activeModifiers.filter(k => k !== modifiers[code]), 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)]); 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 setupKeyboardEvents = useCallback(() => {
const abortController = new AbortController(); const abortController = new AbortController();

View File

@@ -34,7 +34,9 @@ export const useMouseEvents = (
if (!force && settings.mouseMode !== "relative") return; if (!force && settings.mouseMode !== "relative") return;
// Don't send mouse events while reinitializing gadget // Don't send mouse events while reinitializing gadget
if (isReinitializingGadget) return; 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 }); setMouseMove({ x, y, buttons });
}, },
[send, setMouseMove, settings.mouseMode, isReinitializingGadget], [send, setMouseMove, settings.mouseMode, isReinitializingGadget],

View File

@@ -462,11 +462,6 @@ export default function PCHome() {
setDiskChannel(diskDataChannel); setDiskChannel(diskDataChannel);
}; };
const hidDataChannel = pc.createDataChannel("hid");
hidDataChannel.onopen = () => {
useRTCStore.getState().setHidChannel(hidDataChannel);
};
setPeerConnection(pc); setPeerConnection(pc);
}, [ }, [
forceHttp, forceHttp,

92
usb.go
View File

@@ -6,9 +6,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/pion/webrtc/v4"
"kvm/internal/hidrpc"
"kvm/internal/usbgadget" "kvm/internal/usbgadget"
) )
@@ -40,35 +37,13 @@ func initUsbGadget() {
go func() { go func() {
for { for {
checkUSBState() checkUSBState()
time.Sleep(500 * time.Millisecond) time.Sleep(2500 * time.Millisecond)
} }
}() }()
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) { gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
if currentSession != nil { if currentSession != nil {
writeJSONRPCEvent("keyboardLedState", state, currentSession) 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() 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 { func rpcAbsMouseReport(x, y int, buttons uint8) error {
return gadget.AbsMouseReport(x, y, buttons) return gadget.AbsMouseReport(x, y, buttons)
} }
@@ -282,28 +214,6 @@ func rpcReinitializeUsbGadget() error {
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) { gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
if currentSession != nil { if currentSession != nil {
writeJSONRPCEvent("keyboardLedState", state, currentSession) 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) { gadget.SetOnHidDeviceMissing(func(device string, err error) {

View File

@@ -22,7 +22,6 @@ type Session struct {
//AudioTrack *webrtc.TrackLocalStaticSample //AudioTrack *webrtc.TrackLocalStaticSample
ControlChannel *webrtc.DataChannel ControlChannel *webrtc.DataChannel
RPCChannel *webrtc.DataChannel RPCChannel *webrtc.DataChannel
HidChannel *webrtc.DataChannel
DiskChannel *webrtc.DataChannel DiskChannel *webrtc.DataChannel
shouldUmountVirtualMedia bool shouldUmountVirtualMedia bool
} }
@@ -142,9 +141,6 @@ func newSession(sessionConfig SessionConfig) (*Session, error) {
handleTerminalChannel(d) handleTerminalChannel(d)
case "serial": case "serial":
handleSerialChannel(d) handleSerialChannel(d)
case "hid":
session.HidChannel = d
go handleHidChannel(d, session)
default: default:
if strings.HasPrefix(d.Label(), uploadIdPrefix) { if strings.HasPrefix(d.Label(), uploadIdPrefix) {
go handleUploadChannel(d) go handleUploadChannel(d)