Keyboard Backlight Effects w/ sysfs
A quick intro to sysfs and how you can use it to upgrade your keyboard's backlight
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
