Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/hid: import base_device.py from hid-tools

We need to slightly change base_device.py for supporting HID-BPF,
so instead of monkey patching, let's just embed it in the kernel tree.

Link: https://lore.kernel.org/r/20240410-bpf_sources-v1-10-a8bf16033ef8@kernel.org
Reviewed-by: Peter Hutterer <peter.hutterer@who-t.net>
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>

+413 -1
+1 -1
tools/testing/selftests/hid/tests/base.py
··· 12 12 13 13 import logging 14 14 15 - from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile 15 + from .base_device import BaseDevice, EvdevMatch, SysfsFile 16 16 from pathlib import Path 17 17 from typing import Final, List, Tuple 18 18
+412
tools/testing/selftests/hid/tests/base_device.py
··· 1 + #!/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + # -*- coding: utf-8 -*- 4 + # 5 + # Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com> 6 + # Copyright (c) 2017 Red Hat, Inc. 7 + # 8 + # This program is free software: you can redistribute it and/or modify 9 + # it under the terms of the GNU General Public License as published by 10 + # the Free Software Foundation; either version 2 of the License, or 11 + # (at your option) any later version. 12 + # 13 + # This program is distributed in the hope that it will be useful, 14 + # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 + # GNU General Public License for more details. 17 + # 18 + # You should have received a copy of the GNU General Public License 19 + # along with this program. If not, see <http://www.gnu.org/licenses/>. 20 + 21 + import fcntl 22 + import functools 23 + import libevdev 24 + import os 25 + 26 + try: 27 + import pyudev 28 + except ImportError: 29 + raise ImportError("UHID is not supported due to missing pyudev dependency") 30 + 31 + import logging 32 + 33 + import hidtools.hid as hid 34 + from hidtools.uhid import UHIDDevice 35 + from hidtools.util import BusType 36 + 37 + from pathlib import Path 38 + from typing import Any, ClassVar, Dict, List, Optional, Type, Union 39 + 40 + logger = logging.getLogger("hidtools.device.base_device") 41 + 42 + 43 + class SysfsFile(object): 44 + def __init__(self, path): 45 + self.path = path 46 + 47 + def __set_value(self, value): 48 + with open(self.path, "w") as f: 49 + return f.write(f"{value}\n") 50 + 51 + def __get_value(self): 52 + with open(self.path) as f: 53 + return f.read().strip() 54 + 55 + @property 56 + def int_value(self) -> int: 57 + return int(self.__get_value()) 58 + 59 + @int_value.setter 60 + def int_value(self, v: int) -> None: 61 + self.__set_value(v) 62 + 63 + @property 64 + def str_value(self) -> str: 65 + return self.__get_value() 66 + 67 + @str_value.setter 68 + def str_value(self, v: str) -> None: 69 + self.__set_value(v) 70 + 71 + 72 + class LED(object): 73 + def __init__(self, sys_path): 74 + self.max_brightness = SysfsFile(sys_path / "max_brightness").int_value 75 + self.__brightness = SysfsFile(sys_path / "brightness") 76 + 77 + @property 78 + def brightness(self) -> int: 79 + return self.__brightness.int_value 80 + 81 + @brightness.setter 82 + def brightness(self, value: int) -> None: 83 + self.__brightness.int_value = value 84 + 85 + 86 + class PowerSupply(object): 87 + """Represents Linux power_supply_class sysfs nodes.""" 88 + 89 + def __init__(self, sys_path): 90 + self._capacity = SysfsFile(sys_path / "capacity") 91 + self._status = SysfsFile(sys_path / "status") 92 + self._type = SysfsFile(sys_path / "type") 93 + 94 + @property 95 + def capacity(self) -> int: 96 + return self._capacity.int_value 97 + 98 + @property 99 + def status(self) -> str: 100 + return self._status.str_value 101 + 102 + @property 103 + def type(self) -> str: 104 + return self._type.str_value 105 + 106 + 107 + class HIDIsReady(object): 108 + """ 109 + Companion class that binds to a kernel mechanism 110 + and that allows to know when a uhid device is ready or not. 111 + 112 + See :meth:`is_ready` for details. 113 + """ 114 + 115 + def __init__(self: "HIDIsReady", uhid: UHIDDevice) -> None: 116 + self.uhid = uhid 117 + 118 + def is_ready(self: "HIDIsReady") -> bool: 119 + """ 120 + Overwrite in subclasses: should return True or False whether 121 + the attached uhid device is ready or not. 122 + """ 123 + return False 124 + 125 + 126 + class UdevHIDIsReady(HIDIsReady): 127 + _pyudev_context: ClassVar[Optional[pyudev.Context]] = None 128 + _pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None 129 + _uhid_devices: ClassVar[Dict[int, bool]] = {} 130 + 131 + def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None: 132 + super().__init__(uhid) 133 + self._init_pyudev() 134 + 135 + @classmethod 136 + def _init_pyudev(cls: Type["UdevHIDIsReady"]) -> None: 137 + if cls._pyudev_context is None: 138 + cls._pyudev_context = pyudev.Context() 139 + cls._pyudev_monitor = pyudev.Monitor.from_netlink(cls._pyudev_context) 140 + cls._pyudev_monitor.filter_by("hid") 141 + cls._pyudev_monitor.start() 142 + 143 + UHIDDevice._append_fd_to_poll( 144 + cls._pyudev_monitor.fileno(), cls._cls_udev_event_callback 145 + ) 146 + 147 + @classmethod 148 + def _cls_udev_event_callback(cls: Type["UdevHIDIsReady"]) -> None: 149 + if cls._pyudev_monitor is None: 150 + return 151 + event: pyudev.Device 152 + for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None): 153 + if event.action not in ["bind", "remove"]: 154 + return 155 + 156 + logger.debug(f"udev event: {event.action} -> {event}") 157 + 158 + id = int(event.sys_path.strip().split(".")[-1], 16) 159 + 160 + cls._uhid_devices[id] = event.action == "bind" 161 + 162 + def is_ready(self: "UdevHIDIsReady") -> bool: 163 + try: 164 + return self._uhid_devices[self.uhid.hid_id] 165 + except KeyError: 166 + return False 167 + 168 + 169 + class EvdevMatch(object): 170 + def __init__( 171 + self: "EvdevMatch", 172 + *, 173 + requires: List[Any] = [], 174 + excludes: List[Any] = [], 175 + req_properties: List[Any] = [], 176 + excl_properties: List[Any] = [], 177 + ) -> None: 178 + self.requires = requires 179 + self.excludes = excludes 180 + self.req_properties = req_properties 181 + self.excl_properties = excl_properties 182 + 183 + def is_a_match(self: "EvdevMatch", evdev: libevdev.Device) -> bool: 184 + for m in self.requires: 185 + if not evdev.has(m): 186 + return False 187 + for m in self.excludes: 188 + if evdev.has(m): 189 + return False 190 + for p in self.req_properties: 191 + if not evdev.has_property(p): 192 + return False 193 + for p in self.excl_properties: 194 + if evdev.has_property(p): 195 + return False 196 + return True 197 + 198 + 199 + class EvdevDevice(object): 200 + """ 201 + Represents an Evdev node and its properties. 202 + This is a stub for the libevdev devices, as they are relying on 203 + uevent to get the data, saving us some ioctls to fetch the names 204 + and properties. 205 + """ 206 + 207 + def __init__(self: "EvdevDevice", sysfs: Path) -> None: 208 + self.sysfs = sysfs 209 + self.event_node: Any = None 210 + self.libevdev: Optional[libevdev.Device] = None 211 + 212 + self.uevents = {} 213 + # all of the interesting properties are stored in the input uevent, so in the parent 214 + # so convert the uevent file of the parent input node into a dict 215 + with open(sysfs.parent / "uevent") as f: 216 + for line in f.readlines(): 217 + key, value = line.strip().split("=") 218 + self.uevents[key] = value.strip('"') 219 + 220 + # we open all evdev nodes in order to not miss any event 221 + self.open() 222 + 223 + @property 224 + def name(self: "EvdevDevice") -> str: 225 + assert "NAME" in self.uevents 226 + 227 + return self.uevents["NAME"] 228 + 229 + @property 230 + def evdev(self: "EvdevDevice") -> Path: 231 + return Path("/dev/input") / self.sysfs.name 232 + 233 + def matches_application( 234 + self: "EvdevDevice", application: str, matches: Dict[str, EvdevMatch] 235 + ) -> bool: 236 + if self.libevdev is None: 237 + return False 238 + 239 + if application in matches: 240 + return matches[application].is_a_match(self.libevdev) 241 + 242 + logger.error( 243 + f"application '{application}' is unknown, please update/fix hid-tools" 244 + ) 245 + assert False # hid-tools likely needs an update 246 + 247 + def open(self: "EvdevDevice") -> libevdev.Device: 248 + self.event_node = open(self.evdev, "rb") 249 + self.libevdev = libevdev.Device(self.event_node) 250 + 251 + assert self.libevdev.fd is not None 252 + 253 + fd = self.libevdev.fd.fileno() 254 + flag = fcntl.fcntl(fd, fcntl.F_GETFD) 255 + fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK) 256 + 257 + return self.libevdev 258 + 259 + def close(self: "EvdevDevice") -> None: 260 + if self.libevdev is not None and self.libevdev.fd is not None: 261 + self.libevdev.fd.close() 262 + self.libevdev = None 263 + if self.event_node is not None: 264 + self.event_node.close() 265 + self.event_node = None 266 + 267 + 268 + class BaseDevice(UHIDDevice): 269 + # default _application_matches that matches nothing. This needs 270 + # to be set in the subclasses to have get_evdev() working 271 + _application_matches: Dict[str, EvdevMatch] = {} 272 + 273 + def __init__( 274 + self, 275 + name, 276 + application, 277 + rdesc_str: Optional[str] = None, 278 + rdesc: Optional[Union[hid.ReportDescriptor, str, bytes]] = None, 279 + input_info=None, 280 + ) -> None: 281 + self._kernel_is_ready: HIDIsReady = UdevHIDIsReady(self) 282 + if rdesc_str is None and rdesc is None: 283 + raise Exception("Please provide at least a rdesc or rdesc_str") 284 + super().__init__() 285 + if name is None: 286 + name = f"uhid gamepad test {self.__class__.__name__}" 287 + if input_info is None: 288 + input_info = (BusType.USB, 1, 2) 289 + self.name = name 290 + self.info = input_info 291 + self.default_reportID = None 292 + self.opened = False 293 + self.started = False 294 + self.application = application 295 + self._input_nodes: Optional[list[EvdevDevice]] = None 296 + if rdesc is None: 297 + assert rdesc_str is not None 298 + self.rdesc = hid.ReportDescriptor.from_human_descr(rdesc_str) # type: ignore 299 + else: 300 + self.rdesc = rdesc # type: ignore 301 + 302 + @property 303 + def power_supply_class(self: "BaseDevice") -> Optional[PowerSupply]: 304 + ps = self.walk_sysfs("power_supply", "power_supply/*") 305 + if ps is None or len(ps) < 1: 306 + return None 307 + 308 + return PowerSupply(ps[0]) 309 + 310 + @property 311 + def led_classes(self: "BaseDevice") -> List[LED]: 312 + leds = self.walk_sysfs("led", "**/max_brightness") 313 + if leds is None: 314 + return [] 315 + 316 + return [LED(led.parent) for led in leds] 317 + 318 + @property 319 + def kernel_is_ready(self: "BaseDevice") -> bool: 320 + return self._kernel_is_ready.is_ready() and self.started 321 + 322 + @property 323 + def input_nodes(self: "BaseDevice") -> List[EvdevDevice]: 324 + if self._input_nodes is not None: 325 + return self._input_nodes 326 + 327 + if not self.kernel_is_ready or not self.started: 328 + return [] 329 + 330 + self._input_nodes = [ 331 + EvdevDevice(path) 332 + for path in self.walk_sysfs("input", "input/input*/event*") 333 + ] 334 + return self._input_nodes 335 + 336 + def match_evdev_rule(self, application, evdev): 337 + """Replace this in subclasses if the device has multiple reports 338 + of the same type and we need to filter based on the actual evdev 339 + node. 340 + 341 + returning True will append the corresponding report to 342 + `self.input_nodes[type]` 343 + returning False will ignore this report / type combination 344 + for the device. 345 + """ 346 + return True 347 + 348 + def open(self): 349 + self.opened = True 350 + 351 + def _close_all_opened_evdev(self): 352 + if self._input_nodes is not None: 353 + for e in self._input_nodes: 354 + e.close() 355 + 356 + def __del__(self): 357 + self._close_all_opened_evdev() 358 + 359 + def close(self): 360 + self.opened = False 361 + 362 + def start(self, flags): 363 + self.started = True 364 + 365 + def stop(self): 366 + self.started = False 367 + self._close_all_opened_evdev() 368 + 369 + def next_sync_events(self, application=None): 370 + evdev = self.get_evdev(application) 371 + if evdev is not None: 372 + return list(evdev.events()) 373 + return [] 374 + 375 + @property 376 + def application_matches(self: "BaseDevice") -> Dict[str, EvdevMatch]: 377 + return self._application_matches 378 + 379 + @application_matches.setter 380 + def application_matches(self: "BaseDevice", data: Dict[str, EvdevMatch]) -> None: 381 + self._application_matches = data 382 + 383 + def get_evdev(self, application=None): 384 + if application is None: 385 + application = self.application 386 + 387 + if len(self.input_nodes) == 0: 388 + return None 389 + 390 + assert self._input_nodes is not None 391 + 392 + if len(self._input_nodes) == 1: 393 + evdev = self._input_nodes[0] 394 + if self.match_evdev_rule(application, evdev.libevdev): 395 + return evdev.libevdev 396 + else: 397 + for _evdev in self._input_nodes: 398 + if _evdev.matches_application(application, self.application_matches): 399 + if self.match_evdev_rule(application, _evdev.libevdev): 400 + return _evdev.libevdev 401 + 402 + def is_ready(self): 403 + """Returns whether a UHID device is ready. Can be overwritten in 404 + subclasses to add extra conditions on when to consider a UHID 405 + device ready. This can be: 406 + 407 + - we need to wait on different types of input devices to be ready 408 + (Touch Screen and Pen for example) 409 + - we need to have at least 4 LEDs present 410 + (len(self.uhdev.leds_classes) == 4) 411 + - or any other combinations""" 412 + return self.kernel_is_ready