diff --git a/lib/logitech/unifying_receiver/descriptors.py b/lib/logitech/unifying_receiver/descriptors.py index 8b540aac..a33cee08 100644 --- a/lib/logitech/unifying_receiver/descriptors.py +++ b/lib/logitech/unifying_receiver/descriptors.py @@ -63,12 +63,14 @@ def check_features(device, already_known): from collections import namedtuple _DeviceDescriptor = namedtuple('_DeviceDescriptor', - ['name', 'kind', 'product_id', 'codename', 'protocol', 'registers', 'settings']) + ['name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings']) del namedtuple DEVICES = {} -def _D(name, codename=None, kind=None, product_id=None, protocol=None, registers=None, settings=None): +def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None): + assert name + if kind is None: kind = (_hidpp10.DEVICE_KIND.mouse if 'Mouse' in name else _hidpp10.DEVICE_KIND.keyboard if 'Keyboard' in name @@ -89,13 +91,15 @@ def _D(name, codename=None, kind=None, product_id=None, protocol=None, registers DEVICES[codename] = _DeviceDescriptor( name=name, kind=kind, - product_id=product_id, + wpid=wpid, codename=codename, protocol=protocol, registers=registers, settings=settings) - if product_id: - DEVICES[product_id] = DEVICES[codename] + + if wpid: + assert wpid not in DEVICES + DEVICES[wpid] = DEVICES[codename] # # @@ -125,41 +129,47 @@ def _D(name, codename=None, kind=None, product_id=None, protocol=None, registers # no known device uses both # 51 - leds # 63 - mice: DPI -# F1 - firmware info +# * F1 - firmware info # Some registers appear to be universally supported, no matter the HID++ version # (marked with *). The rest may or may not be supported, and their values may or # may not mean the same thing across different devices. +# The 'codename' and 'kind' fields are usually guessed from the device name, +# but in some cases (like the Logitech Cube) that heuristic fails and they have +# to be specified. +# +# The 'protocol' and 'wpid' fields are optional (they can be discovered at +# runtime), but specifying them here speeds up device discovery and reduces the +# USB traffic Solaar has to do to fully identify peripherals. +# Same goes for HID++ 2.0 feature settings (like _feature_fn_swap). +# # The 'registers' field indicates read-only registers, specifying a state. # The 'settings' field indicates a read/write register; based on them Solaar # generates, at runtime, the settings controls in the device panel. -# -# HID++ 2.0 features are not specified here, they are always discovered at -# run-time. # Keyboards -_D('Wireless Keyboard K230', protocol=2.0) +_D('Wireless Keyboard K230', protocol=2.0, wpid='400D') _D('Wireless Keyboard K270') _D('Wireless Keyboard K350') -_D('Wireless Keyboard K360', protocol=2.0, +_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', settings=[ _feature_fn_swap() ], ) -_D('Wireless Touch Keyboard K400', protocol=2.0) -_D('Wireless Keyboard MK700', protocol=1.0, +_D('Wireless Touch Keyboard K400', protocol=2.0, wpid='4024') +_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', registers={'battery_charge': -0x0D, 'battery_status': 0x07}, settings=[ _register_fn_swap(), ], ) -_D('Wireless Solar Keyboard K750', protocol=2.0, +_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', settings=[ _feature_fn_swap() ], ) -_D('Wireless Illuminated Keyboard K800', protocol=1.0, +_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010', registers={'battery_charge': -0x0D, 'battery_status': 0x07, 'leds': 0x51}, settings=[ _register_fn_swap(), @@ -171,7 +181,7 @@ _D('Wireless Illuminated Keyboard K800', protocol=1.0, _D('Wireless Mouse M175', protocol=1.0) _D('Wireless Mouse M185', protocol=1.0) _D('Wireless Mouse M187', protocol=1.0) -_D('Wireless Mouse M215', protocol=1.0) +_D('Wireless Mouse M215', protocol=1.0, wpid='1020') _D('Wireless Mouse M235', protocol=1.0) _D('Wireless Mouse M305', protocol=1.0) _D('Wireless Mouse M310', protocol=1.0) @@ -180,7 +190,7 @@ _D('Wireless Mouse M317') _D('Wireless Mouse M325') _D('Wireless Mouse M345') _D('Wireless Mouse M505') -_D('Wireless Mouse M510', protocol=1.0, +_D('Wireless Mouse M510', protocol=1.0, wpid='1025', registers={'battery_charge': -0x0D, 'battery_status': 0x07}, settings=[ _register_smooth_scroll(), @@ -189,7 +199,7 @@ _D('Wireless Mouse M510', protocol=1.0, _D('Couch Mouse M515', protocol=2.0) _D('Wireless Mouse M525', protocol=2.0) _D('Touch Mouse M600') -_D('Marathon Mouse M705', protocol=1.0, +_D('Marathon Mouse M705', protocol=1.0, wpid='101B', registers={'battery_charge': 0x0D}, settings=[ _register_smooth_scroll(), @@ -199,7 +209,7 @@ _D('Zone Touch Mouse T400') _D('Touch Mouse T620') _D('Logitech Cube', kind=_hidpp10.DEVICE_KIND.mouse, protocol=2.0) _D('Anywhere Mouse MX', codename='Anywhere MX') -_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, +_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A', registers={'battery_charge': -0x0D, 'battery_status': 0x07, 'leds': 0x51}, settings=[ _register_dpi(choices=_PERFORMANCE_MX_DPIS), @@ -213,14 +223,14 @@ _D('Wireless Trackball M570') # Touchpads _D('Wireless Rechargeable Touchpad T650', protocol=2.0) -_D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0) +_D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011') # -# classic Nano devices -# a product_id is necessary to properly identify them +# Classic Nano peripherals (that don't support the Unifying protocol). +# A wpid is necessary to properly identify them. # -_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, product_id='c526', +_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid='100F', registers={'battery_charge': 0x0D, 'battery_status': -0x07}, settings=[ _register_smooth_scroll(), diff --git a/lib/logitech/unifying_receiver/receiver.py b/lib/logitech/unifying_receiver/receiver.py index 975419ca..c765284f 100644 --- a/lib/logitech/unifying_receiver/receiver.py +++ b/lib/logitech/unifying_receiver/receiver.py @@ -15,7 +15,10 @@ from . import base as _base from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import strhex as _strhex -from . import descriptors as _descriptors +from .descriptors import ( + DEVICES as _DESCRIPTORS, + check_features as _check_feature_settings, + ) # # @@ -29,16 +32,26 @@ class PairedDevice(object): def __init__(self, receiver, number, link_notification=None): assert receiver self.receiver = receiver # _proxy(receiver) + assert number > 0 and number <= receiver.max_devices + # Device number, 1..6 for unifying devices, 1 otherwise. self.number = number + # 'device active' flag; requires manual management. self.online = None + # the Wireless PID is unique per device model self.wpid = None + self._descriptor = None + # mose, keyboard, etc (see _hidpp10.DEVICE_KIND) self._kind = None + # Unifying peripherals report a codename. self._codename = None + # the full name of the model self._name = None + # HID++ protocol version, 1.0 or 2.0 self._protocol = None + # serial number (an 8-char hex string) self._serial = None self._firmware = None @@ -46,6 +59,8 @@ class PairedDevice(object): self._registers = None self._settings = None + # Misc stuff that's irrelevant to any functionality, but may be + # displayed in the UI and caching it here helps. self._polling_rate = None self._power_switch = None @@ -75,21 +90,22 @@ class PairedDevice(object): self._polling_rate = 0 self._power_switch = '(unknown)' - descriptor = _descriptors.DEVICES.get(self.receiver.product_id) - if descriptor is None: - self._codename = self.receiver.product_id + if self.wpid is None: + device_info = self.receiver.read_register(0x2B5, 0x04) + if device_info is None: + _log.error("failed to read Nano wpid for device %d of %s", number, receiver) + raise _base.NoSuchDevice(nuber=number, receiver=receiver, error="read Nano wpid") + self.wpid = _strhex(device_info[3:5]) + + self._descriptor = _DESCRIPTORS.get(self.wpid) + if self._descriptor is None: + self._codename = self.receiver.wpid # actually there IS a device, just that we can't identify it # raise _base.NoSuchDevice(nuber=number, receiver=receiver, product_id=receiver.product_id, failed="no descriptor") self._name = 'Unknown device ' + self._codename else: - self._codename = descriptor.codename - self._name = descriptor.name - - if self.wpid is None: - device_info = self.receiver.read_register(0x2B5, 0x04) - if device_info is None: - raise _base.NoSuchDevice(nuber=number, receiver=receiver, error="read Nano wpid") - self.wpid = _strhex(device_info[3:5]) + self._codename = self._descriptor.codename + self._name = self._descriptor.name # the wpid is necessary to properly identify wireless link on/off notifications # also it gets set to None when the device is unpaired @@ -97,13 +113,11 @@ class PairedDevice(object): # knowing the protocol as soon as possible helps reading all other info # and avoids an unecessary ping - if self._codename is not None: - descriptor = _descriptors.DEVICES.get(self._codename) - if descriptor is None: - _log.warn("device without descriptor found: %s (%d of %s)", self._codename, number, receiver) - self._protocol = None if unifying else 1.0 - else: - self._protocol = descriptor.protocol if unifying else 1.0 # may be None + if self.descriptor: + self._protocol = self.descriptor.protocol if unifying else 1.0 # may be None + else: + _log.warn("device without descriptor found: %s - %s (%d of %s)", self.wpid, self._codename, number, receiver) + self._protocol = None if unifying else 1.0 if self._protocol is not None: self.features = _hidpp20.FeaturesArray(self) if self._protocol >= 2.0 else None @@ -113,13 +127,20 @@ class PairedDevice(object): else: self.features = None + @property + def descriptor(self): + if self._descriptor is None: + self._descriptor = _DESCRIPTORS.get(self.wpid) + if self._descriptor is None and self._codename: + self._descriptor = _DESCRIPTORS.get(self._codename) + return self._descriptor + @property def protocol(self): if self._protocol is None: - descriptor = _descriptors.DEVICES.get(self.codename) - if descriptor: - if descriptor.protocol: - self._protocol = descriptor.protocol + if self.descriptor: + if self.descriptor.protocol: + self._protocol = self.descriptor.protocol else: _log.warn("%s: descriptor has no protocol, should be %0.1f", self, self._protocol) @@ -134,7 +155,9 @@ class PairedDevice(object): @property def codename(self): if self._codename is None: - if self.receiver.unifying_supported: + if self.descriptor: + self._codename = self.descriptor.codename + elif self.receiver.unifying_supported: codename = self.receiver.read_register(0x2B5, 0x40 + self.number - 1) if codename: self._codename = codename[2:].rstrip(b'\x00').decode('utf-8') @@ -144,28 +167,24 @@ class PairedDevice(object): @property def name(self): if self._name is None: - if self.protocol >= 2.0 and self.online: + if self.descriptor: + self._name = self.descriptor.name + elif self.protocol >= 2.0 and self.online: self._name = _hidpp20.get_name(self) - if self._name is None: - descriptor = _descriptors.DEVICES.get(self.codename) - if descriptor and descriptor.name is not None: - self._name = descriptor.name return self._name or self.codename or '?' @property def kind(self): if self._kind is None: - if self.receiver.unifying_supported: + if self.descriptor: + self._kind = self.descriptor.kind + elif self.receiver.unifying_supported: pair_info = self.receiver.read_register(0x2B5, 0x20 + self.number - 1) if pair_info: kind = ord(pair_info[7:8]) & 0x0F self._kind = _hidpp10.DEVICE_KIND[kind] if self._kind is None and self.protocol >= 2.0 and self.online: self._kind = _hidpp20.get_kind(self) - if self._kind is None: - descriptor = _descriptors.DEVICES.get(self.codename) - if descriptor and descriptor.kind is not None: - self._kind = descriptor.kind return self._kind or '?' @property @@ -180,31 +199,33 @@ class PairedDevice(object): @property def serial(self): if self._serial is None: - assert self.receiver.unifying_supported - # otherwise it should have been set in the constructor - self._serial = _hidpp10.get_serial(self) + if self.receiver.unifying_supported: + self._serial = _hidpp10.get_serial(self) + else: + self._serial = self.receiver.serial return self._serial or '?' @property def power_switch_location(self): if self._power_switch is None: - assert self.receiver.unifying_supported - ps = self.receiver.read_register(0x2B5, 0x30 + self.number - 1) - if ps is not None: - ps = ord(ps[9:10]) & 0x0F - self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] + if self.receiver.unifying_supported: + ps = self.receiver.read_register(0x2B5, 0x30 + self.number - 1) + if ps is not None: + ps = ord(ps[9:10]) & 0x0F + self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] + else: + self._power_switch = '(unknown)' return self._power_switch @property def polling_rate(self): if self._polling_rate is None: - assert self.receiver.unifying_supported - pair_info = self.receiver.read_register(0x2B5, 0x20 + self.number - 1) - if pair_info is None: - # wtf? - self._polling_rate = 0 + if self.receiver.unifying_supported: + pair_info = self.receiver.read_register(0x2B5, 0x20 + self.number - 1) + if pair_info: + self._polling_rate = ord(pair_info[2:3]) else: - self._polling_rate = ord(pair_info[2:3]) + self._polling_rate = 0 return self._polling_rate @property @@ -217,24 +238,22 @@ class PairedDevice(object): @property def registers(self): if self._registers is None: - descriptor = _descriptors.DEVICES.get(self.codename) - if descriptor is None or descriptor.registers is None: - self._registers = {} + if self.descriptor and self.descriptor.registers: + self._registers = dict(self.descriptor.registers) else: - self._registers = descriptor.registers + self._registers = {} return self._registers @property def settings(self): if self._settings is None: - descriptor = _descriptors.DEVICES.get(self.codename) - if descriptor is None or descriptor.settings is None: - self._settings = [] + if self.descriptor and self.descriptor.settings: + self._settings = [s(self) for s in self.descriptor.settings] else: - self._settings = [s(self) for s in descriptor.settings] + self._settings = [] if self.online and self.features: - _descriptors.check_features(self, self._settings) + _check_feature_settings(self, self._settings) return self._settings def enable_notifications(self, enable=True): diff --git a/lib/logitech/unifying_receiver/settings.py b/lib/logitech/unifying_receiver/settings.py index 07461523..7f94e311 100644 --- a/lib/logitech/unifying_receiver/settings.py +++ b/lib/logitech/unifying_receiver/settings.py @@ -38,6 +38,14 @@ class _Setting(object): def __call__(self, device): assert not hasattr(self, '_value') assert self.device_kind is None or self.device_kind == device.kind + p = device.protocol + if p == 1.0: + # HID++ 1.0 devices do not support features + assert self._rw.kind == _RegisterRW.kind + elif p >= 2.0: + # HID++ 2.0 devices do not support registers + assert self._rw.kind == _FeatureRW.kind + o = _copy(self) o._value = None o._device = device # _proxy(device)