receiver: handle bluetooth-connected devices

This commit is contained in:
Peter F. Patel-Schneider 2020-09-23 17:17:39 -04:00
parent 1e7050595e
commit 815c9755b5
6 changed files with 56 additions and 38 deletions

View File

@ -156,7 +156,7 @@ def close(handle):
return False 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. """Writes some data to the receiver, addressed to a certain device.
:param handle: an open UR handle. :param handle: an open UR handle.
@ -173,7 +173,7 @@ def write(handle, devnumber, data):
assert data is not None assert data is not None
assert isinstance(data, bytes), (repr(data), type(data)) 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) wdata = _pack('!BB18s', 0x11, devnumber, data)
else: else:
wdata = _pack('!BB5s', 0x10, devnumber, data) wdata = _pack('!BB5s', 0x10, devnumber, data)
@ -232,7 +232,7 @@ def _read(handle, timeout):
timeout = int(timeout * 1000) timeout = int(timeout * 1000)
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
except Exception as reason: 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) close(handle)
raise NoReceiver(reason=reason) 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 # 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. """Makes a feature call to a device and waits for a matching reply.
:param handle: an open UR handle. :param handle: an open UR handle.
:param devnumber: attached device number. :param devnumber: attached device number.
@ -357,7 +357,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
ihandle = int(handle) ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None) notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook) _skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data) write(ihandle, devnumber, request_data, long_message)
if no_reply: if no_reply:
return None 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) # 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. """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. :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) ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None) notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook) _skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data) write(ihandle, devnumber, request_data, long_message)
# we consider timeout from this point # we consider timeout from this point
request_started = _timestamp() request_started = _timestamp()

View File

@ -11,10 +11,10 @@ import hidapi as _hid
import solaar.configuration as _configuration import solaar.configuration as _configuration
from . import base as _base from . import base as _base
from . import descriptors as _descriptors
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20 from . import hidpp20 as _hidpp20
from .common import strhex as _strhex from .common import strhex as _strhex
from .descriptors import DEVICES as _DESCRIPTORS
from .i18n import _ from .i18n import _
from .settings_templates import check_feature_settings as _check_feature_settings 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 # the Wireless PID is unique per device model
self.wpid = None self.wpid = None
self.descriptor = None self.descriptor = None
# Bluetooth connections need long messages
self.bluetooth = False
# mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND)
self._kind = None self._kind = None
# Unifying peripherals report a codename. # Unifying peripherals report a codename.
@ -127,7 +128,6 @@ class Device(object):
if device_info is None: if device_info is None:
_log.error('failed to read Nano wpid for device %d of %s', number, receiver) _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') raise _base.NoSuchDevice(number=number, receiver=receiver, error='read Nano wpid')
self.wpid = _strhex(device_info[3:5]) self.wpid = _strhex(device_info[3:5])
self._power_switch = '(' + _('unknown') + ')' self._power_switch = '(' + _('unknown') + ')'
@ -147,7 +147,7 @@ class Device(object):
except Exception: # give up except Exception: # give up
self.handle = None self.handle = None
self.descriptor = _DESCRIPTORS.get(self.wpid) self.descriptor = _descriptors.get_wpid(self.wpid)
if self.descriptor is None: if self.descriptor is None:
# Last chance to correctly identify the device; many Nano # Last chance to correctly identify the device; many Nano
# receivers do not support this call. # receivers do not support this call.
@ -156,21 +156,24 @@ class Device(object):
codename_length = ord(codename[1:2]) codename_length = ord(codename[1:2])
codename = codename[2:2 + codename_length] codename = codename[2:2 + codename_length]
self._codename = codename.decode('ascii') self._codename = codename.decode('ascii')
self.descriptor = _DESCRIPTORS.get(self._codename) self.descriptor = _descriptors.get_codename(self._codename)
else:
self.path = info.path
self.handle = _hid.open_path(self.path)
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: if self.descriptor:
self._name = self.descriptor.name self._name = self.descriptor.name
if self.descriptor.protocol:
self._protocol = self.descriptor.protocol self._protocol = self.descriptor.protocol
if self._codename is None: if self._codename is None:
self._codename = self.descriptor.codename self._codename = self.descriptor.codename
if self._kind is None: if self._kind is None:
self._kind = self.descriptor.kind self._kind = self.descriptor.kind
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
if self._protocol is not None: if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self) self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
@ -181,7 +184,7 @@ class Device(object):
@property @property
def protocol(self): def protocol(self):
if not self._protocol and self.online: 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 # if the ping failed, the peripheral is (almost) certainly offline
self.online = self._protocol is not None self.online = self._protocol is not None
@ -417,7 +420,14 @@ class Device(object):
return None return None
def request(self, request_id, *params, no_reply=False): 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): def feature_request(self, feature, function=0x00, *params, no_reply=False):
if self.protocol >= 2.0: if self.protocol >= 2.0:
@ -425,7 +435,7 @@ class Device(object):
def ping(self): def ping(self):
"""Checks if the device is online, returns True of False""" """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 self.online = protocol is not None
if protocol: if protocol:
self._protocol = protocol self._protocol = protocol

View File

@ -117,7 +117,7 @@ def _wired_devices(dev_path=None):
if dev_path is not None and dev_path != dev_info.path: if dev_path is not None and dev_path != dev_info.path:
continue continue
try: try:
d = Device(None, 0, info=dev_info) d = Device.open(dev_info)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug('[%s] => %s', dev_info.path, d) _log.debug('[%s] => %s', dev_info.path, d)
if d is not None: if d is not None:

View File

@ -137,7 +137,7 @@ def persister(device):
_configuration[key] = entry _configuration[key] = entry
elif device.wpid and not entry: # create now with wpid:serial elif device.wpid and not entry: # create now with wpid:serial
key = '%s:%s' % (device.wpid, device.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) key = '%s:%s' % (device.modelId, device.unitId)
else: # defer until more is known (i.e., device comes on line) else: # defer until more is known (i.e., device comes on line)
return return

View File

@ -324,6 +324,12 @@ def stop_all():
# so mark its saved status to ensure that the status is pushed to the device when it comes back # so mark its saved status to ensure that the status is pushed to the device when it comes back
def ping_all(resuming=False): def ping_all(resuming=False):
for l in _all_listeners.values(): for l in _all_listeners.values():
if l.receiver.isDevice:
if resuming:
l.receiver.status._active = False
l.receiver.ping()
l._status_changed(l.receiver)
else:
count = l.receiver.count() count = l.receiver.count()
if count: if count:
for dev in l.receiver: for dev in l.receiver:

View File

@ -7,12 +7,14 @@
ACTION != "add", GOTO="solaar_end" ACTION != "add", GOTO="solaar_end"
SUBSYSTEM != "hidraw", 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" ATTRS{idVendor}=="046d", GOTO="solaar_apply"
# Lenovo nano receiver # Lenovo nano receiver
ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="6042", GOTO="solaar_apply" ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="6042", GOTO="solaar_apply"
# Bluetooth-connected Logitech devices
KERNELS == "0005:046D:*", GOTO="solaar_apply"
GOTO="solaar_end" GOTO="solaar_end"