Skip to content

fix: hotplug-driven inventory and phantom device-card suppression#346

Merged
AprilNEA merged 8 commits into
AprilNEA:masterfrom
davidbudnick:fix/device-lifecycle
Jul 18, 2026
Merged

fix: hotplug-driven inventory and phantom device-card suppression#346
AprilNEA merged 8 commits into
AprilNEA:masterfrom
davidbudnick:fix/device-lifecycle

Conversation

@davidbudnick

Copy link
Copy Markdown
Contributor

Context

Stale device cards pile up when the same models are used at two locations (work + home G513 / MX Master 3S), and a just-plugged keyboard waits seconds for its configured lighting. Root causes: persisted known_identities resurrect unreachable devices as placeholders with model-key dedup defeated by the flaky extended-model byte (0b034 vs 2b034, #271/#280), and the inventory watcher is a pure 2 s poll — the "async-hid has no listener API" rationale predates async-hid 0.5's watch().

Demo

  • Plug in a wired keyboard: configured lighting applies within ~1 s (hotplug wake + one confirming re-apply that absorbs the boot race).
  • Unplug a Bolt receiver: its paired devices no longer linger as offline cards; a live MX Master 3S no longer renders next to a phantom same-model card.
  • cargo test --workspace passes; clippy/fmt clean.

Changes

  • Expose OS hotplug events from the shared backend: hotplug.rs, transport.rs (openlogi-hid)
  • Wake the inventory watcher on hotplug events, keep the 2 s tick as reconciliation: watchers/inventory.rs
  • Re-apply volatile settings once more after a first sighting: orchestrator.rs
  • Suppress offline placeholders whose receiver is absent and dedup same-model cards by wire PID: state/devices.rs

Notes

  • Verified on macOS (unit tests + hotplug stream smoke test); real plug/unplug hardware pass still pending — hence draft.
  • Follow-up worth discussing: keying device identity on the HID++ 0x0003 serial/unit id with the route as an attribute, so one physical unit keeps one config entry across transports.

@davidbudnick
davidbudnick marked this pull request as ready for review July 2, 2026 21:20
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the pure 2 s polling inventory watcher with a hotplug-driven design (using async-hid 0.5's watch()) and fixes two phantom-card regressions: stale offline placeholders for devices behind an absent receiver (#271/#280) and same-model duplicates caused by the flaky extended-model byte.

  • inventory.rs: wakes early on OS hotplug events via tokio::select!; initialises the stream inside rt.block_on() so a Linux AsyncFd registration failure is a catchable Err rather than a panic; burst-drains with poll_once and degrades to polling if the stream closes.
  • orchestrator.rs: introduces plan_reapply() and a reapply_followup set so first-sighted devices receive one confirming re-apply on the next tick, absorbing the device-boot race that can swallow the very first settings write.
  • devices.rs: adds two gates in append_offline_known — a receiver-presence check (suppresses cards whose Bolt/Unifying receiver is unplugged) and a wire-PID dedup (collapses same-model records regardless of the extended-model byte).

Confidence Score: 5/5

Safe to merge; all three subsystems (hotplug watcher, first-sighting confirmation, phantom-card suppression) are guarded by dedicated unit tests and the changes degrade gracefully when hotplug is unavailable.

The hotplug stream initialisation is correctly placed inside rt.block_on(), fixing the thread-kill risk raised in the previous thread. The plan_reapply logic handles every edge case (offline during followup, wake + first sighting, replug under a new route) with matching test assertions. Receiver-absent filtering and wire-PID dedup in append_offline_known are symmetric in their lowercasing and correctly interact with the existing key-based dedup. No new unsafe code, no new IPC surface, no migrations.

No files require special attention; inventory.rs has the most surface area (new async select loop) but the fallback-to-polling path and stream-end detection are both tested or exercised by the existing WatchState tests.

Important Files Changed

Filename Overview
crates/openlogi-hid/src/hotplug.rs New file bridging async-hid's DeviceEvent stream to a typed HotplugEvent; maps both event variants and re-uses the shared HidBackend singleton.
crates/openlogi-hid/src/transport.rs Adds pub(crate) hid_backend() accessor for the LazyLock singleton; minimal and correct.
crates/openlogi-agent-core/src/watchers/inventory.rs Adds hotplug-driven early wakeup via tokio::select!, correctly initialising the hotplug stream inside rt.block_on() so an AsyncFd registration panic on Linux becomes a catchable Err; burst-drains with poll_once and falls back to polling when the stream closes.
crates/openlogi-agent-core/src/orchestrator.rs Introduces plan_reapply() and a reapply_followup set to schedule one confirming re-apply for first-sighted devices, absorbing the device-boot race. Logic is sound: offline devices drop their followup entry; reapply_all only queues new devices for confirmation, not known ones.
crates/openlogi-gui/src/state/devices.rs Adds two phantom-card suppression gates: receiver-absent filter using receiver_uid_of() with consistent lowercase normalisation, and wire-PID dedup via record_wire_pid(). Sort order ensures the richer record wins when two same-model keys exist.

Reviews (5): Last reviewed commit: "Merge branch 'master' into fix/device-li..." | Re-trigger Greptile

Comment thread crates/openlogi-agent-core/src/watchers/inventory.rs Outdated
Comment thread crates/openlogi-agent-core/src/watchers/inventory.rs Outdated
Comment thread crates/openlogi-gui/src/state/devices.rs
@AprilNEA
AprilNEA merged commit 09c3f0d into AprilNEA:master Jul 18, 2026
10 checks passed
@aprilnea aprilnea Bot mentioned this pull request Jul 18, 2026
AprilNEA added a commit to buliwyf42/OpenLogi that referenced this pull request Jul 18, 2026
Only one `futures-lite` (2.6.1) remains in the tree, so cargo canonicalizes
the `openlogi-agent-core` dependency reference to the unqualified name. The
qualified form landed via AprilNEA#346's merge and every `cargo` run rewrites it,
dirtying the tree; commit the canonical form so `--locked` builds are stable.
AprilNEA pushed a commit that referenced this pull request Jul 18, 2026
… the whole receiver (#251)

Wrap each Bolt slot's HID++ feature walk in a 1 s BOLT_SLOT_PROBE timeout,
mirroring the existing UNIFYING_SLOT_PROBE guard. Without it, one online
device that stops answering its feature 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. A timed-out slot now falls back to its cached / identity data while
the rest of the receiver still enumerates. Further hardens the #218 scenario
beyond the short-transient recovery from #222/#237.

Also canonicalizes a stale futures-lite lockfile entry left by #346's merge so
cargo no longer rewrites Cargo.lock on every run.
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.

2 participants