Raspberry Pi Pico
RGB Keypad Base

The Pico RGB Keypad Base is a Pimoroni product. Like so many of their products, it is absolutely gorgeous. It uses APA102 (dotstar) LEDs to light up a 4x4 silcone keypad of conductive buttons. It is squidgy as far as keypads go but it looks good on your desktop and at a smidgen under £22, is at a reasonable price point.

Pico Circuit

Pimoroni provide drivers for MicroPython but, to make the most of this, you need to be using the HID library in CircuitPython. Working from the snippets of code that several people had put online, I worked out an approach to using the keypad for keyboard macros. Leon had asked for help with this and was struggling to follow the couple of examples he had found online.

For this project, you need 3 libraries,

What I wanted was a system for defining a set of macros and the colours being used for the buttons. I decided to make this as a list in a separate file. I placed this file in my libraries folder and called it pages.py

The pages file consists of your button layouts. You start by defining the colours you use for not pressed, then pressed. Then you have your shortcut which can be a string, a series of keycodes or a consumer control code. Study the examples to see. You only need to make as many 'pages' as you need up to the limit.

The last button is used to cycle through the pages. Leave the colour and shortcut information blank for this.

from adafruit_hid.keycode import Keycode 
from adafruit_hid.consumer_control_code import ConsumerControlCode 
import time 
 
# You can have up to 16 different 'pages' of shortcuts. 
# When you change the page, a button will blink to indicate 
# which page you are now using. 
# Each row in these lists is the default button colour (not pressed), 
# the button colour when pressed, and a  
# list of the keys that need to be pressed. 
# You can have any number of keys being pressed.  
# You can write a string instead of a list of keycodes and have that typed out. 
# Use \n for a line break. 
# You can also write a ConsumerControlCode constant for multimedia buttons. 
 
btns = [0] * 16 
 
btns[0] = [[(64,0,0), (255,0,0), [Keycode.FIVE,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), "Leon says like all the time."], 
        [(64,0,0), (255,0,0), ConsumerControlCode.VOLUME_INCREMENT], 
        [(64,0,0), (255,0,0), ConsumerControlCode.VOLUME_DECREMENT], 
        [(64,0,0), (255,0,0), "5"], 
        [(64,0,0), (255,0,0), [Keycode.SIX,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.SEVEN,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.EIGHT,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.A,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.B,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.C,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.D,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.E,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.F,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.G,Keycode.ENTER]], 
        [(0,0,0), (255,0,0), []]] 
         
btns[1] = [[(64,0,0), (255,0,0), [Keycode.SHIFT,Keycode.L,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.TWO,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.THREE,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.FOUR,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.FIVE,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.SIX,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.SEVEN,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.EIGHT,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.A,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.B,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.C,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.D,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.E,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.F,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.G,Keycode.ENTER]], 
        [(0,0,0), (255,0,0), []]] 
         
btns[2] = [[(64,64,0), (255,255,0), [Keycode.SHIFT,Keycode.L]], 
        [(64,64,0), (255,255,0), [Keycode.SHIFT,Keycode.E]], 
        [(64,64,0), (255,255,0), [Keycode.SHIFT,Keycode.O]], 
        [(64,64,0), (255,255,0), [Keycode.SHIFT,Keycode.N,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.FIVE,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.SIX,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.SEVEN,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.EIGHT,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.A,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.B,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.C,Keycode.ENTER]], 
        [(64,0,0), (255,0,0), [Keycode.D,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.E,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.F,Keycode.ENTER]], 
        [(0,0,64), (255,0,255), [Keycode.G,Keycode.ENTER]], 
        [(0,0,0), (255,0,0), []]] 

This is the main program. It reads the pages file. When you press the last button to move to the next 'page', a button will blink a few times to show you which page you have moved to.

import time 
import board 
import busio 
import usb_hid 
 
from adafruit_bus_device.i2c_device import I2CDevice 
import adafruit_dotstar 
 
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 
 
from digitalio import DigitalInOut, Direction, Pull 
 
from pages import btns 
 
cs = DigitalInOut(board.GP17) 
cs.direction = Direction.OUTPUT 
cs.value = 0 
num_pixels = 16 
pixels = adafruit_dotstar.DotStar(board.GP18, board.GP19, num_pixels, brightness=0.1, auto_write=True) 
i2c = busio.I2C(board.GP5, board.GP4) 
device = I2CDevice(i2c, 0x20) 
kbd = Keyboard(usb_hid.devices) 
layout = KeyboardLayoutUS(kbd) 
cc = ConsumerControl(usb_hid.devices) 
 
def read_button_states(x, y): 
    pressed = [0] * 16 
    with device: 
        device.write(bytes([0x0])) 
        result = bytearray(2) 
        device.readinto(result) 
        b = result[0] | result[1] << 8 
        for i in range(x, y): 
            if not (1 << i) & b: 
                pressed[i] = 1 
            else: 
                pressed[i] = 0 
    return pressed 
 
held = [0] * 16 
page = 0 
 
# works out how many pages there are - stops as soon as a page is 'missing' 
numpages = 0 
for i in range(16): 
    if btns[i]==0: 
        break 
    numpages += 1 
 
# subroutine to blink the button indicating which page has been selected. 
# done whenever a new page is selected 
def blink_page(p): 
    # turn off all pixels 
    for i in range(16): 
        pixels[i] = (0,0,0) 
    # blink the right button 
    for i in range(10): 
        pixels[p] = (0,0,255) 
        time.sleep(0.1) 
        pixels[p] = (0,0,0) 
        time.sleep(0.1) 
 
while True: 
    pressed = read_button_states(0, 16) 
    # find first pressed 
    p = -1 
    for i in range(16): 
        if pressed[i]: 
            p = i 
    if p>=0 and p<15: 
        # something being pressed 
        pixels[p] = btns[page][p][1] 
        if not held[p]: 
            if type(btns[page][p][2]) is list: 
                kbd.send(*btns[page][p][2]) 
            elif type(btns[page][p][2]) is str: 
                layout.write(btns[page][p][2]) 
            else:                
                cc.send(btns[page][p][2])  
            held[p] = 1 
    elif p==15: 
    # change of page 
        if not held[15]: 
            pixels[15] = btns[page][15][1] 
            page = (page + 1) % numpages 
            held[15] = 1 
            blink_page(page) 
    else:
        for i in range(16): 
            pixels[i] = btns[page][i][0] 
            held[i] = 0 
        time.sleep(0.1)