Revamping a Pi Keybow into a Pico Keybow

Posted by codepope on Thursday, April 1, 2021
Last Modified on Sunday, September 1, 2024

So, what do you do with old Pi projects? I had a Pimoroni Keybow on the side and thanks to RedRobotics’ tiny Pico2Pi board, I had the chance to breathe life back into it by replacing the controller board.

Drawing back the Keybow

Back in the day, the original Keybow was an interesting little beastie. A 3x4 keyboard of clicky switches with RGB LEDs as a compact keyboard; add your own Raspberry Pi Zero underneath and use a bare metal Lua implementation to configure the keyboard to send codes.

And that was good. What was bad though was the programming process. You had to remove a micro-SD from the Keybow’s Zero, pop it in your PC, edit the Lua config file, pop it out and restart the Zero. Well I stuck with that for a little biBOREDNOW, not for long as it was just a pain, and the Keybow sat on the side…

Then the Pico arrived. But out of the box, no pins matched and so not useful without having the board outside the frame and trailing wires connecting. Oh no no…. that would not do.

And then the Pico2Pi board arrived. This lets you solder a Pico to the board; with with a socket header or my preferred route for maximum thinness, using the castellations on the Pico to directly solder the Pico to the adapter boart. And now we have a Pico in the shape of a Pi Zero. As an added bonus there’s also a reset button.

All the Pico pins are routed up to equivelant Pi pins on a 40 pin header so we can drop the board in where we previously had a Pi Zero. So I just dismantled the Keybow and did just that.

Keybow from below

The board fitted fine. Next up, the software (which you can find on my Github repo).

Mapping the Keybow

With the hardware assembled it was time to focus on the software. Obviously we can’t use the Pi software. Instead, I loaded up CircuitPython onto the Pico and headed over to pinout.xyz’s Keybow page. And pulled out the reference card for the Pico2Pi which lists which Pico pins come out on which Pi pins.

Now the Keybow is dumb as dust when it comes to the actual keys; all 12 keys are assigned their own GPIO pin, so it’s a matter of mapping the Keybow key number, say Key 1, to the Pi Pin number… GPIO 17, finding GPIO 17 on the reference card and seeing what Pico pin that is. In this case GPIO 17 maps to GP7 on the Pico. Repeat the process another 11 times and you get a basic keyboard mapping.

LEDing the way

But thats forgetting the LEDs, which connecto to GPIO 10 and 11 on the Pi. They are a chain of APA102’s aka Dotstars. The library to drive them is adafruit_dotstar and it available in the standard bundle of libraries. Pro-tip, use circup, a Python tool you run on your Mac/PC to install and update libraries. circup install adafruit_dotstar was all that was needed to download the current and appropriate library bundle, extract the package and copy it into the lib directory of the Pico CircuitPython file system.

With that in place, we can set up the pixels to look pretty:

import adafruit_dotstar

pixels=adafruit_dotstar.DotStar(board.GP2, board.GP3,12)

for i in range(12):
    pixels[i]=(i*20,0,255-(i*20) , 0.5)

But we won’t be doing that in practice…

A Touch of Class

Now the fun part. The keys themselves are numbered in rows, starting bottom left with key 1, then 2 then 3 then up a row to the top. The LEDs on the other hand start top left and descend down, so LED 0 is key 9, LED 1 is key 6, LED 2 is key 3 and LED 3 is key 0. So our mapping table also has to map the LEDs.

It all ends up being wrapped up in a class for the “KeyLight”:

For each key we need to know a key number, which pin number to check, which led to light up and, when it’s pressed, do we send a string or call a function. We’ll also keep tabs on whether the button has been pressed recently and we’ll set a color to be the default for this key…. The one other thing we need to do is activate the pin for input so we can check it.

class KeyLight:
    def __init__(self,keynum,pinnum,lednum,press_string,press_func):
        self.keynum=keynum
        self.pinnum=pinnum
        self.lednum=lednum
        self.press_string=press_string
        self.press_func=press_func
        self.pressed=False
        self.set_base_color((self.keynum*20,0,255-(self.keynum*20) , 0.5))
        self.activate_pin()

    def activate_pin(self):
        self.pin=digitalio.DigitalInOut(self.pinnum)
        self.pin.switch_to_input(pull=digitalio.Pull.UP)

    def set_pixel(self,color):
        pixels[self.lednum]=color

    def get_pixel(self):
        return pixels[self.lednum]

    def set_base_color(self,color):
        self.base_color=color
        self.set_pixel(color)

    def key_pressed(self):
        self.pressed=True

    def key_was_pressed(self):
        return self.pressed

    def key_released(self):
        self.pressed=False

    def key_down(self):
        return self.pin.value==False

Now, with this class we can initialise all the keys, something like this:

keyArray=[]
keyArray.append( KeyLight( 0, board.GP7,   3,  None, key_macroinckey))
keyArray.append( KeyLight( 1, board.GP8,   7,  "1",  None))
keyArray.append( KeyLight( 2, board.GP27,  11, "2",  None))
keyArray.append( KeyLight( 3, board.GP9,   2,  "3",  None))
keyArray.append( KeyLight( 4, board.GP26,  6,  "4",  None))
keyArray.append( KeyLight( 5, board.GP10,  10, "5",  None))
keyArray.append( KeyLight( 6, board.GP11,  1,  "6",  None))
keyArray.append( KeyLight( 7, board.GP18,  5,  "7",  None))
keyArray.append( KeyLight( 8, board.GP12,  9,  "8",  None))
keyArray.append( KeyLight( 9, board.GP16,  0,  "9",  None))
keyArray.append( KeyLight( 10, board.GP17, 4,  "Ten", None))
keyArray.append( KeyLight( 11, board.GP14, 8,  "Eleven", None))

To hold onto all the KeyLight references, we have a keyArray and we append to that as we create KeyLights.

The actual checking if a key is down or not is spun out to a routine outside the class because, well there’s no interrupts available so all the keys have to be scanned. No, I am not going to run a thread per KeyLight, that way lies madness.

Instead we create a loop to scan all the keys. For each key, we check if the key is down, and if it is, we check this isn’t a key that’s recently been pressed.

while True:
    for k in keyArray:
        if k.key_down():
            if not k.key_was_pressed(): 

If it hasn’t been pressed, then we work through the process of pressing it by lighting up the LED, setting the KeyLight to pressed and then acting on either the function or sending the defined string for that key.

                k.set_pixel((255,255,255,0.5))
                k.key_pressed()
                if k.press_func!=None:
                    k.press_func(kbd,layout)
                else:
                    layout.write(k.press_string)

And if the key isn’t being held down, but was previously pressed, we reset its colour and release the key state.

        elif k.key_was_pressed():
                k.key_released()
                k.set_pixel(k.base_color)

That’s it. That’s the loop.

HIDden things

One thing we’ve kind of take for granted is the actual sending of the keys. For this, we’re using Adafruit’s HID support. Most of the work is done upfront in importing and initialising the HID:

import usb_hid

from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)

This gives us two things. A Keyboard where we we can directly control keypresses like control and shift keys in combination with other keys. And a Layout which is a US keyboard which can do all the keypressing for us given ASCII strings. The Layout is used to send the strings we define in the KeyLight instance. For fancier macro-key fiddling, we can use the function in the KeyLight instance. You may recall this:

keyArray.append( KeyLight( 0, board.GP7,   3,  None, key_macroinckey))

This says when this key is pressed call key_macroinckey. The function is passed both the keyboard and layout variables and can use either. The function in this case is:

macro_n=0

def key_macroinckey(kdb,layout):
    global macro_n
    layout.write(f"Macro {macro_n}")  
    macro_n += 1

This function uses the layout but to increment a variable so it type Macro 0, Macro 1, Macro 2 and so on each time the key is pressed. The value is stored in a simple global variable but could easily be another function with more “brains”.

Alternatively, here’s an undo function which uses the keyboard:

def key_undo(kdb,layout):
    kbd.press(Keycode.COMMAND,Keycode.Z)
    kbd.release_all()

There we send the keycodes to do undo on a Mac, (Command and Z) then release the keys. You can find out more about HID at learn.adafruit.com and on the HID Library pages at ReadTheDocs.

Wrapping up

So there’s a complete conversion from Pi to Pico for the Keybow using the Pico2Pi board. What’s improved? Well, now you can reprogram the keyboard without moving micro-SD cards around; the Pico mounts as a file system and you can edit the code and the board auto-restarts. You can also lean on Python for a more extensive and likely familiar experience for creating your perfect macro keyboard.

For me, the next challenge will be to get the Pico to recieve commands from the Mac/PC so it can react to events on the LEDs. And I’m looking forward to seeing how the new 4x4 Keybow with an RP2040 works out Until then, why don’t you start doing some mad macros?