From 26841c05e68b7a32fc3ab7d2e4f89a87c672ff07 Mon Sep 17 00:00:00 2001 From: Hiroaki Tagawa Date: Sat, 25 Jul 2026 10:39:05 +0900 Subject: [PATCH 1/3] fix(hid): widen the Bolt per-slot probe budget for high-latency USB paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream now walks Bolt slots concurrently behind a sequential register pass (the structure this commit originally introduced), and has since raised PROBE_BUDGET to 25 s for the Bluetooth-direct feature walk — wide enough to cover the arrival drain, the register pass and one full slot budget, so only the per-slot cap is left to fix here. That cap is still sized for direct-radio latency: behind a KVM's USB emulation a healthy feature walk takes several seconds, so every slot kept hitting the 3 s cap on every tick and a newly paired device never acquired model info. Raise BOLT_SLOT_PROBE to 10 s; healthy nodes still settle in a couple of seconds — this is a ceiling, not a wait. Verified against a KVM-routed receiver where the shorter cap reproducibly starved both slots. --- crates/openlogi-hid/src/inventory.rs | 38 +++++++++++++--------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/crates/openlogi-hid/src/inventory.rs b/crates/openlogi-hid/src/inventory.rs index 2354bdd1..84a66df4 100644 --- a/crates/openlogi-hid/src/inventory.rs +++ b/crates/openlogi-hid/src/inventory.rs @@ -43,10 +43,10 @@ const MAX_BOLT_SLOTS: u8 = 6; /// /// A timed-out node is skipped and re-probed on the next watcher tick (~2 s), /// and the first probe usually wakes the device so the retry succeeds fast. -/// Slots are probed concurrently on both receiver paths, so a healthy -/// receiver's worst case is the 1.5 s arrival drain plus a single slot's -/// [`BOLT_SLOT_PROBE`] / [`UNIFYING_SLOT_PROBE`] — not their sum — which this -/// stays comfortably above, so awake devices never trip it. +/// Slots are probed concurrently on both receiver paths, so a receiver's worst +/// case is the 1.5 s arrival drain plus the sequential register pass plus a +/// single slot's [`BOLT_SLOT_PROBE`] / [`UNIFYING_SLOT_PROBE`] — not their sum +/// — which this stays comfortably above, so awake devices never trip it. /// /// Sized for the Bluetooth-direct feature walk, the long pole: a ~35-entry /// table over a link that drops individual reports, which `hidpp::device` @@ -68,22 +68,20 @@ const UNIFYING_SLOT_PROBE: Duration = Duration::from_millis(3500); /// Per-slot budget for the HID++ 2.0 feature walk on a Bolt paired device. /// -/// Without a per-slot cap a single online device that stops answering its -/// feature-walk reads burns the whole receiver's [`PROBE_BUDGET`], so -/// `probe_one` times out and the receiver yields *nothing* — every paired device -/// drops to "No devices" even though its pairing-register identity read fine -/// (#218). Capping each slot lets a hung device fall back to its cached / -/// identity-only data while the rest of the receiver still enumerates, mirroring -/// [`UNIFYING_SLOT_PROBE`]. -/// -/// Bolt slots are probed concurrently (see `probe_bolt_receiver`), so this cap -/// bounds each slot independently and does *not* sum across slots — the receiver -/// cycle is the arrival drain plus the single slowest slot. 3 s is generous -/// headroom for a healthy walk: a feature-rich device enumerates a large table -/// one round-trip per feature, and the MX Master 4 (45 features over Bolt) takes -/// ~1–1.6 s even awake. The previous 1 s cap cut that walk off every tick, so -/// the device surfaced permanently with no capabilities or battery. -const BOLT_SLOT_PROBE: Duration = Duration::from_secs(3); +/// Bounds a single device that stops answering its feature-walk reads (seen on +/// a recent macOS IOHID stack with a new MX Master 4) so it falls back to its +/// cached / identity-only data instead of pinning its slot future forever +/// (#218). Slots walk *concurrently* (mirroring the Unifying path), so this +/// budget covers the slowest single slot rather than dividing [`PROBE_BUDGET`] +/// across the slot count. A healthy walk is not always fast either: a +/// feature-rich device enumerates a large table one round-trip per feature +/// (the MX Master 4's 45 features take ~1–1.6 s over Bolt even awake), and on +/// high-latency USB paths (a Bolt receiver behind a KVM's USB emulation) it +/// takes several seconds — the earlier 1 s cap starved every slot there, so a +/// newly paired device could never acquire model info at all. 10 s is generous +/// headroom for degraded-but-alive paths while still fitting [`PROBE_BUDGET`] +/// after the 1.5 s arrival drain and the register pass. +const BOLT_SLOT_PROBE: Duration = Duration::from_secs(10); /// Errors raised while enumerating HID++ devices. #[derive(Debug, Error)] From e765148b7d118f1c5d3052f4d42d37ef5d40ed2e Mon Sep 17 00:00:00 2001 From: Hiroaki Tagawa Date: Tue, 11 Aug 2026 16:17:04 +0900 Subject: [PATCH 2/3] docs(hid): scope the sequential register pass to Bolt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The probe-budget rustdoc described the pairing-register pass as part of every receiver's worst case, but only the Bolt path performs it — Unifying's pairing-info registers use a different sub-register base and are not polled. Refs: https://github.com/AprilNEA/OpenLogi/pull/562#discussion_r3755919637 --- crates/openlogi-hid/src/inventory.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/openlogi-hid/src/inventory.rs b/crates/openlogi-hid/src/inventory.rs index 84a66df4..a90256af 100644 --- a/crates/openlogi-hid/src/inventory.rs +++ b/crates/openlogi-hid/src/inventory.rs @@ -44,9 +44,10 @@ const MAX_BOLT_SLOTS: u8 = 6; /// A timed-out node is skipped and re-probed on the next watcher tick (~2 s), /// and the first probe usually wakes the device so the retry succeeds fast. /// Slots are probed concurrently on both receiver paths, so a receiver's worst -/// case is the 1.5 s arrival drain plus the sequential register pass plus a -/// single slot's [`BOLT_SLOT_PROBE`] / [`UNIFYING_SLOT_PROBE`] — not their sum -/// — which this stays comfortably above, so awake devices never trip it. +/// case is the 1.5 s arrival drain plus a single slot's [`BOLT_SLOT_PROBE`] / +/// [`UNIFYING_SLOT_PROBE`] — not their sum — plus, on Bolt only, the +/// sequential pairing-register pass that precedes the slot walk. This stays +/// comfortably above that, so awake devices never trip it. /// /// Sized for the Bluetooth-direct feature walk, the long pole: a ~35-entry /// table over a link that drops individual reports, which `hidpp::device` @@ -80,7 +81,7 @@ const UNIFYING_SLOT_PROBE: Duration = Duration::from_millis(3500); /// takes several seconds — the earlier 1 s cap starved every slot there, so a /// newly paired device could never acquire model info at all. 10 s is generous /// headroom for degraded-but-alive paths while still fitting [`PROBE_BUDGET`] -/// after the 1.5 s arrival drain and the register pass. +/// after the 1.5 s arrival drain and Bolt's sequential pairing-register pass. const BOLT_SLOT_PROBE: Duration = Duration::from_secs(10); /// Errors raised while enumerating HID++ devices. From a39069d6f2386b5c61d81d77b3473f5f3a6b1ce2 Mon Sep 17 00:00:00 2001 From: Hiroaki Tagawa Date: Tue, 11 Aug 2026 17:13:53 +0900 Subject: [PATCH 3/3] docs(hid): attribute the KVM slot starvation to the 3 s cap it hit The high-latency repro ran against the 3 s cap this PR widens; the 1 s cap is older history (#218) unrelated to the KVM path. --- crates/openlogi-hid/src/inventory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/openlogi-hid/src/inventory.rs b/crates/openlogi-hid/src/inventory.rs index a90256af..a81dadc9 100644 --- a/crates/openlogi-hid/src/inventory.rs +++ b/crates/openlogi-hid/src/inventory.rs @@ -78,7 +78,7 @@ const UNIFYING_SLOT_PROBE: Duration = Duration::from_millis(3500); /// feature-rich device enumerates a large table one round-trip per feature /// (the MX Master 4's 45 features take ~1–1.6 s over Bolt even awake), and on /// high-latency USB paths (a Bolt receiver behind a KVM's USB emulation) it -/// takes several seconds — the earlier 1 s cap starved every slot there, so a +/// takes several seconds — the previous 3 s cap starved every slot there, so a /// newly paired device could never acquire model info at all. 10 s is generous /// headroom for degraded-but-alive paths while still fitting [`PROBE_BUDGET`] /// after the 1.5 s arrival drain and Bolt's sequential pairing-register pass.