Coverage for linuxpy/led.py: 40%
108 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-18 17:55 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-18 17:55 +0200
1#
2# This file is part of the linuxpy project
3#
4# Copyright (c) 2024 Tiago Coutinho
5# Distributed under the GPLv3 license. See LICENSE for more info.
7"""
8Human friendly interface to linux LED subsystem.
10```python
11from linuxpy.led import find
13caps_lock = find(function="capslock")
14print(caps_lock.brightness)
15caps_lock.brightness = caps_lock.max_brightness
16```
18### ULED
20```python
21from linuxpy.led import LED, ULED
23with ULED("uled::simulation") as uled:
24 led = LED.from_name("uled::simulation")
25 print()
26```
28Streaming example:
30```python
31from time import monotonic
33from linuxpy.led import ULED
35with ULED("uled::simulation", max_brightness=100) as uled:
36 # Open another terminal and type:
37 # echo 10 > /sys/class/leds/uled::simulation/brightness
38 for brightness in uled.stream():
39 print(f"[{monotonic():.6f}]: {brightness}")
40```
41"""
43import pathlib
44import select
45import struct
47from linuxpy.device import BaseDevice
48from linuxpy.sysfs import LED_PATH, Attr, Device, Int
49from linuxpy.types import Callable, Iterable, Optional, Union
50from linuxpy.util import make_find
52# https://www.kernel.org/doc/html/latest/leds/leds-class.html
55def decode_trigger(text):
56 start = text.index("[")
57 end = text.index("]")
58 return text[start + 1 : end]
61def decode_triggers(text):
62 return [i[1:-1] if i.startswith("[") else i for i in text.split()]
65def decode_brightness(data: bytes) -> int:
66 return int.from_bytes(data, "little")
69def split_name(fname):
70 if nb_colons := fname.count(":"):
71 parts = fname.split(":")
72 return ("", *parts) if nb_colons == 1 else parts
73 return "", "", fname
76class LED(Device):
77 """Main LED class"""
79 _devicename = None
80 _color = None
81 _function = None
83 brightness = Int()
84 max_brightness = Int()
85 trigger = Attr(decode=decode_trigger)
86 triggers = Attr("trigger", decode=decode_triggers)
88 def __repr__(self):
89 klass_name = type(self).__name__
90 return f"{klass_name}({self.name})"
92 def _build_name(self):
93 devicename, color, function = split_name(self.syspath.stem)
94 self._devicename = devicename
95 self._color = color
96 self._function = function
98 @classmethod
99 def from_name(cls, name) -> "LED":
100 """Create a LED from the name that corresponds /sys/class/leds/<name>"""
101 return cls.from_syspath(LED_PATH / name)
103 @property
104 def name(self) -> str:
105 """LED name from the naming <devicename:color:function>"""
106 return self.syspath.stem
108 @property
109 def devicename(self) -> str:
110 """LED device name from the naming <devicename:color:function>"""
111 if self._devicename is None:
112 self._build_name()
113 return self._devicename
115 @property
116 def color(self) -> str:
117 """LED color from the naming <devicename:color:function>"""
118 if self._color is None:
119 self._build_name()
120 return self._color
122 @property
123 def function(self) -> str:
124 """LED function from the naming <devicename:color:function>"""
125 if self._function is None:
126 self._build_name()
127 return self._function
129 @property
130 def trigger_enabled(self) -> bool:
131 """Tells if the LED trigger is enabled"""
132 return self.trigger != "none"
134 @property
135 def brightness_events_path(self):
136 return self.syspath / "brightness_hw_changed"
138 def __iter__(self):
139 if not self.brightness_events_path.exists():
140 raise ValueError("This LED does not support hardware events")
141 with self.brightness_events_path.open("rb", buffering=0) as fobj:
142 yield decode_brightness(fobj.read(4))
143 fobj.seek(0)
146class ULED(BaseDevice):
147 """
148 LED class for th userspace LED. This can be useful for testing triggers and
149 can also be used to implement virtual LEDs.
150 """
152 PATH = "/dev/uleds"
154 def __init__(self, name: str, max_brightness: int = 1, **kwargs):
155 self.name = name
156 self.max_brightness = max_brightness
157 self._brightness = None
158 super().__init__(self.PATH, **kwargs)
160 def _on_open(self):
161 data = struct.pack("64si", self.name.encode(), self.max_brightness)
162 self._fobj.write(data)
163 self._brightness = self.brightness
165 def read(self) -> bytes:
166 """Read new brightness. Blocks until brightness changes"""
167 if not self.is_blocking:
168 select.select((self,), (), ())
169 return self.raw_read()
171 def raw_read(self) -> bytes:
172 return self._fobj.read()
174 @property
175 def brightness(self) -> int:
176 """Read new brightness. Blocks until brightness changes"""
177 data = self.raw_read()
178 if data is not None:
179 self._brightness = decode_brightness(data)
180 return self._brightness
182 def stream(self) -> Iterable[int]:
183 """Infinite stream of brightness change events"""
184 while True:
185 data = self.read()
186 self._brightness = decode_brightness(data)
187 yield self._brightness
190def iter_device_paths() -> Iterable[pathlib.Path]:
191 """Iterable of all LED syspaths (/sys/class/leds)"""
192 yield from LED_PATH.iterdir()
195def iter_devices() -> Iterable[LED]:
196 """Iterable over all LED devices"""
197 return (LED.from_syspath(path) for path in iter_device_paths())
200_find = make_find(iter_devices, needs_open=False)
203def find(find_all: bool = False, custom_match: Optional[Callable] = None, **kwargs) -> Union[LED, Iterable[LED], None]:
204 """
205 If find_all is False:
207 Find a LED follwing the criteria matched by custom_match and kwargs.
208 If no LED is found matching the criteria it returns None.
209 Default is to return a random LED device.
211 If find_all is True:
213 The result is an iterator.
214 Find all LEDs that match the criteria custom_match and kwargs.
215 If no LED is found matching the criteria it returns an empty iterator.
216 Default is to return an iterator over all LEDs found on the system.
217 """
218 return _find(find_all, custom_match, **kwargs)
221def main():
222 for dev in sorted(iter_devices(), key=lambda dev: dev.syspath.stem):
223 print(f"{dev.syspath.stem:32} {dev.trigger:16} {dev.brightness:4}")
226if __name__ == "__main__": 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true
227 main()