/blog/

2024 0322 Linux keyboard LED control

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

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).