base_device.py 13 KB

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