Post

Keyboard Backlight Effects w/ sysfs

A quick intro to sysfs and how you can use it to upgrade your keyboard's backlight

Keyboard Backlight Effects w/ sysfs

Backligiht Effect Demo of the backlight effect

Several years ago, my Thinkpad T460’s display broke so I replaced it with an X1 Carbon. The model I got came with keyboard backlighting, and whenever I hit Fn-Space the light cycles through its 3 modes: off, low, and high.

I wanted to see if I could add any light effects with the LEDs, and along the way I learned a little bit about sysfs virtual file-system mounted at /sys.

Device Drivers

Device drivers control the machine’s hardware, and the kernel selectively loads these drivers depending on the hardware available. What sysfs give us are virtual files to interface with the hardware under /sys that work with file operations. For example, if I want to read the current percentage of my laptop’s battery, sysfs provides me this interface:

1
2
[mike@machine ~]$ cat /sys/class/power_supply/BAT0/capacity
90

On my X1 Carbon, the kernel selectively loads the thinkpad_acpi.c platform driver when my laptop boots. This driver manages several Thinkpad components, including the LEDs. If we take a look at the source code for thinkpad_acpi.c, notice that it declares the following struct:

1
2
3
4
5
6
7
8
9
static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
 .led_classdev = {
  .name  = "tpacpi::kbd_backlight",
  .max_brightness = 2,
  .flags  = LED_BRIGHT_HW_CHANGED,
  .brightness_set_blocking = &kbdlight_sysfs_set,
  .brightness_get = &kbdlight_sysfs_get,
 }
};

This static struct tpacpi_led_kbdlight is the central data structure used in communication between the Thinkpad keyboard backlight and sysfs. The fields .brightness_get and .brightness_set_blocking point to driver functions for controlling the backlight, and sysfs exposes these functions to the user by mapping them to virtual files under /sys/class/leds/tpacpi::kbd_backlight/. Let’s take a look at the contents of this directory:

1
2
[mike@machine ~]$ ls /sys/class/leds/tpacpi::kbd_backlight/
brightness  brightness_hw_changed  device  max_brightness  power  subsystem  trigger  uevent

Notice that a lot of the fields from the struct are present here. We have max_brightness which can be read to see that the highest mode for the LEDs is ‘2’. We also have our LED_BRIGHT_HW_CHANGED flag mapped to brightness_hw_changed, and most importantly we see that there’s a brightness file. This file can be read and written to, and these operations map directly to the aforementioned .brightness_get and .brightness_set_blocking functions. Let’s test out the .brightness_get function:

1
2
3
4
5
6
[mike@machine ~]$ # Reading the file when the backlight is at maximum brightness:
[mike@machine ~]$ cat /sys/class/leds/tpacpi::kbd_backlight/brightness
2
[mike@machine ~]$ # Now after hitting Fn-Space and toggling the brightness off:
[mike@machine ~]$ cat /sys/class/leds/tpacpi::kbd_backlight/brightness
0

Now that we’ve covered sysfs and figured out that we can interact with the backlight through the virtual file at /sys/class/leds/tpacpi::kbd_backlight/brightness, we have a pretty straightforward way of making that pulsing light effect. All we have to do is write a Bash script that goes back and forth setting this file’s value from 0 (off) to 2 (max brightness):

1
2
3
4
5
6
7
8
#!/bin/bash

while true; do
 for VAL in 0 2; do
  echo $VAL >/sys/class/leds/tpacpi::kbd_backlight/brightness
  sleep 1
 done
done

Extra

For those who want a more out-of-the-box solution, feel free to use the script I’ve pasted below. Kudos to the Arch Wiki which gave me the idea to use upower, I haven’t tested it on many devices but upower should make the pulse effect vendor agnostic because it circumvents sysfs and works with drivers directly. This script does assume your backlight driver uses modes in the range 0-2, but that can be modified using the -q flag. Make sure to install both upower and bc for this to work on your machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash

ORIGINAL_VALUE=$(dbus-send \
--type=method_call \
--print-reply=literal \
--system \
--dest="org.freedesktop.UPower" \
"/org/freedesktop/UPower/KbdBacklight" "org.freedesktop.UPower.KbdBacklight.GetBrightness" | awk '{print $NF}')
SEQUENCE="12"
PERIOD=2

function cleanup {
  dbus-send \
  --system \
  --type=method_call \
  --dest="org.freedesktop.UPower" \
  "/org/freedesktop/UPower/KbdBacklight" "org.freedesktop.UPower.KbdBacklight.SetBrightness" int32:$ORIGINAL_VALUE
}

trap cleanup EXIT

while getopts "q:t:h" opt; do
  case $opt in
    q )
        SEQUENCE=$OPTARG
        ;;
    t )
        PERIOD=$OPTARG
        ;;
    h )
        echo "pulse: Cycle through backlight modes"
        echo "-q SEQ : Set the sequence of mode values as a string ex. 012, defaults to 12"
        echo "-t SEC: Set the cycle period in seconds, defaults to 2"
        exit 0
        ;;
    \? ) echo 'Usage: pulse [-q SEQ] [-t SEC] [-h]'
        exit 1
        ;;
  esac
done

WAIT=$(echo "scale=2; $PERIOD / ${#SEQUENCE}" | bc)

while true; do
    for (( i=0; i<${#SEQUENCE}; i++ )); do
      VAL=${SEQUENCE:$i:1}
      dbus-send \
        --system \
        --type=method_call \
        --dest="org.freedesktop.UPower" \
        "/org/freedesktop/UPower/KbdBacklight" "org.freedesktop.UPower.KbdBacklight.SetBrightness" int32:$VAL
      sleep $WAIT
    done
done
This post is licensed under CC BY 4.0 by the author.