2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8 -*-
5 # Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6 # Copyright (c) 2017 Red Hat, Inc.
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.
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.
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/>.
29 raise ImportError("UHID is not supported due to missing pyudev dependency")
33 import hidtools
.hid
as hid
34 from hidtools
.uhid
import UHIDDevice
35 from hidtools
.util
import BusType
37 from pathlib
import Path
38 from typing
import Any
, ClassVar
, Dict
, List
, Optional
, Tuple
, Type
, Union
40 logger
= logging
.getLogger("hidtools.device.base_device")
43 class SysfsFile(object):
44 def __init__(self
, path
):
47 def __set_value(self
, value
):
48 with
open(self
.path
, "w") as f
:
49 return f
.write(f
"{value}\n")
51 def __get_value(self
):
52 with
open(self
.path
) as f
:
53 return f
.read().strip()
56 def int_value(self
) -> int:
57 return int(self
.__get
_value
())
60 def int_value(self
, v
: int) -> None:
64 def str_value(self
) -> str:
65 return self
.__get
_value
()
68 def str_value(self
, v
: str) -> None:
73 def __init__(self
, sys_path
):
74 self
.max_brightness
= SysfsFile(sys_path
/ "max_brightness").int_value
75 self
.__brightness
= SysfsFile(sys_path
/ "brightness")
78 def brightness(self
) -> int:
79 return self
.__brightness
.int_value
82 def brightness(self
, value
: int) -> None:
83 self
.__brightness
.int_value
= value
86 class PowerSupply(object):
87 """Represents Linux power_supply_class sysfs nodes."""
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")
95 def capacity(self
) -> int:
96 return self
._capacity
.int_value
99 def status(self
) -> str:
100 return self
._status
.str_value
103 def type(self
) -> str:
104 return self
._type
.str_value
107 class HIDIsReady(object):
109 Companion class that binds to a kernel mechanism
110 and that allows to know when a uhid device is ready or not.
112 See :meth:`is_ready` for details.
115 def __init__(self
: "HIDIsReady", uhid
: UHIDDevice
) -> None:
118 def is_ready(self
: "HIDIsReady") -> bool:
120 Overwrite in subclasses: should return True or False whether
121 the attached uhid device is ready or not.
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, Tuple
[bool, int]]] = {}
131 def __init__(self
: "UdevHIDIsReady", uhid
: UHIDDevice
) -> None:
132 super().__init
__(uhid
)
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()
143 UHIDDevice
._append
_fd
_to
_poll
(
144 cls
._pyudev
_monitor
.fileno(), cls
._cls
_udev
_event
_callback
148 def _cls_udev_event_callback(cls
: Type
["UdevHIDIsReady"]) -> None:
149 if cls
._pyudev
_monitor
is None:
152 for event
in iter(functools
.partial(cls
._pyudev
_monitor
.poll
, 0.02), None):
153 if event
.action
not in ["bind", "remove", "unbind"]:
156 logger
.debug(f
"udev event: {event.action} -> {event}")
158 id = int(event
.sys_path
.strip().split(".")[-1], 16)
160 device_ready
, count
= cls
._uhid
_devices
.get(id, (False, 0))
162 ready
= event
.action
== "bind"
163 if not device_ready
and ready
:
165 cls
._uhid
_devices
[id] = (ready
, count
)
167 def is_ready(self
: "UdevHIDIsReady") -> Tuple
[bool, int]:
169 return self
._uhid
_devices
[self
.uhid
.hid_id
]
174 class EvdevMatch(object):
178 requires
: List
[Any
] = [],
179 excludes
: List
[Any
] = [],
180 req_properties
: List
[Any
] = [],
181 excl_properties
: List
[Any
] = [],
183 self
.requires
= requires
184 self
.excludes
= excludes
185 self
.req_properties
= req_properties
186 self
.excl_properties
= excl_properties
188 def is_a_match(self
: "EvdevMatch", evdev
: libevdev
.Device
) -> bool:
189 for m
in self
.requires
:
192 for m
in self
.excludes
:
195 for p
in self
.req_properties
:
196 if not evdev
.has_property(p
):
198 for p
in self
.excl_properties
:
199 if evdev
.has_property(p
):
204 class EvdevDevice(object):
206 Represents an Evdev node and its properties.
207 This is a stub for the libevdev devices, as they are relying on
208 uevent to get the data, saving us some ioctls to fetch the names
212 def __init__(self
: "EvdevDevice", sysfs
: Path
) -> None:
214 self
.event_node
: Any
= None
215 self
.libevdev
: Optional
[libevdev
.Device
] = None
218 # all of the interesting properties are stored in the input uevent, so in the parent
219 # so convert the uevent file of the parent input node into a dict
220 with
open(sysfs
.parent
/ "uevent") as f
:
221 for line
in f
.readlines():
222 key
, value
= line
.strip().split("=")
223 self
.uevents
[key
] = value
.strip('"')
225 # we open all evdev nodes in order to not miss any event
229 def name(self
: "EvdevDevice") -> str:
230 assert "NAME" in self
.uevents
232 return self
.uevents
["NAME"]
235 def evdev(self
: "EvdevDevice") -> Path
:
236 return Path("/dev/input") / self
.sysfs
.name
238 def matches_application(
239 self
: "EvdevDevice", application
: str, matches
: Dict
[str, EvdevMatch
]
241 if self
.libevdev
is None:
244 if application
in matches
:
245 return matches
[application
].is_a_match(self
.libevdev
)
248 f
"application '{application}' is unknown, please update/fix hid-tools"
250 assert False # hid-tools likely needs an update
252 def open(self
: "EvdevDevice") -> libevdev
.Device
:
253 self
.event_node
= open(self
.evdev
, "rb")
254 self
.libevdev
= libevdev
.Device(self
.event_node
)
256 assert self
.libevdev
.fd
is not None
258 fd
= self
.libevdev
.fd
.fileno()
259 flag
= fcntl
.fcntl(fd
, fcntl
.F_GETFD
)
260 fcntl
.fcntl(fd
, fcntl
.F_SETFL
, flag | os
.O_NONBLOCK
)
264 def close(self
: "EvdevDevice") -> None:
265 if self
.libevdev
is not None and self
.libevdev
.fd
is not None:
266 self
.libevdev
.fd
.close()
268 if self
.event_node
is not None:
269 self
.event_node
.close()
270 self
.event_node
= None
273 class BaseDevice(UHIDDevice
):
274 # default _application_matches that matches nothing. This needs
275 # to be set in the subclasses to have get_evdev() working
276 _application_matches
: Dict
[str, EvdevMatch
] = {}
282 rdesc_str
: Optional
[str] = None,
283 rdesc
: Optional
[Union
[hid
.ReportDescriptor
, str, bytes
]] = None,
286 self
._kernel
_is
_ready
: HIDIsReady
= UdevHIDIsReady(self
)
287 if rdesc_str
is None and rdesc
is None:
288 raise Exception("Please provide at least a rdesc or rdesc_str")
291 name
= f
"uhid gamepad test {self.__class__.__name__}"
292 if input_info
is None:
293 input_info
= (BusType
.USB
, 1, 2)
295 self
.info
= input_info
296 self
.default_reportID
= None
299 self
.application
= application
300 self
._input
_nodes
: Optional
[list[EvdevDevice
]] = None
302 assert rdesc_str
is not None
303 self
.rdesc
= hid
.ReportDescriptor
.from_human_descr(rdesc_str
) # type: ignore
305 self
.rdesc
= rdesc
# type: ignore
308 def power_supply_class(self
: "BaseDevice") -> Optional
[PowerSupply
]:
309 ps
= self
.walk_sysfs("power_supply", "power_supply/*")
310 if ps
is None or len(ps
) < 1:
313 return PowerSupply(ps
[0])
316 def led_classes(self
: "BaseDevice") -> List
[LED
]:
317 leds
= self
.walk_sysfs("led", "**/max_brightness")
321 return [LED(led
.parent
) for led
in leds
]
324 def kernel_is_ready(self
: "BaseDevice") -> bool:
325 return self
._kernel
_is
_ready
.is_ready()[0] and self
.started
328 def kernel_ready_count(self
: "BaseDevice") -> int:
329 return self
._kernel
_is
_ready
.is_ready()[1]
332 def input_nodes(self
: "BaseDevice") -> List
[EvdevDevice
]:
333 if self
._input
_nodes
is not None:
334 return self
._input
_nodes
336 if not self
.kernel_is_ready
or not self
.started
:
339 self
._input
_nodes
= [
341 for path
in self
.walk_sysfs("input", "input/input*/event*")
343 return self
._input
_nodes
345 def match_evdev_rule(self
, application
, evdev
):
346 """Replace this in subclasses if the device has multiple reports
347 of the same type and we need to filter based on the actual evdev
350 returning True will append the corresponding report to
351 `self.input_nodes[type]`
352 returning False will ignore this report / type combination
360 def _close_all_opened_evdev(self
):
361 if self
._input
_nodes
is not None:
362 for e
in self
._input
_nodes
:
366 self
._close
_all
_opened
_evdev
()
371 def start(self
, flags
):
376 self
._close
_all
_opened
_evdev
()
378 def next_sync_events(self
, application
=None):
379 evdev
= self
.get_evdev(application
)
380 if evdev
is not None:
381 return list(evdev
.events())
385 def application_matches(self
: "BaseDevice") -> Dict
[str, EvdevMatch
]:
386 return self
._application
_matches
388 @application_matches.setter
389 def application_matches(self
: "BaseDevice", data
: Dict
[str, EvdevMatch
]) -> None:
390 self
._application
_matches
= data
392 def get_evdev(self
, application
=None):
393 if application
is None:
394 application
= self
.application
396 if len(self
.input_nodes
) == 0:
399 assert self
._input
_nodes
is not None
401 if len(self
._input
_nodes
) == 1:
402 evdev
= self
._input
_nodes
[0]
403 if self
.match_evdev_rule(application
, evdev
.libevdev
):
404 return evdev
.libevdev
406 for _evdev
in self
._input
_nodes
:
407 if _evdev
.matches_application(application
, self
.application_matches
):
408 if self
.match_evdev_rule(application
, _evdev
.libevdev
):
409 return _evdev
.libevdev
412 """Returns whether a UHID device is ready. Can be overwritten in
413 subclasses to add extra conditions on when to consider a UHID
414 device ready. This can be:
416 - we need to wait on different types of input devices to be ready
417 (Touch Screen and Pen for example)
418 - we need to have at least 4 LEDs present
419 (len(self.uhdev.leds_classes) == 4)
420 - or any other combinations"""
421 return self
.kernel_is_ready