Een tijdje geleden maakte ik een MacroPad en onlangs heb ik de code verbeterd! In deze post toon ik kort wat geavanceerde code om een MacroPad-klasse te maken waarmee je via een decorator eigen functies aan toetsaanslagen kunt koppelen. De oorspronkelijke code en de instructies om er zelf een te bouwen vind je in de originele post.

Voltooide MacroPad

Eventgestuurde bibliotheek

Het idee voor deze bibliotheek kreeg ik van de KeyBow 2040, die dit specifiek voor zijn eigen hardware implementeert. Daardoor wordt de code om hun toetsenblok daadwerkelijk te programmeren een stuk minder complex. In het onderstaande voorbeeld zie je dat je gewoon de gewenste functie schrijft en @keybow.on_press(key) toevoegt boven de functie die moet worden uitgevoerd wanneer key wordt ingedrukt. Eens kijken of ik dit naar mijn MacroPad kan overbrengen.

# Example code from the KeyBow 2040 GitHub Repo

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

Hiervoor moet een nieuwe bibliotheek worden gemaakt die de MacroPad kan instellen, toetsaanslagen kan verwerken en het mogelijk maakt om aan elke ingedrukte toets nieuwe functies te koppelen. Dit is een geavanceerd stukje code dat ik niet stap voor stap zal bespreken, maar het is een uitstekend voorbeeld van hoe je een bibliotheek kunt maken met een klasse waarin gebruikers hun eigen functies kunnen invoegen. Die worden dan op specifieke punten in de code van de klasse uitgevoerd. De bibliotheek regelt ook de lichteffecten, zodat de gebruiker dat zelf niet hoeft te doen.

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
    
    def fade(self, value=900):
        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)
                
        self.fade()

class Macropad(object):
    def __init__(self):
        print("Init Macropad")
        
        # 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)

Dit bestand moet worden opgeslagen als macropad.py (download het hier) en in de hoofdmap van de Pi Pico worden geplaatst die het toetsenbord aanstuurt.

Veel nettere code

Nu de bibliotheek klaar is, hoeven we alleen nog het bestand code.py te maken waarin staat wat elke knop doet. Net als in de vorige versie bootsen we een toetsenbord na en koppelen we enkele sneltoetsen aan de verschillende knoppen.

from macropad import Macropad
macropad = Macropad()
buttons = macropad.buttons

@macropad.on_press(buttons[0])
def press_first_button(button):
    print(f"pressed the first button")

@macropad.on_press(buttons[1])
def press_second_button(button):
    print(f"pressed the second button")

Of in combinatie met de Adafruit HID-bibliotheek om een toetsenbord na te bootsen. Eén kanttekening: een decorator in een lus gebruiken kan lastig zijn.

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

from macropad import Macropad

keyboard = Keyboard(usb_hid.devices)

macropad = Macropad()
buttons = macropad.buttons

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:
    @macropad.on_press(btn)
    def press_button(button):
        print(f"pressed {button.number}")
        keyboard.press(*button_mapping[button.number])

    @macropad.on_release(btn)
    def release_button(button):
        print(f"released {button.number}")
        keyboard.release(*button_mapping[button.number])
    
while True:
    macropad.update()

Mijn sneltoetsen

Ik heb drie knoppen toegewezen om tussen verschillende virtuele bureaubladen te wisselen en het overzicht te tonen. Omdat ik momenteel maar één scherm heb (zij het een behoorlijk groot), geeft de mogelijkheid om met één druk op de knop naar een ander bureaublad te gaan een ervaring die vergelijkbaar is met een opstelling met twee schermen. Mogelijk zelfs beter, want je kunt afleidende apps zoals je mail op een ander virtueel bureaublad openzetten en alleen overschakelen wanneer je daar zin in hebt. Binnenkomende mail schreeuwt zo niet voortdurend om aandacht op een tweede scherm. Ctrl + F4 sluit een browsertabblad, terwijl ctrl + F5 een webpagina volledig vernieuwt (handig tijdens het ontwikkelen).

Voor de vier andere knoppen zoek ik nog een goede toepassing. Iemand suggesties?

Conclusie

Je hebt misschien al gewerkt met bibliotheken die decorators gebruiken (zoals Flask), maar om dit zelf op te zetten is wat denk- en knutselwerk nodig. In dit geval loont het wel om een stap verder te gaan en een bibliotheek te maken die de toetsaanslagen afhandelt. Zo kun je je concentreren op de code die moet worden uitgevoerd wanneer een knop wordt ingedrukt.