For initial field testing, sweep all 256 candidates instead of stopping
on first hit. Logs every address that responds (sent_addr, response_addr,
first 8 bytes) at INFO level so we can discover if 0x00, 0xFF, or other
addresses have special behavior.
The first responding address is still used as the device_addr. Revert to
short-circuit once we've confirmed there are no special addresses worth
trying first.
Write one candidate, read briefly (20ms), move to next. Stops on first
hit. Avoids overwhelming the dongle's RX buffer with 256 back-to-back
frames where the response could get lost.
Worst case (addr=0xFF): 256 × 20ms ≈ 5s.
Typical G522 (addr=0x23): 36 × 20ms ≈ 0.7s.
The previous probe sent a single all-zeros frame and waited for the
dongle to respond — but the dongle silently drops frames with the
wrong device_addr, producing no response.
Now send a valid ROOT.GetProtocolVersion request for every possible
device_addr (0x00–0xFF). The dongle ignores the 255 wrong addresses
and responds only to the correct one. The response carries the real
address at byte[1]. 256 writes complete in under 100ms on USB; the
read phase (3 x 500ms) catches the single response.
This discovers the address during synchronous init, before the
listener starts, eliminating the need for deferred init when the
headset is already powered on.
The 0x50 variant requires a device-specific address byte at frame[1]
on every TX frame. Until now we left state.device_addr=None and sent
0x00 as a placeholder, relying on the device to either be permissive
enough to respond, or to send an unsolicited frame early enough for
_unwrap_centurion_frame to learn the address passively.
Strict firmware silently drops device_addr=0x00 requests, which breaks
dongle feature discovery before it can start: _discover_dongle_features
times out, has_bridge is False, create_centurion_receiver falls through
to create_device, Device.__init__ leaves _protocol=None, and a later
read_battery() dispatches HID++ 1.0 read_register(BATTERY_CHARGE) that
the dongle rejects with INVALID_SUB_ID_COMMAND.
Port strain08's fix from LGSTrayEx (commits 1439b27a + c6d21972): right
after registering a 0x50 handle, write a 64-byte all-zero frame with
just the report ID set. That elicits an error/unsolicited response
whose byte[1] is the real device address. Read up to 3 x 500ms until
a matching frame arrives, then store the address on the handle state
so subsequent TX frames carry it correctly.
On timeout the probe logs a warning and leaves device_addr=None, so
behavior falls back to the current 0x00-placeholder path (no regression
for devices where the probe isn't needed). The passive learn-on-first-RX
in _unwrap_centurion_frame is preserved as a second line of defense.
The G522 uses a Centurion protocol variant with report ID 0x50 that adds
a device address byte at frame position [1], shifting all CPL fields by
+1 compared to the PRO X 2's 0x51 variant. This commit adds transport-
layer support for both variants while consolidating per-handle centurion
state into a single CenturionHandleState dataclass.
Key changes:
- Consolidate _centurion_handles (set) and _centurion_protocol_versions
(dict) into a single dict[int, CenturionHandleState] keyed by handle
- Add _unwrap_centurion_frame() helper that auto-detects 0x50 vs 0x51
from raw frame bytes and learns the device address on first RX
- Add _centurion_frame_header() to build the correct TX header per variant
- Detect both report IDs in udev report descriptor parsing
- Adjust bridge fragment chunk sizes for 0x50's extra header byte
- Propagate full CenturionHandleState when opening per-thread handles
Wire format verified against G522 diagnostic logs from LGSTrayEx#15.
* Treat empty hidraw read as device removal (EOF)
When select() reports a hidraw fd as readable but os.read() returns
empty bytes, that's EOF per POSIX — the device has been removed.
Previously this was silently treated as no data, causing the listener
to loop indefinitely on a gone device instead of cleaning up.
* Fix test_ping_errors missing mocks for _read_input_buffer and write
The test was not mocking _read_input_buffer or write, so ping() would
call into real hidapi.read() with a fake handle (fd 1). The empty-read
EOF detection added in the previous commit made this consistently fail
by raising OSError → NoReceiver before reaching the mocked _read path.
Add the same mocks used by the adjacent test_request_errors.
---------
Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
* refactoring(logitech_receiver/notifications): change to enums PairingError and BoltPairingError
* refactoring(logitech_receiver/notifications): change to enums PairingError and BoltPairingError (Fix pre-commit checks)
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum
* refactor(logitech_receiver/base.py): create unit tests for request function before replacing ERROR NamedInts by IntEnum
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum (add exclusion for macOS)
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum (fix for python < 3.10)
* refactor(solaar/cli./probe.py): create unit tests for run function before replacing ERROR NamedInts by IntEnum (focusing on the call order when receiving errors)
* refactor(solaar/cli./probe.py): refactor register processing to handle short and long registers in a single loop structure for improved readability and reduced code duplication.
* refactor(logitech_receiver/hidpp10_constants.py): replace ERROR NamedInt by IntEnum.
* refactor(logitech_receiver/hidpp10_constants.py): distinguish hidpp10 and hidpp20 errors in the code for readibility.
* refactor(logitech_receiver/hidpp20_constants.py): replace ERROR NamedInt by IntEnum.
* refactor(logitech_receiver/hidpp20_constants.py): replace ERROR NamedInt by IntEnum. (fix problem with | operator when typing with python 3.8)
* feature(hide on startup option): Visual test (not binded yet) DRAFT
* refactor(solaar/cli./probe.py): create unit tests for run function before replacing ERROR NamedInts by IntEnum (focusing on the call order when receiving errors)
* refactor(solaar/cli./probe.py): refactor register processing to handle short and long registers in a single loop structure for improved readability and reduced code duplication.
* refactor(logitech_receiver/hidpp10_constants.py): replace ERROR NamedInt by IntEnum.
* refactor(logitech_receiver/hidpp20_constants.py): replace ERROR NamedInt by IntEnum.
* refactor(logitech_receiver/hidpp20_constants.py): replace ERROR NamedInt by IntEnum. (fix problem with | operator when typing with python 3.8)
* feature(hide on startup option): Visual test (not binded yet) DRAFT
* Merge: Refactor: hidpp20 to use enum
* Merge: Refactor: hidpp20 to use enum (fix test)
---------
Co-authored-by: some_developer <some.developper.44@gmail.com>
* refactoring(logitech_receiver/notifications): change to enums PairingError and BoltPairingError
* refactoring(logitech_receiver/notifications): change to enums PairingError and BoltPairingError (Fix pre-commit checks)
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum
* refactor(logitech_receiver/base.py): create unit tests for request function before replacing ERROR NamedInts by IntEnum
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum (add exclusion for macOS)
* refactor(logitech_receiver/base.py): create unit tests for ping function before replacing ERRORNamedInts by IntEnum (fix for python < 3.10)