| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- #!/bin/env python3
- # SPDX-License-Identifier: GPL-2.0
- # -*- coding: utf-8 -*-
- #
- # Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
- # Copyright (c) 2017 Red Hat, Inc.
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- import dataclasses
- import fcntl
- import functools
- import libevdev
- import os
- import threading
- try:
- import pyudev
- except ImportError:
- raise ImportError("UHID is not supported due to missing pyudev dependency")
- import logging
- import hidtools.hid as hid
- from hidtools.uhid import UHIDDevice
- from hidtools.util import BusType
- from pathlib import Path
- from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union
- logger = logging.getLogger("hidtools.device.base_device")
- class SysfsFile(object):
- def __init__(self, path):
- self.path = path
- def __set_value(self, value):
- with open(self.path, "w") as f:
- return f.write(f"{value}\n")
- def __get_value(self):
- with open(self.path) as f:
- return f.read().strip()
- @property
- def int_value(self) -> int:
- return int(self.__get_value())
- @int_value.setter
- def int_value(self, v: int) -> None:
- self.__set_value(v)
- @property
- def str_value(self) -> str:
- return self.__get_value()
- @str_value.setter
- def str_value(self, v: str) -> None:
- self.__set_value(v)
- class LED(object):
- def __init__(self, sys_path):
- self.max_brightness = SysfsFile(sys_path / "max_brightness").int_value
- self.__brightness = SysfsFile(sys_path / "brightness")
- @property
- def brightness(self) -> int:
- return self.__brightness.int_value
- @brightness.setter
- def brightness(self, value: int) -> None:
- self.__brightness.int_value = value
- class PowerSupply(object):
- """Represents Linux power_supply_class sysfs nodes."""
- def __init__(self, sys_path):
- self._capacity = SysfsFile(sys_path / "capacity")
- self._status = SysfsFile(sys_path / "status")
- self._type = SysfsFile(sys_path / "type")
- @property
- def capacity(self) -> int:
- return self._capacity.int_value
- @property
- def status(self) -> str:
- return self._status.str_value
- @property
- def type(self) -> str:
- return self._type.str_value
- @dataclasses.dataclass
- class HidReadiness:
- is_ready: bool = False
- count: int = 0
- class HIDIsReady(object):
- """
- Companion class that binds to a kernel mechanism
- and that allows to know when a uhid device is ready or not.
- See :meth:`is_ready` for details.
- """
- def __init__(self: "HIDIsReady", uhid: UHIDDevice) -> None:
- self.uhid = uhid
- def is_ready(self: "HIDIsReady") -> HidReadiness:
- """
- Overwrite in subclasses: should return True or False whether
- the attached uhid device is ready or not.
- """
- return HidReadiness()
- class UdevHIDIsReady(HIDIsReady):
- _pyudev_context: ClassVar[Optional[pyudev.Context]] = None
- _pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None
- _uhid_devices: ClassVar[Dict[int, HidReadiness]] = {}
- def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None:
- super().__init__(uhid)
- self._init_pyudev()
- @classmethod
- def _init_pyudev(cls: Type["UdevHIDIsReady"]) -> None:
- if cls._pyudev_context is None:
- cls._pyudev_context = pyudev.Context()
- cls._pyudev_monitor = pyudev.Monitor.from_netlink(cls._pyudev_context)
- cls._pyudev_monitor.filter_by("hid")
- cls._pyudev_monitor.start()
- UHIDDevice._append_fd_to_poll(
- cls._pyudev_monitor.fileno(), cls._cls_udev_event_callback
- )
- @classmethod
- def _cls_udev_event_callback(cls: Type["UdevHIDIsReady"]) -> None:
- if cls._pyudev_monitor is None:
- return
- event: pyudev.Device
- for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None):
- if event.action not in ["bind", "remove", "unbind"]:
- return
- logger.debug(f"udev event: {event.action} -> {event}")
- id = int(event.sys_path.strip().split(".")[-1], 16)
- readiness = cls._uhid_devices.setdefault(id, HidReadiness())
- ready = event.action == "bind"
- if not readiness.is_ready and ready:
- readiness.count += 1
- readiness.is_ready = ready
- def is_ready(self: "UdevHIDIsReady") -> HidReadiness:
- try:
- return self._uhid_devices[self.uhid.hid_id]
- except KeyError:
- return HidReadiness()
- class EvdevMatch(object):
- def __init__(
- self: "EvdevMatch",
- *,
- requires: List[Any] = [],
- excludes: List[Any] = [],
- req_properties: List[Any] = [],
- excl_properties: List[Any] = [],
- ) -> None:
- self.requires = requires
- self.excludes = excludes
- self.req_properties = req_properties
- self.excl_properties = excl_properties
- def is_a_match(self: "EvdevMatch", evdev: libevdev.Device) -> bool:
- for m in self.requires:
- if not evdev.has(m):
- return False
- for m in self.excludes:
- if evdev.has(m):
- return False
- for p in self.req_properties:
- if not evdev.has_property(p):
- return False
- for p in self.excl_properties:
- if evdev.has_property(p):
- return False
- return True
- class EvdevDevice(object):
- """
- Represents an Evdev node and its properties.
- This is a stub for the libevdev devices, as they are relying on
- uevent to get the data, saving us some ioctls to fetch the names
- and properties.
- """
- def __init__(self: "EvdevDevice", sysfs: Path) -> None:
- self.sysfs = sysfs
- self.event_node: Any = None
- self.libevdev: Optional[libevdev.Device] = None
- self.uevents = {}
- # all of the interesting properties are stored in the input uevent, so in the parent
- # so convert the uevent file of the parent input node into a dict
- with open(sysfs.parent / "uevent") as f:
- for line in f.readlines():
- key, value = line.strip().split("=")
- self.uevents[key] = value.strip('"')
- # we open all evdev nodes in order to not miss any event
- self.open()
- @property
- def name(self: "EvdevDevice") -> str:
- assert "NAME" in self.uevents
- return self.uevents["NAME"]
- @property
- def evdev(self: "EvdevDevice") -> Path:
- return Path("/dev/input") / self.sysfs.name
- def matches_application(
- self: "EvdevDevice", application: str, matches: Dict[str, EvdevMatch]
- ) -> bool:
- if self.libevdev is None:
- return False
- if application in matches:
- return matches[application].is_a_match(self.libevdev)
- logger.error(
- f"application '{application}' is unknown, please update/fix hid-tools"
- )
- assert False # hid-tools likely needs an update
- def open(self: "EvdevDevice") -> libevdev.Device:
- self.event_node = open(self.evdev, "rb")
- self.libevdev = libevdev.Device(self.event_node)
- assert self.libevdev.fd is not None
- fd = self.libevdev.fd.fileno()
- flag = fcntl.fcntl(fd, fcntl.F_GETFD)
- fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
- return self.libevdev
- def close(self: "EvdevDevice") -> None:
- if self.libevdev is not None and self.libevdev.fd is not None:
- self.libevdev.fd.close()
- self.libevdev = None
- if self.event_node is not None:
- self.event_node.close()
- self.event_node = None
- class BaseDevice(UHIDDevice):
- # default _application_matches that matches nothing. This needs
- # to be set in the subclasses to have get_evdev() working
- _application_matches: Dict[str, EvdevMatch] = {}
- def __init__(
- self,
- name,
- application,
- rdesc_str: Optional[str] = None,
- rdesc: Optional[Union[hid.ReportDescriptor, str, bytes]] = None,
- input_info=None,
- ) -> None:
- self._kernel_is_ready: HIDIsReady = UdevHIDIsReady(self)
- if rdesc_str is None and rdesc is None:
- raise Exception("Please provide at least a rdesc or rdesc_str")
- super().__init__()
- if name is None:
- name = f"uhid gamepad test {self.__class__.__name__}"
- if input_info is None:
- input_info = (BusType.USB, 1, 2)
- self.name = name
- self.info = input_info
- self.default_reportID = None
- self.opened = False
- self.started = False
- self.application = application
- self._input_nodes: Optional[list[EvdevDevice]] = None
- if rdesc is None:
- assert rdesc_str is not None
- self.rdesc = hid.ReportDescriptor.from_human_descr(rdesc_str) # type: ignore
- else:
- self.rdesc = rdesc # type: ignore
- @property
- def power_supply_class(self: "BaseDevice") -> Optional[PowerSupply]:
- ps = self.walk_sysfs("power_supply", "power_supply/*")
- if ps is None or len(ps) < 1:
- return None
- return PowerSupply(ps[0])
- @property
- def led_classes(self: "BaseDevice") -> List[LED]:
- leds = self.walk_sysfs("led", "**/max_brightness")
- if leds is None:
- return []
- return [LED(led.parent) for led in leds]
- @property
- def kernel_is_ready(self: "BaseDevice") -> bool:
- return self._kernel_is_ready.is_ready().is_ready and self.started
- @property
- def kernel_ready_count(self: "BaseDevice") -> int:
- return self._kernel_is_ready.is_ready().count
- @property
- def input_nodes(self: "BaseDevice") -> List[EvdevDevice]:
- if self._input_nodes is not None:
- return self._input_nodes
- if not self.kernel_is_ready or not self.started:
- return []
- # Starting with kernel v6.16, an event is emitted when
- # userspace opens a kernel device, and for some devices
- # this translates into a SET_REPORT.
- # Because EvdevDevice(path) opens every single evdev node
- # we need to have a separate thread to process the incoming
- # SET_REPORT or we end up having to wait for the kernel
- # timeout of 5 seconds.
- done = False
- def dispatch():
- while not done:
- self.dispatch(1)
- t = threading.Thread(target=dispatch)
- t.start()
- self._input_nodes = [
- EvdevDevice(path)
- for path in self.walk_sysfs("input", "input/input*/event*")
- ]
- done = True
- t.join()
- return self._input_nodes
- def match_evdev_rule(self, application, evdev):
- """Replace this in subclasses if the device has multiple reports
- of the same type and we need to filter based on the actual evdev
- node.
- returning True will append the corresponding report to
- `self.input_nodes[type]`
- returning False will ignore this report / type combination
- for the device.
- """
- return True
- def open(self):
- self.opened = True
- def _close_all_opened_evdev(self):
- if self._input_nodes is not None:
- for e in self._input_nodes:
- e.close()
- def __del__(self):
- self._close_all_opened_evdev()
- def close(self):
- self.opened = False
- def start(self, flags):
- self.started = True
- def stop(self):
- self.started = False
- self._close_all_opened_evdev()
- def next_sync_events(self, application=None):
- evdev = self.get_evdev(application)
- if evdev is not None:
- return list(evdev.events())
- return []
- @property
- def application_matches(self: "BaseDevice") -> Dict[str, EvdevMatch]:
- return self._application_matches
- @application_matches.setter
- def application_matches(self: "BaseDevice", data: Dict[str, EvdevMatch]) -> None:
- self._application_matches = data
- def get_evdev(self, application=None):
- if application is None:
- application = self.application
- if len(self.input_nodes) == 0:
- return None
- assert self._input_nodes is not None
- if len(self._input_nodes) == 1:
- evdev = self._input_nodes[0]
- if self.match_evdev_rule(application, evdev.libevdev):
- return evdev.libevdev
- else:
- for _evdev in self._input_nodes:
- if _evdev.matches_application(application, self.application_matches):
- if self.match_evdev_rule(application, _evdev.libevdev):
- return _evdev.libevdev
- def is_ready(self):
- """Returns whether a UHID device is ready. Can be overwritten in
- subclasses to add extra conditions on when to consider a UHID
- device ready. This can be:
- - we need to wait on different types of input devices to be ready
- (Touch Screen and Pen for example)
- - we need to have at least 4 LEDs present
- (len(self.uhdev.leds_classes) == 4)
- - or any other combinations"""
- return self.kernel_is_ready
|