Coverage for linuxpy/led.py: 40%

108 statements  

« 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. 

6 

7""" 

8Human friendly interface to linux LED subsystem. 

9 

10```python 

11from linuxpy.led import find 

12 

13caps_lock = find(function="capslock") 

14print(caps_lock.brightness) 

15caps_lock.brightness = caps_lock.max_brightness 

16``` 

17 

18### ULED 

19 

20```python 

21from linuxpy.led import LED, ULED 

22 

23with ULED("uled::simulation") as uled: 

24 led = LED.from_name("uled::simulation") 

25 print() 

26``` 

27 

28Streaming example: 

29 

30```python 

31from time import monotonic 

32 

33from linuxpy.led import ULED 

34 

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""" 

42 

43import pathlib 

44import select 

45import struct 

46 

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 

51 

52# https://www.kernel.org/doc/html/latest/leds/leds-class.html 

53 

54 

55def decode_trigger(text): 

56 start = text.index("[") 

57 end = text.index("]") 

58 return text[start + 1 : end] 

59 

60 

61def decode_triggers(text): 

62 return [i[1:-1] if i.startswith("[") else i for i in text.split()] 

63 

64 

65def decode_brightness(data: bytes) -> int: 

66 return int.from_bytes(data, "little") 

67 

68 

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 

74 

75 

76class LED(Device): 

77 """Main LED class""" 

78 

79 _devicename = None 

80 _color = None 

81 _function = None 

82 

83 brightness = Int() 

84 max_brightness = Int() 

85 trigger = Attr(decode=decode_trigger) 

86 triggers = Attr("trigger", decode=decode_triggers) 

87 

88 def __repr__(self): 

89 klass_name = type(self).__name__ 

90 return f"{klass_name}({self.name})" 

91 

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 

97 

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) 

102 

103 @property 

104 def name(self) -> str: 

105 """LED name from the naming <devicename:color:function>""" 

106 return self.syspath.stem 

107 

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 

114 

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 

121 

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 

128 

129 @property 

130 def trigger_enabled(self) -> bool: 

131 """Tells if the LED trigger is enabled""" 

132 return self.trigger != "none" 

133 

134 @property 

135 def brightness_events_path(self): 

136 return self.syspath / "brightness_hw_changed" 

137 

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) 

144 

145 

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 """ 

151 

152 PATH = "/dev/uleds" 

153 

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) 

159 

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 

164 

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() 

170 

171 def raw_read(self) -> bytes: 

172 return self._fobj.read() 

173 

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 

181 

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 

188 

189 

190def iter_device_paths() -> Iterable[pathlib.Path]: 

191 """Iterable of all LED syspaths (/sys/class/leds)""" 

192 yield from LED_PATH.iterdir() 

193 

194 

195def iter_devices() -> Iterable[LED]: 

196 """Iterable over all LED devices""" 

197 return (LED.from_syspath(path) for path in iter_device_paths()) 

198 

199 

200_find = make_find(iter_devices, needs_open=False) 

201 

202 

203def find(find_all: bool = False, custom_match: Optional[Callable] = None, **kwargs) -> Union[LED, Iterable[LED], None]: 

204 """ 

205 If find_all is False: 

206 

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. 

210 

211 If find_all is True: 

212 

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) 

219 

220 

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}") 

224 

225 

226if __name__ == "__main__": 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true

227 main()