Adding a USB two-button keyboard control to my up-cycled Raspberry Pi photo frame

Gareth Cronin
7 min readFeb 12, 2023

--

Back in November I wrote an article about the transformation of an unused Raspberry Pi 3, a secondhand monitor bought for New Zealand $11.50 and a few low cost bits and pieces into a digital photo frame for my nine year-old son Sam’s bedroom.

Sam has inherited my habit of always looking for that next little improvement, and he asked me if there was a way he could have a button to switch albums on his photo frame. Using the handy Linux application, FEH, I have the frame cycling around a mixture of photos of our family, his artworks, and our cat. He really wanted to be able to switch themes, including to photos of other people’s cats. I explained that the monitor it was attached to had no buttons that could send a message back to the Pi, and that a full keyboard or mouse would be unwieldy on a bookshelf. This started me wondering if there was such a thing as a one or two button keyboard.

The two-button keyboard

One of the great features of the Raspberry Pi is its four built-in USB ports. Combined with Linux, that opens a world of accessories. Despite my gadget-using tendencies, I had never bought anything from Ali Express, but this did seem like a good project to start with!

A search for “one button usb keyboard”, “mini keyboard” and similar led me to this little beauty:

The two-button keyboard

RGB illumination, claimed Linux compatibility, two buttons with a cable, and all for $11.72 New Zealand dollars (about 7 USD)… what more could I ask for? I ordered it on New Year’s Day, and it arrived just on five weeks later. Not bad for a trip from China to New Zealand at that price.

Modifying the existing solution

The existing solution uses an autostart entry so the default Raspberry Pi OS window manager, LXDE, executes a shell script. The script sets the DISPLAY environment variable and runs FEH, pointing it to a single flat “images” directory. My plan was to create an “albums” directory where the FEH slideshow would read from one sub-directory at a time. When a button press triggered an album swap, I would kill the running FEH process and spawn a new one, pointing at the next album in the directory.

Listening to the two-button keyboard

I tried visiting the URL supplied on a tiny piece of paper that came with the button and promised Linux-specific help. I wasn’t surprised to find myself looking at a blank and broken page. However, it seemed likely that the keyboard would register itself as a standard USB human interface device (HID) so I thought I’d start there.

I tailed /var/log/messages and plugged the keyboard in to see what would happen:

Feb 12 09:57:01 raspberrypi kernel: [  442.970292] input: SayoDevice SayoDevice O2L RGB as /devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.0/0003:8089:0003.0001/input/input1
Feb 12 09:57:01 raspberrypi kernel: [ 443.041221] hid-generic 0003:8089:0003.0001: input,hidraw0: USB HID v1.11 Keyboard [SayoDevice SayoDevice O2L RGB] on usb-3f980000.usb-1.5/input0
Feb 12 09:57:01 raspberrypi kernel: [ 443.050574] input: SayoDevice SayoDevice O2L RGB Mouse as /devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.1/0003:8089:0003.0002/input/input3
Feb 12 09:57:01 raspberrypi kernel: [ 443.052180] input: SayoDevice SayoDevice O2L RGB Consumer Control as /devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.1/0003:8089:0003.0002/input/input4
Feb 12 09:57:01 raspberrypi kernel: [ 443.120131] hid-generic 0003:8089:0003.0002: input,hiddev96,hidraw1: USB HID v1.11 Mouse [SayoDevice SayoDevice O2L RGB] on usb-3f980000.usb-1.5/input1

As I hoped, there’s a line there for a “USB HID v1.11 Keyboard” registered. Some Googling on dumping USB input led me a post that explained how to use usbhid-dump. I ran lsusb -v and found the keyboard was on bus 1 with an ID of 4. That meant I could run the following command, which gave the output shown when I tapped each of the two keys in turn:

sudo usbhid-dump -s 1:4 -f -e stream

.001:004:000:STREAM 1676149865.547411
00 00 1D 00 00 00 00 00

.001:004:000:STREAM 1676149865.668390
00 00 00 00 00 00 00 00

.001:004:000:STREAM 1676149878.193390
00 00 1B 00 00 00 00 00

.001:004:000:STREAM 1676149878.329384
00 00 00 00 00 00 00 00

My guess was that the populated octet in the first and third lines is a key code of some sort, where the left key has the code 0x1D and the right was 0x1B. This was good news — at worst I could do a nasty script with sudo and grep to get to a result.

Key of death

I had the existing photo slide show running when I started experimenting, and I was surprised to discover that the slideshow exited completely when the right button on the keyboard was pressed. The X Server kept running, but it was like a SIGHUP or SIGTERM had been sent to the backgrounded process that spawned FEH.

I thought I might need to start digging into remapping the keys, but then a Stack Exchange post led me to a simpler solution. The xinit tool can issue a command to tell the X Server to ignore a USB device altogether. Given that I planned to process the keyboard events in the background in a separate process and respawn FEH as required, this seemed adequate.

I used xinput to list the devices according to X:

gareth@raspberrypi:~ $ DISPLAY=:0.0 xinput list
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=4 [slave pointer (2)]
⎜ ↳ vc4 id=6 [slave pointer (2)]
⎜ ↳ SayoDevice SayoDevice O2L RGB Mouse id=8 [slave pointer (2)]
⎜ ↳ SayoDevice SayoDevice O2L RGB Consumer Control id=10 [slave pointer (2)]
⎣ Virtual core keyboard id=3 [master keyboard (2)]
↳ Virtual core XTEST keyboard id=5 [slave keyboard (3)]
↳ vc4 id=7 [slave keyboard (3)]
↳ SayoDevice SayoDevice O2L RGB id=9 [slave keyboard (3)]
↳ SayoDevice SayoDevice O2L RGB Consumer Control id=11 [slave keyboard (3)]

I added this line to the script that starts the slideshow:

DISPLAY=:0.0 xinput disable "SayoDevice SayoDevice O2L RGB"

It is possible to tell it to disable a device by ID, but the order in which the devices are registered with the USB layer in Linux can change, so using the name is much more reliable.

Once disabled, I could press either of the keys and see the events in usbhid-dump without the slide show process being killed!

Processing the keyboard input

Another Stack Overflow post led me to Python and the evdev package as a reliable way to process raw USB input data on a Pi. My Pi is running Python 3, so I installed the package with pip3 install evdev. The package includes a tool that can dump input and information about USB devices. I ran it with python -m evdev.evtest and discovered that the keyboard could be listened to, without sudo on /dev/input/event0.

A simple script to show the two key presses looks like this:

from evdev import InputDevice, ecodes

device = InputDevice("/dev/input/event0")
for event in device.read_loop():
if event.type == ecodes.EV_KEY:
if(event.code == 44 and event.value == 0 ):
print('Left button up')
if(event.code == 45 and event.value == 0 ):
print('Right button up')

Note the “value” property on the event. There are two events generated for each key press: 1 is key down and 0 is key up.

Putting it all together

I’m not a Python developer: my usage is limited to Jupyter notebooks and occasionally troubleshooting NLP scripts quite a few years ago. I had a conversation with Chat GPT about enumerating sub-directories, shelling out commands to the OS, checking the length of arrays, loops and conditionals. For the most part that was very helpful. The exception was its suggestion to use the complex (but admittedly more robust) process management libraries for shelling out. When I was typing though, Github Copilot jumped in with a suggestion to use os.system, which was much simpler. It’s just like having my own team of AI hacker AIs!

The working script looks like this:

from evdev import InputDevice, ecodes
import os

PATH = "./albums"
DEVICE = "/dev/input/event0"

def list_subdirectories(path):
return [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))]

currentAlbum = 0
subdirectories = list_subdirectories(PATH)

def next_album(currentAlbum):
nextAlbum = (currentAlbum + 1) % len(subdirectories)
return nextAlbum

def run_fef():
os.system("killall feh")
os.system('DISPLAY=:0.0 xinput disable "SayoDevice SayoDevice O2L RGB"')
os.system("DISPLAY=:0.0 feh --randomize --full-screen --slideshow-delay 30 --auto-rotate " + PATH + "/" + subdirectories[currentAlbum] + " &")

run_fef()

device = InputDevice(DEVICE)
for event in device.read_loop():
if event.type == ecodes.EV_KEY:
if(event.code == 44 and event.value == 0 ):
currentAlbum = next_album(currentAlbum)
print('Album now displaying: ' + subdirectories[currentAlbum])
run_fef()

The finishing touches

I could now cycle albums by pushing the left key on the two-button keyboard. It didn’t seem right to leave the right key without a job though! I had a read through the FEH documentation to see if there was a way to pause and resume the slideshow with OS signals. There isn’t, but there is a way to force the next and previous photos with SIGUSR1 and SIGUSR2. I decided I’d go with advancing the photo manually using SIGUSR1. I added this to the Python script:

# ... as above
if(event.code == 45 and event.value == 0 ):
print('Advancing photo')
os.system("killall -s SIGUSR1 feh")

I changed my autostart entry to call my Python script rather than the shell script from the existing solution and it was all done!

Happy Sam.

--

--

Gareth Cronin
Gareth Cronin

Written by Gareth Cronin

Technology leader in Auckland, New Zealand: start-up founder, father of two, maker of t-shirts and small software products

No responses yet