fix(agent): rearm control capture after device reconnect - #450
Conversation
Greptile SummaryThis PR restores HID++ control capture (gesture button, DPI toggle, thumb wheel) after a Logitech device reconnects or switches transports, without requiring an agent restart. It does so through a coordinated set of changes across five crates: a
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| crates/openlogi-agent-core/src/orchestrator.rs | Adds capture_rearm_generation: Arc<AtomicU64> to SharedRuntime and increments it when the selected device reconnects or the system wakes, triggering re-arm in the gesture watcher. Refactors the !changed early-return into an if/else so the generation bump runs unconditionally after both branches. Well-tested with three new unit tests. |
| crates/openlogi-agent-core/src/watchers/gesture.rs | Introduces CaptureTarget struct (route + thumbwheel arm + gesture divert + rearm_generation) and the targeted stop_for_target_change function that issues Revoked on same-route/new-generation changes and Graceful otherwise. The manage loop is updated to carry the new struct and consume the generation signal correctly. |
| crates/openlogi-hid/src/gesture.rs | Adds wait_for_capture_exit that polls chan.is_connected() every second alongside the shutdown oneshot, enabling the session to self-terminate on physical disconnect. Teardown is now an explicit match that skips restore writes on Revoked or Disconnected. |
| crates/openlogi-hid/src/inventory.rs | Adds retained_nodes (union of enumerated nodes and live cached nodes) and append_live_cached_channels to prevent a transient IOHIDManager enumeration gap from evicting a still-connected Bluetooth channel. The retained set is passed to registry.retain_nodes so the registry is consistent. |
| crates/openlogi-hid/src/inventory/probe.rs | Extracts probe_unifying_features (cached capabilities + live reachability check) and assemble_unifying_device to decouple feature caching from device liveness. Devices with a CacheOutcome::Seen result (no battery feature or battery refresh failed) now perform a HID++ root ping before being marked offline, fixing the stale-online problem. |
| crates/openlogi-hid/src/transport.rs | Adds connected: AtomicBool to AsyncHidChannel. Both read_report and write_report call mark_disconnected() on HidError::Disconnected. is_connected() exposes the flag upward and is gated to non-Windows. AcqRel/Acquire ordering is correct for the flag. |
| crates/openlogi-hidpp/src/channel.rs | Adds is_connected() to the RawHidChannel trait with a default of true (preserves existing behaviour for transports that cannot detect disconnects, e.g. Windows) and exposes it on HidppChannel. |
| crates/openlogi-agent/src/main.rs | Passes shared.capture_rearm_generation through to spawn_with_registry. One-line change, correct wiring. |
| crates/openlogi-agent/src/pairing.rs | Initialises capture_rearm_generation: Arc::new(0.into()) in the test SharedRuntime stub. Correct. |
| crates/openlogi-hid/src/inventory/tests.rs | Adds live_cached_channel_survives_a_transient_enumeration_gap unit test for retained_nodes, verifying that a connected cached channel is kept and a disconnected one is dropped. |
Sequence Diagram
sequenceDiagram
participant OS as OS / async_hid
participant Transport as AsyncHidChannel
participant Inventory as Enumerator
participant Orchestrator as Orchestrator
participant Watcher as GestureWatcher (manage)
participant Session as CaptureSession
Note over Session: Device connected, capture active
OS->>Transport: Disconnected error (read/write)
Transport->>Transport: "mark_disconnected() → connected=false"
loop Every CAPTURE_HEALTH_POLL (1s)
Session->>Transport: chan.is_connected()
Transport-->>Session: false
Session->>Session: CaptureExit::Disconnected
end
Session->>Session: drop listener, clear capture slot
Session-->>Watcher: done_tx.send(epoch) [Err::ChannelDisconnected logged]
Watcher->>Watcher: "should_rearm() → true → current=None"
Note over Inventory: Next inventory tick
Inventory->>Transport: is_connected() on cached channels
Transport-->>Inventory: false → excluded from retained_nodes
Inventory->>Orchestrator: on_inventory_update (device offline)
Orchestrator->>Orchestrator: selected_needs_capture_rearm → false (still offline)
Note over OS: Device reconnects
OS->>Inventory: enumerate() includes node again
Inventory->>Inventory: append_live_cached_channels (or fresh channel)
Inventory->>Orchestrator: on_inventory_update (device online)
Orchestrator->>Orchestrator: reapply_volatile_settings
Orchestrator->>Orchestrator: capture_rearm_generation.fetch_add(1)
loop Next TARGET_POLL tick
Watcher->>Watcher: load capture_rearm_generation → new value
Watcher->>Watcher: "CaptureTarget { rearm_generation: new }"
Watcher->>Watcher: stop_for_target_change → Revoked (same route, new gen)
Watcher->>Session: stop.send(Revoked)
Session->>Session: disarm skipped, slot cleared
Watcher->>Watcher: spawn new capture session
end
Reviews (5): Last reviewed commit: "fix(agent): rearm control capture after ..." | Re-trigger Greptile
f192bb3 to
598386f
Compare
598386f to
8c04bff
Compare
8c04bff to
ad19141
Compare
Summary
Restore programmable controls after a Logitech device reconnects or switches between Bluetooth and Unifying, without requiring an agent restart.
Changes
openlogi-hidpp/openlogi-hidopenlogi-agent-core/openlogi-agentTesting
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --locked -- -D warningscargo test --workspace --locked