HID Keyboard or MIDI Keyboard?

The code for this project is not working well, and we do not recommend running the code as is for now. We will be working on improved code.

The code is written so that you can use the keypad as either an HID keyboard or a MIDI controller.

After importing the libraries, you can set either keyboard_mode or midi_mode to True to choose your device type.

import board
import displayio
import digitalio
from adafruit_st7789 import ST7789
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import usb_midi
import adafruit_midi
from adafruit_midi.note_on          import NoteOn
from adafruit_midi.note_off         import NoteOff

#  if you want to use this as an HID keyboard, set keyboard_mode to True
#  otherwise, set it to False
keyboard_mode = True
#  if you want to use this as a MIDI keyboard, set midi_mode to True
#  otherwise, set it to False
midi_mode = False

Setup for HID Keyboard Shortcuts

You can also customize what each key does in each mode. The default settings for keyboard_mode are shortcuts for save, cut, copy and paste. You can modify these in the if statement, if keyboard_mode:. The variables assigned to the keycodes for each key are stored in the shortcuts array.

#  change keyboard shortcuts here
#  defaults are shortcuts for save, cut, copy & paste
#  comment out ctrl depending on windows or macOS
if keyboard_mode:
    keyboard = Keyboard(usb_hid.devices)
    #  modifier for windows
    ctrl = Keycode.CONTROL
    #  modifier for macOS
    #  ctrl = Keycode.COMMAND
    key0 = Keycode.S
    key1 = Keycode.X
    key2 = Keycode.C
    key3 = Keycode.V
    shortcuts = [key0, key1, key2, key3]

Setup for MIDI Notes

The default MIDI notes for midi_mode are setup in the if statement if midi_mode: and are stored in the midi_notes array.

#  change MIDI note numbers here
if midi_mode:
    midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
    midi_notes = [60, 61, 62, 63]

SPI TFT Screen Setup

The keypad uses a TFT screen that uses SPI to communicate with the QT Py RP2040. This requires defining pins for tft_cs, tft_dc and reset in the code.

#  spi display setup
spi = board.SPI()
tft_cs = board.D7
tft_dc = board.D5

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=board.D6

#  display setup
display = ST7789(display_bus, width=240, height=240, rowstart=80)

Party Parrot Bitmap Setup

The party parrot bitmap is sized so that each tile is 240x240 to fill the TFT screen. This makes for a larger than normal bitmap file since it has 10 tiles (that's 240x2400!). The code is using OnDiskBitmap() to load the bitmap so that you don't run into any memory allocation issues.

# CircuitPython 6 & 7 compatible
#  bitmap setup
bitmap = displayio.OnDiskBitmap(open("/parrot-240-sheet.bmp", "rb"))

# Create a TileGrid to hold the bitmap
parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()),
                                 tile_height=240, tile_width=240)

# # CircuitPython 7+ compatible
# bitmap = displayio.OnDiskBitmap("/parrot-240-sheet.bmp")

# Create a TileGrid to hold the bitmap
# parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,
#                                tile_height=240, tile_width=240)

# Create a Group to hold the TileGrid
group = displayio.Group()

# Add the TileGrid to the Group

# Add the Group to the Display

Key Input Setup

The keys are setup as digital inputs using a for statement that iterates through the key_pins array. This array has the QT Py RP2040's pins that the keys are connected to.

#  digital pins for the buttons
key_pins = [board.A0, board.A1, board.A2, board.A3]

#  array for buttons
keys = []

#  setup buttons as inputs
for key in key_pins:
    key_pin = digitalio.DigitalInOut(key)
    key_pin.direction = digitalio.Direction.INPUT
    key_pin.pull = digitalio.Pull.UP

Variables and States

p and a are variables that are used in the loop to track the index position of the tile grid. This advances the party parrot animation on the screen.

The key_states array is used to track the states of the four keys for debouncing. When a key is not pressed, its state is False and when it is pressed, its state is True.

p = 0 #  variable for tilegrid index
a = 0 #  variable for tile position

#  states for buttons
key0_pressed = False
key1_pressed = False
key2_pressed = False
key3_pressed = False

#  array for button states
key_states = [key0_pressed, key1_pressed, key2_pressed, key3_pressed]

The Loop

At the beginning of the loop, the tile grid's index is set to p. This will allow for the tile grid to advance as p's value changes.

while True:
    #  default tile grid position
    parrot0_grid[a] = p

Pressing a Key

If one of the keys is pressed and its corresponding state is False, then p increases in value by 1 and its state is updated to True.

#  iterate through 4 buttons
    for i in range(4):
        inputs = keys[i]
        #  if button is pressed...
        if not inputs.value and key_states[i] is False:
            #  tile grid advances by 1 frame
            p += 1
            #  update button state
            key_states[i] = True


If the keypad is setup to be in midi_mode, then a NoteOn message is sent for the corresponding MIDI note number from the midi_notes array.

#  if a midi keyboard...
            if midi_mode:
                #  send NoteOn for corresponding MIDI note
                midi.send(NoteOn(midi_notes[i], 120))

HID Keyboard Mode

If the keypad is setup to be in keyboard_mode, then keyboard.send() is used to send the keycode(s) that correspond to the pressed key.

#  if an HID keyboard...
            if keyboard_mode:
                #  send keyboard output for corresponding keycode
                #  the default includes a modifier along with the keycode
                keyboard.send(ctrl, shortcuts[i])

Reset the Party

Since the party parrot's tile grid has 10 tiles, it needs to have its index reset to 0 when it goes above 9.

#  if the tile grid's index is at 9...
            if p > 9:
                #  reset the index to 0
                p = 0

Releasing a Key

When you release a key and the key's state is True, the state is updated to False to reset its state.

Additionally, if you're in midi_mode, a NoteOff message is sent for the corresponding MIDI note number. An equivalent action is not needed for the keyboard_mode since keyboard.send() includes a release message.

#  if the button is released...
        if inputs.value and key_states[i] is True:
            #  update button state
            key_states[i] = False
            #  if a midi keyboard...
            if midi_mode:
                #  send NoteOff for corresponding MIDI note
                midi.send(NoteOff(midi_notes[i], 120))

This guide was first published on Jun 29, 2021. It was last updated on Jun 29, 2021.

This page (CircuitPython Code Walkthrough) was last updated on Jun 27, 2021.

Text editor powered by tinymce.