hidapi: process hid report descriptors to identify devices
This commit is contained in:
parent
afada652e8
commit
3e90c3bc8a
|
@ -0,0 +1,3 @@
|
|||
[submodule "python-hid-parser"]
|
||||
path = python-hid-parser
|
||||
url = https://github.com/usb-tools/python-hid-parser
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../python-hid-parser/hid_parser
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 4b7944f4999e152c678cd7fa76278b7e2535c3ff
|
Loading…
Reference in New Issue