Commit Graph

18 Commits

Author SHA1 Message Date
Ken Sanislo 7851acb850 Tighten Centurion probe and silence FeaturesArray query spam
Three cleanup fixes after field testing confirmed the brute-force probe
finds device_addr=0x23 on the G522 (no special addresses at 0x00/0xFF):

1. Short-circuit the probe on first hit instead of sweeping all 256. The
   diagnostic full-sweep served its purpose — only 0x23 responds.
2. Drop per-candidate read timeout from 20ms to 5ms. USB round-trip is
   <1ms, so 5ms is 5x margin. Worst case: 1.3s. Typical: 180ms.
3. Short-circuit FeaturesArray.__getitem__ for Centurion devices. All
   parent + sub-device features are enumerated upfront by
   _check_centurion(). If a feature isn't in the dict after _check(),
   it genuinely doesn't exist — skip the raw ROOT.GetFeature query that
   the dongle rejects with LOGITECH_ERROR. Eliminates cycling
   {0002}..{000F} error log spam during settings init and speeds up
   check_feature_settings() by ~225 round trips per device.
2026-05-10 15:39:17 -07:00
Ken Sanislo 0eb592d6e1 Full-sweep probe: log all responding device_addr candidates
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 556c83d0a4 Probe device_addr one at a time instead of blasting all 256
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 6f3e6fccd3 Brute-force probe all 256 device_addr candidates on 0x50 handles
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo f73fe2d295 Probe Centurion 0x50 device address on handle open
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo e5f7da6bf5 Add G522 LIGHTSPEED headset support (Centurion 0x50 transport variant)
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.
2026-05-10 15:39:16 -07:00
Ken Sanislo 25865994cb
device: Treat empty hidraw read as device removal (EOF) (#3174)
* 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>
2026-04-14 11:56:01 -04:00
MattHag 3186d880fc base: Refactor device filtering
Related #2273
2025-01-01 11:20:28 -05:00
MattHag 5d86c74df4 base: Turn filter_products_of_interest into a public function
Related #2273
2025-01-01 11:20:28 -05:00
MattHag 5cf7cbfd5d base: Improve tests of known receivers
Related #2273
2025-01-01 11:20:28 -05:00
Romain Loutrel a19461b29d
refactor: replace ERROR NamedInts by IntEnum (#2645)
* 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>
2024-11-02 10:17:50 -04:00
Romain Loutrel 79ffbda903
change pairing error values to intenums
* 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)
2024-10-23 16:22:22 -04:00
MattHag 1f85ec01e7 base: Add more unit tests
Make internal functions private.
2024-10-11 07:42:38 -04:00
MattHag 37e2ac80ba base: Add test for filter_products_of_interest 2024-10-11 07:42:38 -04:00
MattHag 8d0672ac3c base_usb: Add external interface
Clean up, type hint and tests base_usb and related modules.
2024-10-11 07:42:38 -04:00
MattHag aa354dc4c4 Replace global sw_id with function call
Add test for it.
2024-10-08 14:35:16 -04:00
MattHag 2442299539 base: Simplify receiver info retrieval
- Remove comments with unused receivers
- Simplify receiver hardcoded info
2024-10-08 14:35:16 -04:00
MattHag 0d225f6cb1
test: Test base product information
* test: Test base product information

Related #1097

* Fix dependencies for gi
2024-03-10 10:11:02 -04:00