-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32 from tiagocoutinho/led
Add led support
- Loading branch information
Showing
9 changed files
with
404 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# 💡 Led API | ||
|
||
::: linuxpy.led |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# 💡 Led | ||
|
||
Human friendly interface to linux led handling. | ||
|
||
Without further ado: | ||
|
||
<div class="termy" data-ty-macos> | ||
<span data-ty="input" data-ty-prompt="$">python</span> | ||
<span data-ty="input" data-ty-prompt=">>>">from linuxpy.led import find</span> | ||
<span data-ty="input" data-ty-prompt=">>>">caps_lock = find(function="capslock")</span> | ||
<span data-ty="input" data-ty-prompt=">>>">print(caps_lock.max_brightness)</span> | ||
<span data-ty>1</span> | ||
<span data-ty="input" data-ty-prompt=">>>">print(caps_lock.brightness)</span> | ||
<span data-ty>0</span> | ||
<span data-ty="input" data-ty-prompt=">>>">caps_lock.brightness = 1</span> | ||
<span data-ty="input" data-ty-prompt=">>>">print(caps_lock.brightness)</span> | ||
<span data-ty>1</span> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import sys | ||
|
||
if len(sys.argv) != 2: | ||
print("Requires <device-name> argument", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
import logging | ||
|
||
from linuxpy.led import ULED | ||
|
||
logging.basicConfig(level="WARNING", format="%(asctime)-15s: %(message)s") | ||
|
||
try: | ||
with ULED(sys.argv[1], max_brightness=100) as uled: | ||
for brightness in uled.stream(): | ||
logging.warning("%d", brightness) | ||
except KeyboardInterrupt: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# | ||
# This file is part of the linuxpy project | ||
# | ||
# Copyright (c) 2024 Tiago Coutinho | ||
# Distributed under the GPLv3 license. See LICENSE for more info. | ||
|
||
""" | ||
Human friendly interface to linux LED subsystem. | ||
```python | ||
from linuxpy.led import find | ||
caps_lock = find(function="capslock") | ||
print(caps_lock.brightness) | ||
caps_lock.brightness = caps_lock.max_brightness | ||
``` | ||
### ULED | ||
```python | ||
from linuxpy.led import LED, ULED | ||
with ULED("uled::simulation") as uled: | ||
led = LED.from_name("uled::simulation") | ||
print() | ||
``` | ||
Streaming example: | ||
```python | ||
from time import monotonic | ||
from linuxpy.led import ULED | ||
with ULED("uled::simulation", max_brightness=100) as uled: | ||
# Open another terminal and type: | ||
# echo 10 > /sys/class/leds/uled::simulation/brightness | ||
for brightness in uled.stream(): | ||
print(f"[{monotonic():.6f}]: {brightness}") | ||
``` | ||
""" | ||
|
||
import pathlib | ||
import select | ||
import struct | ||
|
||
from linuxpy.device import BaseDevice | ||
from linuxpy.sysfs import LED_PATH, Attr, Device, Int | ||
from linuxpy.types import Callable, Iterable, Optional, Union | ||
from linuxpy.util import make_find | ||
|
||
# https://www.kernel.org/doc/html/latest/leds/leds-class.html | ||
|
||
|
||
def decode_trigger(text): | ||
start = text.index("[") | ||
end = text.index("]") | ||
return text[start + 1 : end] | ||
|
||
|
||
def decode_triggers(text): | ||
return [i[1:-1] if i.startswith("[") else i for i in text.split()] | ||
|
||
|
||
class LED(Device): | ||
"""Main LED class""" | ||
|
||
_devicename = None | ||
_color = None | ||
_function = None | ||
|
||
brightness = Int() | ||
max_brightness = Int() | ||
trigger = Attr(decode=decode_trigger) | ||
triggers = Attr("trigger", decode=decode_triggers) | ||
|
||
def __repr__(self): | ||
klass_name = type(self).__name__ | ||
return f"{klass_name}({self.name})" | ||
|
||
def _build_name(self): | ||
fname = self.syspath.stem | ||
if ":" in fname: | ||
if fname.count(":") == 1: | ||
devicename = "" | ||
color, function = fname.split(":") | ||
else: | ||
devicename, color, function = fname.split(":") | ||
else: | ||
devicename, color, function = "", "", fname | ||
self._devicename = devicename | ||
self._color = color | ||
self._function = function | ||
|
||
@classmethod | ||
def from_name(cls, name) -> "LED": | ||
"""Create a LED from the name that corresponds /sys/class/leds/<name>""" | ||
return cls.from_syspath(LED_PATH / name) | ||
|
||
@property | ||
def name(self) -> str: | ||
"""LED name from the naming <devicename:color:function>""" | ||
return self.syspath.stem | ||
|
||
@property | ||
def devicename(self) -> str: | ||
"""LED device name from the naming <devicename:color:function>""" | ||
if self._devicename is None: | ||
self._build_name() | ||
return self._devicename | ||
|
||
@property | ||
def color(self) -> str: | ||
"""LED color from the naming <devicename:color:function>""" | ||
if self._color is None: | ||
self._build_name() | ||
return self._color | ||
|
||
@property | ||
def function(self) -> str: | ||
"""LED function from the naming <devicename:color:function>""" | ||
if self._function is None: | ||
self._build_name() | ||
return self._function | ||
|
||
@property | ||
def trigger_enabled(self) -> bool: | ||
"""Tells if the LED trigger is enabled""" | ||
return self.trigger != "none" | ||
|
||
|
||
class ULED(BaseDevice): | ||
""" | ||
LED class for th userspace LED. This can be useful for testing triggers and | ||
can also be used to implement virtual LEDs. | ||
""" | ||
|
||
PATH = "/dev/uleds" | ||
|
||
def __init__(self, name: str, max_brightness: int = 1, **kwargs): | ||
self.name = name | ||
self.max_brightness = max_brightness | ||
self._brightness = None | ||
super().__init__(self.PATH, **kwargs) | ||
|
||
@staticmethod | ||
def decode(data: bytes) -> int: | ||
return int.from_bytes(data, "little") | ||
|
||
def _on_open(self): | ||
data = struct.pack("64si", self.name.encode(), self.max_brightness) | ||
self._fobj.write(data) | ||
self._brightness = self.brightness | ||
|
||
def read(self) -> bytes: | ||
"""Read new brightness. Blocks until brightness changes""" | ||
if not self.is_blocking: | ||
select.select((self,), (), ()) | ||
return self.raw_read() | ||
|
||
def raw_read(self) -> bytes: | ||
return self._fobj.read() | ||
|
||
@property | ||
def brightness(self) -> int: | ||
"""Read new brightness. Blocks until brightness changes""" | ||
data = self.raw_read() | ||
if data is not None: | ||
self._brightness = self.decode(data) | ||
return self._brightness | ||
|
||
def stream(self) -> Iterable[int]: | ||
"""Infinite stream of brightness change events""" | ||
while True: | ||
data = self.read() | ||
self._brightness = self.decode(data) | ||
yield self._brightness | ||
|
||
|
||
def iter_device_paths() -> Iterable[pathlib.Path]: | ||
"""Iterable of all LED syspaths (/sys/class/leds)""" | ||
yield from LED_PATH.iterdir() | ||
|
||
|
||
def iter_devices() -> Iterable[LED]: | ||
"""Iterable over all LED devices""" | ||
return (LED.from_syspath(path) for path in iter_device_paths()) | ||
|
||
|
||
_find = make_find(iter_devices) | ||
|
||
|
||
def find(find_all: bool = False, custom_match: Optional[Callable] = None, **kwargs) -> Union[LED, Iterable[LED], None]: | ||
""" | ||
If find_all is False: | ||
Find a LED follwing the criteria matched by custom_match and kwargs. | ||
If no LED is found matching the criteria it returns None. | ||
Default is to return a random LED device. | ||
If find_all is True: | ||
The result is an iterator. | ||
Find all LEDs that match the criteria custom_match and kwargs. | ||
If no LED is found matching the criteria it returns an empty iterator. | ||
Default is to return an iterator over all LEDs found on the system. | ||
""" | ||
return _find(find_all, custom_match, **kwargs) | ||
|
||
|
||
def main(): | ||
for dev in sorted(iter_devices(), key=lambda dev: dev.syspath.stem): | ||
print(f"{dev.syspath.stem:32} {dev.trigger:16} {dev.brightness:4}") | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.