diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index 0a66eb9b..d9f08932 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -6,6 +6,7 @@ from logging import INFO as _INFO from logging import getLogger import hidapi as _hid +import solaar.configuration as _configuration from . import base as _base from . import hidpp10 as _hidpp10 @@ -59,6 +60,14 @@ class Device(object): self._protocol = None # serial number (an 8-char hex string) self._serial = None + # unit id (distinguishes within a model - the same as serial) + self._unitId = None + # model id (contains identifiers for the transports of the device) + self._modelId = None + # map from transports to product identifiers + self._tid_map = None + # persister holds settings + self._persister = None self._firmware = None self._keys = None @@ -196,6 +205,29 @@ class Device(object): self._name = _hidpp20.get_name(self) return self._name or self.codename or ('Unknown device %s' % (self.wpid or self.product_id)) + @property + def unitId(self): + if not self._unitId: + if self.online and self.protocol >= 2.0: + self._unitId, self._modelId, self._tid_map = _hidpp20.get_ids(self) + if _log.isEnabledFor(_INFO) and self._serial and self._serial != self._unitId: + _log.info('%s: unitId %s does not match serial %s', self, self._unitId, self._serial) + return self._unitId + + @property + def modelId(self): + if not self._modelId: + if self.online and self.protocol >= 2.0: + self._unitId, self._modelId, self._tid_map = _hidpp20.get_ids(self) + return self._modelId + + @property + def tid_map(self): + if not self._tid_map: + if self.online and self.protocol >= 2.0: + self._unitId, self._modelId, self._tid_map = _hidpp20.get_ids(self) + return self._tid_map + @property def kind(self): if not self._kind: @@ -285,7 +317,7 @@ class Device(object): def settings(self): if self._settings is None: self._settings = [] - if self.descriptor and self.descriptor.settings: + if self.descriptor and self.descriptor.settings and self.persister: self._settings = [] for s in self.descriptor.settings: try: @@ -300,6 +332,12 @@ class Device(object): self._feature_settings_checked = _check_feature_settings(self, self._settings) return self._settings + @property + def persister(self): + if not self._persister: + self._persister = _configuration.persister(self) + return self._persister + def get_kind_from_index(self, index, receiver): """Get device kind from 27Mhz device index""" # accordingly to drivers/hid/hid-logitech-dj.c diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index eaccc779..1f247d9b 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -1050,6 +1050,21 @@ def get_firmware(device): return tuple(fw) +def get_ids(device): + """Reads a device's ids (unit and model numbers)""" + ids = feature_request(device, FEATURE.DEVICE_FW_VERSION) + unitId = ids[1:5] + modelId = ids[7:13] + transport_bits = ord(ids[6:7]) + offset = 0 + tid_map = {} + for transport, flag in [('btid', 0x1), ('btleid', 0x02), ('wpid', 0x04), ('usbid', 0x08)]: + if transport_bits & flag: + tid_map[transport] = modelId[offset:offset + 2].hex().upper() + offset = offset + 2 + return (unitId.hex().upper(), modelId.hex().upper(), tid_map) + + def get_kind(device): """Reads a device's type. diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 829a49bc..fc4621eb 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -91,7 +91,10 @@ def _print_device(dev, num=None): print(' %d: %s' % (num or dev.number, dev.name)) print(' Device path :', dev.path) - print(' USB id : 046d:%s' % (dev.wpid or dev.product_id)) + if dev.wpid: + print(' WPID : %s' % dev.wpid) + if dev.product_id: + print(' USB id : 046d:%s' % dev.product_id) print(' Codename :', dev.codename) print(' Kind :', dev.kind) if dev.protocol: @@ -101,6 +104,10 @@ def _print_device(dev, num=None): if dev.polling_rate: print(' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate)) print(' Serial number:', dev.serial) + if dev.modelId: + print(' Model ID: ', dev.modelId) + if dev.unitId: + print(' Unit ID: ', dev.unitId) if dev.firmware: for fw in dev.firmware: print(' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) @@ -126,7 +133,6 @@ def _print_device(dev, num=None): if dev.online and dev.features: print(' Supports %d HID++ 2.0 features:' % len(dev.features)) - dev.persister = None # Give the device a fake persister dev_settings = [] _settings_templates.check_feature_settings(dev, dev_settings) for index, feature in enumerate(dev.features): @@ -202,6 +208,8 @@ def _print_device(dev, num=None): for fw in _hidpp20.get_firmware(dev): extras = _strhex(fw.extras) if fw.extras else '' print(' Firmware: %s %s %s %s' % (fw.kind, fw.name, fw.version, extras)) + unitId, modelId, tid_map = _hidpp20.get_ids(dev) + print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) elif feature == _hidpp20.FEATURE.REPORT_RATE: print(' Polling Rate (ms): %d' % _hidpp20.get_polling_rate(dev)) elif feature == _hidpp20.FEATURE.BATTERY_STATUS or feature == _hidpp20.FEATURE.BATTERY_VOLTAGE: diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py index 2cb64730..68e70d7a 100644 --- a/lib/solaar/configuration.py +++ b/lib/solaar/configuration.py @@ -36,6 +36,8 @@ _file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json') _KEY_VERSION = '_version' _KEY_NAME = '_name' +_KEY_MODEL_ID = '_modelId' +_KEY_UNIT_ID = '_unitId' _configuration = {} @@ -82,8 +84,8 @@ def save(): if _log.isEnabledFor(_INFO): _log.info('saved %s to %s', _configuration, _file_path) return True - except Exception: - _log.error('failed to save to %s', _file_path) + except Exception as e: + _log.error('failed to save to %s: %s', _file_path, e) def _cleanup(d): @@ -96,38 +98,56 @@ def _cleanup(d): _cleanup(value) -def _device_key(device): - return '%s:%s' % (device.wpid, device.serial) - - class _DeviceEntry(dict): - def __init__(self, *args, **kwargs): - super(_DeviceEntry, self).__init__(*args, **kwargs) + def __init__(self, device, **kwargs): + super(_DeviceEntry, self).__init__(**kwargs) + self[_KEY_NAME] = device.name + self.update(device) def __setitem__(self, key, value): super(_DeviceEntry, self).__setitem__(key, value) save() + def update(self, device): + if device.modelId: + self[_KEY_MODEL_ID] = device.modelId + if device.unitId: + self[_KEY_UNIT_ID] = device.unitId -def _device_entry(device): + +def persister(device): if not _configuration: _load() - device_key = _device_key(device) - c = _configuration.get(device_key) or {} + entry = {} + key = None + if device.wpid: # connected via receiver + entry = _configuration.get('%s:%s' % (device.wpid, device.serial), {}) + if entry or device.protocol == 1.0: # found entry or create entry for old-style devices + key = '%s:%s' % (device.wpid, device.serial) + elif not entry and device.modelId: # online new-style device so look for modelId and unitId + for k, c in _configuration.items(): + if isinstance(c, dict) and c.get(_KEY_MODEL_ID) == device.modelId and c.get(_KEY_UNIT_ID) == device.unitId: + entry = c # use the entry that matches modelId and unitId + key = k + break + if device.wpid and entry: # move entry to wpid:serial + del _configuration[key] + key = '%s:%s' % (device.wpid, device.serial) + _configuration[key] = entry + elif device.wpid and not entry: # create now with wpid:serial + key = '%s:%s' % (device.wpid, device.serial) + else: # create now with modelId:unitId + key = '%s:%s' % (device.modelId, device.unitId) + else: # defer until more is known (i.e., device comes on line) + return - if not isinstance(c, _DeviceEntry): - c[_KEY_NAME] = device.name - c = _DeviceEntry(c) - _configuration[device_key] = c + if key and not isinstance(entry, _DeviceEntry): + entry = _DeviceEntry(device, **entry) + _configuration[key] = entry - return c + return entry def attach_to(device): - """Apply the last saved configuration to a device.""" - if not _configuration: - _load() - - persister = _device_entry(device) - device.persister = persister + pass