If you are new to CircuitPython, we recommend you start with the Welcome to CircuitPython Guide then come back here.

Adafruit recommends installing and using the Mu editor on your computer to edit CircuitPython files. Mu is available for Windows, macOS, and Linux. You can learn about Mu in this guide.

CircuitPython Preparation

To prepare the NeoTrellis M4 to run the code, follow these steps:

  • Update the bootloader for NeoTrellis from the Trellis M4 guide
  • Install the latest version of CircuitPython (at least 4.0.0 Alpha 3) for NeoTrellis M4.

Click the link above to download the latest UF2 file.

Download and save it to your desktop (or wherever is handy).

Plug your NeoTrellis M4 Express into your computer using a known-good USB cable.

A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.

Double-click the Reset button next to the USB connector on your board, and you will see the status DotStar RGB LED turn green. If it turns red, check the USB cable, try another USB port, etc.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

You will see a new disk drive appear called TRELM4BOOT.

Drag the adafruit_circuitpython_etc.uf2 file to TRELM4BOOT.

The LED will flash. Then, the TRELM4BOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

  • Get the latest CircuitPython library pack matching your version of CircuitPython and save it onto your hard drive. You will need three .mpy files and one folder within the library pack for this guide. Drag the files listed below over into the /lib folder on CIRCUITPY:
    • adafruit_trellism4.mpy
    • adafruit_hid folder
    • neopixel.mpy
    • adafruit_matrixkeypad.mpy
USB keyboards and mice show up on your computer as 'HID' devices, which stands for 'Human Interface Device'

HID Keyboard Basics

This guide page has a great intro to CircuitPython HID Keyboard.

For even more details, check out the documentation at https://circuitpython.readthedocs.io/projects/hid/en/latest/ which includes all of the keycodes and media codes you can use.

First, we'll import the adafruit_hid library into our program. This will allow us to make calls to send keyboard keys and media keys.

Download: file
from adafruit_hid.keyboard import Keyboard
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)
cc = ConsumerControl(usb_hid.devices)

Keyboard Press/Release

Now we can send this command to "type" the letter 'a':

kbd.press(Keycode.A)

kbd.release(Keycode.A)

This would send a lowercase 'a' to the computer just as if you had typed it yourself. To send a capital 'A', we'd add the shift key to the command like this:

kbd.press(Keycode.SHIFT, Keycode.A)

kbd.release(Keycode.SHIFT, Keycode.A)

This is pretty cool, since it means we can layer on lots of keys all at the same time, just like you do on your physical keyboard when using keyboard shortcuts!

So, if there's some keyboard shortcut you want to use (or create for yourself in something like Quicksilver or AutoKeys) that is command+option+ctrl+a the CircuitPython code would look like this:

kbd.press(Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.A)

kbd.release(Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.A)

The adafruit_hid library allows for operating system specific names such as 'Keycode.COMMAND' on macOS which is 'Keycode.WINDOWS' on Windows. Or, you can use the generic 'Keycode.GUI' on any operating system. Same goes for 'ALT/OPTION'

Media Control

There is a second command we'll use when we want to adjust volume, play/pause, skip tracks, and so on with media such as songs and videos. These are often represented on a physical keyboard as icons silkscreened onto the rightmost function keys.

In USB HID speak, these are known as "Consumer Control codes". To play or pause a track we'll use this command:

cc.send(ConsumerControlCode.PLAY_PAUSE)

Launch Deck Code

Here's the full code for the Launch Deck. Copy it and then paste it into Mu and save it to your Trellis M4 as code.py

Now, you'll need to edit the keycodes sent in the code to the ones you want to use! Just remember to match them up with your app launcher, such as Quicksilver, AutoKeys, or whatever you choose!

#  Launch Deck Trellis M4
#  USB HID button box for launching applications, media control, camera switching and more
#  Use it with your favorite keyboard controlled launcher, such as Quicksilver and AutoHotkey

import time
import random
import adafruit_trellism4
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

# Rotation of the trellis. 0 is when the USB is upself.
# The grid coordinates used below require portrait mode of 90 or 270
ROTATION = 270

# the two command types -- MEDIA for ConsumerControlCodes, KEY for Keycodes
# this allows button press to send the correct HID command for the type specified
MEDIA = 1
KEY = 2
# button mappings
# customize these for your desired postitions, colors, and keyboard combos
# specify (button coordinate): (color hex value, command type, command/keycodes)
keymap = {
    (0,0): (0x001100, MEDIA, ConsumerControlCode.PLAY_PAUSE),
    (1,0): (0x110011, MEDIA, ConsumerControlCode.SCAN_PREVIOUS_TRACK),
    (2,0): (0x110011, MEDIA, ConsumerControlCode.SCAN_NEXT_TRACK),
    (3,0): (0x000033, MEDIA, ConsumerControlCode.VOLUME_INCREMENT),

    (0,1): (0x110000, MEDIA, ConsumerControlCode.MUTE),
    # intentional blank button
    # intentional blank button
    (3,1): ((0,0,10), MEDIA, ConsumerControlCode.VOLUME_DECREMENT),

    (0,2): (0x551100, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.ONE)),
    (1,2): (0x221100, KEY, (Keycode.CONTROL, Keycode.SHIFT, Keycode.TAB)),  # back cycle tabs
    (2,2): (0x221100, KEY, (Keycode.CONTROL, Keycode.TAB)),  # cycle tabs
    (3,2): (0x333300, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.TWO)),

    (0,3): (0x001155, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.THREE)),
    # intentional blank button
    # intentional blank button
    (3,3): (0x330000, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.FOUR)),

    (0,4): (0x005511, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.FIVE)),
    (1,4): (0x440000, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.SIX)),
    # intentional blank button
    (3,4): (0x003300, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.EIGHT)),

    (0,5): (0x222222, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.W)),
    (1,5): (0x000044, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.E)),
    # intentional blank button
    (3,5): (0x332211, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.T)),

    (0,6): (0x001133, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.C)),
    (1,6): (0x331100, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.V)),
    (2,6): (0x111111, KEY, (Keycode.GUI, Keycode.SHIFT, Keycode.FOUR)),  # screen shot
    (3,6): (0x110000, KEY, (Keycode.GUI, Keycode.ALT, Keycode.CONTROL, Keycode.N)),

    (0,7): (0x060606, KEY, (Keycode.GUI, Keycode.H)),  # hide front app, all windows
    (1,7): (0x222200, KEY, (Keycode.GUI, Keycode.GRAVE_ACCENT)),  # cycle windows of app
    (2,7): (0x010001, KEY, (Keycode.GUI, Keycode.SHIFT, Keycode.TAB)),  # cycle apps backards
    (3,7): (0x010001, KEY, (Keycode.GUI, Keycode.TAB))}  # cycle apps forwards


# Time in seconds to stay lit before sleeping.
TIMEOUT = 90

# Time to take fading out all of the keys.
FADE_TIME = 1

# Once asleep, how much time to wait between "snores" which fade up and down one button.
SNORE_PAUSE = 0.5

# Time in seconds to take fading up the snoring LED.
SNORE_UP = 2

# Time in seconds to take fading down the snoring LED.
SNORE_DOWN = 1

TOTAL_SNORE = SNORE_PAUSE + SNORE_UP + SNORE_DOWN

kbd = Keyboard(usb_hid.devices)
cc = ConsumerControl(usb_hid.devices)

trellis = adafruit_trellism4.TrellisM4Express(rotation=ROTATION)
for button in keymap:
    trellis.pixels[button] = keymap[button][0]

current_press = set()
last_press = time.monotonic()
snore_count = -1
while True:
    pressed = set(trellis.pressed_keys)
    now = time.monotonic()
    sleep_time = now - last_press
    sleeping = sleep_time > TIMEOUT
    for down in pressed - current_press:
        if down in keymap and not sleeping:
            print("down", down)
            # Lower the brightness so that we don't draw too much current when we turn all of
            # the LEDs on.
            trellis.pixels.brightness = 0.2
            trellis.pixels.fill(keymap[down][0])
            if keymap[down][1] == KEY:
                kbd.press(*keymap[down][2])
            else:
                cc.send(keymap[down][2])
            # else if the entry starts with 'l' for layout.write
        last_press = now
    for up in current_press - pressed:
        if up in keymap:
            print("up", up)
            if keymap[up][1] == KEY:
                kbd.release(*keymap[up][2])

    # Reset the LEDs when there was something previously pressed (current_press) but nothing now
    # (pressed).
    if not pressed and current_press:
        trellis.pixels.brightness = 1
        trellis.pixels.fill((0, 0, 0))
        for button in keymap:
            trellis.pixels[button] = keymap[button][0]

    if not sleeping:
        snore_count = -1
    else:
        sleep_time -= TIMEOUT
        # Fade all out
        if sleep_time < FADE_TIME:
            brightness = (1 - sleep_time / FADE_TIME)
        # Snore by pausing and then fading a random button up and back down.
        else:
            sleep_time -= FADE_TIME
            current_snore = int(sleep_time / TOTAL_SNORE)
            # Detect a new snore and pick a new button
            if current_snore > snore_count:
                button = random.choice(list(keymap.keys()))
                trellis.pixels.fill((0, 0, 0))
                trellis.pixels[button] = keymap[button][0]
                snore_count = current_snore

            sleep_time = sleep_time % TOTAL_SNORE
            if sleep_time < SNORE_PAUSE:
                brightness = 0
            else:
                sleep_time -= SNORE_PAUSE
                if sleep_time < SNORE_UP:
                    brightness = sleep_time / SNORE_UP
                else:
                    sleep_time -= SNORE_UP
                    brightness = 1 - sleep_time / SNORE_DOWN
        trellis.pixels.brightness = brightness
    current_press = pressed

Customizing

In order to customize the Launch Deck, you'll want to know a few things about how the Trellis M4 code in CircuitPython works. This guide is a great place to read up on all the details, but we'll go over a few specifics here.

Single, Unmodified Keystrokes

If you want to use the Launch Deck to send a single, unmodified keystroke, we need to enclose it in brackets. This is because of the code's expectation of a tuple.

So, to send a letter g with the bottom, left button in portrait orientation, this would be the command (replacing line 61 in the code above):

(0, 7): (0x052405, KEY, ([Keycode.G])),

Button Coordinates

The Trellis M4 can be oriented in landscape (wide) or portrait (tall) modes. We've set the rotation to portrait mode, so the button grid coordinate system consists of eight rows of four buttons. Here's an example with the rotation set at 90.

Add a Button

Let's say you wanted to add a button to the second row. Currently, there are two unused ones in that row in our example code.

This is the code currently:

(0,1): (0x110000, MEDIA, ConsumerControlCode.MUTE),
# intentional blank button
# intentional blank button
(3,1): ((0,0,10), MEDIA, ConsumerControlCode.VOLUME_DECREMENT),

Here you can see we've made it easy to drop in two more buttons by commenting out the second and third lines.

So, let's add a shortcut to the second button that is green and can be used to force a reload on a web page in a web browser inside of Windows (you can also add this on a macOS machine by following these instructions). That's the ctrl+F5 key combo.

First, we need to specify the button coordinate: (1,0):

Then, we'll specify the green color. You can use an RGB color value such as (0,50,0) or a hexidecimal color value. (0x004400) You don't want them too bright, so these are dimmer green values.

Next, we specify that this is a keycode, rather than a media key with the KEY flag.

Lastly, we list out any keys that will be held down simultaneously, in this case (Keycode.CONTROL, Keycode.F5)

This is the full line of code: (1,1): (0x004400, KEY, (Keycode.CONTROL, Keycode.F5))

And, here it is inserted among the rest of that row:

(0,1): (0x110000, MEDIA, ConsumerControlCode.MUTE),
(1,1): (0x004400, KEY, (Keycode.CONTROL, Keycode.F5)),
# intentional blank button
(3,1): ((0,0,10), MEDIA, ConsumerControlCode.VOLUME_DECREMENT),

Edit a Button

If you want to edit the functionality or color of an existing button, simply edit that section. For example, to set the mute button to blue:

(0,1): (0x000011, MEDIA, ConsumerControlCode.MUTE),

Button Removal

To remove a button you can simply delete the line of code, or just comment it out with a '#' symbol like this:

# (0,1): (0x110000, MEDIA, ConsumerControlCode.MUTE),
(1,1): (0x004400, KEY, (Keycode.CONTROL, Keycode.F5)),
# intentional blank button
(3,1): ((0,0,10), MEDIA, ConsumerControlCode.VOLUME_DECREMENT),

That will effectively remove the first button of the second row.

Layout Tips

Here's how I have mine laid out. Here are a few tips for an easy-to-use Launch Deck:

  • Color code buttons to their icons
  • Group like things together in rows or columns. The entire upper two row set is used for media control here
  • Notice the brighter colored Vol + above the dimmer Vol - which mimics a remote control layout
  • Leave some space. Blank buttons help serve as visual landmarks for the important functional buttons nearby

This guide was first published on Dec 01, 2018. It was last updated on Dec 01, 2018.

This page (Code with CircuitPython) was last updated on Nov 06, 2020.