Skip to content

usb_hid: honor a boot protocol request that arrives after startup - #11282

Open
mikeysklar wants to merge 3 commits into
adafruit:mainfrom
mikeysklar:fix-1136-boot-hid
Open

usb_hid: honor a boot protocol request that arrives after startup#11282
mikeysklar wants to merge 3 commits into
adafruit:mainfrom
mikeysklar:fix-1136-boot-hid

Conversation

@mikeysklar

@mikeysklar mikeysklar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

In boot protocol the boot device sends without a report ID, and other devices do not send.

Why

usb_hid_setup_devices() swaps in the boot device only at VM start, so a later SET_PROTOCOL(boot) is missed.

Reports keep their report ID, and a boot-protocol host reads that byte as modifiers.

A phantom modifier results: 01 reads as left Ctrl, 03 from CONSUMER_CONTROL as Ctrl+Shift. Fixes #1136.

Hardware tested

Feather RP2040. Real SET_PROTOCOL(boot) over usbfs on Ubuntu 26.04.1, three trials per build.

in boot protocol baseline d897c15 this branch
keyboard 9 bytes, 01 prefix 8 bytes
consumer control 3 bytes, 03 prefix not sent

Report protocol is byte-identical before and after, and the default configuration still enumerates and runs.

Also checked at a macOS pre-boot screen on an M2, same either way, so those reports have another cause.

Not tested against a real BIOS, GRUB or KVM.

AI assistance

Written with an LLM agent (Claude). I ran the boards myself; numbers are from the usbfs harness.

@dhalbert

dhalbert commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

I worked on this a lot years ago, as you can tell. I am surprised you need a new report descriptor without the Report ID slot. Part of the point of boot keyboard and boot mouse is that the descriptor sent by the device is ignored. Instead the host assumes the standard descriptors, as described in https://www.usb.org/sites/default/files/hid1_12.pdf in the boot devices sections

So is this a work-around for something that Macs are not doing properly in their "BIOS"? I think the original code solved the issue on PC's.

@mikeysklar

Copy link
Copy Markdown
Collaborator Author

You're right about the descriptor. The bug is timing: the swap runs only at VM start, so a later SET_PROTOCOL is missed. Not Mac specific. I'll cut it to that.

@mikeysklar mikeysklar changed the title usb_hid: drop the report-ID prefix for a sole boot keyboard or mouse usb_hid: honor a boot protocol request that arrives after startup Sep 1, 2026
@mikeysklar

mikeysklar commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Found a second bug in my own fix, fixed in 2d2ccf2. With two devices enabled only the boot
device lost its report ID, so the other one still put its ID in the host's modifier byte.
CONSUMER_CONTROL sent 03 01 00, read as Ctrl+Shift held.

Testing is a real SET_PROTOCOL(boot) over usbfs now, not simulated. Feather RP2040,
Ubuntu 26.04.1, three trials per build.

host in boot protocol keyboard consumer control
b697e83 8 bytes 3 bytes, 03 prefix
2d2ccf2 8 bytes not sent

Single boot keyboard, baseline d897c15 stays at 9 bytes where b697e83 drops to 8.

Not Mac specific. The original code does work on PCs, but only when the request lands
before code.py starts. This is the case where it arrives after.

mikeysklar and others added 2 commits September 2, 2026 19:22
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 adafruit#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.
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) <noreply@anthropic.com>
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 <noreply@anthropic.com>
@mikeysklar

Copy link
Copy Markdown
Collaborator Author

Yes. Tested the default HID devices (no boot_device) on a Feather RP2040 under Ubuntu. Reports are identical before and after the change, in both report protocol and after the host sends SET_PROTOCOL(boot). No regression. Also added a small guard so the new code only runs when a boot device is enabled, in 3b30ce4.

@tannewt
tannewt requested a review from dhalbert September 3, 2026 17:04
lynt-smitka added a commit to MakerClassCZ/circuitpython that referenced this pull request Sep 4, 2026
`python-version: 3.x` resolves to 3.14 now. Many of the Zephyr build
requirements have no 3.14 wheels yet, so `west packages pip --install`
builds 16 of them from source (grpcio_tools, ruamel.yaml.clib, esptool,
docopt, ...) in every zephyr-cp board job: 303 s, against 33 s for the
same command in the zephyr-tests job, which already pins 3.13.

Pin 3.13 for the zephyr-cp port only; the other ports install their
Python dependencies from the cached venv and are left on 3.x.

Measured on the Build CI run of adafruit#11282 (job "ports (zephyr-cp) / board
(nordic_nrf54l15dk)" against "zephyr-tests / zephyr"): 29 zephyr-cp
board jobs per wide PR, about 4.5 minutes each.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to use HID keyboard in boot OS (macOS boot screen)

2 participants