base_usb: Add external interface

Clean up, type hint and tests base_usb and related modules.
This commit is contained in:
MattHag 2024-09-28 01:14:32 +02:00 committed by Peter F. Patel-Schneider
parent a75c4b9679
commit 8d0672ac3c
4 changed files with 96 additions and 58 deletions

View File

@ -78,7 +78,7 @@ def exit():
# The filterfn is used to determine whether this is a device of interest to Solaar. # The filterfn is used to determine whether this is a device of interest to Solaar.
# It is given the bus id, vendor id, and product id and returns a dictionary # It is given the bus id, vendor id, and product id and returns a dictionary
# 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, filter_func: typing.Callable[[int, int, int, bool, bool], dict[str, typing.Any]]):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"Dbus event {action} {device}") logger.debug(f"Dbus event {action} {device}")
hid_device = device.find_parent("hid") hid_device = device.find_parent("hid")
@ -113,11 +113,11 @@ def _match(action, device, filterfn):
"Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s", device.device_node, bid, vid, pid, e "Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s", device.device_node, bid, vid, pid, e
) )
filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long) filtered_result = filter_func(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long)
if not filter: if not filtered_result:
return return
interface_number = filter.get("usb_interface") interface_number = filtered_result.get("usb_interface")
isDevice = filter.get("isDevice") isDevice = filtered_result.get("isDevice")
if action == "add": if action == "add":
hid_driver_name = hid_device.properties.get("DRIVER") hid_driver_name = hid_device.properties.get("DRIVER")
@ -260,7 +260,7 @@ def monitor_glib(glib: GLib, callback, filterfn):
m.start() m.start()
def enumerate(filterfn): def enumerate(filter_func: typing.Callable[[int, int, int, bool, bool], dict[str, typing.Any]]):
"""Enumerate the HID Devices. """Enumerate the HID Devices.
List all the HID devices attached to the system, optionally filtering by List all the HID devices attached to the system, optionally filtering by
@ -272,7 +272,7 @@ def enumerate(filterfn):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("Starting dbus enumeration") logger.debug("Starting dbus enumeration")
for dev in pyudev.Context().list_devices(subsystem="hidraw"): for dev in pyudev.Context().list_devices(subsystem="hidraw"):
dev_info = _match("add", dev, filterfn) dev_info = _match("add", dev, filter_func)
if dev_info: if dev_info:
yield dev_info yield dev_info

View File

@ -94,26 +94,23 @@ for _ignore, d in descriptors.DEVICES.items():
KNOWN_DEVICE_IDS.append(_bluetooth_device(d.btid)) KNOWN_DEVICE_IDS.append(_bluetooth_device(d.btid))
def other_device_check(bus_id: int, vendor_id: int, product_id: int): def other_device_check(bus_id: int, vendor_id: int, product_id: int) -> dict[str, Any] | None:
"""Check whether product is a Logitech USB-connected or Bluetooth device based on bus, vendor, and product IDs """Check whether product is a Logitech USB-connected or Bluetooth device based on bus, vendor, and product IDs
This allows Solaar to support receiverless HID++ 2.0 devices that it knows nothing about""" This allows Solaar to support receiverless HID++ 2.0 devices that it knows nothing about"""
if vendor_id != LOGITECH_VENDOR_ID: if vendor_id != LOGITECH_VENDOR_ID:
return return
if bus_id == BusID.USB: device_info = None
if product_id >= 0xC07D and product_id <= 0xC094 or product_id >= 0xC32B and product_id <= 0xC344: if bus_id == BusID.USB and (0xC07D <= product_id <= 0xC094 or 0xC32B <= product_id <= 0xC344):
return _usb_device(product_id, 2) device_info = _usb_device(product_id, 2)
elif bus_id == BusID.BLUETOOTH: elif bus_id == BusID.BLUETOOTH and (0xB012 <= product_id <= 0xB0FF or 0xB317 <= product_id <= 0xB3FF):
if product_id >= 0xB012 and product_id <= 0xB0FF or product_id >= 0xB317 and product_id <= 0xB3FF: device_info = _bluetooth_device(product_id)
return _bluetooth_device(product_id) return device_info
def product_information(usb_id: int) -> dict[str, Any]: def product_information(usb_id: int) -> dict[str, Any]:
"""Returns hardcoded information from USB receiver.""" """Returns hardcoded information from USB receiver."""
for receiver in base_usb.KNOWN_RECEIVER: return base_usb.get_receiver_info(usb_id)
if usb_id == receiver.get("product_id"):
return receiver
raise ValueError(f"Unknown receiver type: 0x{usb_id:02X}")
_SHORT_MESSAGE_SIZE = 7 _SHORT_MESSAGE_SIZE = 7
@ -142,7 +139,7 @@ _DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
_PING_TIMEOUT = DEFAULT_TIMEOUT _PING_TIMEOUT = DEFAULT_TIMEOUT
def match(record, bus_id, vendor_id, product_id): def _match(record: dict[str, Any], bus_id: int, vendor_id: int, product_id: int):
return ( return (
(record.get("bus_id") is None or record.get("bus_id") == bus_id) (record.get("bus_id") is None or record.get("bus_id") == bus_id)
and (record.get("vendor_id") is None or record.get("vendor_id") == vendor_id) and (record.get("vendor_id") is None or record.get("vendor_id") == vendor_id)
@ -150,14 +147,22 @@ def match(record, bus_id, vendor_id, product_id):
) )
def filter_receivers(bus_id: int, vendor_id: int, product_id: int, hidpp_short=False, hidpp_long=False): def filter_receivers(
"""Check that this product is a Logitech receiver bus_id: int, vendor_id: int, product_id: int, hidpp_short: bool = False, hidpp_long: bool = False
) -> dict[str, Any]:
"""Check that this product is a Logitech receiver.
Filters based on bus_id, vendor_id and product_id.
If so return the receiver record for further checking. If so return the receiver record for further checking.
""" """
for record in base_usb.KNOWN_RECEIVER: # known receivers try:
if match(record, bus_id, vendor_id, product_id): record = base_usb.get_receiver_info(product_id)
if _match(record, bus_id, vendor_id, product_id):
return record return record
except ValueError:
pass
if vendor_id == LOGITECH_VENDOR_ID and 0xC500 <= product_id <= 0xC5FF: # unknown receiver if vendor_id == LOGITECH_VENDOR_ID and 0xC500 <= product_id <= 0xC5FF: # unknown receiver
return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": False} return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": False}
@ -167,13 +172,14 @@ def receivers():
yield from hidapi.enumerate(filter_receivers) yield from hidapi.enumerate(filter_receivers)
def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): def filter(bus_id: int, vendor_id: int, product_id: int, hidpp_short: bool = False, hidpp_long: bool = 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"""
record = filter_receivers(bus_id, vendor_id, product_id, hidpp_short, hidpp_long) record = filter_receivers(bus_id, vendor_id, product_id, hidpp_short, hidpp_long)
if record: # known or unknown receiver if record: # known or unknown receiver
return record return record
for record in KNOWN_DEVICE_IDS: for record in KNOWN_DEVICE_IDS:
if match(record, bus_id, vendor_id, product_id): if _match(record, bus_id, vendor_id, product_id):
return record return record
if hidpp_short or hidpp_long: # unknown devices that use HID++ 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} return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": True}

View File

@ -14,20 +14,21 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
## According to Logitech, they use the following product IDs (as of September 2020) """Collection of known Logitech product IDs.
## USB product IDs for receivers: 0xC526 - 0xC5xx
## Wireless PIDs for hidpp10 devices: 0x2006 - 0x2019
## Wireless PIDs for hidpp20 devices: 0x4002 - 0x4097, 0x4101 - 0x4102
## USB product IDs for hidpp20 devices: 0xC07D - 0xC094, 0xC32B - 0xC344
## Bluetooth product IDs (for hidpp20 devices): 0xB012 - 0xB0xx, 0xB32A - 0xB3xx
# USB ids of Logitech wireless receivers. According to Logitech, they use the following product IDs (as of September 2020)
# Only receivers supporting the HID++ protocol can go in here. USB product IDs for receivers: 0xC526 - 0xC5xx
Wireless PIDs for hidpp10 devices: 0x2006 - 0x2019
Wireless PIDs for hidpp20 devices: 0x4002 - 0x4097, 0x4101 - 0x4102
USB product IDs for hidpp20 devices: 0xC07D - 0xC094, 0xC32B - 0xC344
Bluetooth product IDs (for hidpp20 devices): 0xB012 - 0xB0xx, 0xB32A - 0xB3xx
USB ids of Logitech wireless receivers.
Only receivers supporting the HID++ protocol can go in here.
"""
from solaar.i18n import _ from solaar.i18n import _
from logitech_receiver.common import LOGITECH_VENDOR_ID
# max_devices is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03, default # max_devices is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03, default
# to 1. # to 1.
# may_unpair is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03, # may_unpair is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03,
@ -36,8 +37,10 @@ from logitech_receiver.common import LOGITECH_VENDOR_ID
## should this last be changed so that may_unpair is used for all receivers? writing to Registers.RECEIVER_PAIRING ## should this last be changed so that may_unpair is used for all receivers? writing to Registers.RECEIVER_PAIRING
## doesn't seem right ## doesn't seem right
LOGITECH_VENDOR_ID = 0x046D
def _bolt_receiver(product_id):
def _bolt_receiver(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -49,7 +52,7 @@ def _bolt_receiver(product_id):
} }
def _unifying_receiver(product_id): def _unifying_receiver(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -60,7 +63,7 @@ def _unifying_receiver(product_id):
} }
def _nano_receiver(product_id): def _nano_receiver(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -72,7 +75,7 @@ def _nano_receiver(product_id):
} }
def _nano_receiver_no_unpair(product_id): def _nano_receiver_no_unpair(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -85,7 +88,7 @@ def _nano_receiver_no_unpair(product_id):
} }
def _nano_receiver_max2(product_id): def _nano_receiver_max2(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -98,20 +101,7 @@ def _nano_receiver_max2(product_id):
} }
def _nano_receiver_maxn(product_id, max): def _lenovo_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 1,
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"max_devices": max,
"may_unpair": False,
"re_pairs": True,
}
def _lenovo_receiver(product_id):
return { return {
"vendor_id": 6127, "vendor_id": 6127,
"product_id": product_id, "product_id": product_id,
@ -122,7 +112,7 @@ def _lenovo_receiver(product_id):
} }
def _lightspeed_receiver(product_id): def _lightspeed_receiver(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -133,7 +123,7 @@ def _lightspeed_receiver(product_id):
} }
def _ex100_receiver(product_id): def _ex100_receiver(product_id: int) -> dict:
return { return {
"vendor_id": LOGITECH_VENDOR_ID, "vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id, "product_id": product_id,
@ -147,7 +137,7 @@ def _ex100_receiver(product_id):
# Receivers added here should also be listed in # Receivers added here should also be listed in
# share/solaar/io.github.pwr_solaar.solaar.metainfo.xml # share/solaar/io.github.pwr_solaar.solaar.meta-info.xml
# Look in https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h # Look in https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h
# Bolt receivers (marked with the yellow lightning bolt logo) # Bolt receivers (marked with the yellow lightning bolt logo)
@ -184,7 +174,7 @@ LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xC547)
# EX100 old style receiver pre-unifying protocol # EX100 old style receiver pre-unifying protocol
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517) EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517)
KNOWN_RECEIVER = ( KNOWN_RECEIVERS = (
BOLT_RECEIVER_C548, BOLT_RECEIVER_C548,
UNIFYING_RECEIVER_C52B, UNIFYING_RECEIVER_C52B,
UNIFYING_RECEIVER_C532, UNIFYING_RECEIVER_C532,
@ -210,3 +200,23 @@ KNOWN_RECEIVER = (
LIGHTSPEED_RECEIVER_C547, LIGHTSPEED_RECEIVER_C547,
EX100_27MHZ_RECEIVER_C517, EX100_27MHZ_RECEIVER_C517,
) )
def get_receiver_info(product_id: int) -> dict:
"""Returns hardcoded information about Logitech receiver.
Parameters
----------
product_id
Product ID of receiver e.g. 0xC548 for a Logitech Bolt receiver.
Returns
-------
dict
Product info with mandatory vendor_id, product_id,
usb_interface, name, receiver_kind
"""
for receiver in KNOWN_RECEIVERS:
if product_id == receiver.get("product_id"):
return receiver
raise ValueError(f"Unknown product ID '0x{product_id:02X}")

View File

@ -24,6 +24,28 @@ def test_product_information(usb_id, expected_name, expected_receiver_kind):
assert res["receiver_kind"] == expected_receiver_kind assert res["receiver_kind"] == expected_receiver_kind
def test_filter_receivers_known():
bus_id = 2
vendor_id = 0x046D
product_id = 0xC548
receiver_info = base.filter_receivers(bus_id, vendor_id, product_id)
assert receiver_info["name"] == "Bolt Receiver"
assert receiver_info["receiver_kind"] == "bolt"
def test_filter_receivers_unknown():
bus_id = 1
vendor_id = 0x046D
product_id = 0xC500
receiver_info = base.filter_receivers(bus_id, vendor_id, product_id)
assert receiver_info["bus_id"] == bus_id
assert receiver_info["product_id"] == product_id
def test_get_next_sw_id(): def test_get_next_sw_id():
res1 = base._get_next_sw_id() res1 = base._get_next_sw_id()
res2 = base._get_next_sw_id() res2 = base._get_next_sw_id()