Skip to content

fix(agent): rearm control capture after device reconnect - #450

Open
Phecda wants to merge 4 commits into
AprilNEA:masterfrom
Phecda:fix/bluetooth-capture-reconnect
Open

fix(agent): rearm control capture after device reconnect#450
Phecda wants to merge 4 commits into
AprilNEA:masterfrom
Phecda:fix/bluetooth-capture-reconnect

Conversation

@Phecda

@Phecda Phecda commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Restore programmable controls after a Logitech device reconnects or switches between Bluetooth and Unifying, without requiring an agent restart.

Changes

  • openlogi-hidpp / openlogi-hid
    • Expose HID transport liveness and stop stale capture sessions after confirmed disconnects.
    • Keep cached Unifying capabilities separate from current device reachability.
    • Confirm Unifying reachability with a root ping after battery refresh errors.
    • Retain live cached HID channels when the OS temporarily omits their vendor collection from enumeration.
    • Abandon stale firmware state on disconnect while preserving normal restoration for ordinary route or configuration changes.
  • openlogi-agent-core / openlogi-agent
    • Re-arm control capture when the selected device reconnects.
    • Re-arm capture after system wake.
    • Add regression coverage for reconnect and capture lifecycle behavior.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • Hardware-tested on macOS with an MX Master 3:
    • Bluetooth → Unifying → Bluetooth switching
    • Auxiliary and gesture actions recovered on both transports
  • Captured a Bluetooth-direct HID++ enumeration gap in agent and macOS logs. The retention fix is regression-tested and running in the debug agent, but the intermittent gap has not recurred during post-fix hardware testing.
  • The transient Unifying battery-error fallback was covered by an automated HID++ channel test but was not reproduced on hardware.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR restores programmable HID++ controls (gesture button, thumb wheel) after a Logitech device reconnects or the system wakes, without requiring an agent restart. It threads a generation counter through the capture stack so the gesture watcher can distinguish a reconnect on the same Unifying route from a routine configuration change, and selects Abandon vs Restore when tearing down the old session accordingly.

  • Transport / inventory layer: AsyncHidChannel now sets an AtomicBool on HidError::Disconnected, exposing is_connected() so the inventory can retain temporarily-omitted BLE nodes and the capture health-poll can exit stale sessions within 1 second; Unifying probe separates cached immutable capabilities from live reachability, adding a root-ping fallback when the battery feature is absent or fails.
  • Orchestrator: removes the early return for unchanged routes so system-wake events still increment capture_rearm_generation; a new selected_needs_capture_rearm helper gates increments on the selected device's offline→online transition (or reapply_all).
  • Gesture watcher: CaptureTarget now carries rearm_generation; stop_for_transition returns Abandon when the generation changed on the same route (reconnect), Restore for all other transitions; the existing done_tx epoch mechanism handles unexpected session exits and drives re-arm at the TARGET_POLL rate.

Confidence Score: 5/5

The change is safe to merge; all new code paths are guarded, well-tested, and handle failure cases gracefully.

Every new code path has corresponding unit tests including mock HID++ channel implementations that exercise disconnect detection, battery-error fallback, and the Abandon/Restore split. The generation counter is only ever incremented (never reset), Ordering::Relaxed is appropriate for an eventually-consistent tick-polled counter, and the stop_for_transition logic is covered by tests for same-route/different-generation, different-route, and None transitions. The existing done_tx epoch mechanism ensures the watcher recovers quickly from unexpected session exits without the orchestrator's help.

No files require special attention; the inventory probe refactor in probe.rs is the most intricate change but is covered by dedicated tests for the battery-error and liveness-cache separation paths.

Important Files Changed

Filename Overview
crates/openlogi-hidpp/src/channel.rs Adds is_connected() default method to RawHidChannel trait (defaults to true) and delegates it through HidppChannel; safe extension point that preserves backward compatibility for transports without disconnect detection.
crates/openlogi-hid/src/transport.rs Adds an AtomicBool connected field to AsyncHidChannel; calls mark_disconnected() on HidError::Disconnected in both write_report and read_report, preserving the existing park-on-disconnect behaviour while exposing a queryable liveness flag.
crates/openlogi-hid/src/gesture.rs Introduces CaptureStop::Abandon variant and a 1-second health-poll loop (wait_for_capture_exit) that queries chan.is_connected(); ChannelDisconnected exit now skips disarm (firmware state already reset) and returns an error that propagates via the existing done_tx epoch mechanism in the watcher.
crates/openlogi-hid/src/inventory.rs Adds retained_nodes and append_live_cached_channels to keep HID nodes alive during transient OS enumeration gaps (macOS IOHIDManager BLE quirk); nodes are only dropped when their transport's is_connected() returns false, preventing false disconnects.
crates/openlogi-hid/src/inventory/probe.rs Splits probe_unifying_slot into probe_unifying_features (capability cache + liveness) and assemble_unifying_device; separates cached capabilities from live reachability, fixing a bug where cached features kept a device appearing online after it moved to Bluetooth; adds a root-ping fallback when battery refresh fails.
crates/openlogi-agent-core/src/orchestrator.rs Adds capture_rearm_generation: Arc to SharedRuntime; refactors the refresh if/else to remove the early-return for unchanged routes (so system-wake events still increment the generation); introduces selected_needs_capture_rearm to gate increments on the selected device's connectivity transition.
crates/openlogi-agent-core/src/watchers/gesture.rs Introduces CaptureTarget struct (route + flags + rearm_generation) and stop_for_transition which selects Abandon vs Restore based on whether the generation changed on the same route; integrates capture_rearm_generation into every tick's target computation so reconnects are detected without a route change.
crates/openlogi-agent/src/main.rs Passes capture_rearm_generation from SharedRuntime to the gesture watcher spawn call; one-line plumbing change.
crates/openlogi-agent/src/pairing.rs Adds capture_rearm_generation: Arc::new(0.into()) to the test SharedRuntime constructor; minimal test fixture update.

Sequence Diagram

sequenceDiagram
    participant OS as OS/IOHIDManager
    participant Inv as Enumerator
    participant Orch as Orchestrator
    participant Watcher as GestureWatcher
    participant Session as CaptureSession

    Note over Session: Device disconnects
    Session->>Session: HidError::Disconnected → mark_disconnected()
    Session->>Session: "health poll: is_connected()=false"
    Session->>Watcher: done_tx.send(epoch) [ChannelDisconnected]
    Watcher->>Watcher: "should_rearm→true: current=None, stop=None"

    OS->>Inv: "enumeration tick (node absent or is_connected=false)"
    Inv->>Inv: retained_nodes excludes dead channel
    Inv->>Orch: device offline

    Note over OS: Device reconnects
    OS->>Inv: enumeration tick (new node)
    Inv->>Orch: device online (offline→online transition)
    Orch->>Orch: selected_needs_capture_rearm→true
    Orch->>Watcher: capture_rearm_generation.fetch_add(1)

    Watcher->>Watcher: tick: want.rearm_generation changed (same route)
    Watcher->>Session: stop_for_transition → CaptureStop::Abandon
    Note over Session: skip disarm (firmware state already reset)
    Watcher->>Session: spawn new CaptureSession (fresh channel)
Loading

Reviews (3): Last reviewed commit: "fix(hid): retain live channels across en..." | Re-trigger Greptile

@Phecda
Phecda force-pushed the fix/bluetooth-capture-reconnect branch from f192bb3 to 598386f Compare July 23, 2026 03:33
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.

1 participant