receiver: add model and node ID and use in configurations

This commit is contained in:
Peter F. Patel-Schneider 2020-09-18 17:18:46 -04:00
parent 58823763ea
commit b1d4b2f3cd
4 changed files with 106 additions and 25 deletions

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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