Skip to content

fix(logi): restore receiver diverts after idle wake - #1012

Draft
LJAYi wants to merge 1 commit into
Caldis:masterfrom
LJAYi:codex/fix-logi-receiver-wake
Draft

fix(logi): restore receiver diverts after idle wake#1012
LJAYi wants to merge 1 commit into
Caldis:masterfrom
LJAYi:codex/fix-logi-receiver-wake

Conversation

@LJAYi

@LJAYi LJAYi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Motivation

Some Logitech devices connected through a receiver can lose their temporary HID++ control diversion after being idle, without emitting the receiver's HID++ 1.0 0x41 disconnect/reconnect notifications.

This was reproduced with a Logitech M720 Triathlon through a Unifying receiver:

  • the M720 stopped reporting for more than five minutes;
  • it resumed with a normal HID++ 2.0 peripheral report, without any slot disconnect/reconnect notification;
  • Back, Forward, and MultiPlatform Gesture no longer produced diverted button events;
  • manually applying Re-Divert restored the events immediately.

The existing reconnect path already restores diversion when 0x41 notifications are present, but it cannot run when the receiver continues to consider the slot connected.

Before the fix

Relevant excerpt from the HID++ debug log (unrelated traffic omitted):

[12:33:05.201] RX: 11 01 23 10 01 ...
[12:38:40.935] RX: 11 01 04 00 01 01 01 ...  # normal peripheral report after 335.7 s idle
# no slot 1 DISCONNECTED / CONNECTED notification
# Back, Forward, and Gesture produce no diverted button events
[12:40:16.310] REPROG.SetControlReporting(CID=0x00D0 MultiPlatform Gesture, divert=ON)
[12:40:16.319] REPROG.SetControlReporting(CID=0x0053 Back, divert=ON)
[12:40:16.321] REPROG.SetControlReporting(CID=0x0056 Forward, divert=ON)
[12:40:17.659] BUTTON EVENT: 0x00D0 (MultiPlatform Gesture)

Reproduction

  1. Connect a Logitech M720 Triathlon through a Unifying receiver.
  2. Bind Back, Forward, and MultiPlatform Gesture in Mos and verify that they work.
  3. Leave the M720 completely untouched for at least six minutes. A trackpad may be used during this period, but do not move or click the M720.
  4. Wake the M720 by moving it, then press Back, Forward, and Gesture.
  5. Before this change, the controls no longer produce diverted button events.
  6. Open HID++ Debug and click Re-Divert; the controls immediately work again.

What changed

  • Track the last peripheral-report time for each managed receiver slot.
  • Treat the first report after at least 60 seconds of silence as an implicit wake.
  • Reapply the current binding-derived diversion state after 1 and 2 seconds, allowing the peripheral time to finish waking.
  • Cancel remaining recovery work as soon as a diverted button event arrives.
  • Cancel and clear pending wake recovery state during teardown.

The recovery is event-driven and bounded. It adds no polling, does not use the malformed SetControlReporting ACK returned by the M720 as confirmation, and does not change BLE behavior.

Validation

  • scripts/qa/lint-logi-boundary.sh
  • LogiReceiverConnectionStateTests: 33 passed, 0 failed
  • Full test suite: 539 total; 536 passed, 0 failed, 3 real-device tests skipped as designed
  • Debug and Release arm64 builds
  • Real-device validation with the M720 and Unifying receiver:
    • no 0x41 disconnect/reconnect notification occurred;
    • Mos detected the first peripheral report after approximately six minutes idle;
    • Back, Forward, and MultiPlatform Gesture were automatically diverted again;
    • Forward button events resumed without using Re-Divert;
    • the second scheduled attempt was cancelled after the first real button event.

After the fix

Relevant excerpt from the HID++ debug log (unrelated traffic omitted):

[17:11:40.173] BUTTON EVENT: all released
[17:17:52.030] RX: 11 01 04 00 01 01 01 ...  # normal peripheral report after 371.9 s idle
# no slot 1 DISCONNECTED / CONNECTED notification
[17:17:52.031] Slot 1 resumed after idle; scheduling divert restore
[17:17:53.084] REPROG.SetControlReporting(CID=0x0053 Back, divert=ON)
[17:17:53.087] REPROG.SetControlReporting(CID=0x0056 Forward, divert=ON)
[17:17:53.090] REPROG.SetControlReporting(CID=0x00D0 MultiPlatform Gesture, divert=ON)
[17:17:53.345] BUTTON EVENT: 0x0056 (Forward)

The full raw logs contain receiver enumeration and unrelated paired-device traffic, so they are not committed to the repository. They can be provided to reviewers if needed.

Behavior and compatibility

This only affects managed HID++ receiver slots that have an initialized REPROG feature and active diverted bindings. The first report for a slot and normal report traffic do not trigger recovery. Existing receiver reconnect handling and BLE sessions are unchanged.

The main behavior tradeoff is that a managed receiver slot with active diverted bindings will receive a bounded re-divert after at least 60 seconds without peripheral reports, even if the idle period was not caused by sleep. SetControlReporting is idempotent for the desired state, and no polling or persistent timer is introduced.

@Caldis Caldis left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is real and your evidence chain is solid (idle resume without 0x41, manual Re-Divert restoring events immediately). But the current implementation conflicts with a hard performance constraint this codebase carries from a painful lesson, so I can't take it as-is.

Why unguarded re-diverting is a red line here

We previously shipped (and then removed, in 2a573be / docs/plans/2026-05-03-logi-ble-hidpp-divert-postmortem.md §4.2) a family of "keep the HID++ state alive by re-sending things" mechanisms — notification watchdog, ping pulses, protocol probe matrices. Real-device experience showed they only briefly wake the device, fight with Logitech Options+ over HID++ ownership (both sides re-asserting divert = oscillation), and add constant device load. The standing rule since then: anything that re-sends HID++ requests repeatedly over time is out; discrete one-shot actions on connect/disconnect/user events are fine.

This PR's shape trips that rule in daily use: a mouse crosses the 60s idle threshold many times per hour (reading, video, stepping away). Each wake schedules two unconditional redivertAllControls() passes (1s + 2s) — a full SetControlReporting volley per bound CID, twice, regardless of whether divert was actually lost. That's recurring HID++ write traffic keyed to normal usage patterns, with no "already correct" guard — exactly the keepalive failure mode in a new coat. The [1.0, 2.0] double-shot also reads as compensating for uncertain timing rather than a confirmed causal fix.

Requested change: probe, then repair

On detected idle-wake, send a single GetControlReporting for one representative bound CID (or walk the bound set) and compare against the expected divert state. Only if the readback shows divert lost, re-send — once — and mark the slot repaired until the next disconnect/wake transition. That converts N unconditional writes into 1 read on the common path (divert intact), keeps the fix event-driven, and adds the oscillation guard the red line requires. Bonus: the probe result gives you a definitive log line proving the firmware actually dropped tmpDivert on idle, which would settle the root cause beyond inference.

Please also coordinate with #1013

Both PRs are symptoms of the same device behavior (M720 sleeping without receiver notifications): #1013 handles "never taken over", this one handles "taken over, divert silently lost". Ideally they share one wake-inference mechanism with per-slot state (your lastReceiverReportTime tracking here and the offline→online inference there are two views of the same signal). A combined design would avoid two parallel detectors with different thresholds living in the same report path.

@LJAYi
LJAYi marked this pull request as draft August 16, 2026 09:28
@LJAYi

LJAYi commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed context and for pointing me to the earlier postmortem. I agree that the current unconditional two-pass re-divert is the wrong shape for this codebase. Even though it is bounded per wake, normal idle cycles would still turn it into recurring HID++ write traffic, and the interaction risk with Options+ is not worth taking.

I’m going to rework this around probe-then-repair: infer a possible wake, read back GetControlReporting, and only issue a single repair when the expected temporary divert state is actually missing. I’ll also coordinate it with #1013 so the two paths share per-slot activity state instead of growing separate wake detectors.

While re-validating the stock 4.2.1 behavior, I noticed an additional edge case that needs more controlled testing before I can describe it confidently. I don’t want to speculate from incomplete traces, so I’ll finish the baseline and revised-build comparisons first and report back once the behavior is reproducible and understood.

The next step is to capture the actual transition with reporting-state readback before and after wake, then validate the conditional repair on the real device. I’ve moved the PR back to draft until that evidence and the revised design are solid.

@Caldis

Caldis commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Moving to draft for evidence-first rework is exactly the right call — no rush on this side. The probe-then-repair direction and the plan to share per-slot activity state with #1013 (now merged) are both approved in principle; looking forward to the readback traces. If the additional edge case you found turns out to be a separate defect, feel free to file it independently so it doesn't block this PR.

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