Skip to content

Commit

Permalink
Merge pull request #32 from tiagocoutinho/led
Browse files Browse the repository at this point in the history
Add led support
  • Loading branch information
tiagocoutinho authored Aug 19, 2024
2 parents eec3313 + 9a28bb1 commit d299e61
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 4 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ jobs:
uses: actions/checkout@v4
- name: Install necessary packages
run: |
sudo apt-get update
sudo apt-get install -y linux-modules-extra-$(uname -r)
sudo apt-get install -y linux-image-$(uname -r) linux-modules-extra-$(uname -r)
- name: Setup user groups
run: |
echo KERNEL==\"uinput\", SUBSYSTEM==\"misc\" GROUP=\"docker\", MODE:=\"0666\" | sudo tee /etc/udev/rules.d/99-$USER.rules
Expand All @@ -31,8 +30,8 @@ jobs:
cat /etc/udev/rules.d/99-$USER.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo modprobe -i uinput
sudo modprobe -i vivid n_devs=1 node_types=0xe1d3d vid_cap_nr=190 vid_out_nr=191 meta_cap_nr=192 meta_out_nr=193
sudo modprobe -a uinput
sudo modprobe vivid n_devs=1 node_types=0xe1d3d vid_cap_nr=190 vid_out_nr=191 meta_cap_nr=192 meta_out_nr=193
lsmod
ls -lsa /dev/video*
- name: Set up Python ${{ matrix.python-version }}
Expand Down
3 changes: 3 additions & 0 deletions docs/api/led.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 💡 Led API

::: linuxpy.led
18 changes: 18 additions & 0 deletions docs/user_guide/led.md
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>
18 changes: 18 additions & 0 deletions examples/led/uledmon.py
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
217 changes: 217 additions & 0 deletions linuxpy/led.py
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()
1 change: 1 addition & 0 deletions linuxpy/sysfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
DEVICE_PATH = MOUNT_PATH / "bus/usb/devices"
CLASS_PATH = MOUNT_PATH / "class"
THERMAL_PATH = CLASS_PATH / "thermal"
LED_PATH = CLASS_PATH / "leds"


class Mode(enum.Enum):
Expand Down
12 changes: 12 additions & 0 deletions linuxpy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import asyncio
import contextlib
import random
import string

from .types import Callable, Iterable, Iterator, Optional, Sequence, Union

Expand Down Expand Up @@ -198,3 +200,13 @@ def tuple(self):

def bit_indexes(number):
return [i for i, c in enumerate(bin(number)[:1:-1]) if c == "1"]


ascii_alphanumeric = string.ascii_letters + string.digits


def random_name(min_length=32, max_length=32):
if not (k := random.randint(min_length, max_length)):
return ""
first = random.choice(string.ascii_letters)
return first + "".join(random.choices(ascii_alphanumeric, k=k - 1))
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ nav:
- User guide:
- user_guide/index.md
- user_guide/input.md
- user_guide/led.md
- user_guide/midi.md
- user_guide/thermal.md
- user_guide/video.md
- API Reference:
- api/index.md
- api/input.md
- api/led.md
- api/midi.md
- api/thermal.md
- api/video.md
Expand Down
Loading

0 comments on commit d299e61

Please sign in to comment.