Skip to content

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

Open
laofun wants to merge 1 commit into
AprilNEA:masterfrom
laofun:pr/unifying-wireless-notifications
Open

fix(unifying): enable wireless notifications so paired devices enumerate#309
laofun wants to merge 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 events, mirroring Solaar's approach. It also corrects the online-status derivation (from a wrong event bit to feature-walk success), fixes the name sub-register base (0x40 vs. Bolt's 0x60), and deduplicates repeated 0x41 arrivals per slot.

  • Wireless notification enable: set_wireless_notifications(true) is called before trigger_device_arrival; uses a read-modify-write on register 0x00 to avoid clobbering SOFTWARE_PRESENT.
  • Online flag fix: online is now derived from probe.capabilities.is_some() instead of event.online, which was always wrong for triggered arrivals (bit6 always set in wire-captured payloads).
  • Name reader: New read_codename_unifying / parse_codename_unifying functions read from sub-register base 0x40 + (n-1) with length clamping; three unit tests added to cover normal, over-long, and short-response cases.

Confidence Score: 4/5

Safe to merge for the core enumeration fix; one logic gap in error handling could transiently clear the device list on a first-cycle HID read failure.

The wireless-notification enable, name-register fix, and online-flag correction are all well-motivated and wire-verified. The one concern is drain_device_arrival_unifying: when set_wireless_notifications fails but trigger_device_arrival succeeds, the function returns Some([]) rather than None, telling the ledger 'no devices found' instead of 'probe unreliable — use last snapshot.' On a fresh attach combined with a transient HID read error, this would show all paired devices as offline for that cycle.

crates/openlogi-hid/src/inventory.rs — specifically the error handling in drain_device_arrival_unifying

Important Files Changed

Filename Overview
crates/openlogi-hid/src/inventory.rs Adds wireless-notification enable before arrival drain, dedup+sort for duplicate 0x41 events, Unifying-specific name reader with length clamping, and corrects online flag to use feature-walk success. One logic gap: a failed set_wireless_notifications is silently swallowed, causing Some([]) to be returned rather than None when the WIRELESS bit was not already set.
crates/openlogi-hidpp/src/receiver/unifying.rs Adds Register::Notifications = 0x00 enum variant and set_wireless_notifications() with read-modify-write to avoid clobbering SOFTWARE_PRESENT. Adds clarifying NOTE to DeviceCodename = 0x60 about the Bolt-vs-Unifying base address discrepancy.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Inv as inventory.rs
    participant Uni as UnifyingReceiver
    participant HW as Receiver HW

    Inv->>Uni: drain_device_arrival_unifying()
    Uni->>HW: read_register(0x00) [Notifications]
    HW-->>Uni: current flags
    Uni->>HW: "write_register(0x00, flags | WIRELESS)"
    HW-->>Uni: ACK
    Note over Inv,HW: WIRELESS bit now set — 0x41 events will flow
    Uni->>HW: trigger_device_arrival (write 0x02)
    HW-->>Uni: ACK
    HW-)Uni: 0x41 DeviceConnection (slot 1)
    HW-)Uni: 0x41 DeviceConnection (slot 1, duplicate)
    Uni-->>Inv: Some([conn_slot1, conn_slot1])
    Note over Inv: Dedup by slot index (HashMap), then sort
    Inv->>Uni: read_long_register(0xFF, 0xB5, [0x40+slot-1, …])
    HW-->>Uni: [sub, len, name bytes, padding]
    Note over Inv: parse_codename_unifying clamps len
    Inv->>HW: "probe_or_reuse(slot, online=true)"
    HW-->>Inv: capabilities (Some → online, None → offline)
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 Inv as inventory.rs
    participant Uni as UnifyingReceiver
    participant HW as Receiver HW

    Inv->>Uni: drain_device_arrival_unifying()
    Uni->>HW: read_register(0x00) [Notifications]
    HW-->>Uni: current flags
    Uni->>HW: "write_register(0x00, flags | WIRELESS)"
    HW-->>Uni: ACK
    Note over Inv,HW: WIRELESS bit now set — 0x41 events will flow
    Uni->>HW: trigger_device_arrival (write 0x02)
    HW-->>Uni: ACK
    HW-)Uni: 0x41 DeviceConnection (slot 1)
    HW-)Uni: 0x41 DeviceConnection (slot 1, duplicate)
    Uni-->>Inv: Some([conn_slot1, conn_slot1])
    Note over Inv: Dedup by slot index (HashMap), then sort
    Inv->>Uni: read_long_register(0xFF, 0xB5, [0x40+slot-1, …])
    HW-->>Uni: [sub, len, name bytes, padding]
    Note over Inv: parse_codename_unifying clamps len
    Inv->>HW: "probe_or_reuse(slot, online=true)"
    HW-->>Inv: capabilities (Some → online, None → offline)
Loading

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

Comment thread crates/openlogi-hid/src/inventory.rs
@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.

The receiver only re-broadcasts 0x41 device-arrival events while wireless
notifications (register 0x00) are on; without it an actively-connected
device showed "no paired devices". Add set_wireless_notifications() and
enable it before draining arrivals (Solaar does the same before listing).

Derive online from the HID++ 2.0 feature walk succeeding — the crate's
event.online reads the wrong notification byte (always "offline" here) and
trigger_device_arrival re-broadcasts 0x41 for offline slots too. Read
device names at the Unifying 0x40 base (not Bolt's 0x60), clamp the
device-reported name length so a bogus byte can't over-read the register,
and dedup repeated arrivals per slot.
@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.

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