Path: blob/master/tools/testing/selftests/hid/tests/base_device.py
26308 views
#!/bin/env python31# SPDX-License-Identifier: GPL-2.02# -*- coding: utf-8 -*-3#4# Copyright (c) 2017 Benjamin Tissoires <[email protected]>5# Copyright (c) 2017 Red Hat, Inc.6#7# This program is free software: you can redistribute it and/or modify8# it under the terms of the GNU General Public License as published by9# the Free Software Foundation; either version 2 of the License, or10# (at your option) any later version.11#12# This program is distributed in the hope that it will be useful,13# but WITHOUT ANY WARRANTY; without even the implied warranty of14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the15# GNU General Public License for more details.16#17# You should have received a copy of the GNU General Public License18# along with this program. If not, see <http://www.gnu.org/licenses/>.1920import dataclasses21import fcntl22import functools23import libevdev24import os25import threading2627try:28import pyudev29except ImportError:30raise ImportError("UHID is not supported due to missing pyudev dependency")3132import logging3334import hidtools.hid as hid35from hidtools.uhid import UHIDDevice36from hidtools.util import BusType3738from pathlib import Path39from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union4041logger = logging.getLogger("hidtools.device.base_device")424344class SysfsFile(object):45def __init__(self, path):46self.path = path4748def __set_value(self, value):49with open(self.path, "w") as f:50return f.write(f"{value}\n")5152def __get_value(self):53with open(self.path) as f:54return f.read().strip()5556@property57def int_value(self) -> int:58return int(self.__get_value())5960@int_value.setter61def int_value(self, v: int) -> None:62self.__set_value(v)6364@property65def str_value(self) -> str:66return self.__get_value()6768@str_value.setter69def str_value(self, v: str) -> None:70self.__set_value(v)717273class LED(object):74def __init__(self, sys_path):75self.max_brightness = SysfsFile(sys_path / "max_brightness").int_value76self.__brightness = SysfsFile(sys_path / "brightness")7778@property79def brightness(self) -> int:80return self.__brightness.int_value8182@brightness.setter83def brightness(self, value: int) -> None:84self.__brightness.int_value = value858687class PowerSupply(object):88"""Represents Linux power_supply_class sysfs nodes."""8990def __init__(self, sys_path):91self._capacity = SysfsFile(sys_path / "capacity")92self._status = SysfsFile(sys_path / "status")93self._type = SysfsFile(sys_path / "type")9495@property96def capacity(self) -> int:97return self._capacity.int_value9899@property100def status(self) -> str:101return self._status.str_value102103@property104def type(self) -> str:105return self._type.str_value106107108@dataclasses.dataclass109class HidReadiness:110is_ready: bool = False111count: int = 0112113114class HIDIsReady(object):115"""116Companion class that binds to a kernel mechanism117and that allows to know when a uhid device is ready or not.118119See :meth:`is_ready` for details.120"""121122def __init__(self: "HIDIsReady", uhid: UHIDDevice) -> None:123self.uhid = uhid124125def is_ready(self: "HIDIsReady") -> HidReadiness:126"""127Overwrite in subclasses: should return True or False whether128the attached uhid device is ready or not.129"""130return HidReadiness()131132133class UdevHIDIsReady(HIDIsReady):134_pyudev_context: ClassVar[Optional[pyudev.Context]] = None135_pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None136_uhid_devices: ClassVar[Dict[int, HidReadiness]] = {}137138def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None:139super().__init__(uhid)140self._init_pyudev()141142@classmethod143def _init_pyudev(cls: Type["UdevHIDIsReady"]) -> None:144if cls._pyudev_context is None:145cls._pyudev_context = pyudev.Context()146cls._pyudev_monitor = pyudev.Monitor.from_netlink(cls._pyudev_context)147cls._pyudev_monitor.filter_by("hid")148cls._pyudev_monitor.start()149150UHIDDevice._append_fd_to_poll(151cls._pyudev_monitor.fileno(), cls._cls_udev_event_callback152)153154@classmethod155def _cls_udev_event_callback(cls: Type["UdevHIDIsReady"]) -> None:156if cls._pyudev_monitor is None:157return158event: pyudev.Device159for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None):160if event.action not in ["bind", "remove", "unbind"]:161return162163logger.debug(f"udev event: {event.action} -> {event}")164165id = int(event.sys_path.strip().split(".")[-1], 16)166167readiness = cls._uhid_devices.setdefault(id, HidReadiness())168169ready = event.action == "bind"170if not readiness.is_ready and ready:171readiness.count += 1172173readiness.is_ready = ready174175def is_ready(self: "UdevHIDIsReady") -> HidReadiness:176try:177return self._uhid_devices[self.uhid.hid_id]178except KeyError:179return HidReadiness()180181182class EvdevMatch(object):183def __init__(184self: "EvdevMatch",185*,186requires: List[Any] = [],187excludes: List[Any] = [],188req_properties: List[Any] = [],189excl_properties: List[Any] = [],190) -> None:191self.requires = requires192self.excludes = excludes193self.req_properties = req_properties194self.excl_properties = excl_properties195196def is_a_match(self: "EvdevMatch", evdev: libevdev.Device) -> bool:197for m in self.requires:198if not evdev.has(m):199return False200for m in self.excludes:201if evdev.has(m):202return False203for p in self.req_properties:204if not evdev.has_property(p):205return False206for p in self.excl_properties:207if evdev.has_property(p):208return False209return True210211212class EvdevDevice(object):213"""214Represents an Evdev node and its properties.215This is a stub for the libevdev devices, as they are relying on216uevent to get the data, saving us some ioctls to fetch the names217and properties.218"""219220def __init__(self: "EvdevDevice", sysfs: Path) -> None:221self.sysfs = sysfs222self.event_node: Any = None223self.libevdev: Optional[libevdev.Device] = None224225self.uevents = {}226# all of the interesting properties are stored in the input uevent, so in the parent227# so convert the uevent file of the parent input node into a dict228with open(sysfs.parent / "uevent") as f:229for line in f.readlines():230key, value = line.strip().split("=")231self.uevents[key] = value.strip('"')232233# we open all evdev nodes in order to not miss any event234self.open()235236@property237def name(self: "EvdevDevice") -> str:238assert "NAME" in self.uevents239240return self.uevents["NAME"]241242@property243def evdev(self: "EvdevDevice") -> Path:244return Path("/dev/input") / self.sysfs.name245246def matches_application(247self: "EvdevDevice", application: str, matches: Dict[str, EvdevMatch]248) -> bool:249if self.libevdev is None:250return False251252if application in matches:253return matches[application].is_a_match(self.libevdev)254255logger.error(256f"application '{application}' is unknown, please update/fix hid-tools"257)258assert False # hid-tools likely needs an update259260def open(self: "EvdevDevice") -> libevdev.Device:261self.event_node = open(self.evdev, "rb")262self.libevdev = libevdev.Device(self.event_node)263264assert self.libevdev.fd is not None265266fd = self.libevdev.fd.fileno()267flag = fcntl.fcntl(fd, fcntl.F_GETFD)268fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)269270return self.libevdev271272def close(self: "EvdevDevice") -> None:273if self.libevdev is not None and self.libevdev.fd is not None:274self.libevdev.fd.close()275self.libevdev = None276if self.event_node is not None:277self.event_node.close()278self.event_node = None279280281class BaseDevice(UHIDDevice):282# default _application_matches that matches nothing. This needs283# to be set in the subclasses to have get_evdev() working284_application_matches: Dict[str, EvdevMatch] = {}285286def __init__(287self,288name,289application,290rdesc_str: Optional[str] = None,291rdesc: Optional[Union[hid.ReportDescriptor, str, bytes]] = None,292input_info=None,293) -> None:294self._kernel_is_ready: HIDIsReady = UdevHIDIsReady(self)295if rdesc_str is None and rdesc is None:296raise Exception("Please provide at least a rdesc or rdesc_str")297super().__init__()298if name is None:299name = f"uhid gamepad test {self.__class__.__name__}"300if input_info is None:301input_info = (BusType.USB, 1, 2)302self.name = name303self.info = input_info304self.default_reportID = None305self.opened = False306self.started = False307self.application = application308self._input_nodes: Optional[list[EvdevDevice]] = None309if rdesc is None:310assert rdesc_str is not None311self.rdesc = hid.ReportDescriptor.from_human_descr(rdesc_str) # type: ignore312else:313self.rdesc = rdesc # type: ignore314315@property316def power_supply_class(self: "BaseDevice") -> Optional[PowerSupply]:317ps = self.walk_sysfs("power_supply", "power_supply/*")318if ps is None or len(ps) < 1:319return None320321return PowerSupply(ps[0])322323@property324def led_classes(self: "BaseDevice") -> List[LED]:325leds = self.walk_sysfs("led", "**/max_brightness")326if leds is None:327return []328329return [LED(led.parent) for led in leds]330331@property332def kernel_is_ready(self: "BaseDevice") -> bool:333return self._kernel_is_ready.is_ready().is_ready and self.started334335@property336def kernel_ready_count(self: "BaseDevice") -> int:337return self._kernel_is_ready.is_ready().count338339@property340def input_nodes(self: "BaseDevice") -> List[EvdevDevice]:341if self._input_nodes is not None:342return self._input_nodes343344if not self.kernel_is_ready or not self.started:345return []346347# Starting with kernel v6.16, an event is emitted when348# userspace opens a kernel device, and for some devices349# this translates into a SET_REPORT.350# Because EvdevDevice(path) opens every single evdev node351# we need to have a separate thread to process the incoming352# SET_REPORT or we end up having to wait for the kernel353# timeout of 5 seconds.354done = False355356def dispatch():357while not done:358self.dispatch(1)359360t = threading.Thread(target=dispatch)361t.start()362363self._input_nodes = [364EvdevDevice(path)365for path in self.walk_sysfs("input", "input/input*/event*")366]367done = True368t.join()369return self._input_nodes370371def match_evdev_rule(self, application, evdev):372"""Replace this in subclasses if the device has multiple reports373of the same type and we need to filter based on the actual evdev374node.375376returning True will append the corresponding report to377`self.input_nodes[type]`378returning False will ignore this report / type combination379for the device.380"""381return True382383def open(self):384self.opened = True385386def _close_all_opened_evdev(self):387if self._input_nodes is not None:388for e in self._input_nodes:389e.close()390391def __del__(self):392self._close_all_opened_evdev()393394def close(self):395self.opened = False396397def start(self, flags):398self.started = True399400def stop(self):401self.started = False402self._close_all_opened_evdev()403404def next_sync_events(self, application=None):405evdev = self.get_evdev(application)406if evdev is not None:407return list(evdev.events())408return []409410@property411def application_matches(self: "BaseDevice") -> Dict[str, EvdevMatch]:412return self._application_matches413414@application_matches.setter415def application_matches(self: "BaseDevice", data: Dict[str, EvdevMatch]) -> None:416self._application_matches = data417418def get_evdev(self, application=None):419if application is None:420application = self.application421422if len(self.input_nodes) == 0:423return None424425assert self._input_nodes is not None426427if len(self._input_nodes) == 1:428evdev = self._input_nodes[0]429if self.match_evdev_rule(application, evdev.libevdev):430return evdev.libevdev431else:432for _evdev in self._input_nodes:433if _evdev.matches_application(application, self.application_matches):434if self.match_evdev_rule(application, _evdev.libevdev):435return _evdev.libevdev436437def is_ready(self):438"""Returns whether a UHID device is ready. Can be overwritten in439subclasses to add extra conditions on when to consider a UHID440device ready. This can be:441442- we need to wait on different types of input devices to be ready443(Touch Screen and Pen for example)444- we need to have at least 4 LEDs present445(len(self.uhdev.leds_classes) == 4)446- or any other combinations"""447return self.kernel_is_ready448449450