From e5bd8db19eadabb34586585bb4facae919711d00 Mon Sep 17 00:00:00 2001 From: Aron W Date: Fri, 15 May 2026 20:03:52 +0200 Subject: [PATCH] Fix G502 X receiver persistence matching --- lib/logitech_receiver/descriptors.py | 1 + lib/solaar/configuration.py | 14 +++-- tests/solaar/test_configuration.py | 81 ++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/solaar/test_configuration.py diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index f2c9bbd3..3201ffcc 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -423,6 +423,7 @@ _D("G703 Hero Gaming Mouse", codename="G703 Hero", usbid=0xC090) _D("G903 Hero Gaming Mouse", codename="G903 Hero", usbid=0xC091) _D(None, kind=DEVICE_KIND.mouse, usbid=0xC092, interface=1) # two mice share this ID _D("M500S Mouse", codename="M500S", usbid=0xC093, interface=1) +_D("G502 X Lightspeed Gaming Mouse", codename="G502 X Lightspeed", wpid="409F", usbid=0xC098) # _D('G600 Gaming Mouse', codename='G600 Gaming', usbid=0xc24a, interface=1) # not an HID++ device _D("G500 Gaming Mouse", codename="G500 Gaming", usbid=0xC068, interface=1, protocol=1.0) _D("G500s Gaming Mouse", codename="G500s Gaming", usbid=0xC24E, interface=1, protocol=1.0) diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py index dcbd7578..43a57749 100644 --- a/lib/solaar/configuration.py +++ b/lib/solaar/configuration.py @@ -224,16 +224,23 @@ yaml.add_representer(NamedInt, named_int_representer) # A device can be identified by a combination of WPID and serial number (for receiver-connected devices) -# or a combination of modelId and unitId (for direct-connected devices). +# or a combination of modelId and unitId (for direct-connected devices). Some devices report the same +# physical ID as the receiver serial in one mode and as the direct unitId in another, so match those too +# when another stable identifier agrees. # But some devices have empty (all zero) modelIds and unitIds. Use the device name as a backup for the modelId. # The worst situation is a receiver-connected device that Solaar has never seen on-line # that is directly connected. Here there is no way to realize that the two devices are the same. # So new entries are not created for unseen off-line receiver-connected devices def persister(device): def match(wpid, serial, modelId, unitId, c): + same_model = modelId and modelId == c.get(_KEY_MODEL_ID) + same_wpid = wpid and wpid == c.get(_KEY_WPID) + same_name = device_name and device_name == c.get(_KEY_NAME) + cross_unit_serial = (unitId and unitId == c.get(_KEY_SERIAL)) or (serial and serial == c.get(_KEY_UNIT_ID)) return ( - (wpid and wpid == c.get(_KEY_WPID) and serial and serial == c.get(_KEY_SERIAL)) - or (modelId and modelId == c.get(_KEY_MODEL_ID) and unitId and unitId == c.get(_KEY_UNIT_ID)) + (same_wpid and serial and serial == c.get(_KEY_SERIAL)) + or (same_model and unitId and unitId == c.get(_KEY_UNIT_ID)) + or (cross_unit_serial and (same_model or same_wpid or same_name)) or ( c.get(_KEY_WPID) is None and c.get(_KEY_SERIAL) is None @@ -250,6 +257,7 @@ def persister(device): # some devices report modelId and unitId as zero so use name and serial for them modelId = device.modelId if device.modelId != "000000000000" else device._name if device._name else None unitId = device.unitId if device.unitId != "00000000" else device._serial if device._serial else None + device_name = device.name for c in _config: if isinstance(c, _DeviceEntry) and match(device.wpid, device._serial, modelId, unitId, c): entry = c diff --git a/tests/solaar/test_configuration.py b/tests/solaar/test_configuration.py new file mode 100644 index 00000000..a483e08e --- /dev/null +++ b/tests/solaar/test_configuration.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass + +import pytest + +from solaar import configuration + + +@dataclass +class FakeDevice: + name: str + wpid: str + _serial: str + modelId: str + unitId: str + online: bool = True + + @property + def _name(self): + return self.name + + @property + def serial(self): + return self._serial + + +@pytest.fixture(autouse=True) +def reset_config(): + original_config = configuration._config + configuration._config = ["test-version"] + yield + configuration._config = original_config + + +def test_persister_matches_receiver_serial_to_direct_unit_id(): + direct_entry = configuration._DeviceEntry( + _NAME="G502 X LIGHTSPEED", + _wpid="409F", + _modelId="409FC0980000", + _unitId="92050B60", + sensitivity=1200, + ) + configuration._config.append(direct_entry) + + receiver_device = FakeDevice( + name="G502 X LIGHTSPEED", + wpid="409F", + _serial="92050B60", + modelId="409FC0980000", + unitId=None, + ) + + entry = configuration.persister(receiver_device) + + assert entry is direct_entry + assert entry["sensitivity"] == 1200 + assert entry["_serial"] == "92050B60" + + +def test_persister_matches_direct_unit_id_to_receiver_serial(): + receiver_entry = configuration._DeviceEntry( + _NAME="G502 X LIGHTSPEED", + _wpid="409F", + _serial="92050B60", + sensitivity=1200, + ) + configuration._config.append(receiver_entry) + + direct_device = FakeDevice( + name="G502 X LIGHTSPEED", + wpid="409F", + _serial=None, + modelId="409FC0980000", + unitId="92050B60", + ) + + entry = configuration.persister(direct_device) + + assert entry is receiver_entry + assert entry["sensitivity"] == 1200 + assert entry["_modelId"] == "409FC0980000" + assert entry["_unitId"] == "92050B60"