A while ago I’ve made a MacroPad and I recently improved the code! In this post I’ll briefly show some advanced code to create a MacroPad class which allows custom functions to be added to key presses using a decorator. The original code as well as instructions how to build one yourself can be found in the original post.

## Event based library

I got the idea for implementing this library from the KeyBow 2040 that implements this specifically for their hardware. This makes the code to actually program their keypad a lot less complex. You can see in the example below, that you simply write the function you want and add @keybow.on_press(key) above the function you want to run when the key is pressed. Let’s see if this can be ported to my MacroPad.

# Example code from the KeyBow 2040 GitHub Repo

@keybow.on_press(key)
def press_handler(key):
key.led_on()


To achieve this a new library needs to be created that can set up the MacroPad, handle keystrokes and allow you to attach new functions to each key pressed. This is an advanced bit of code, and I won’t go through each part step by step, but it is a great example how you can create a library with a class that allows users to mix in their own functions that are triggered at specific point in the class’ code. It will also handle the light effects so this doesn’t need to be handled by the user.

import board
import digitalio
import pwmio
import time

# Configuration, which LED pins are used, which buttons, how buttons map to macros
led_pins = [board.GP18,board.GP17,board.GP16,board.GP21,board.GP20,board.GP19, board.GP27, board.GP26,board.GP22]
button_pins = [board.GP13,board.GP14,board.GP15, board.GP10,board.GP11,board.GP12,board.GP7,board.GP8,board.GP9]

class Button(object):
def __init__(self, button_index, button_pin, led_pin, repeat=True, repeat_time=0.075, first_repeat_time=0.5):
self.number = button_index

self.button = digitalio.DigitalInOut(button_pin)
self.button.direction = digitalio.Direction.INPUT
self.button.pull = digitalio.Pull.UP

self.led = pwmio.PWMOut(led_pin, frequency=1000, duty_cycle=0)
self.last_pressed = 0

self.triggered = False

self.on_press = None
self.on_release = None

self.repeat = repeat
self.repeat_time = repeat_time
self.first_repeat_time = first_repeat_time
self.first_repeat = True

self.time_of_last_press = time.monotonic()

@property
def pressed(self):
return not self.button.value

def set_duty_cycle(self, value):
self.led.duty_cycle = value

self.led.duty_cycle = max(self.led.duty_cycle - value, 0)

def update(self):
self.time_since_last_press = time.monotonic() - self.time_of_last_press

if self.pressed and (not self.triggered or
(self.time_since_last_press > self.repeat_time and self.repeat and not self.first_repeat) or
(self.time_since_last_press > self.first_repeat_time and self.first_repeat and self.repeat)):

self.time_of_last_press = time.monotonic()

if self.time_since_last_press > self.first_repeat_time and self.first_repeat and self.triggered:
self.first_repeat = False

self.triggered = True

self.set_duty_cycle(65025)

if self.on_press is not None:
self.on_press(self)

elif self.triggered and not self.pressed:
self.first_repeat = True
self.triggered = False

if self.on_release is not None:
self.on_release(self)

def __init__(self):

# Set up buttons
self.buttons = []
for ix, (bp, lp) in enumerate(zip(button_pins, led_pins)):
self.buttons.append(Button(ix, bp, lp))

def on_press(self, button, handler=None):
if button is None:
return

def attach_handler(handler):
button.on_press = handler

if handler is not None:
attach_handler(handler)
else:
return attach_handler

def on_release(self, button, handler=None):
if button is None:
return

def attach_handler(handler):
button.on_release = handler

if handler is not None:
attach_handler(handler)
else:
return attach_handler

def update(self):
for btn in self.buttons:
btn.update()

time.sleep(0.01)


This file needs to be saved as macropad.py (download it here) and stored in the root directory of the Pi Pico powering the pad.

## Much cleaner code

With the library in place, we just need to create the code.py file that specifies what each button does. Like in the previous version, we’ll emulate a keyboard and attach some shortcuts to each key.

from macropad import Macropad

def press_first_button(button):
print(f"pressed the first button")

def press_second_button(button):
print(f"pressed the second button")


Or in combination with the Adafruit HID library to emulate a keyboard. One caveat, using the decorator in a loop can be tricky.

import usb_hid

keyboard = Keyboard(usb_hid.devices)

button_mapping = [
[Keycode.LEFT_CONTROL, Keycode.WINDOWS, Keycode.LEFT_ARROW],
[Keycode.WINDOWS, Keycode.TAB],
[Keycode.LEFT_CONTROL, Keycode.WINDOWS, Keycode.RIGHT_ARROW],
[Keycode.LEFT_CONTROL, Keycode.F4],
[Keycode.LEFT_CONTROL, Keycode.F5],
[Keycode.LEFT_CONTROL, Keycode.F6],
[Keycode.LEFT_CONTROL, Keycode.F7],
[Keycode.LEFT_CONTROL, Keycode.F8],
[Keycode.LEFT_CONTROL, Keycode.F9]]

for btn in buttons:
def press_button(button):
print(f"pressed {button.number}")
keyboard.press(*button_mapping[button.number])

def release_button(button):
print(f"released {button.number}")
keyboard.release(*button_mapping[button.number])

while True: