From 815c9755b585531bd13486819570bbacc6d9fbbe Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Wed, 23 Sep 2020 17:17:39 -0400 Subject: [PATCH] receiver: handle bluetooth-connected devices --- lib/logitech_receiver/base.py | 14 +++---- lib/logitech_receiver/device.py | 46 +++++++++++++-------- lib/solaar/cli/__init__.py | 2 +- lib/solaar/configuration.py | 2 +- lib/solaar/listener.py | 26 +++++++----- rules.d/42-logitech-unify-permissions.rules | 4 +- 6 files changed, 56 insertions(+), 38 deletions(-) diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index b3871308..a6d5f7cd 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -156,7 +156,7 @@ def close(handle): return False -def write(handle, devnumber, data): +def write(handle, devnumber, data, long_message=False): """Writes some data to the receiver, addressed to a certain device. :param handle: an open UR handle. @@ -173,7 +173,7 @@ def write(handle, devnumber, data): assert data is not None assert isinstance(data, bytes), (repr(data), type(data)) - if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82': + if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82': wdata = _pack('!BB18s', 0x11, devnumber, data) else: wdata = _pack('!BB5s', 0x10, devnumber, data) @@ -232,7 +232,7 @@ def _read(handle, timeout): timeout = int(timeout * 1000) data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) except Exception as reason: - _log.error('read failed, assuming handle %r no longer available', handle) + _log.warn('read failed, assuming handle %r no longer available', handle) close(handle) raise NoReceiver(reason=reason) @@ -320,7 +320,7 @@ del namedtuple # a very few requests (e.g., host switching) do not expect a reply, but use no_reply=True with extreme caution -def request(handle, devnumber, request_id, *params, no_reply=False, return_error=False): +def request(handle, devnumber, request_id, *params, no_reply=False, return_error=False, long_message=False): """Makes a feature call to a device and waits for a matching reply. :param handle: an open UR handle. :param devnumber: attached device number. @@ -357,7 +357,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error ihandle = int(handle) notifications_hook = getattr(handle, 'notifications_hook', None) _skip_incoming(handle, ihandle, notifications_hook) - write(ihandle, devnumber, request_data) + write(ihandle, devnumber, request_data, long_message) if no_reply: return None @@ -443,7 +443,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error # raise DeviceUnreachable(number=devnumber, request=request_id) -def ping(handle, devnumber): +def ping(handle, devnumber, long_message=False): """Check if a device is connected to the receiver. :returns: The HID protocol supported by the device, as a floating point number, if the device is active. @@ -467,7 +467,7 @@ def ping(handle, devnumber): ihandle = int(handle) notifications_hook = getattr(handle, 'notifications_hook', None) _skip_incoming(handle, ihandle, notifications_hook) - write(ihandle, devnumber, request_data) + write(ihandle, devnumber, request_data, long_message) # we consider timeout from this point request_started = _timestamp() diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index cd12082b..33af91bc 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -11,10 +11,10 @@ import hidapi as _hid import solaar.configuration as _configuration from . import base as _base +from . import descriptors as _descriptors from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import strhex as _strhex -from .descriptors import DEVICES as _DESCRIPTORS from .i18n import _ from .settings_templates import check_feature_settings as _check_feature_settings @@ -51,7 +51,8 @@ class Device(object): # the Wireless PID is unique per device model self.wpid = None self.descriptor = None - + # Bluetooth connections need long messages + self.bluetooth = False # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) self._kind = None # Unifying peripherals report a codename. @@ -127,7 +128,6 @@ class Device(object): if device_info is None: _log.error('failed to read Nano wpid for device %d of %s', number, receiver) raise _base.NoSuchDevice(number=number, receiver=receiver, error='read Nano wpid') - self.wpid = _strhex(device_info[3:5]) self._power_switch = '(' + _('unknown') + ')' @@ -147,7 +147,7 @@ class Device(object): except Exception: # give up self.handle = None - self.descriptor = _DESCRIPTORS.get(self.wpid) + self.descriptor = _descriptors.get_wpid(self.wpid) if self.descriptor is None: # Last chance to correctly identify the device; many Nano # receivers do not support this call. @@ -156,21 +156,24 @@ class Device(object): codename_length = ord(codename[1:2]) codename = codename[2:2 + codename_length] self._codename = codename.decode('ascii') - self.descriptor = _DESCRIPTORS.get(self._codename) - - if self.descriptor: - self._name = self.descriptor.name - self._protocol = self.descriptor.protocol - if self._codename is None: - self._codename = self.descriptor.codename - if self._kind is None: - self._kind = self.descriptor.kind + self.descriptor = _descriptors.get_codename(self._codename) else: self.path = info.path self.handle = _hid.open_path(self.path) - self.product_id = info.product_id - self._serial = ''.join(info.serial.split('-')).upper() self.online = True + self.product_id = info.product_id + self.bluetooth = info.bus_id == 0x0005 + self.descriptor = _descriptors.get_btid(self.product_id + ) if self.bluetooth else _descriptors.get_usbid(self.product_id) + + if self.descriptor: + self._name = self.descriptor.name + if self.descriptor.protocol: + self._protocol = self.descriptor.protocol + if self._codename is None: + self._codename = self.descriptor.codename + if self._kind is None: + self._kind = self.descriptor.kind if self._protocol is not None: self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self) @@ -181,7 +184,7 @@ class Device(object): @property def protocol(self): if not self._protocol and self.online: - self._protocol = _base.ping(self.handle or self.receiver.handle, self.number) + self._protocol = _base.ping(self.handle or self.receiver.handle, self.number, long_message=self.bluetooth) # if the ping failed, the peripheral is (almost) certainly offline self.online = self._protocol is not None @@ -417,7 +420,14 @@ class Device(object): return None def request(self, request_id, *params, no_reply=False): - return _base.request(self.handle or self.receiver.handle, self.number, request_id, *params, no_reply=no_reply) + return _base.request( + self.handle or self.receiver.handle, + self.number, + request_id, + *params, + no_reply=no_reply, + long_message=self.bluetooth + ) def feature_request(self, feature, function=0x00, *params, no_reply=False): if self.protocol >= 2.0: @@ -425,7 +435,7 @@ class Device(object): def ping(self): """Checks if the device is online, returns True of False""" - protocol = _base.ping(self.handle or self.receiver.handle, self.number) + protocol = _base.ping(self.handle or self.receiver.handle, self.number, long_message=self.bluetooth) self.online = protocol is not None if protocol: self._protocol = protocol diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py index 72722362..5f983c06 100644 --- a/lib/solaar/cli/__init__.py +++ b/lib/solaar/cli/__init__.py @@ -117,7 +117,7 @@ def _wired_devices(dev_path=None): if dev_path is not None and dev_path != dev_info.path: continue try: - d = Device(None, 0, info=dev_info) + d = Device.open(dev_info) if _log.isEnabledFor(_DEBUG): _log.debug('[%s] => %s', dev_info.path, d) if d is not None: diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py index 68e70d7a..6916ec3d 100644 --- a/lib/solaar/configuration.py +++ b/lib/solaar/configuration.py @@ -137,7 +137,7 @@ def persister(device): _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 + elif not entry: # 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 diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index bf018aa2..778fdddf 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -324,16 +324,22 @@ def stop_all(): # so mark its saved status to ensure that the status is pushed to the device when it comes back def ping_all(resuming=False): for l in _all_listeners.values(): - count = l.receiver.count() - if count: - for dev in l.receiver: - if resuming: - dev.status._active = False - dev.ping() - l._status_changed(dev) - count -= 1 - if not count: - break + if l.receiver.isDevice: + if resuming: + l.receiver.status._active = False + l.receiver.ping() + l._status_changed(l.receiver) + else: + count = l.receiver.count() + if count: + for dev in l.receiver: + if resuming: + dev.status._active = False + dev.ping() + l._status_changed(dev) + count -= 1 + if not count: + break _status_callback = None diff --git a/rules.d/42-logitech-unify-permissions.rules b/rules.d/42-logitech-unify-permissions.rules index 93d58f63..27a65ccb 100644 --- a/rules.d/42-logitech-unify-permissions.rules +++ b/rules.d/42-logitech-unify-permissions.rules @@ -7,12 +7,14 @@ ACTION != "add", GOTO="solaar_end" SUBSYSTEM != "hidraw", GOTO="solaar_end" -# Logitech receivers and direct-connected devices +# USB-connected Logitech receivers and devices ATTRS{idVendor}=="046d", GOTO="solaar_apply" # Lenovo nano receiver ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="6042", GOTO="solaar_apply" +# Bluetooth-connected Logitech devices +KERNELS == "0005:046D:*", GOTO="solaar_apply" GOTO="solaar_end"