base_device.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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. import dataclasses
  21. import fcntl
  22. import functools
  23. import libevdev
  24. import os
  25. import threading
  26. try:
  27. import pyudev
  28. except ImportError:
  29. raise ImportError("UHID is not supported due to missing pyudev dependency")
  30. import logging
  31. import hidtools.hid as hid
  32. from hidtools.uhid import UHIDDevice
  33. from hidtools.util import BusType
  34. from pathlib import Path
  35. from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union
  36. logger = logging.getLogger("hidtools.device.base_device")
  37. class SysfsFile(object):
  38. def __init__(self, path):
  39. self.path = path
  40. def __set_value(self, value):
  41. with open(self.path, "w") as f:
  42. return f.write(f"{value}\n")
  43. def __get_value(self):
  44. with open(self.path) as f:
  45. return f.read().strip()
  46. @property
  47. def int_value(self) -> int:
  48. return int(self.__get_value())
  49. @int_value.setter
  50. def int_value(self, v: int) -> None:
  51. self.__set_value(v)
  52. @property
  53. def str_value(self) -> str:
  54. return self.__get_value()
  55. @str_value.setter
  56. def str_value(self, v: str) -> None:
  57. self.__set_value(v)
  58. class LED(object):
  59. def __init__(self, sys_path):
  60. self.max_brightness = SysfsFile(sys_path / "max_brightness").int_value
  61. self.__brightness = SysfsFile(sys_path / "brightness")
  62. @property
  63. def brightness(self) -> int:
  64. return self.__brightness.int_value
  65. @brightness.setter
  66. def brightness(self, value: int) -> None:
  67. self.__brightness.int_value = value
  68. class PowerSupply(object):
  69. """Represents Linux power_supply_class sysfs nodes."""
  70. def __init__(self, sys_path):
  71. self._capacity = SysfsFile(sys_path / "capacity")
  72. self._status = SysfsFile(sys_path / "status")
  73. self._type = SysfsFile(sys_path / "type")
  74. @property
  75. def capacity(self) -> int:
  76. return self._capacity.int_value
  77. @property
  78. def status(self) -> str:
  79. return self._status.str_value
  80. @property
  81. def type(self) -> str:
  82. return self._type.str_value
  83. @dataclasses.dataclass
  84. class HidReadiness:
  85. is_ready: bool = False
  86. count: int = 0
  87. class HIDIsReady(object):
  88. """
  89. Companion class that binds to a kernel mechanism
  90. and that allows to know when a uhid device is ready or not.
  91. See :meth:`is_ready` for details.
  92. """
  93. def __init__(self: "HIDIsReady", uhid: UHIDDevice) -> None:
  94. self.uhid = uhid
  95. def is_ready(self: "HIDIsReady") -> HidReadiness:
  96. """
  97. Overwrite in subclasses: should return True or False whether
  98. the attached uhid device is ready or not.
  99. """
  100. return HidReadiness()
  101. class UdevHIDIsReady(HIDIsReady):
  102. _pyudev_context: ClassVar[Optional[pyudev.Context]] = None
  103. _pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None
  104. _uhid_devices: ClassVar[Dict[int, HidReadiness]] = {}
  105. def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None:
  106. super().__init__(uhid)
  107. self._init_pyudev()
  108. @classmethod
  109. def _init_pyudev(cls: Type["UdevHIDIsReady"]) -> None:
  110. if cls._pyudev_context is None:
  111. cls._pyudev_context = pyudev.Context()
  112. cls._pyudev_monitor = pyudev.Monitor.from_netlink(cls._pyudev_context)
  113. cls._pyudev_monitor.filter_by("hid")
  114. cls._pyudev_monitor.start()
  115. UHIDDevice._append_fd_to_poll(
  116. cls._pyudev_monitor.fileno(), cls._cls_udev_event_callback
  117. )
  118. @classmethod
  119. def _cls_udev_event_callback(cls: Type["UdevHIDIsReady"]) -> None:
  120. if cls._pyudev_monitor is None:
  121. return
  122. event: pyudev.Device
  123. for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None):
  124. if event.action not in ["bind", "remove", "unbind"]:
  125. return
  126. logger.debug(f"udev event: {event.action} -> {event}")
  127. id = int(event.sys_path.strip().split(".")[-1], 16)
  128. readiness = cls._uhid_devices.setdefault(id, HidReadiness())
  129. ready = event.action == "bind"
  130. if not readiness.is_ready and ready:
  131. readiness.count += 1
  132. readiness.is_ready = ready
  133. def is_ready(self: "UdevHIDIsReady") -> HidReadiness:
  134. try:
  135. return self._uhid_devices[self.uhid.hid_id]
  136. except KeyError:
  137. return HidReadiness()
  138. class EvdevMatch(object):
  139. def __init__(
  140. self: "EvdevMatch",
  141. *,
  142. requires: List[Any] = [],
  143. excludes: List[Any] = [],
  144. req_properties: List[Any] = [],
  145. excl_properties: List[Any] = [],
  146. ) -> None:
  147. self.requires = requires
  148. self.excludes = excludes
  149. self.req_properties = req_properties
  150. self.excl_properties = excl_properties
  151. def is_a_match(self: "EvdevMatch", evdev: libevdev.Device) -> bool:
  152. for m in self.requires:
  153. if not evdev.has(m):
  154. return False
  155. for m in self.excludes:
  156. if evdev.has(m):
  157. return False
  158. for p in self.req_properties:
  159. if not evdev.has_property(p):
  160. return False
  161. for p in self.excl_properties:
  162. if evdev.has_property(p):
  163. return False
  164. return True
  165. class EvdevDevice(object):
  166. """
  167. Represents an Evdev node and its properties.
  168. This is a stub for the libevdev devices, as they are relying on
  169. uevent to get the data, saving us some ioctls to fetch the names
  170. and properties.
  171. """
  172. def __init__(self: "EvdevDevice", sysfs: Path) -> None:
  173. self.sysfs = sysfs
  174. self.event_node: Any = None
  175. self.libevdev: Optional[libevdev.Device] = None
  176. self.uevents = {}
  177. # all of the interesting properties are stored in the input uevent, so in the parent
  178. # so convert the uevent file of the parent input node into a dict
  179. with open(sysfs.parent / "uevent") as f:
  180. for line in f.readlines():
  181. key, value = line.strip().split("=")
  182. self.uevents[key] = value.strip('"')
  183. # we open all evdev nodes in order to not miss any event
  184. self.open()
  185. @property
  186. def name(self: "EvdevDevice") -> str:
  187. assert "NAME" in self.uevents
  188. return self.uevents["NAME"]
  189. @property
  190. def evdev(self: "EvdevDevice") -> Path:
  191. return Path("/dev/input") / self.sysfs.name
  192. def matches_application(
  193. self: "EvdevDevice", application: str, matches: Dict[str, EvdevMatch]
  194. ) -> bool:
  195. if self.libevdev is None:
  196. return False
  197. if application in matches:
  198. return matches[application].is_a_match(self.libevdev)
  199. logger.error(
  200. f"application '{application}' is unknown, please update/fix hid-tools"
  201. )
  202. assert False # hid-tools likely needs an update
  203. def open(self: "EvdevDevice") -> libevdev.Device:
  204. self.event_node = open(self.evdev, "rb")
  205. self.libevdev = libevdev.Device(self.event_node)
  206. assert self.libevdev.fd is not None
  207. fd = self.libevdev.fd.fileno()
  208. flag = fcntl.fcntl(fd, fcntl.F_GETFD)
  209. fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
  210. return self.libevdev
  211. def close(self: "EvdevDevice") -> None:
  212. if self.libevdev is not None and self.libevdev.fd is not None:
  213. self.libevdev.fd.close()
  214. self.libevdev = None
  215. if self.event_node is not None:
  216. self.event_node.close()
  217. self.event_node = None
  218. class BaseDevice(UHIDDevice):
  219. # default _application_matches that matches nothing. This needs
  220. # to be set in the subclasses to have get_evdev() working
  221. _application_matches: Dict[str, EvdevMatch] = {}
  222. def __init__(
  223. self,
  224. name,
  225. application,
  226. rdesc_str: Optional[str] = None,
  227. rdesc: Optional[Union[hid.ReportDescriptor, str, bytes]] = None,
  228. input_info=None,
  229. ) -> None:
  230. self._kernel_is_ready: HIDIsReady = UdevHIDIsReady(self)
  231. if rdesc_str is None and rdesc is None:
  232. raise Exception("Please provide at least a rdesc or rdesc_str")
  233. super().__init__()
  234. if name is None:
  235. name = f"uhid gamepad test {self.__class__.__name__}"
  236. if input_info is None:
  237. input_info = (BusType.USB, 1, 2)
  238. self.name = name
  239. self.info = input_info
  240. self.default_reportID = None
  241. self.opened = False
  242. self.started = False
  243. self.application = application
  244. self._input_nodes: Optional[list[EvdevDevice]] = None
  245. if rdesc is None:
  246. assert rdesc_str is not None
  247. self.rdesc = hid.ReportDescriptor.from_human_descr(rdesc_str) # type: ignore
  248. else:
  249. self.rdesc = rdesc # type: ignore
  250. @property
  251. def power_supply_class(self: "BaseDevice") -> Optional[PowerSupply]:
  252. ps = self.walk_sysfs("power_supply", "power_supply/*")
  253. if ps is None or len(ps) < 1:
  254. return None
  255. return PowerSupply(ps[0])
  256. @property
  257. def led_classes(self: "BaseDevice") -> List[LED]:
  258. leds = self.walk_sysfs("led", "**/max_brightness")
  259. if leds is None:
  260. return []
  261. return [LED(led.parent) for led in leds]
  262. @property
  263. def kernel_is_ready(self: "BaseDevice") -> bool:
  264. return self._kernel_is_ready.is_ready().is_ready and self.started
  265. @property
  266. def kernel_ready_count(self: "BaseDevice") -> int:
  267. return self._kernel_is_ready.is_ready().count
  268. @property
  269. def input_nodes(self: "BaseDevice") -> List[EvdevDevice]:
  270. if self._input_nodes is not None:
  271. return self._input_nodes
  272. if not self.kernel_is_ready or not self.started:
  273. return []
  274. # Starting with kernel v6.16, an event is emitted when
  275. # userspace opens a kernel device, and for some devices
  276. # this translates into a SET_REPORT.
  277. # Because EvdevDevice(path) opens every single evdev node
  278. # we need to have a separate thread to process the incoming
  279. # SET_REPORT or we end up having to wait for the kernel
  280. # timeout of 5 seconds.
  281. done = False
  282. def dispatch():
  283. while not done:
  284. self.dispatch(1)
  285. t = threading.Thread(target=dispatch)
  286. t.start()
  287. self._input_nodes = [
  288. EvdevDevice(path)
  289. for path in self.walk_sysfs("input", "input/input*/event*")
  290. ]
  291. done = True
  292. t.join()
  293. return self._input_nodes
  294. def match_evdev_rule(self, application, evdev):
  295. """Replace this in subclasses if the device has multiple reports
  296. of the same type and we need to filter based on the actual evdev
  297. node.
  298. returning True will append the corresponding report to
  299. `self.input_nodes[type]`
  300. returning False will ignore this report / type combination
  301. for the device.
  302. """
  303. return True
  304. def open(self):
  305. self.opened = True
  306. def _close_all_opened_evdev(self):
  307. if self._input_nodes is not None:
  308. for e in self._input_nodes:
  309. e.close()
  310. def __del__(self):
  311. self._close_all_opened_evdev()
  312. def close(self):
  313. self.opened = False
  314. def start(self, flags):
  315. self.started = True
  316. def stop(self):
  317. self.started = False
  318. self._close_all_opened_evdev()
  319. def next_sync_events(self, application=None):
  320. evdev = self.get_evdev(application)
  321. if evdev is not None:
  322. return list(evdev.events())
  323. return []
  324. @property
  325. def application_matches(self: "BaseDevice") -> Dict[str, EvdevMatch]:
  326. return self._application_matches
  327. @application_matches.setter
  328. def application_matches(self: "BaseDevice", data: Dict[str, EvdevMatch]) -> None:
  329. self._application_matches = data
  330. def get_evdev(self, application=None):
  331. if application is None:
  332. application = self.application
  333. if len(self.input_nodes) == 0:
  334. return None
  335. assert self._input_nodes is not None
  336. if len(self._input_nodes) == 1:
  337. evdev = self._input_nodes[0]
  338. if self.match_evdev_rule(application, evdev.libevdev):
  339. return evdev.libevdev
  340. else:
  341. for _evdev in self._input_nodes:
  342. if _evdev.matches_application(application, self.application_matches):
  343. if self.match_evdev_rule(application, _evdev.libevdev):
  344. return _evdev.libevdev
  345. def is_ready(self):
  346. """Returns whether a UHID device is ready. Can be overwritten in
  347. subclasses to add extra conditions on when to consider a UHID
  348. device ready. This can be:
  349. - we need to wait on different types of input devices to be ready
  350. (Touch Screen and Pen for example)
  351. - we need to have at least 4 LEDs present
  352. (len(self.uhdev.leds_classes) == 4)
  353. - or any other combinations"""
  354. return self.kernel_is_ready