I write a couple of scripts to control keyboard LEDs on Linux.
They use the Linux /dev and /sys filesystems to control keyboard LEDs
for capslock/numlock/scrolllock/kana/compose.
These LEDs are sometimes known as USB HID host status indicators.
Capslock/numlock/scrolllock are widely known;
apparently kana is a mode for Japanese and compose is a mode for non-ASCII characters.
The host operating system controls these lights
by passing signals over USB.
Linux allows the root user to read and set their status via the /sys virtual filesystem under
/sys/class/leds
.
lkbm.sh
A controller for the lkbm QMK keymap for the Ploopy Nano trackball. The lkbm keymap allows changing settings on the Nano, including toggling between using the trackball to control the cursor and using it as a scroll ball; toggling between three different DPI settings to control movement speed; and entering reset/bootloader mode to flash new firmware. It does this by receiving keyboard LED signals for capslock and scroll lock (the Nano is powered by QMK, and thus aside from presenting as a mouse to the operating system it also presents as a keyboard which can receive these LED signals).
(Anyone reading this far might also be interested in dual wielding Ploopy Nano trackballs. The lkbm-based firmware in that repo is more advanced and can receive more commands, at the cost of slower command execution. My script doesn’t send those extended commands, but it would not be hard to modify it to do so.)
lkbm.sh
is a simple shell script for sending those signals.
It looks for a device called Ploopy_Trackball_Nano
and sends the sequences of caps/num lock to it.
This is a simpler and shorter but less featureful script than lkcmd.sh
(see below for that).
./lkbm.sh
./lkbm.sh: Send commands to Ploopy Trackball Nano via caps/scroll/num lock LED signals
Usage: ./lkbm.sh [scroll|dpi|reset|status|help]
lkbm.sh
source code
#!/bin/sh
set -e
usage() {
cat <<ENDUSAGE
$0: Send commands to Ploopy Trackball Nano via caps/scroll/num lock LED signals
Usage: $0 [scroll|dpi|reset|status|help]
ENDUSAGE
}
inputdev=/dev/input/by-id/usb-PloopyCo_Trackball_Nano-event-kbd
evntname="$(basename "$(readlink -f $inputdev)")"
# e.g. "event22"
sysclassdev=/sys/class/input/$evntname/device
sysinputname="$(echo $sysclassdev/*::capslock | sed 's|.*/\(input[0-9]*\).*|\1|')"
# e.g. "input1251"
caps=/sys/class/leds/$sysinputname::capslock/brightness
scroll=/sys/class/leds/$sysinputname::scrolllock/brightness
num=/sys/class/leds/$sysinputname::numlock/brightness
# We have to wait a very small amount of time or the LED changes don't seem to register
lkbmwait() {
busybox sleep 0.002s
}
toggle_scroll() {
echo 1 > "$num"
lkbmwait
echo 0 > "$num"
lkbmwait
}
cycle_dpi() {
echo 1 > "$caps"
lkbmwait
echo 0 > "$caps"
lkbmwait
}
enter_reset() {
echo 0 > "$caps"
lkbmwait
echo 1 > "$caps"
lkbmwait
echo 1 > "$num"
lkbmwait
echo 0 > "$num"
lkbmwait
}
read_status() {
c="$(cat "$caps")"
s="$(cat "$scroll")"
n="$(cat "$num")"
echo "Input device path: $inputdev"
echo "Event device name: $evntname"
echo "/sys/class even device: $sysclassdev"
echo "/sys/class input device: $sysinputname"
echo "Capslock: $c"
echo "Scroll lock: $s"
echo "Numlock: $n"
}
case "$1" in
scroll) toggle_scroll;;
dpi) cycle_dpi;;
reset) enter_reset;;
status) read_status;;
*) usage;;
esac
lkcmd.sh
lkcmd.sh
is a longer script that can exercise more fine grained control over the LEDs.
You can list available keyboards with -l
,
fuzzy match on any unique part of a keyboard name like Ploopy
,
read all the LED statuses (even if the particular keyboard doesn’t have any physical LEDs) with -t
,
or set any combination of them in a single command.
You can toggle them to the opposite of their current state, or set them on or off explicitly.
You can also control the Ploopy lkbm firmware as above with -p
.
This is a longer and more complicated version of llkm.sh
with more features.
> ./lkcmd.sh -h
./lkcmd.sh: Change capslock/compose/kana/numlock/scrolllock LEDs of attached keyboards.
Usage: ./lkcmd.sh [KEYBOARD] [-A CAPSLOCK] [-o COMPOSE] [-k KANA] [-n NUMLOCK] [-s SCROLLLOCK] [-p PLOOPYMODE] [-t]
Arguments
KEYBOARD A keyboard identifier.
This can be the shortest substring that identifies a board
based on files in /dev/input/by-id.
-A CAPSLOCK Set the capslock LED
-o COMPOSE Set the compose LED
-k KANA Set the kana LED
-n NUMLOCK Set the numlock LED
-s SCROLLLOCK Set the scrolllock LED
-p PLOOPYMODE Send an LKBM (LED Key BitMask) command to a Ploopy Nano
-l List possible keyboard input devices;
any fragment of the filename can be used as a KEYBOARD argument
-t Read the status of all LEDs of a board
(after any of the other changes are sent, if applicable)
Setting LEDs:
Setting any of the five LEDs accepts a 0 or 1 for off or on;
any other value toggles the current setting.
Ploopy LKBM commands:
These are commands defined in the lkbm Ploopy Nano firwmare.
<https://github.com/qmk/qmk_firmware/tree/master/keyboards/ploopyco/trackball_nano/keymaps/lkbm>
Available commands:
scroll Toggle between using the device as a regular trackball
or using it as a 2D scrollball.
dpi Toggle between three DPI settings that control mouse speed.
reset Enter reset/bootloader mode that allows flashing firmware.
lkcmd.sh
source code
#!/bin/sh
set -eu
usage() {
cat <<ENDUSAGE
$0: Change capslock/compose/kana/numlock/scrolllock LEDs of attached keyboards.
Usage: $0 [KEYBOARD] [-A CAPSLOCK] [-o COMPOSE] [-k KANA] [-n NUMLOCK] [-s SCROLLLOCK] [-p PLOOPYMODE] [-t]
Arguments
KEYBOARD A keyboard identifier.
This can be the shortest substring that identifies a board
based on files in /dev/input/by-id.
-A CAPSLOCK Set the capslock LED
-o COMPOSE Set the compose LED
-k KANA Set the kana LED
-n NUMLOCK Set the numlock LED
-s SCROLLLOCK Set the scrolllock LED
-p PLOOPYMODE Send an LKBM (LED Key BitMask) command to a Ploopy Nano
-l List possible keyboard input devices;
any fragment of the filename can be used as a KEYBOARD argument
-t Read the status of all LEDs of a board
(after any of the other changes are sent, if applicable)
Setting LEDs:
Setting any of the five LEDs accepts a 0 or 1 for off or on;
any other value toggles the current setting.
Ploopy LKBM commands:
These are commands defined in the lkbm Ploopy Nano firwmare.
<https://github.com/qmk/qmk_firmware/tree/master/keyboards/ploopyco/trackball_nano/keymaps/lkbm>
Available commands:
scroll Toggle between using the device as a regular trackball
or using it as a 2D scrollball.
dpi Toggle between three DPI settings that control mouse speed.
reset Enter reset/bootloader mode that allows flashing firmware.
(This doesn't seem to be working at the moment for some reason.)
ENDUSAGE
}
# A path in /dev/input/by-id that refers to the keyboard input event device.
# Because it is a keyboard, it should end in -event-kbd.
# It will be a symlink to a file in /dev/input/ called event.*
inputdev=
# The name (not full path) of the event.* file in /dev/input/
evntname=
# A path to /sys/class/input/$evntname/device.
# This will be a symlink to a folder like /sys/class/input/input.*
sysclassdev=
# The name (not full path) of the input.* file in /sys/class/input/
sysinputname=
# Once we have this, we can use paths like the following to set the capslock LED:
# /sys/class/leds/"$sysinputname"::capslock/brightness
ledcapslock=
ledcompose=
ledkana=
lednumlock=
ledscrolllock=
# Populate the global variables above
findkbd() {
if test -e "$1" && test "${1#/}"; then
# If it exists and is an absolute path, let the user pass whatever
inputdev="$1"
else
# The links should be in /dev/input/by-id and end in -event-kbd
# We can find any substring of the file so that
# the user can just pass a product name in most cases.
foundmulti=
for f in /dev/input/by-id/*"$1"*; do
# Ignore files that end in -ifXX-event-kbd like:
# /dev/input/by-id/usb-ZSA_Technology_Labs_Voyager-if01-event-kbd
# but find files that otherwise end in -event-kbd like:
# /dev/input/by-id/usb-ZSA_Technology_Labs_Voyager-event-kbd
case "$f" in
*-if[0-9][0-9]-event-kbd)
continue;;
*-event-kbd)
if test "$inputdev"; then
echo "Found multiple devices that match string: $1"
ls -1 /dev/input/by-id/*"$1"* | grep 'event-kbd$' | grep -v 'if[0-9][0-9]-event-kbd$'
exit 1
fi
inputdev="$f"
;;
esac
done
fi
if test -z "$inputdev"; then
echo "Could not find a keyboard that matches string: $1"
exit 1
fi
evntname="$(basename "$(readlink -f $inputdev)")"
# e.g. "event22"
sysclassdev=/sys/class/input/$evntname/device
sysinputname="$(echo $sysclassdev/*::capslock | sed 's|.*/\(input[0-9]*\).*|\1|')"
# e.g. "input1251"
ledcapslock=/sys/class/leds/$sysinputname::capslock/brightness
ledcompose=/sys/class/leds/$sysinputname::compose/brightness
ledkana=/sys/class/leds/$sysinputname::kana/brightness
ledscrolllock=/sys/class/leds/$sysinputname::scrolllock/brightness
lednumlock=/sys/class/leds/$sysinputname::numlock/brightness
}
# We have to wait a very small amount of time or the LED changes don't seem to register
lkbmwait() {
busybox sleep 0.02s
}
# Toggle between using the trackball as normal and using it as a 2D scrollball
lkbm_toggle_scroll() {
echo 1 > "$lednumlock"
lkbmwait
echo 0 > "$lednumlock"
lkbmwait
}
# Cycle between slow/mid/fast DPI settings for the tracball
lkbm_cycle_dpi() {
echo 1 > "$ledcapslock"
lkbmwait
echo 0 > "$ledcapslock"
lkbmwait
}
# Enter the bootloader where new firmware can be sent (not working?)
lkbm_enter_reset() {
echo 0 > "$ledcapslock"
lkbmwait
echo 1 > "$ledcapslock"
lkbmwait
echo 1 > "$lednumlock"
lkbmwait
echo 0 > "$lednumlock"
lkbmwait
}
# Read the status of the keyboard LEDs
read_status() {
A="$(cat "$ledcapslock")"
o="$(cat "$ledcompose")"
k="$(cat "$ledkana")"
s="$(cat "$ledscrolllock")"
n="$(cat "$lednumlock")"
echo "Input device path: $inputdev"
echo "Event device name: $evntname"
echo "/sys/class event device: $sysclassdev"
echo "/sys/class input device: $sysinputname"
echo "Capslock: $A"
echo "Compose: $o"
echo "Kana: $k"
echo "Scroll lock: $s"
echo "Numlock: $n"
}
# List all input devices by their ID;
# the user can use any unique fragment of the filename as the KEYBOARD argument.
list_keyboard_input_devices() {
ls -1 /dev/input/by-id/*-event-kbd | grep -v 'if[0-9][0-9]-event-kbd$'
}
# Set an LED
setled() {
ledpath="$1"
value="$2"
if ! test -e "$ledpath"; then
echo "LED brightness file does not exist at $ledpath"
exit 1
fi
case "$value" in
0|1) echo "$value" > "$ledpath";;
*)
if test "$(tr -d '\n' <"$ledpath")" -eq 0; then
echo 1 >"$ledpath"
else
echo 0 >"$ledpath"
fi;;
esac
}
kbd=
setcapslock=
setcompose=
setkana=
setnumlock=
setscrolllock=
readstatus=
ploopycmd=
while test $# -gt 0; do
case "$1" in
-h|--help) usage; exit;;
-A) setcapslock="$2"; shift 2;;
-o) setcompose="$2"; shift 2;;
-k) setkana="$2"; shift 2;;
-n) setnumlock="$2"; shift 2;;
-s) setscrolllock="$2"; shift 2;;
-l) list_keyboard_input_devices; exit;;
-t) readstatus=1; shift;;
-p)
# If a keyboard was not passed in Ploopy LKBM mode, guess
if test -z "$kbd"; then
kbd="PloopyCo_Trackball_Nano"
fi
ploopycmd="$2"
shift 2;;
-*) usage; echo ""; echo "Invalid option: $1"; exit 1;;
*)
if test -z "$kbd"; then
kbd="$1"
else
usage; echo ""; echo "Invalid argument: $1"; exit 1;
fi
shift;;
esac
done
if test -z "$kbd"; then
usage
echo ""
echo "No keyboard argument passed"
exit 1
fi
findkbd "$kbd"
if test "$ploopycmd"; then
case "$ploopycmd" in
scroll) lkbm_toggle_scroll;;
dpi) lkbm_cycle_dpi;;
reset) lkbm_enter_reset;;
*) usage;;
esac
else
test "$setcapslock" && setled "$ledcapslock" "$setcapslock"
test "$setcompose" && setled "$ledcompose" "$setcompose"
test "$setkana" && setled "$ledkana" "$setkana"
test "$setnumlock" && setled "$lednumlock" "$setnumlock"
test "$setscrolllock" && setled "$ledscrolllock" "$setscrolllock"
fi
test "$readstatus" && read_status
Changelog
- 2024-03-25: Fix lkcmd ploopy commands, increase wait time for more reliable commands