Skip to content

fix(unifying): enable wireless notifications so paired devices enumerate#309

Merged
AprilNEA merged 1 commit into
AprilNEA:masterfrom
laofun:pr/unifying-wireless-notifications
Jul 18, 2026
Merged

fix(unifying): enable wireless notifications so paired devices enumerate#309
AprilNEA merged 1 commit into
AprilNEA:masterfrom
laofun:pr/unifying-wireless-notifications

Conversation

@laofun

@laofun laofun commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

A device on a Unifying receiver wouldn't show up even while connected. The
receiver only re-broadcasts arrival events while wireless notifications
(register 0x00) are on, and we never enabled it — so the trigger got ACK'd
but emitted nothing. Solaar does this before listing.

Changes

  • Enable wireless notifications before draining arrivals.
  • Derive online from the feature walk succeeding (event.online was always
    "offline" here).
  • Read device names at Unifying's 0x40 base instead of Bolt's 0x60, and
    clamp the reported length.
  • Dedup repeated arrivals per slot.

Testing

Tested on an MX Master 2S over a Unifying receiver. Added unit tests for the
name parser (normal, over-long length, short response).

@laofun
laofun force-pushed the pr/unifying-wireless-notifications branch from 58d6a8e to f68b17d Compare June 22, 2026 15:46
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes Unifying receiver enumeration by enabling wireless notifications (register 0x00) before triggering device-arrival broadcasts, which are silently dropped by the firmware until that flag is set. The PR also patches three related issues found during debugging: incorrect sub-register base for Unifying device-name reads (0x40 vs Bolt's 0x60), a bad online-status source (event.online was always reporting "offline" due to a wrong bit), and duplicate 0x41 events for the same slot.

  • Adds set_wireless_notifications with a careful read-modify-write to avoid clobbering SOFTWARE_PRESENT (written by the pairing flow on the same register).
  • Derives online from whether the HID++ 2.0 feature walk succeeds this cycle, with an explicit caveat about cache-hit stale windows self-healing on the next forced re-probe.
  • Adds parse_codename_unifying with length clamping and three unit tests covering the normal, over-long, and short-response cases.

Confidence Score: 5/5

The fix is safe to merge; it correctly enables the notification register before triggering arrival events, uses read-modify-write to avoid clobbering concurrent pairing state, and the new name parser bounds-checks the firmware-reported length.

All three changed files make narrow, well-reasoned corrections to the Unifying enumeration path. The read-modify-write on register 0x00 is the right pattern, the sub-register arithmetic is wire-verified, and the dedup+sort logic is straightforward. The acknowledged stale-cache caveat for the online flag is a pre-existing limitation of the caching layer, not a new regression.

No files require special attention.

Important Files Changed

Filename Overview
crates/openlogi-hidpp/src/receiver/unifying.rs Adds Register::Notifications = 0x00 and set_wireless_notifications with a correct read-modify-write; adds a clarifying NOTE on DeviceCodename = 0x60 to prevent future misuse.
crates/openlogi-hid/src/inventory/probe.rs Calls set_wireless_notifications(true) before the arrival drain, adds per-slot dedup + sort, fixes Unifying name reads via new read_codename_unifying / parse_codename_unifying, and derives online from feature-walk success.
crates/openlogi-hid/src/inventory/tests.rs Adds three targeted unit tests for parse_codename_unifying covering normal, over-long length byte, and short-response edge cases.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant P as probe_unifying_receiver
    participant D as drain_device_arrival_unifying
    participant R as UnifyingReceiver (HID++)
    participant S as probe_unifying_slot

    P->>D: "drain_device_arrival_unifying(&unifying)"
    D->>R: set_wireless_notifications(true) [NEW]
    R-->>D: ACK (register 0x00 written)
    D->>R: listen() — subscribe to 0x41 events
    D->>R: trigger_device_arrival() (register 0x02)
    R-->>D: 0x41 DeviceConnection (slot N) x M events
    D-->>P: "Some(Vec<UnifyingDeviceConnection>)"

    P->>P: dedup by slot (HashMap) + sort_by_key(index) [NEW]

    loop for each slot (concurrent)
        P->>S: probe_unifying_slot(conn)
        S->>R: read_long_register(0xB5, 0x40+slot-1) [NEW Unifying base]
        R-->>S: [sub, len, name]
        S->>R: "probe_or_reuse(online=true) [NEW: always probe]"
        R-->>S: "ProbedFeatures { capabilities, battery }"
        S-->>P: "PairedDevice { online: capabilities.is_some() } [NEW liveness]"
    end

    P-->>P: "NodeProbe { inventory, healthy, complete }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant P as probe_unifying_receiver
    participant D as drain_device_arrival_unifying
    participant R as UnifyingReceiver (HID++)
    participant S as probe_unifying_slot

    P->>D: "drain_device_arrival_unifying(&unifying)"
    D->>R: set_wireless_notifications(true) [NEW]
    R-->>D: ACK (register 0x00 written)
    D->>R: listen() — subscribe to 0x41 events
    D->>R: trigger_device_arrival() (register 0x02)
    R-->>D: 0x41 DeviceConnection (slot N) x M events
    D-->>P: "Some(Vec<UnifyingDeviceConnection>)"

    P->>P: dedup by slot (HashMap) + sort_by_key(index) [NEW]

    loop for each slot (concurrent)
        P->>S: probe_unifying_slot(conn)
        S->>R: read_long_register(0xB5, 0x40+slot-1) [NEW Unifying base]
        R-->>S: [sub, len, name]
        S->>R: "probe_or_reuse(online=true) [NEW: always probe]"
        R-->>S: "ProbedFeatures { capabilities, battery }"
        S-->>P: "PairedDevice { online: capabilities.is_some() } [NEW liveness]"
    end

    P-->>P: "NodeProbe { inventory, healthy, complete }"
Loading

Reviews (5): Last reviewed commit: "fix(unifying): enable wireless notificat..." | Re-trigger Greptile

Comment thread crates/openlogi-hid/src/inventory.rs Outdated
@laofun
laofun force-pushed the pr/unifying-wireless-notifications branch from f68b17d to 9aff9b7 Compare June 22, 2026 16:00
@laofun

laofun commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both review notes: the deduped connection list is now sorted by slot so device order is stable across probe cycles, and added a note on InfoSubRegister::DeviceCodename clarifying that 0x60 is the Bolt base while Unifying name reads go through the wire-verified 0x40 path in inventory.rs.

@laofun laofun closed this Jun 22, 2026
@laofun laofun reopened this Jun 22, 2026

@AprilNEA AprilNEA 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.

This looks like the right direction overall — enabling the Unifying notification stream before trigger_device_arrival, correcting the 0x40 name base, and deduping repeated arrivals are all well-scoped, and CI is green.

One edge I’d tighten: set_wireless_notifications(true) currently writes the whole notifications register as [0, 1, 0]. That sets the wireless bit, but it also clears any other notification flags already enabled on register 0x00. In this repo, the pairing flow writes [0x00, 0x09, 0x00] (WIRELESS | SOFTWARE_PRESENT) before streaming pairing events, so an inventory poll during/near pairing could accidentally drop the software-present bit or any future notification flag.

Could we make this read-modify-write, or at least use the same named notification flag constants as pairing.rs so the intended bitmask is explicit? The rest of the PR looks good to me.

@laofun
laofun force-pushed the pr/unifying-wireless-notifications branch from 9aff9b7 to f2b7ef5 Compare June 25, 2026 03:03
@laofun

laofun commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — switched set_wireless_notifications to read-modify-write: it now reads the notifications register first and only flips the WIRELESS bit, so SOFTWARE_PRESENT and any other flags are preserved.

@chrematt

chrematt commented Jul 6, 2026

Copy link
Copy Markdown

Can confirm this fix works. Setup: MX Master 3 (wpid 4082) on a Unifying receiver (046d:c52b), macOS 26 (Apple Silicon).

On stock v0.6.19 the receiver enumerates (pairing_count=Some(1)) but every probe cycle ends with online devices differ from pairing count; … expected=1 found=0, so the device never surfaces in the GUI (or flashes in briefly after pairing and disappears again). A direct HID++ probe against the receiver (enable wireless notifications on register 0x00, then trigger a fake arrival via 0x02) immediately yields a 0x41 arrival for slot 1 with the link established — the device was online the whole time; the agent just never turned notifications on. Exactly what this PR describes.

Note the branch no longer applies cleanly onto v0.6.19 (inventory.rs has since been split into inventory/probe.rs etc.), so I ported the changes onto the v0.6.19 tag to test. With the port applied, the slot enumerates stably across ticks as online=true … codename=Some("MX Master 3"), the name reads correctly via the Unifying 0x40 base, and the three parse tests pass. Happy to share the rebased version if that helps get this merged.

(Unrelated: occasional device probe timed out — treating as a failed probe budget=5s warnings remain, but that looks like #251 and the node ledger bridges them.)

@rafaelje

Copy link
Copy Markdown

Confirmed that this fix works on my setup, GPT-5.6 SOL make it work:

  • macOS 26.5.1, Apple Silicon
  • OpenLogi v0.6.19
  • Logitech MX Master 3
  • Unifying receiver 046d:c52b
  • Mouse WPID 4082, slot 1

With the stock v0.6.19 agent, every inventory cycle reported:

receiver reports pairing count pairing_count=Some(1)
drained device-arrival events events=0
online devices differ from pairing count; offline devices not yet surfaced for Unifying expected=1 found=0

The receiver knew that one device was paired, but trigger_device_arrival() produced no 0x41 events because wireless notifications had not been
enabled. Consequently, the GUI briefly detected the mouse and then marked it offline.

I ported this PR to the split inventory layout in the v0.6.19 tag. After enabling the WIRELESS bit in notification register 0x00 before triggering
arrivals, every inventory cycle consistently reports:

drained device-arrival events events=1
unifying paired slot slot=1 online=true wpid=4082 kind=Mouse codename=Some("MX Master 3")

The corrected Unifying codename register (0x40 + slot - 1) also returns MX Master 3 correctly. Control capture, gesture buttons, DPI buttons,
thumbwheel handling, and the macOS event hook are active.

The agent is running continuously through launchd, and the mouse no longer disappears from the GUI. The OpenLogi HID and HID++ test suites also
pass: 212 tests passed, 0 failed.

This confirms that the missing wireless-notification initialization is the root cause for this MX Master 3 / Unifying receiver combination.

mishahawthorn added a commit to mishahawthorn/OpenLogi that referenced this pull request Jul 17, 2026
Port of AprilNEA#309 onto master (0.6.19 probe.rs layout). A device on a Unifying
receiver would flap offline seconds after connecting: the receiver only
re-broadcasts 0x41 arrival events while wireless notifications (register
0x00) are enabled, which was never done, so trigger_device_arrival was
ACK'd but emitted nothing for already-connected devices.

- hidpp: add Register::Notifications + set_wireless_notifications (RMW of
  the WIRELESS bit, preserving SOFTWARE_PRESENT).
- probe.rs: enable wireless notifications before draining arrivals; dedup
  and sort arrivals per slot; derive online from the feature walk
  succeeding (event.online reads the wrong byte and is always set); read
  Unifying codenames at base 0x40 (not Bolt's 0x60) with a clamped length.
- Inline unit tests for the Unifying codename parser.

Verified on MX Master 3 over a Unifying receiver: surfaces online with
correct codename and stays online while in use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mishahawthorn added a commit to mishahawthorn/OpenLogi that referenced this pull request Jul 17, 2026
Port of AprilNEA#309 onto master (0.6.19 probe.rs layout). A device on a Unifying
receiver would flap offline seconds after connecting: the receiver only
re-broadcasts 0x41 arrival events while wireless notifications (register
0x00) are enabled, which was never done, so trigger_device_arrival was
ACK'd but emitted nothing for already-connected devices.

- hidpp: add Register::Notifications + set_wireless_notifications (RMW of
  the WIRELESS bit, preserving SOFTWARE_PRESENT).
- probe.rs: enable wireless notifications before draining arrivals; dedup
  and sort arrivals per slot; derive online from the feature walk
  succeeding (event.online reads the wrong byte and is always set); read
  Unifying codenames at base 0x40 (not Bolt's 0x60) with a clamped length.
- Inline unit tests for the Unifying codename parser.

Verified on MX Master 3 over a Unifying receiver: surfaces online with
correct codename and stays online while in use.
The receiver only re-broadcasts 0x41 device-arrival events while wireless
notifications are on; with them off, trigger_device_arrival is ACK'd but emits
nothing, so a paired online device never surfaces. Enable them (read-modify-
write of just the WIRELESS bit so the pairing flow's SOFTWARE_PRESENT isn't
clobbered) before draining, matching Solaar.

Also on the Unifying path:
- dedup the arrival burst per slot and sort by slot so a device isn't listed
  twice and the order stays stable across cycles;
- read names from the Unifying base 0x40+(n-1) via read_codename_unifying
  (0x60 is Bolt-only), which works even while the device is offline;
- treat a successful feature walk — not the unreliable event.online byte — as
  the liveness signal, always probing and reporting online = walk succeeded.
@AprilNEA
AprilNEA force-pushed the pr/unifying-wireless-notifications branch from f2b7ef5 to c882724 Compare July 18, 2026 14:15
@AprilNEA
AprilNEA merged commit 8b7317c into AprilNEA:master Jul 18, 2026
12 checks passed
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.

4 participants