From b697e8363cbb5c0120d68912182bb81494ae2335 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 30 Aug 2026 20:41:51 -0700 Subject: [PATCH 1/3] usb_hid: honor a boot protocol request that arrives after startup usb_hid_setup_devices() swaps in the boot keyboard or mouse, whose report ID is 0, but it only runs from usb_setup_with_vm() at VM start. A SET_PROTOCOL(boot) arriving while code.py is already running is not acted on until the next VM restart, so reports keep their report-ID prefix while the host is reading them as 8-byte boot reports. That matches bitboy85's report in #1136: get_boot_device() returns 1 yet a phantom left Ctrl is held, because the 0x01 prefix lands in the modifier byte. Check tud_hid_get_protocol() in send_report() instead, and drop the report ID while the host has the interface in boot protocol. Measured on a Metro RP2040, with the host request simulated by setting TinyUSB's protocol_mode over SWD: before, reports stay 9 bytes after the switch; after, they become 8 bytes on the next send. Default HID configuration is unchanged. --- shared-module/usb_hid/Device.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shared-module/usb_hid/Device.c b/shared-module/usb_hid/Device.c index 42f5790123e..d9fea3d562e 100644 --- a/shared-module/usb_hid/Device.c +++ b/shared-module/usb_hid/Device.c @@ -119,6 +119,16 @@ void common_hal_usb_hid_device_send_report(usb_hid_device_obj_t *self, uint8_t * mp_arg_validate_length(len, self->in_report_lengths[id_idx], MP_QSTR_report); + // The host can enter boot protocol mid-run, after the setup-time swap. + const uint8_t boot_device = usb_hid_boot_device(); + if (report_id != 0 && + self->usage_page == HID_USAGE_PAGE_DESKTOP && + ((boot_device == 1 && self->usage == HID_USAGE_DESKTOP_KEYBOARD) || + (boot_device == 2 && self->usage == HID_USAGE_DESKTOP_MOUSE)) && + tud_hid_get_protocol() == HID_PROTOCOL_BOOT) { + report_id = 0; + } + // Wait until interface is ready, timeout = 2 seconds uint64_t end_ticks = supervisor_ticks_ms64() + 2000; while ((supervisor_ticks_ms64() < end_ticks) && !tud_hid_ready()) { From 2d2ccf2cc9eb6b212d75c00807fb1ea258885b09 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 2 Sep 2026 19:16:25 -0700 Subject: [PATCH 2/3] usb_hid: do not send non-boot device reports in boot protocol In boot protocol the host reads the fixed 8-byte boot report and ignores every other device on the interface. send_report() stripped the report ID for the boot device but still sent the other devices' reports, so a second enabled device put its report ID in the host's modifier byte. Enabling KEYBOARD and CONSUMER_CONTROL together sent 03 01 00, read as Ctrl+Shift held. usb_hid_setup_devices() already replaces the device tuple when the host asks before code.py starts. Do the same at send time for a request that arrives later. Also sample the protocol after the ready wait rather than before it. hidd_reset() zeroes the interface and HID_PROTOCOL_BOOT is 0, so a report starting during a bus reset could read BOOT, wait two seconds, then send unprefixed into an interface that had re-enumerated back into report protocol. Measured on an Adafruit Feather RP2040, host Ubuntu 26.04.1, using a real SET_PROTOCOL(boot) control transfer over usbfs. Consumer control reports seen while the host held boot protocol: 20 per trial before, 0 after, three trials each. Co-Authored-By: Claude Opus 5 (1M context) --- shared-bindings/usb_hid/Device.c | 4 ++++ shared-bindings/usb_hid/__init__.c | 4 ++++ shared-module/usb_hid/Device.c | 23 +++++++++++++---------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/shared-bindings/usb_hid/Device.c b/shared-bindings/usb_hid/Device.c index 66c9c01ad6e..aa8af56a185 100644 --- a/shared-bindings/usb_hid/Device.c +++ b/shared-bindings/usb_hid/Device.c @@ -156,6 +156,10 @@ static mp_obj_t usb_hid_device_make_new(const mp_obj_type_t *type, size_t n_args //| The ``report`` itself will be discarded, to prevent unwanted extraneous characters, //| mouse clicks, etc. //| +//| If the host has put the interface in boot protocol, the boot device sends its report +//| without a report ID, and `send_report()` on any other device discards the report, +//| because the host reads only the boot device. +//| //| Note: Host operating systems allow enabling and disabling specific devices //| and kinds of devices to do wakeup. //| The defaults are different for different operating systems. diff --git a/shared-bindings/usb_hid/__init__.c b/shared-bindings/usb_hid/__init__.c index 47f7f03c493..1bedaf3edf0 100644 --- a/shared-bindings/usb_hid/__init__.c +++ b/shared-bindings/usb_hid/__init__.c @@ -25,6 +25,10 @@ //| the `devices` tuple is **replaced** when ``code.py`` starts with a single-element tuple //| containing a `Device` that describes the boot device chosen (keyboard or mouse). //| The request for a boot device overrides any other HID devices. +//| +//| If the host requests a boot device after ``code.py`` has started, `devices` is not replaced. +//| The boot device then sends reports in the boot format, with no report ID, and +//| `Device.send_report()` on any other device does nothing, because the host ignores it. //| """ //| //| diff --git a/shared-module/usb_hid/Device.c b/shared-module/usb_hid/Device.c index d9fea3d562e..ffdda7377dd 100644 --- a/shared-module/usb_hid/Device.c +++ b/shared-module/usb_hid/Device.c @@ -119,16 +119,6 @@ void common_hal_usb_hid_device_send_report(usb_hid_device_obj_t *self, uint8_t * mp_arg_validate_length(len, self->in_report_lengths[id_idx], MP_QSTR_report); - // The host can enter boot protocol mid-run, after the setup-time swap. - const uint8_t boot_device = usb_hid_boot_device(); - if (report_id != 0 && - self->usage_page == HID_USAGE_PAGE_DESKTOP && - ((boot_device == 1 && self->usage == HID_USAGE_DESKTOP_KEYBOARD) || - (boot_device == 2 && self->usage == HID_USAGE_DESKTOP_MOUSE)) && - tud_hid_get_protocol() == HID_PROTOCOL_BOOT) { - report_id = 0; - } - // Wait until interface is ready, timeout = 2 seconds uint64_t end_ticks = supervisor_ticks_ms64() + 2000; while ((supervisor_ticks_ms64() < end_ticks) && !tud_hid_ready()) { @@ -140,6 +130,19 @@ void common_hal_usb_hid_device_send_report(usb_hid_device_obj_t *self, uint8_t * mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("USB busy")); } + if (tud_hid_get_protocol() == HID_PROTOCOL_BOOT) { + // In boot protocol the host reads the fixed boot report, which carries no report ID, + // and ignores every device other than the boot device. + uint8_t boot_device = usb_hid_boot_device(); + if (self->usage_page == HID_USAGE_PAGE_DESKTOP && + ((boot_device == 1 && self->usage == HID_USAGE_DESKTOP_KEYBOARD) || + (boot_device == 2 && self->usage == HID_USAGE_DESKTOP_MOUSE))) { + report_id = 0; + } else { + return; + } + } + if (!tud_hid_report(report_id, report, len)) { mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("USB error")); } From 3b30ce4e535e4b69f4a773c7fc86cacfdb0fef6b Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 3 Sep 2026 08:36:24 -0700 Subject: [PATCH 3/3] usb_hid: skip the boot protocol path when no boot device is enabled With boot_device=0 there is no boot device to match, so a boot protocol request would have silently dropped every report. Co-Authored-By: Claude Fable 5.1 --- shared-module/usb_hid/Device.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-module/usb_hid/Device.c b/shared-module/usb_hid/Device.c index ffdda7377dd..adcea84904e 100644 --- a/shared-module/usb_hid/Device.c +++ b/shared-module/usb_hid/Device.c @@ -130,10 +130,10 @@ void common_hal_usb_hid_device_send_report(usb_hid_device_obj_t *self, uint8_t * mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("USB busy")); } - if (tud_hid_get_protocol() == HID_PROTOCOL_BOOT) { + const uint8_t boot_device = usb_hid_boot_device(); + if (boot_device != 0 && tud_hid_get_protocol() == HID_PROTOCOL_BOOT) { // In boot protocol the host reads the fixed boot report, which carries no report ID, // and ignores every device other than the boot device. - uint8_t boot_device = usb_hid_boot_device(); if (self->usage_page == HID_USAGE_PAGE_DESKTOP && ((boot_device == 1 && self->usage == HID_USAGE_DESKTOP_KEYBOARD) || (boot_device == 2 && self->usage == HID_USAGE_DESKTOP_MOUSE))) {