feat(usb-hid): queue and replay HID reports across USB not-ready windows#3384
Open
akira-toriyama wants to merge 1 commit into
Open
feat(usb-hid): queue and replay HID reports across USB not-ready windows#3384akira-toriyama wants to merge 1 commit into
akira-toriyama wants to merge 1 commit into
Conversation
Merged
2 tasks
akira-toriyama
added a commit
to akira-toriyama/canon
that referenced
this pull request
Jun 9, 2026
…#46) 両 patch を zmkfirmware/zmk へ Kconfig gate (default n) 付きで PR 済: - security-changed-auto-unpair → zmkfirmware/zmk#3385 - usb-hid-prime-on-ready → zmkfirmware/zmk#3384 merge され次第 patches/zmk/ から畳む方針。dongle-roadmap.md と patches/zmk/README.md の該当節を「提案準備中」から「PR 中」状態に 更新する。 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
akira-toriyama
force-pushed
the
feat/usb-hid-replay-on-ready
branch
from
June 11, 2026 01:38
0ea1f07 to
c58e5bf
Compare
akira-toriyama
added a commit
to akira-toriyama/canon
that referenced
this pull request
Jun 11, 2026
#43 で導入した usb-hid-prime-on-ready.patch は USB_DC_SUSPEND のみで report を enqueue していたが、PC スリープ復帰時の USB state 遷移は SUSPEND → RESET → CONNECTED → CONFIGURED と段階的なため、その途中の RESET / DISCONNECTED / UNKNOWN / ERROR 状態の瞬間に来た打鍵は -ENODEV で破棄されていた。 これがスリープ復帰で「たまに 1 キー要る」症状の正体。 USB_DC_SUSPEND と同様に not-ready 系の全 case でも enqueue するよう 拡張。queue は depth 8 / drop-oldest なので memory は bounded で DISCONNECTED 永続化のリスク無し。dongle 構成ではケーブル抜き = 電源喪失 = code 走らないので副作用なし。 upstream PR (zmkfirmware/zmk#3384) にも同じ修正を反映予定。 実機検証: スリープ復帰サイクル数回で 1 キー消失が再発しないこと、 dongle 抜き差し / 長時間無操作の既存ケースも regression 無しを確認。 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
akira-toriyama
force-pushed
the
feat/usb-hid-replay-on-ready
branch
from
June 12, 2026 02:04
c58e5bf to
880e6ba
Compare
akira-toriyama
added a commit
to akira-toriyama/canon
that referenced
this pull request
Jun 12, 2026
0ベース調査で発見した flush 側の欠陥 4 件を修正する。 症状「スリープ復帰でたまに 1 キー必要」の firmware 側残り要因。 1. semaphore 枯渇で flush が諦める (本命): bus reset は armed 済み IN 転送を callback 無しで破棄する (usb_dc_nrfx.c の EP_ABORTED は log のみ)。in_ready_cb が呼ばれず hid_sem が 0 のまま残り、flush の k_sem_take(50ms) が timeout して queue を放置していた。通常送信パスと同じく take 結果を advisory に し、write 失敗時のみ took==0 の場合に give back する。 2. flush リトライ追加: write 失敗 (mid-enumeration の -EAGAIN 等) 時、bus が active なら 100ms 後に再 flush。今までは次の状態変化イベント任せだった。 3. k_work_reschedule 化: k_work_schedule は pending 中の work の deadline を維持するため、 RESUME -> RESET -> CONFIGURED が 100ms 内に連続すると flush が CONFIGURED 直後 ~0ms で発火し、host HID binding race の猶予が 無効化されていた。reschedule で最後の遷移から猶予を取り直す。 suspend 突入時の無駄な flush 予約も廃止。 4. 順序保証 + stale queue クリア: resume 後の生 report が queue 内の古い report を追い越すと press/release が逆転しスタックキー化するため、queue 非空時は append して flush に順序配送させる。物理切断 (DISCONNECTED) では queue を破棄し、replug 時の幽霊入力を防ぐ。 実機検証: - 検証付き flash (post-flash USB enumeration 確認) で導入 - キーボード / トラックボールとも正常動作を確認 - 本 patch から再ビルドしたバイナリが実機動作中のものと sha1 一致 (b5f48517) であることを確認 - sleep-resume の残症状は継続観察中 upstream PR (zmkfirmware/zmk#3384) にも同修正を反映予定。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
akira-toriyama
added a commit
to akira-toriyama/canon
that referenced
this pull request
Jun 13, 2026
* 🐛 fix(dongle): sleep 復帰の USB_DC_CONNECTED でも HID report を queue 化 #48 で SUSPEND / ERROR / RESET / DISCONNECTED / UNKNOWN を queue 化 したが、スリープ復帰時の状態遷移 SUSPEND → RESET → CONNECTED → CONFIGURED の USB_DC_CONNECTED の瞬間に打鍵が来た場合、これは default 分岐に落ちて hid_int_ep_write が試みられる。endpoint は connected だが未 configured なので silent fail し、report が消失する。 これがスリープ復帰で「たまに 1 キー要る」 intermittent 症状の残り。 USB_DC_CONNECTED も not-ready 系の enqueue 対象に追加。default 分岐は CONFIGURED / RESUME / CLEAR_HALT / SOF (= 実際に送れる状態) だけが 通る形に整理される。 upstream PR (zmkfirmware/zmk#3384) にも同じ修正を反映予定。 実機検証: スリープ復帰サイクル数回で 1 キー消失再発無し、 dongle 抜き差し / 長時間無操作も regression 無しを確認。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(dongle): HID replay queue の flush を deep-wake 経路に対して堅牢化 0ベース調査で発見した flush 側の欠陥 4 件を修正する。 症状「スリープ復帰でたまに 1 キー必要」の firmware 側残り要因。 1. semaphore 枯渇で flush が諦める (本命): bus reset は armed 済み IN 転送を callback 無しで破棄する (usb_dc_nrfx.c の EP_ABORTED は log のみ)。in_ready_cb が呼ばれず hid_sem が 0 のまま残り、flush の k_sem_take(50ms) が timeout して queue を放置していた。通常送信パスと同じく take 結果を advisory に し、write 失敗時のみ took==0 の場合に give back する。 2. flush リトライ追加: write 失敗 (mid-enumeration の -EAGAIN 等) 時、bus が active なら 100ms 後に再 flush。今までは次の状態変化イベント任せだった。 3. k_work_reschedule 化: k_work_schedule は pending 中の work の deadline を維持するため、 RESUME -> RESET -> CONFIGURED が 100ms 内に連続すると flush が CONFIGURED 直後 ~0ms で発火し、host HID binding race の猶予が 無効化されていた。reschedule で最後の遷移から猶予を取り直す。 suspend 突入時の無駄な flush 予約も廃止。 4. 順序保証 + stale queue クリア: resume 後の生 report が queue 内の古い report を追い越すと press/release が逆転しスタックキー化するため、queue 非空時は append して flush に順序配送させる。物理切断 (DISCONNECTED) では queue を破棄し、replug 時の幽霊入力を防ぐ。 実機検証: - 検証付き flash (post-flash USB enumeration 確認) で導入 - キーボード / トラックボールとも正常動作を確認 - 本 patch から再ビルドしたバイナリが実機動作中のものと sha1 一致 (b5f48517) であることを確認 - sleep-resume の残症状は継続観察中 upstream PR (zmkfirmware/zmk#3384) にも同修正を反映予定。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(imprint): 半体の deep sleep を全廃し離席後の 1 打目消失を解消 「3 時間離席後、左右それぞれ 1 打目が反応しない」症状の対策。 真因: 半体は Cyboard shield 既定 (ZMK_SLEEP=y / IDLE_SLEEP_TIMEOUT= 900000) により 15 分無操作で sys_poweroff = 完全電源断していた。 電源断からの 1 打目は GPIO wake トリガーに物理的に消費される (firmware 非稼働中は記録不能) ため、いかなる firmware 側 buffering でも救えない。左右が独立に 1 打ずつ食われるのはこのため。 USB 有線運用では USB 給電が sleep を抑止するため発生せず、dongle 化 (電池駆動化) で初めて顕在化した。ZMK_SLEEP=n で有線時代と同じ挙動に 戻る。 電池影響: 常時 connected-idle (~50µA) でも待機消費は月 2〜4% 程度で 許容範囲 (#41 の BLE tight 化のような radio wake 30 倍とは桁違い)。 備考: #41 では「ZMK_SLEEP=n だと peripheral が起動しなくなる」と判断 して見送ったが、当時は flash 成否の検証手段が無く、flash 破損 (壊れた app イメージで起動不能) の誤診だった。検証付き flash (post-flash 再マウント監視) で =n を再テストし、左右ともキーボード・ トラックボール正常動作を確認した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add CONFIG_ZMK_USB_HID_REPLAY_ON_READY (default n) which buffers HID reports that arrive while the USB endpoint is in any not-ready state (USB_DC_SUSPEND, ERROR, RESET, DISCONNECTED, UNKNOWN, CONNECTED) into a small ring buffer, then flushes them shortly after USB transitions back to USB_DC_CONFIGURED / USB_DC_RESUME. Why --- zmk_usb_hid_send_report currently drops every report issued while the USB endpoint is not ready: USB_DC_SUSPEND returns usb_wakeup_request() (the report itself is discarded), USB_DC_DISCONNECTED / RESET / UNKNOWN / ERROR return -ENODEV, and USB_DC_CONNECTED falls into the default branch where hid_int_ep_write is attempted on a bus that is not yet configured (silent failure). The host HID stack additionally drops the first input report from a freshly bound interface (a binding-race quirk observed on macOS). Together these surface as: - "First key after dongle replug is ignored" - "First key on host sleep resume is lost or needs an extra press" - intermittent, because the user's press lands on a transient USB state on the resume path (SUSPEND / RESET / CONNECTED) and macOS re-enumerates with one or more extra bus resets on deep wake. This is particularly visible on USB-dongle keyboard setups where a BLE central forwards HID over USB; the host-side resume latency is on the order of hundreds of ms and traverses several short-lived USB device-controller states, so the user's first keystrokes fall inside that window. What ---- Every not-ready case in zmk_usb_hid_send_report now enqueues the report bytes before returning its existing error. A listener on zmk_usb_conn_state_changed schedules a delayable work item that flushes the queue once the host is HID-ready again; the configurable flush delay (default 100 ms) gives the host time to finish HID interface binding before the firmware resends. The flush path is hardened against deep-wake re-enumeration hazards found while testing on real hardware: - The semaphore take in the flush is advisory (mirroring the normal send path, which ignores its take result): a bus reset aborts an armed IN transfer without raising the write-complete callback (usb_dc_nrfx EP_ABORTED is log-only), stranding the semaphore at 0. Blocking on it would abandon the queue for the whole wake cycle. - A failed flush write retries on a short timer while the bus is active, instead of waiting for a status-change event that may never come (e.g. -EAGAIN mid-enumeration). - The listener uses k_work_reschedule, not k_work_schedule: scheduling an already-pending work keeps the OLD deadline, so a RESUME -> RESET -> CONFIGURED cluster would otherwise fire the flush ~0ms after CONFIGURED, inside the very host binding race the delay avoids. The listener also ignores suspend entry (which still maps to a HID-ready conn state) instead of scheduling a pointless flush. - Live reports never overtake queued ones: HID reports are absolute state snapshots, so a live release racing past its queued press would leave that key logically held on the host. While the queue is non-empty, ready-path reports append and flush in order. - The queue is cleared on USB_DC_DISCONNECTED: replaying hours-old keystrokes on replug is phantom input, not recovery. The queue is sized at build time: - CONFIG_ZMK_USB_HID_REPLAY_QUEUE_DEPTH (default 8, range 1..64) - CONFIG_ZMK_USB_HID_REPLAY_REPORT_MAX_LEN (default 16 B, range 8..64) - CONFIG_ZMK_USB_HID_REPLAY_FLUSH_DELAY_MS (default 100 ms, range 0..1000) Overflow drops the oldest entry. Oversized reports are dropped at enqueue with a LOG_WRN. Static RAM cost with defaults is queue depth * max length + a mutex + a delayable work item. Default is `n` to preserve existing behaviour; firmwares hit by the above symptoms can opt in. Related: zmkfirmware#2686 Signed-off-by: Akira Toriyama <noreply@anthropic.com>
akira-toriyama
force-pushed
the
feat/usb-hid-replay-on-ready
branch
from
June 13, 2026 00:02
880e6ba to
8b34f68
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Add
CONFIG_ZMK_USB_HID_REPLAY_ON_READY(defaultn). When enabled,zmk_usb_hid_send_reportenqueues HID reports issued during any USBnot-ready state (
SUSPEND,ERROR,RESET,DISCONNECTED,UNKNOWN,CONNECTED) into a small static ring buffer, and alistener on
zmk_usb_conn_state_changedschedules a delayable workitem to flush the queue shortly after USB transitions back to a ready
state.
Why
zmk_usb_hid_send_reportcurrently drops every report issued whilethe USB endpoint is not ready:
USB_DC_SUSPEND→ returnsusb_wakeup_request()(the report itselfis discarded).
USB_DC_DISCONNECTED/RESET/UNKNOWN/ERROR→ returns-ENODEV.USB_DC_CONNECTED→ falls into the default branch, wherehid_int_ep_writeis attempted on a not-yet-configured bus andfails silently.
The host HID stack additionally drops the first input report from a
freshly bound interface — a binding-race quirk observed on macOS.
Together these surface as:
intermittent, because the user's press lands on a transient USB
state (
SUSPEND/RESET/CONNECTED) on the resume path, and macOSre-enumerates with one or more extra bus resets on deep wake.
This is especially visible on USB-dongle keyboard setups where a BLE
central forwards HID over USB — the host-side resume latency is on
the order of hundreds of ms and traverses several short-lived USB
device-controller states, so the user's first keystrokes fall inside
that window.
Related: #2686 (the BLE deep-sleep variant of the same symptom class —
out of scope here; a peripheral powered off in deep sleep consumes its
wake keypress at the hardware level before firmware runs).
How
Every not-ready case in
zmk_usb_hid_send_reportenqueues the reportbytes before returning its existing error. A new ZMK listener on
zmk_usb_conn_state_changedschedules a delayable work item when USBreaches a ready state; the flush worker drains the ring buffer via
hid_int_ep_write. The configurable flush delay defaults to 100 ms,which experimentally is enough on macOS to let the host finish HID
interface binding before the resend.
The flush path is hardened against deep-wake re-enumeration hazards
found during on-hardware testing:
path, which ignores its take result): a bus reset aborts an armed IN
transfer without raising the write-complete callback
(
usb_dc_nrfxEP_ABORTEDis log-only), stranding the semaphore at0; blocking on it would abandon the queue for the whole wake cycle.
write fails (e.g.
-EAGAINmid-enumeration), instead of waiting fora status-change event that may never come.
k_work_reschedulein the listener:k_work_scheduleon analready-pending work keeps the old deadline, so a
RESUME → RESET → CONFIGUREDcluster would otherwise fire the flush~0 ms after
CONFIGURED— inside the host binding race the delayexists to avoid. Suspend entry (which still maps to a HID-ready conn
state) no longer schedules a pointless flush.
release racing past its queued press would leave that key logically
held on the host. While the queue is non-empty, ready-path reports
append and flush in order.
USB_DC_DISCONNECTED: replaying hours-oldkeystrokes on replug is phantom input, not recovery.
Queue sizing is configurable so embedded targets can trade RAM for
robustness:
CONFIG_ZMK_USB_HID_REPLAY_QUEUE_DEPTH— default 8, range 1..64CONFIG_ZMK_USB_HID_REPLAY_REPORT_MAX_LEN— default 16 B, range 8..64CONFIG_ZMK_USB_HID_REPLAY_FLUSH_DELAY_MS— default 100 ms, range 0..1000Overflow drops the oldest entry. Oversized reports are dropped at
enqueue with a
LOG_WRN. Default-off so the static memory cost(
depth × max_len+ one mutex + one delayable work item) is onlypaid by firmwares that opt in.
Testing
Tested in production on a dongle-based BLE split setup (XIAO BLE
central + two
assimilator-btperipherals forwarding HID over USB)on macOS. With the option enabled:
that macOS itself consumes by design (its HID pipeline cancels
key-downs within 500 ms of wake and while the display is off — that
part is host behaviour common to all keyboards, not fixable in
firmware).
typing, including immediately after resume.
Default-off path is byte-identical (
#if-guarded), verified bybuilding the same target with and without the option enabled.
Downstream use: akira-toriyama/canon
runs this change as an out-of-tree patch (
patches/zmk/usb-hid-prime-on-ready.patch)pending this PR.
PR check-list