embassy-usb HID: `RequestHandler` cannot restore Report protocol on USB bus reset
Summary
The HID class delegates protocol-mode state (Boot/Report) to
RequestHandler::get_protocol / set_protocol, but the handler is never
notified of a USB bus reset.
HID 1.11 Appendix F.3 requires a Boot Keyboard to return on reset to the non-boot protocol described by its Report descriptor, i.e. Report protocol. Section 7.2.6 separately specifies Report protocol as the initialization default.
Consequently, a handler that accepts SET_PROTOCOL(Boot) and stores the
selected mode has no trait-level way to restore Report mode on bus reset:
after SET_PROTOCOL(Boot) followed by a bus reset, GET_PROTOCOL still
reports Boot. The default handler does not expose the problem because it
rejects Boot mode and always reports Report mode.
Environment / versions
- embassy-usb 0.6.0 (crates.io)
mainate4b4ba6(checked 2026-08-31)
Both have the same behavior.
Details
In both versions, Control::reset() only resets out_report_offset:
fn reset(&mut self) {
self.out_report_offset.store(0, Ordering::Release);
}It does not notify request_handler, and RequestHandler exposes no USB
bus-reset or reinitialization callback.
At the same time, HID_REQ_SET_PROTOCOL and HID_REQ_GET_PROTOCOL are
routed to RequestHandler::set_protocol / get_protocol, so for
implementations that support switching modes, the authoritative protocol
state has to live on the user side.
The in-tree HID keyboard/mouse examples likewise keep the selected mode in
an AtomicU8 accessed by their RequestHandler, so this is not specific
to one downstream's storage choice.
Reproduction
- Implement
RequestHandlerwith protocol state initiallyHidProtocolMode::Report. - Accept
SET_PROTOCOL(Boot)and storeBoot. - Trigger a USB bus reset.
- Issue
GET_PROTOCOL.
The handler sees steps 2 and 4 but gets no notification for step 3, so it
still answers Boot.
Expected behavior
A Boot Keyboard implementation that accepts SET_PROTOCOL(Boot) needs a
supported way to observe a USB bus reset and restore Report mode: after the
reset, GET_PROTOCOL should answer HidProtocolMode::Report, and
application code should be able to see the restored mode when producing
input reports.
Possible fixes
- Add a default no-op USB bus-reset callback to
RequestHandler, invoked fromControl::reset(). Existing implementations would not be required to add a method, and handlers that own protocol state could restore it there. This preserves the current ownership model, although compliance would still depend on each handler implementing the callback correctly. - Store the authoritative protocol mode in HID
State, similarly to the existing sharedout_report_offset:Controlupdates it onSET_PROTOCOL, resets it to Report inreset(), and answersGET_PROTOCOLfrom it, while a handle/accessor lets application code select the appropriate report format. Bigger change, and it needs a compatibility plan for the existingRequestHandler::get_protocol.
My preference is option 1 because it preserves the current ownership model and does not require changes to existing implementations. I can open a PR implementing it with a regression test if that direction is acceptable.
Context
I hit this while implementing boot-keyboard SET_PROTOCOL support in a
keyboard firmware library (RMK). We currently work around it by storing
the protocol mode in shared state that a device-level
embassy_usb::Handler::reset() clears. This works, but it couples
device-level reset handling to state otherwise managed through the
per-interface HID RequestHandler.
Related: #6725 discusses the examples starting in Boot mode instead of the Report-mode initialization default. This issue is narrower: even when a handler starts in Report mode, the HID API provides no way to restore that state after a later USB bus reset. The protocol callbacks themselves were introduced in #4691; this issue concerns the missing reset lifecycle for the state managed through them.
Source: embassy-rs/embassy