Skip to content

Fix reconnect replay and recover from HID transport failures#82

Merged
TomBadash merged 8 commits into
TomBadash:masterfrom
hieshima:fix/reconnect-replay-family
Apr 11, 2026
Merged

Fix reconnect replay and recover from HID transport failures#82
TomBadash merged 8 commits into
TomBadash:masterfrom
hieshima:fix/reconnect-replay-family

Conversation

@hieshima

Copy link
Copy Markdown
Collaborator

Problem

After sleep, reconnect, or cold start, Mouser could get stuck in a half-recovered state:

  • Settings lost ([Bug] sleep/resume issue #81, Lost mouse DPI settings #48): saved DPI and Smart Shift were not reliably restored because the production replay path had no settle/retry. That logic only existed in dead code (_apply_device_settings(), which had zero callers).
  • Gesture/mode_shift recovery gaps (Mapped gesture_click cannot be recognized after mouse restarted #56): diverted button callbacks could stop firing after reconnect, and there were no regressions locking this behavior down.
  • Silent transport death: if the HID device handle went stale (for example IOHIDDeviceSetReport failed: 0xE00002C2 on macOS), the app stayed "connected" and kept polling Smart Shift every 15 seconds instead of reconnecting.
  • Stale UI updates: replay sent Smart Shift state as a bare string instead of the dict the backend expected, so smartShiftChanged could fire without actually updating config.

Fix

Phased replay

Deleted the dead _apply_device_settings() helper and moved its settle/retry semantics into the live replay worker:

  • Phase A (immediate): write saved Smart Shift so the wheel converges quickly
  • Phase B (+3 s): re-fetch the HID handle, then write saved DPI and Smart Shift again
  • Phase C (+5 s, only on failure): one final retry

Also moved _emit_status() outside _replay_lock to avoid callback-under-lock.

Smart Shift serialization

Replaced the unsynchronized _pending_smart_shift busy-wait path with threading.Event plus separate call/slot locks. Reconnect cleanup now aborts pending Smart Shift requests so waiters never read stale state. Smart Shift polling is also suppressed while replay is in flight.

Payload normalization

All engine-to-backend Smart Shift callbacks now use:
{"mode": str, "enabled": bool, "threshold": int}

Backend now returns early on non-dict payloads, and smartShiftChanged only fires after a valid update.

Transport failure recovery

In _request(), active-session tx/rx exceptions now raise IOError, which drives the existing cleanup/reconnect path through _main_loop(). Discovery-time probe failures still return None.

This is intentionally error-code-agnostic and does not depend on any specific IOKit error code.

Regression coverage

Added regressions for:

  • replay restoring DPI and Smart Shift through the real worker path
  • gesture and mode_shift callbacks firing again after reconnect
  • _divert_extras() re-arming on reconnect and held state clearing before disconnect callbacks
  • backend ignoring non-dict Smart Shift payloads without config mutation or signal emission
  • Smart Shift polling being skipped during replay
  • active-session transport failures raising reconnect-driving errors while discovery failures remain non-fatal

Scope

  • No config migration, no schema bump, and no new user-facing settings
  • DPI still has the same pending-slot race shape, but it is deferred here because it has lower concurrency pressure
  • engine.set_smart_shift() and set_dpi() remain synchronous; async dispatch is follow-up work
  • IOHIDManagerRegisterDeviceRemovalCallback would be a more complete disconnect signal on macOS, but that is also follow-up work

Tested on

  • macOS 15.5 (MX Master 3S over Bluetooth): full local test suite passed (222 tests)
  • Nobara 43 KDE Wayland (MX Master 3S over Bluetooth): full local test suite passed (217 tests)

@TomBadash

Copy link
Copy Markdown
Owner

Hey @hieshima, I pulled this PR locally and tested the reconnect/recovery flow on my side, including disconnect/reconnect scenarios and recovery after a HID read failure. I also verified that the saved device state is restored correctly after reconnect, Smart Shift comes back, DPI is replayed correctly, and gesture handling keeps working as expected. Everything looks good from my testing.

I also want to say I’m genuinely very impressed by the quality of this PR, and honestly by all of your PRs so far. They’ve been thoughtful, well-scoped, and very helpful.

If this is something that would interest you, I’d be very happy to talk about making you a maintainer. If you want, feel free to ping me somewhere easier for real-time chat, however you prefer.

@TomBadash TomBadash merged commit 4e29741 into TomBadash:master Apr 11, 2026
1 check passed
@hieshima

Copy link
Copy Markdown
Collaborator Author

Hey @TomBadash, thank you for reviewing my PRs. I'd like to be a maintainer.

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