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 from logging import getLogger
import hidapi as _hid import hidapi as _hid
import solaar.configuration as _configuration
from . import base as _base from . import base as _base
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
@ -59,6 +60,14 @@ class Device(object):
self._protocol = None self._protocol = None
# serial number (an 8-char hex string) # serial number (an 8-char hex string)
self._serial = None 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._firmware = None
self._keys = None self._keys = None
@ -196,6 +205,29 @@ class Device(object):
self._name = _hidpp20.get_name(self) self._name = _hidpp20.get_name(self)
return self._name or self.codename or ('Unknown device %s' % (self.wpid or self.product_id)) 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 @property
def kind(self): def kind(self):
if not self._kind: if not self._kind:
@ -285,7 +317,7 @@ class Device(object):
def settings(self): def settings(self):
if self._settings is None: if self._settings is None:
self._settings = [] self._settings = []
if self.descriptor and self.descriptor.settings: if self.descriptor and self.descriptor.settings and self.persister:
self._settings = [] self._settings = []
for s in self.descriptor.settings: for s in self.descriptor.settings:
try: try:
@ -300,6 +332,12 @@ class Device(object):
self._feature_settings_checked = _check_feature_settings(self, self._settings) self._feature_settings_checked = _check_feature_settings(self, self._settings)
return 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): def get_kind_from_index(self, index, receiver):
"""Get device kind from 27Mhz device index""" """Get device kind from 27Mhz device index"""
# accordingly to drivers/hid/hid-logitech-dj.c # accordingly to drivers/hid/hid-logitech-dj.c

View File

@ -1050,6 +1050,21 @@ def get_firmware(device):
return tuple(fw) 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): def get_kind(device):
"""Reads a device's type. """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(' %d: %s' % (num or dev.number, dev.name))
print(' Device path :', dev.path) 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(' Codename :', dev.codename)
print(' Kind :', dev.kind) print(' Kind :', dev.kind)
if dev.protocol: if dev.protocol:
@ -101,6 +104,10 @@ def _print_device(dev, num=None):
if dev.polling_rate: if dev.polling_rate:
print(' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate)) print(' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate))
print(' Serial number:', dev.serial) print(' Serial number:', dev.serial)
if dev.modelId:
print(' Model ID: ', dev.modelId)
if dev.unitId:
print(' Unit ID: ', dev.unitId)
if dev.firmware: if dev.firmware:
for fw in dev.firmware: for fw in dev.firmware:
print(' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) 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: if dev.online and dev.features:
print(' Supports %d HID++ 2.0 features:' % len(dev.features)) print(' Supports %d HID++ 2.0 features:' % len(dev.features))
dev.persister = None # Give the device a fake persister
dev_settings = [] dev_settings = []
_settings_templates.check_feature_settings(dev, dev_settings) _settings_templates.check_feature_settings(dev, dev_settings)
for index, feature in enumerate(dev.features): for index, feature in enumerate(dev.features):
@ -202,6 +208,8 @@ def _print_device(dev, num=None):
for fw in _hidpp20.get_firmware(dev): for fw in _hidpp20.get_firmware(dev):
extras = _strhex(fw.extras) if fw.extras else '' extras = _strhex(fw.extras) if fw.extras else ''
print(' Firmware: %s %s %s %s' % (fw.kind, fw.name, fw.version, extras)) 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: elif feature == _hidpp20.FEATURE.REPORT_RATE:
print(' Polling Rate (ms): %d' % _hidpp20.get_polling_rate(dev)) print(' Polling Rate (ms): %d' % _hidpp20.get_polling_rate(dev))
elif feature == _hidpp20.FEATURE.BATTERY_STATUS or feature == _hidpp20.FEATURE.BATTERY_VOLTAGE: 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_VERSION = '_version'
_KEY_NAME = '_name' _KEY_NAME = '_name'
_KEY_MODEL_ID = '_modelId'
_KEY_UNIT_ID = '_unitId'
_configuration = {} _configuration = {}
@ -82,8 +84,8 @@ def save():
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info('saved %s to %s', _configuration, _file_path) _log.info('saved %s to %s', _configuration, _file_path)
return True return True
except Exception: except Exception as e:
_log.error('failed to save to %s', _file_path) _log.error('failed to save to %s: %s', _file_path, e)
def _cleanup(d): def _cleanup(d):
@ -96,38 +98,56 @@ def _cleanup(d):
_cleanup(value) _cleanup(value)
def _device_key(device):
return '%s:%s' % (device.wpid, device.serial)
class _DeviceEntry(dict): class _DeviceEntry(dict):
def __init__(self, *args, **kwargs): def __init__(self, device, **kwargs):
super(_DeviceEntry, self).__init__(*args, **kwargs) super(_DeviceEntry, self).__init__(**kwargs)
self[_KEY_NAME] = device.name
self.update(device)
def __setitem__(self, key, value): def __setitem__(self, key, value):
super(_DeviceEntry, self).__setitem__(key, value) super(_DeviceEntry, self).__setitem__(key, value)
save() 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: if not _configuration:
_load() _load()
device_key = _device_key(device) entry = {}
c = _configuration.get(device_key) or {} 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): if key and not isinstance(entry, _DeviceEntry):
c[_KEY_NAME] = device.name entry = _DeviceEntry(device, **entry)
c = _DeviceEntry(c) _configuration[key] = entry
_configuration[device_key] = c
return c return entry
def attach_to(device): def attach_to(device):
"""Apply the last saved configuration to a device.""" pass
if not _configuration:
_load()
persister = _device_entry(device)
device.persister = persister