Skip to content

fix(linux): stabilize connection state and replay HID settings on reconnect#55

Open
hieshima wants to merge 3 commits intoTomBadash:masterfrom
hieshima:fix/linux-logitech-connection-state
Open

fix(linux): stabilize connection state and replay HID settings on reconnect#55
hieshima wants to merge 3 commits intoTomBadash:masterfrom
hieshima:fix/linux-logitech-connection-state

Conversation

@hieshima
Copy link
Copy Markdown
Contributor

Summary

This PR improves Linux reconnect correctness by separating evdev input readiness from HID++ feature readiness, and by restoring HID-only settings when HID++ becomes available again.

What changes

  • Connection state stability on Linux

    • The mouse now appears in the UI as soon as evdev successfully grabs it, without waiting for HID++ to connect.
    • Device info upgrades cleanly from evdev fallback data to HID++ data when the HID path becomes available later.
    • Linux evdev rediscovery now ignores non-Logitech candidates, which prevents wrong-device grabs after reconnect.
  • HID setting replay

    • Saved DPI and Smart Shift settings are now replayed when HID++ feature readiness returns.
    • This applies after startup and after BT reconnect.
    • Previously these settings were only restored once at launch via a delayed startup path, so reconnecting the mouse could leave device-side settings out of sync.
  • UI gating

    • DPI controls stay hidden until HID++ is actually ready.
    • This avoids presenting HID-only controls during the evdev-only window.
    • Smart Shift gating continues to follow feature support.
  • Diagnostics

    • If a HID candidate opens successfully but REPROG_V4 is not found on any tested device index, Mouser now logs that explicitly instead of falling through to a generic reconnect retry.

Notes

  • This PR focuses on correctness first.
  • On Linux:
    • mouseConnected continues to mean the input path is ready.
    • hidFeaturesReady is used to gate HID-only controls and replay HID-only settings.
  • No config version changes were needed.
  • No new config keys were added.

Cross-platform note

The HID++ communication path is cross-platform by design. This round of fixes was developed and runtime-tested on Linux (Nobara 43 KDE Wayland). macOS and Windows share the same HID++ replay and UI-gating code paths, but were not runtime-tested at the same depth in this round.

Validation

Targeted validation:

  • uv run python -m unittest tests.test_backend tests.test_engine tests.test_hid_gesture tests.test_mouse_hook tests.test_app_detector

Covered cases include:

  • evdev connects before HID++ and the mouse still appears in the UI
  • HID++ later upgrades the device info cleanly
  • DPI and Smart Shift replay when HID feature readiness returns
  • replay failures surface through the existing status message path
  • duplicate connected refreshes do not restart the battery poller
  • non-Logitech evdev candidates are ignored on reconnect
  • opened-without-REPROG_V4 failures are logged explicitly

Follow-up work

I have a separate reconnect polish commit that keeps retries aggressive for longer after disconnect, reuses the last successful HID candidate and device index, and improves reconnect diagnostics. I kept it out of this PR to focus the first review on correctness.

Separately, I prototyped a pyudev-based wakeup path to reduce reconnect latency further, but I left that experiment out of this PR. In my testing, the dominant reconnect delay was upstream of Mouser: there was still a long gap before Linux re-exposed usable input/hidraw nodes after BT reconnect. Because the watcher only helps after those nodes exist, the improvement over staged polling was not perceptible in my setup.

Keep Linux in a single connected-state model while restoring HID-only settings when
HID feature readiness returns after startup or reconnect.

This commit combines the existing second-stage UI/backend cleanup with the
first reconnect-focused fix. The UI continues to treat evdev readiness as the
main connection signal, while HID-only controls stay gated behind
hidFeaturesReady so DPI controls do not appear during the evdev-only gap.

On the engine side, reconnect handling now watches hid_features_ready instead
of plain connected=True. When HID identity transitions from unavailable to
available, Engine queues a daemon replay worker that reapplies the saved DPI
and Smart Shift mode. The temporary delayed startup restore path is kept only
as a transitional safety net and now routes through the same replay helper,
while staying dormant once a HID-ready replay has already been requested.

Replay failures are no longer silent. Engine now exposes a status callback,
and Backend forwards those messages onto the existing statusMessage signal with
queued cross-thread delivery so the UI can surface replay problems without new
public API.

This commit also closes a Linux diagnostics gap in hid_gesture: if a candidate
interface opens successfully but REPROG_V4 cannot be found on any tested
device index, Mouser now logs that explicitly instead of falling through to a
generic reconnect retry.

Tests added and updated:
- engine: hid_features_ready detection, duplicate refreshes not restarting the
  battery poller, HID-ready replay triggering, startup fallback suppression,
  replay failure status callback
- backend: hidFeaturesReady refreshes without re-emitting a false connection
  edge, engine status callback wiring, replay failure reaching statusMessage
- hid_gesture: opened-without-REPROG_V4 diagnostic, successful discovery path
  still unchanged

Validation:
- uv run python -m unittest tests.test_backend tests.test_engine
  tests.test_hid_gesture tests.test_mouse_hook tests.test_app_detector
@farfromrefug
Copy link
Copy Markdown

farfromrefug commented Mar 27, 2026

@hieshima i tried your PR. My mouse (Mx anywhere 3) is "recognized" as i i see on the UI, button mappings are working. But dpi is is not
I tracked it to _vendor_hid_infos not finding info, _hid.enumerate(LOGI_VID, 0) returns no info.

EDIT: i got it to work
I did 2 things:

sudo tee /etc/udev/rules.d/99-logitech-hidraw.rules > /dev/null <<'RULE'
# Allow user access to Logitech hidraw and uinput
ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", MODE="0660", GROUP="input"
KERNEL=="uinput", MODE="0660", GROUP="input"
RULE

sudo udevadm control --reload
sudo udevadm trigger

But also bumped hid dependency in requirements.txt.
Not sure what made the difference but now my system sees my mouse through hid

@TomBadash
Copy link
Copy Markdown
Owner

Thanks @hieshima for the continued work on Linux support -- this is shaping up really nicely. The approach of separating evdev readiness from HID++ feature readiness makes a lot of sense, and the HID settings replay on reconnect is a solid improvement.

And @farfromrefug, thanks for jumping in and testing with your MX Anywhere 3! Your feedback about the udev rules and hid dependency is really valuable.

A few things I want to make sure are solid before merging:

The udev rules issue @farfromrefug hit -- should we document the required udev rules somewhere (README or a setup script)? If _hid.enumerate(LOGI_VID, 0) returns nothing without them, that's going to be a common stumbling block for Linux users.

The hid dependency bump -- @farfromrefug, could you clarify which version you bumped to? If that's needed for Linux HID to work, we should include it in this PR.

Cross-platform safety -- the PR notes that macOS and Windows share the same HID++ replay and UI-gating code paths but weren't runtime-tested at the same depth. I tested on Windows + MX Master 3S and can confirm the existing HID++ paths are working fine on master, but I want to make sure nothing regresses. @hieshima, are there any code paths that could affect Windows/macOS behavior, or are the changes scoped to Linux-only branches?

The follow-up reconnect polish commit -- sounds promising. Happy to review that as a separate PR once this one lands.

I don't have a Linux setup to test on my end, so I'm relying on you both to validate. If you can confirm the above points, I'm ready to merge.

Looking forward to hearing how it holds up -- let me know if you run into anything else!

@farfromrefug
Copy link
Copy Markdown

@TomBadash i was talking about "hid" dep. But i am really not sure this made a difference as i did both at the same time.
About the _hid.enumerate(LOGI_VID, 0) yes this is how i found the issue. Maybe the app could show a warning when it is the case? Like first is your mouse "on" and then second on linux it could add a dialog with the code you can run to try and fix it. I would say all within the app for easy user access

The existing Linux permissions note only mentioned evdev and uinput.
HID++ features (DPI, battery, Smart Shift) also need read/write
access to /dev/hidraw*, which defaults to root-only on most distros.
Add the udev rule snippet so Linux users can enable HID++ without
running as root.
@hieshima hieshima force-pushed the fix/linux-logitech-connection-state branch from a5f1714 to b5f20f1 Compare March 30, 2026 14:27
@hieshima
Copy link
Copy Markdown
Contributor Author

Thanks @TomBadash for the review, and @farfromrefug for testing on Ubuntu — really helpful to get coverage on a second distro.

Addressing each point:

udev rules documentation

Already done — commit b5f20f1 on this branch adds a Linux Prerequisites section to the README with the udev rules snippet. I also like @farfromrefug's idea of showing an in-app hint when _hid.enumerate(LOGI_VID, 0) returns empty. The evdev/HID++ split already gives us the right detection point (hid_features_ready stays false while evdev_ready is true), so this would be a clean follow-up PR.

hid dependency bump

@farfromrefug — the udev rules are what fixed it. /dev/hidraw* is restricted to root by default on most distros; without the udev rule granting access, no Python HID library version can enumerate the device. The hidapi>=0.14 in requirements.txt is sufficient.

Cross-platform safety

The changes are scoped to Linux-only code paths:

  • _evdev_ready / _hid_ready / _refresh_device_state() only exist inside the elif sys.platform == "linux" MouseHook class
  • _find_mouse_device() filtering non-Logitech candidates is Linux-only
  • build_evdev_connected_device_info() is only called from Linux MouseHook
    The shared code that changed is _on_connection_change() and _replay_saved_settings_worker() in engine.py, but these use hid_features_ready which resolves to the same behavior on macOS/Windows (HID++ connects immediately there, no evdev layer). @TomBadash your Windows test confirming existing HID++ paths work is good coverage for that.

Follow-up

I have a reconnect polish commit ready — will open as a separate PR once this lands.

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.

3 participants