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`,
|
You may have to install `gcc` and the Python development package (`python3-dev` or `python3-devel`,
|
||||||
depending on your distribution).
|
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,
|
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.
|
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 errno as _errno
|
||||||
import os as _os
|
import os as _os
|
||||||
|
import warnings as _warnings
|
||||||
|
|
||||||
# the tuple object we'll expose when enumerating devices
|
# the tuple object we'll expose when enumerating devices
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from logging import DEBUG as _DEBUG
|
from logging import INFO as _INFO
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from select import select as _select
|
from select import select as _select
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from time import time as _timestamp
|
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 Context as _Context
|
||||||
from pyudev import Device as _Device
|
from pyudev import Device as _Device
|
||||||
from pyudev import DeviceNotFoundError
|
from pyudev import DeviceNotFoundError
|
||||||
|
@ -43,8 +46,8 @@ from pyudev import Monitor as _Monitor
|
||||||
|
|
||||||
_log = getLogger(__name__)
|
_log = getLogger(__name__)
|
||||||
del getLogger
|
del getLogger
|
||||||
|
|
||||||
native_implementation = 'udev'
|
native_implementation = 'udev'
|
||||||
|
fileopen = open
|
||||||
|
|
||||||
DeviceInfo = namedtuple(
|
DeviceInfo = namedtuple(
|
||||||
'DeviceInfo', [
|
'DeviceInfo', [
|
||||||
|
@ -59,6 +62,8 @@ DeviceInfo = namedtuple(
|
||||||
'driver',
|
'driver',
|
||||||
'bus_id',
|
'bus_id',
|
||||||
'isDevice',
|
'isDevice',
|
||||||
|
'hidpp_short',
|
||||||
|
'hidpp_long',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
del namedtuple
|
del namedtuple
|
||||||
|
@ -92,17 +97,38 @@ def exit():
|
||||||
# with the required hid_driver and usb_interface and whether this is a receiver or device.
|
# with the required hid_driver and usb_interface and whether this is a receiver or device.
|
||||||
def _match(action, device, filterfn):
|
def _match(action, device, filterfn):
|
||||||
hid_device = device.find_parent('hid')
|
hid_device = device.find_parent('hid')
|
||||||
if not hid_device:
|
if not hid_device: # only HID devices are of interest to Solaar
|
||||||
return
|
return
|
||||||
hid_id = hid_device.get('HID_ID')
|
hid_id = hid_device.get('HID_ID')
|
||||||
if not hid_id:
|
if not hid_id:
|
||||||
return # there are reports that sometimes the id isn't set up right so be defensive
|
return # there are reports that sometimes the id isn't set up right so be defensive
|
||||||
bid, vid, pid = hid_id.split(':')
|
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:
|
if not filter:
|
||||||
return
|
return
|
||||||
|
|
||||||
hid_driver = filter.get('hid_driver')
|
hid_driver = filter.get('hid_driver')
|
||||||
interface_number = filter.get('usb_interface')
|
interface_number = filter.get('usb_interface')
|
||||||
isDevice = filter.get('isDevice')
|
isDevice = filter.get('isDevice')
|
||||||
|
@ -118,13 +144,14 @@ def _match(action, device, filterfn):
|
||||||
return
|
return
|
||||||
|
|
||||||
intf_device = device.find_parent('usb', 'usb_interface')
|
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')
|
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
|
||||||
if _log.isEnabledFor(_DEBUG):
|
# print('*** usb interface', action, device, 'usb_interface:', intf_device, usb_interface, interface_number)
|
||||||
_log.debug(
|
if _log.isEnabledFor(_INFO):
|
||||||
'Found device BID %s VID %s PID %s INTERFACE %s FILTER %s', bid, vid, pid, usb_interface, interface_number
|
_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
|
return
|
||||||
attrs = intf_device.attributes if intf_device else None
|
attrs = intf_device.attributes if intf_device else None
|
||||||
|
|
||||||
|
@ -139,7 +166,9 @@ def _match(action, device, filterfn):
|
||||||
serial=hid_device.get('HID_UNIQ'),
|
serial=hid_device.get('HID_UNIQ'),
|
||||||
release=attrs.get('bcdDevice') if attrs else None,
|
release=attrs.get('bcdDevice') if attrs else None,
|
||||||
manufacturer=attrs.get('manufacturer') 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
|
return d_info
|
||||||
|
|
||||||
|
@ -157,7 +186,9 @@ def _match(action, device, filterfn):
|
||||||
serial=None,
|
serial=None,
|
||||||
release=None,
|
release=None,
|
||||||
manufacturer=None,
|
manufacturer=None,
|
||||||
product=None
|
product=None,
|
||||||
|
hidpp_short=None,
|
||||||
|
hidpp_long=None,
|
||||||
)
|
)
|
||||||
return d_info
|
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))
|
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"""
|
"""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
|
for record in _RECEIVER_USB_IDS: # known receivers
|
||||||
if match(record, bus_id, vendor_id, product_id):
|
if match(record, bus_id, vendor_id, product_id):
|
||||||
|
@ -118,26 +118,28 @@ def receivers():
|
||||||
yield from _hid.enumerate(filter_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"""
|
"""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
|
for record in _DEVICE_IDS: # known devices
|
||||||
if match(record, bus_id, vendor_id, product_id):
|
if match(record, bus_id, vendor_id, product_id):
|
||||||
return record
|
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():
|
def receivers_and_devices():
|
||||||
"""Enumerate all the USB-connected and Bluetooth devices attached to the machine."""
|
"""Enumerate all the receivers and devices directly attached to the machine."""
|
||||||
yield from _hid.enumerate(filter_devices)
|
yield from _hid.enumerate(filter)
|
||||||
|
|
||||||
|
|
||||||
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 notify_on_receivers_glib(callback):
|
def notify_on_receivers_glib(callback):
|
||||||
"""Watch for matching devices and notifies the callback on the GLib thread."""
|
"""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.path = path
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.product_id = None
|
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:
|
if receiver:
|
||||||
assert number > 0 and number <= 15 # some receivers have devices past their max # of devices
|
assert number > 0 and number <= 15 # some receivers have devices past their max # of devices
|
||||||
|
@ -172,7 +174,9 @@ class Device:
|
||||||
@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, 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
|
# if the ping failed, the peripheral is (almost) certainly offline
|
||||||
self.online = self._protocol is not None
|
self.online = self._protocol is not None
|
||||||
|
|
||||||
|
@ -430,7 +434,7 @@ class Device:
|
||||||
request_id,
|
request_id,
|
||||||
*params,
|
*params,
|
||||||
no_reply=no_reply,
|
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
|
protocol=self.protocol
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -114,14 +114,15 @@ def _receivers(dev_path=None):
|
||||||
_sys.exit('%s: error: %s' % (NAME, str(e)))
|
_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 import Device
|
||||||
from logitech_receiver.base import wired_devices
|
from logitech_receiver import Receiver
|
||||||
for dev_info in wired_devices():
|
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:
|
if dev_path is not None and dev_path != dev_info.path:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
d = Device.open(dev_info)
|
d = Device.open(dev_info) if dev_info.isDevice else Receiver.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:
|
||||||
|
@ -198,13 +199,12 @@ def run(cli_args=None, hidraw_path=None):
|
||||||
assert action in actions
|
assert action in actions
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = list(_receivers(hidraw_path))
|
|
||||||
if action == 'show' or action == 'probe' or action == 'config':
|
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:
|
if not c:
|
||||||
raise Exception('No devices found')
|
raise Exception('No devices found')
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
m = import_module('.' + action, package=__name__)
|
m = import_module('.' + action, package=__name__)
|
||||||
m.run(c, args, _find_receiver, _find_device)
|
m.run(c, args, _find_receiver, _find_device)
|
||||||
|
|
|
@ -309,9 +309,7 @@ def start_all():
|
||||||
|
|
||||||
if _log.isEnabledFor(_INFO):
|
if _log.isEnabledFor(_INFO):
|
||||||
_log.info('starting receiver listening threads')
|
_log.info('starting receiver listening threads')
|
||||||
for device_info in _base.receivers():
|
for device_info in _base.receivers_and_devices():
|
||||||
_process_receiver_event('add', device_info)
|
|
||||||
for device_info in _base.wired_devices():
|
|
||||||
_process_receiver_event('add', device_info)
|
_process_receiver_event('add', device_info)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4b7944f4999e152c678cd7fa76278b7e2535c3ff
|
Loading…
Reference in New Issue