Compare commits

...

3 Commits

Author SHA1 Message Date
Marc Brooks 3818ac3df1
Merge 0321a7473a into 703625d59a 2025-09-25 23:39:13 +02:00
Marc Brooks 0321a7473a
Move the HID keyboard descriptor LED state
Seems to interfere with boot mode
2025-09-25 16:36:28 -05:00
Marc Brooks 704d65bde6
Reduce traffic during pastes
Suspend KeyDownMessages while processing a macro.
Make sure we don't emit huge debugging traces.
Allow 30 seconds for RPC to finish (not ideal)
Reduced default delay between keys (and allow as low as 0)
2025-09-25 12:25:10 -05:00
5 changed files with 67 additions and 25 deletions

View File

@ -65,15 +65,17 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
func onHidMessage(msg hidQueueMessage, session *Session) {
data := msg.Data
dataLen := len(data)
scopedLogger := hidRPCLogger.With().
Str("channel", msg.channel).
Bytes("data", data).
Int("data_len", dataLen).
Bytes("data", data[:min(dataLen, 32)]).
Logger()
scopedLogger.Debug().Msg("HID RPC message received")
if len(data) < 1 {
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
if dataLen < 1 {
scopedLogger.Warn().Msg("received empty data in HID RPC message handler")
return
}
@ -96,7 +98,7 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
r <- nil
}()
select {
case <-time.After(1 * time.Second):
case <-time.After(30 * time.Second):
scopedLogger.Warn().Msg("HID RPC message timed out")
case <-r:
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")

View File

@ -31,6 +31,8 @@ var keyboardReportDesc = []byte{
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
0x09, 0x06, /* USAGE (Keyboard) */
0xa1, 0x01, /* COLLECTION (Application) */
/* 8 modifier bits */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
@ -39,27 +41,47 @@ var keyboardReportDesc = []byte{
0x75, 0x01, /* REPORT_SIZE (1) */
0x95, 0x08, /* REPORT_COUNT (8) */
0x81, 0x02, /* INPUT (Data,Var,Abs) */
/* 8 bits of padding */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
/* 6 key codes for the 104 key keyboard */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
/* LED report 5 bits for Num Lock through Kana */
0x95, 0x05, /* REPORT_COUNT (5) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
/* 1 bit of padding for the Power LED (ignored) */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (3) */
0x75, 0x03, /* REPORT_SIZE (1) */
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
/* LED report 1 bit for Shift */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x07, /* USAGE_MINIMUM (Shift) */
0x29, 0x07, /* USAGE_MAXIMUM (Shift) */
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
/* 1 bit of padding for the rest of the byte */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (1) */
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
0xc0, /* END_COLLECTION */
}
@ -153,6 +175,16 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
u.onKeysDownChange = &f
}
var suspendedKeyDownMessages bool = false
func (u *UsbGadget) SuspendKeyDownMessages() {
suspendedKeyDownMessages = true
}
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
suspendedKeyDownMessages = false
}
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
u.onKeepAliveReset = &f
}
@ -169,9 +201,9 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) {
}
// TODO: make this configurable
// We currently hardcode the duration to 100ms
// We currently hardcode the duration to the default of 100ms
// However, it should be the same as the duration of the keep-alive reset called baseExtension.
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() {
u.kbdAutoReleaseTimers[key] = time.AfterFunc(DefaultAutoReleaseDuration, func() {
u.performAutoRelease(key)
})
}
@ -314,6 +346,7 @@ var keyboardWriteHidFileLock sync.Mutex
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
keyboardWriteHidFileLock.Lock()
defer keyboardWriteHidFileLock.Unlock()
if err := u.openKeyboardHidFile(); err != nil {
return err
}
@ -353,7 +386,7 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
u.keysDownState = state
u.keyboardStateLock.Unlock()
if u.onKeysDownChange != nil {
if u.onKeysDownChange != nil && !suspendedKeyDownMessages {
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
}
return state
@ -484,6 +517,10 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
}
err := u.keyboardWriteHidFile(modifier, keys)
if err != nil {
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
}
return u.UpdateKeysDown(modifier, keys), err
}

View File

@ -1132,7 +1132,11 @@ func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool {
}
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error {
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro")
logger.Debug().Int("macro_steps", len(macro)).Msg("Executing keyboard macro")
// don't report keyboard state changes while executing the macro
gadget.SuspendKeyDownMessages()
defer gadget.ResumeSuspendKeyDownMessages()
for i, step := range macro {
delay := time.Duration(step.Delay) * time.Millisecond
@ -1153,7 +1157,8 @@ func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacro
case <-time.After(delay):
// Sleep completed normally
case <-ctx.Done():
// make sure keyboard state is reset
// make sure keyboard state is reset and the client gets notified
gadget.ResumeSuspendKeyDownMessages()
err := rpcKeyboardReport(0, keyboardClearStateKeys)
if err != nil {
logger.Warn().Err(err).Msg("failed to reset keyboard state")

View File

@ -188,18 +188,18 @@ export default function PasteModal() {
type="number"
label="Delay between keys"
placeholder="Delay between keys"
min={50}
min={0}
max={65534}
value={delayValue}
onChange={e => {
setDelayValue(parseInt(e.target.value, 10));
}}
/>
{delayValue < 50 || delayValue > 65534 && (
{delayValue < defaultDelay || delayValue > 65534 && (
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
Delay must be between 50 and 65534
Delay should be between 20 and 65534
</span>
</div>
)}

View File

@ -277,7 +277,6 @@ export default function useKeyboard() {
cancelKeepAlive();
}, [cancelKeepAlive]);
// executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay.
// The keys and modifiers are pressed together and held for the delay duration.
@ -292,9 +291,7 @@ export default function useKeyboard() {
for (const [_, step] of steps.entries()) {
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
const modifierMask: number = (step.modifiers || [])
.map(mod => modifiers[mod])
.reduce((acc, val) => acc + val, 0);
// If the step has keys and/or modifiers, press them and hold for the delay
@ -306,6 +303,7 @@ export default function useKeyboard() {
sendKeyboardMacroEventHidRpc(macro);
}, [sendKeyboardMacroEventHidRpc]);
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
const promises: (() => Promise<void>)[] = [];