diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..24146f48 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "python-hid-parser"] + path = python-hid-parser + url = https://github.com/usb-tools/python-hid-parser diff --git a/docs/installation.md b/docs/installation.md index b98f118c..284d4a62 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -32,6 +32,9 @@ in Fedora you need `gtk3` and `python3-gobject`. You may have to install `gcc` and the Python development package (`python3-dev` or `python3-devel`, depending on your distribution). +Solaar also uses the hidtools library from the Python hid-tools project. +To install this library use `pip install --user hid-tools`. + If you are running a version of Python different from the system version, you may need to use pip to install projects that provide the above Python packages. diff --git a/lib/hid_parser b/lib/hid_parser new file mode 120000 index 00000000..b3fe592c --- /dev/null +++ b/lib/hid_parser @@ -0,0 +1 @@ +../python-hid-parser/hid_parser \ No newline at end of file diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index a130abe1..22e129fa 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -26,15 +26,18 @@ necessary. import errno as _errno import os as _os +import warnings as _warnings # the tuple object we'll expose when enumerating devices from collections import namedtuple -from logging import DEBUG as _DEBUG +from logging import INFO as _INFO from logging import getLogger from select import select as _select from time import sleep from time import time as _timestamp +from hid_parser import ReportDescriptor as _ReportDescriptor +from hid_parser import Usage as _Usage from pyudev import Context as _Context from pyudev import Device as _Device from pyudev import DeviceNotFoundError @@ -43,8 +46,8 @@ from pyudev import Monitor as _Monitor _log = getLogger(__name__) del getLogger - native_implementation = 'udev' +fileopen = open DeviceInfo = namedtuple( 'DeviceInfo', [ @@ -59,6 +62,8 @@ DeviceInfo = namedtuple( 'driver', 'bus_id', 'isDevice', + 'hidpp_short', + 'hidpp_long', ] ) del namedtuple @@ -92,17 +97,38 @@ def exit(): # with the required hid_driver and usb_interface and whether this is a receiver or device. def _match(action, device, filterfn): hid_device = device.find_parent('hid') - if not hid_device: + if not hid_device: # only HID devices are of interest to Solaar return hid_id = hid_device.get('HID_ID') if not hid_id: return # there are reports that sometimes the id isn't set up right so be defensive bid, vid, pid = hid_id.split(':') - filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16)) + try: # if report descriptor does not indicate HID++ capabilities then this device is not of interest to Solaar + hidpp_short = hidpp_long = False + devfile = '/sys' + hid_device.get('DEVPATH') + '/report_descriptor' + with fileopen(devfile, 'rb') as fd: + with _warnings.catch_warnings(): + _warnings.simplefilter('ignore') + rd = _ReportDescriptor(fd.read()) + hidpp_short = 0x10 in rd.input_report_ids and 6 * 8 == int( + rd.get_input_report_size(0x10) + ) and _Usage(0xFF00, 0x0001) in rd.get_input_items(0x10)[0].usages + hidpp_long = 0x11 in rd.input_report_ids and 19 * 8 == int( + rd.get_input_report_size(0x11) + ) and _Usage(0xFF00, 0x0002) in rd.get_input_items(0x11)[0].usages + if not hidpp_short and not hidpp_long: + return + except Exception as e: # if can't process report descriptor fall back to old scheme + hidpp_short = hidpp_long = None + _log.warn('Report Descriptor not processed for BID %s VID %s PID %s: %s', bid, vid, pid, e) + hid_hid_device = hid_device.find_parent('hid') + if hid_hid_device: + return # these are devices connected through a receiver so don't pick them up here + + filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long) if not filter: return - hid_driver = filter.get('hid_driver') interface_number = filter.get('usb_interface') isDevice = filter.get('isDevice') @@ -118,13 +144,14 @@ def _match(action, device, filterfn): return intf_device = device.find_parent('usb', 'usb_interface') - # print ("*** usb interface", action, device, "usb_interface:", intf_device) usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') - if _log.isEnabledFor(_DEBUG): - _log.debug( - 'Found device BID %s VID %s PID %s INTERFACE %s FILTER %s', bid, vid, pid, usb_interface, interface_number + # print('*** usb interface', action, device, 'usb_interface:', intf_device, usb_interface, interface_number) + if _log.isEnabledFor(_INFO): + _log.info( + 'Found device BID %s VID %s PID %s HID++ %s %s USB %s %s', bid, vid, pid, hidpp_short, hidpp_long, + usb_interface, interface_number ) - if not (interface_number is None or interface_number == usb_interface): + if not (hidpp_short or hidpp_long or interface_number is None or interface_number == usb_interface): return attrs = intf_device.attributes if intf_device else None @@ -139,7 +166,9 @@ def _match(action, device, filterfn): serial=hid_device.get('HID_UNIQ'), release=attrs.get('bcdDevice') if attrs else None, manufacturer=attrs.get('manufacturer') if attrs else None, - product=attrs.get('product') if attrs else None + product=attrs.get('product') if attrs else None, + hidpp_short=hidpp_short, + hidpp_long=hidpp_long, ) return d_info @@ -157,7 +186,9 @@ def _match(action, device, filterfn): serial=None, release=None, manufacturer=None, - product=None + product=None, + hidpp_short=None, + hidpp_long=None, ) return d_info diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 878a4a79..4324c4e4 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -106,7 +106,7 @@ def match(record, bus_id, vendor_id, product_id): and (record.get('product_id') is None or record.get('product_id') == product_id)) -def filter_receivers(bus_id, vendor_id, product_id): +def filter_receivers(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): """Check that this product is a Logitech receiver and if so return the receiver record for further checking""" for record in _RECEIVER_USB_IDS: # known receivers if match(record, bus_id, vendor_id, product_id): @@ -118,26 +118,28 @@ def receivers(): yield from _hid.enumerate(filter_receivers) -def filter_devices(bus_id, vendor_id, product_id): +def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): """Check that this product is of interest and if so return the device record for further checking""" + for record in _RECEIVER_USB_IDS: # known receivers + if match(record, bus_id, vendor_id, product_id): + return record for record in _DEVICE_IDS: # known devices if match(record, bus_id, vendor_id, product_id): return record - return _other_device_check(bus_id, vendor_id, product_id) # USB and BT devices unknown to Solaar + if hidpp_short or hidpp_long: # unknown devices that use HID++ + return {'vendor_id': vendor_id, 'product_id': product_id, 'bus_id': bus_id, 'isDevice': True} + elif hidpp_short is None and hidpp_long is None: # unknown devices in correct range of IDs + return _other_device_check(bus_id, vendor_id, product_id) -def wired_devices(): - """Enumerate all the USB-connected and Bluetooth devices attached to the machine.""" - yield from _hid.enumerate(filter_devices) - - -def filter_either(bus_id, vendor_id, product_id): - return filter_receivers(bus_id, vendor_id, product_id) or filter_devices(bus_id, vendor_id, product_id) +def receivers_and_devices(): + """Enumerate all the receivers and devices directly attached to the machine.""" + yield from _hid.enumerate(filter) def notify_on_receivers_glib(callback): """Watch for matching devices and notifies the callback on the GLib thread.""" - return _hid.monitor_glib(callback, filter_either) + return _hid.monitor_glib(callback, filter) # diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index f9006020..e2426be1 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -42,6 +42,8 @@ class Device: self.path = path self.handle = handle self.product_id = None + self.hidpp_short = info.hidpp_short if info else None + self.hidpp_long = info.hidpp_long if info else None if receiver: assert number > 0 and number <= 15 # some receivers have devices past their max # of devices @@ -172,7 +174,9 @@ class Device: @property def protocol(self): if not self._protocol and self.online: - self._protocol = _base.ping(self.handle or self.receiver.handle, self.number, long_message=self.bluetooth) + self._protocol = _base.ping( + self.handle or self.receiver.handle, self.number, long_message=self.bluetooth or self.hidpp_short is False + ) # if the ping failed, the peripheral is (almost) certainly offline self.online = self._protocol is not None @@ -430,7 +434,7 @@ class Device: request_id, *params, no_reply=no_reply, - long_message=self.bluetooth or self.protocol >= 2.0, + long_message=self.bluetooth or self.hidpp_short is False or self.protocol >= 2.0, protocol=self.protocol ) diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py index 3117ce39..ac441907 100644 --- a/lib/solaar/cli/__init__.py +++ b/lib/solaar/cli/__init__.py @@ -114,14 +114,15 @@ def _receivers(dev_path=None): _sys.exit('%s: error: %s' % (NAME, str(e))) -def _wired_devices(dev_path=None): +def _receivers_and_devices(dev_path=None): from logitech_receiver import Device - from logitech_receiver.base import wired_devices - for dev_info in wired_devices(): + from logitech_receiver import Receiver + from logitech_receiver.base import receivers_and_devices + for dev_info in receivers_and_devices(): if dev_path is not None and dev_path != dev_info.path: continue try: - d = Device.open(dev_info) + d = Device.open(dev_info) if dev_info.isDevice else Receiver.open(dev_info) if _log.isEnabledFor(_DEBUG): _log.debug('[%s] => %s', dev_info.path, d) if d is not None: @@ -198,13 +199,12 @@ def run(cli_args=None, hidraw_path=None): assert action in actions try: - c = list(_receivers(hidraw_path)) if action == 'show' or action == 'probe' or action == 'config': - c += list(_wired_devices(hidraw_path)) - + c = list(_receivers_and_devices(hidraw_path)) + else: + c = list(_receivers(hidraw_path)) if not c: raise Exception('No devices found') - from importlib import import_module m = import_module('.' + action, package=__name__) m.run(c, args, _find_receiver, _find_device) diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 841e1f43..6c6a1fe1 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -309,9 +309,7 @@ def start_all(): if _log.isEnabledFor(_INFO): _log.info('starting receiver listening threads') - for device_info in _base.receivers(): - _process_receiver_event('add', device_info) - for device_info in _base.wired_devices(): + for device_info in _base.receivers_and_devices(): _process_receiver_event('add', device_info) diff --git a/python-hid-parser b/python-hid-parser new file mode 160000 index 00000000..4b7944f4 --- /dev/null +++ b/python-hid-parser @@ -0,0 +1 @@ +Subproject commit 4b7944f4999e152c678cd7fa76278b7e2535c3ff