NeoTrellis Explorations

Adafruit NeoTrellis is a 4x4 button+Neopixel array that can be chained on i2c bus. I am exploring its programming and uses.

Similar projects worth following
The Adafruit NeoTrellis is a 4x4 button/NeoPixel array on an i2c connected board. It is also sold as kit with acrylic case, elastomer keypad and Feather M4 processor board. There is also a kit that uses 4 boards to make an 8x8 button array. Similar devices are sold as expensive midi or video control boxes. This product lets us build a custom controller with usb, bluetooth and other connections, and perhaps some other sensors and effectors.

This project explores building and programming the NeoTrellis, especially with Circuit Python.

The Adafruit NeoTrellis RGB Driver PCB for 4x4 Keypad is a board  with i2c connections and an  array of 4x4 button pads, each  with a NeoPixel (RGB). Adafruit also sells number of accessories and kits around the board. The   Silicone Elastomer 4x4 Button Keypadacrylic case, and Feather M4 can also be purchased as a bundled kit.  You can drive the board with any i2c device, such as arduino, raspberryPi, etc. There is a kit that pairs 4 of the keypad boards with a single Feather M4 for a large 8x8 keypad.  I chose to start with a single board, the Adafruit Feather M4 and CircuitPython, mostly to explore the chip and the Circuit Python system.

Adafruit has a nice Learn project for building the kit and basic programming (arduino or Circuit Python).

Note there is a similar but very different PCB kit the Adafruit NeoTrellis M4.  This board has a 4x8 button/neopixel array, but these are directly connected to the M4, not via i2c, which as we will see makes the NeoTrellis 4x4 a very different beast.

Basic Kit Build  and Initial Coding

It was pretty easy getting the board wired  up - simple 4 soldered connections to a JST-PH 4pin connector wire. It was annoying to note that this connector is NOT the same as the 4 wire connection used by Qwiic and Stemma QT.  Those use smaller JST SH connectors.  I had purchased a number  of those for previous projects, and some new ones. I could have gone with simple soldered connections on both ends, but I really prefer at least one connector so parts  can be separated easily.

Building the case was also pretty easy, following the Learn project.  I did have to do it at least twice, as one of the solder connections broke.  I found it wasnt too hard to pick up the outer acrylic in one piece if you squeeze a bit.  Once assembled it looks good.

Programming the basic example from the Learn also went pretty well.  On a previous project, I found the MU Editor is very nice for these boards, but the  'autoreload' feature gets triggered every second or so by something on my PC. (someday i need to track that down).  There is a simple fix for this.  Insert the following  code snippet at the beginning of the program:

import supervisor


I pulled the code bundle from the Learn pages and THEN went back and read the bits before it.  The example code uses the common code for initializing the i2c connection:

import board
# create the i2c object for the trellis
i2c_bus = board.I2C()

However, this does NOT work on a Feather M4, because apparently the I2C() function is not implemented in the board module for Feather M4 boards.  So you MUST use the alternative:

from board import SCL, SDA
import busio

#create the i2c object for the trellis
i2c_bus = busio.I2C(SCL, SDA)

I had a similar issue with the raspberryPi Pico.  Not sure why but guess the rather impressive team at Adafruit/CircuitPython are a  bit busy.  Note that while SCL and SDA are defined in board, these are only the primary i2c pins.  Many boards support alternative pins and you can easily use those.

Once those two hiccups were fixed, the example worked just fine.  It blinks the corresponding neoPixel when a button is pressed. Not super impressive but a decent starting example.

Code for project at this state is in github repository at examples/

But its Boring. and doesnt communicate.

Communication is pretty easy as a MIDI device. Did that on last project, although with an rPi Pico, but still with CircuitPython its pretty high level setup.  There are examples for doing HID interfaces (look like a USB keyboard, joystick, mouse, etc). So I'll save that for later.

Blinking LED Animations - THATS why we use NeoPixels

Better Blinking. Thats what we use NeoPixels for! So time to dig into neopixel coding with...

Read more »

  • Official Seesaw Updated

    Jerry Isdale07/25/2022 at 19:39 0 comments

    The official Adafruit_CircuitPython_seesaw library has been updated with the NeoPixel changes from NeraDoc that made this project possible.  Update works fine, so no need to get the branch anymore!

  • Adding Midi NoteOn/NoteOff

    Jerry Isdale07/20/2022 at 02:33 0 comments

    So far the neotrellis is a somewhat pretty blinkie toy.  We can make it a bit more useful by having each key send a different Midi Note over USB.  Press and send Midi.NoteOn, Release and send Midi.NoteOff.   A PC (mac etc) application can receive the MIdi data and use it for application control (music synthesizer, video switcher commands, etc).  We did the same thing back in theAstral TV midi controller project that inspired this one.

    The new modular design makes it really easy to implement.  We add a module called that exposes three methods:

    • setup_midi() - sets up the module by creating an adafruit_midi.MIDI object
    • send_note_on(idx) - sends a NoteOn message with requested note index
    • and send_note_off()- sends a NoteOff message with requested note index

    A couple module constants let the client application chose the midi channel and notes associated with the 16 keys. Simply edit these values and rerun the application.  A fancier version might read them at runtime from a configuration file.

    import usb_midi
    import adafruit_midi
    from adafruit_midi.note_on import NoteOn
    from adafruit_midi.note_off import NoteOff
    # constants for channel, velocity and notes to associate with keys
    # caveat: programmers count channels from 0, normal folks count from 1
    # you can see difference viewing midi traffic using
    # we send zero, it receives 0 but prints out 1
    __midi_channel = 0
    # 16 midi notes to associate with keypad by number
    __midiNotes = [
        60, 61, 62, 63,
        64, 65, 66, 67,
        68, 69, 70, 71,
        72, 73, 74, 75,
    __midi = None
    def setup_midi():
        global __midi, __midi_channel
        #  MIDI setup as MIDI out device
        print("setup midi out device")
        __midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=__midi_channel)
    def send_note_on(idx):
        global __midi
        print("YO send_note_on", idx, __midi)
        if __midi is None:
            print("Midi not defined")
            print("send midi NoteOn ", idx, __midiNotes[idx], __midi)
            __midi.send(NoteOn(__midiNotes[idx], 120))
    def send_note_off(idx):
        global __midi
        if __midi is None:
            print("Midi not defined")
            __midi.send(NoteOff(__midiNotes[idx], 120))
            print("send midi NoteOff ", idx, __midiNotes[idx])

    That's all there is to the midi module. It is pretty simple with more data checking and print statements than real function stuff.  Adding it to the rest of the application is also pretty simple - add four lines to the module.

    First add the import at the top

    import neotrellis_midi

    Then in setup_keypad(), call the setup_midi()


    Then down in doKey(), we add lines to send NoteOn and NoteOff.  In the NeoTrellis.EDGE_RISING block, we add


    and then in the NeoTrellis.EDGE_FALLING block, we add


    Thats it.   The neotrellis will now send the midi commands.

    You can use a midi enabled browser, like Google Chrome, to view MIDI traffic on the MidiView web page

    It is interesting to note that while we sending data on midi channel 0, the tool reports it as channel 1.  This is because musicians and other normal people count from 1, while programmers start with zero.  It is the 0 index into the array, right?

    Anyway the neotrellis has now become a useful midi keypad device.  yay!!

  • Walkthru neotrellis_keypad module

    Jerry Isdale07/19/2022 at 19:27 0 comments

    The module encapsulates the actions that happen when a key is pressed. A client application need only call setup_keypad() and then in the forever loop, call trellis.sync().  Internally, the doKey() function is associated with each of 16 keys on the neotrellis, and is activated

    After the required imports there are a couple module variables. keyColors is an array of  Color, one for each of the 16 keys (hopefully, we dont check size, etc).  keyAnimations is likewise an array associating each key with an animation. 

    The setup_keypad() function fills in the globals and sets all 16 neotrellis keys to call doKey() when they are pressed and again when released.

    from adafruit_neotrellis.neotrellis import NeoTrellis
    import adafruit_led_animation.color as Color
    # local modules
    import onboard_neopixel
    import neotrellis_animations
    keyColors = None
    keyAnimations = None
    __trellis = None
    # could be module but short enough for inline
    # arrays to map key index to animation and colors
    def setup_keypad(trellis):
        global keyColors
        global keyAnimations
        global __trellis
        __trellis = trellis
        keyColors = neotrellis_animations.rainbowPalette
        keyAnimations = neotrellis_animations.trellisAnimations
        # associate 16 trellis keys with doKey() for both press and release
        for i in range(16):
            # activate rising edge events on all keys; key pressed
            __trellis.activate_key(i, NeoTrellis.EDGE_RISING)
            # activate falling edge events on all keys; key released
            __trellis.activate_key(i, NeoTrellis.EDGE_FALLING)
            # set all keys to trigger the doKey() callback
            __trellis.callbacks[i] = doKey
        # --------------- Ready for Main Loop ------------
        # but first lets print out the key colors
        print("key colors", keyColors)
        for clr in keyColors:
            print(hex(clr), end=", ")

    The doKey() is a callback function. It is triggered by trellis.sync() and given an event.  The event holds a couple entries of interest: edge and number. Depending on whether event.edge shows key was just pressed or just released, we do different actions.

    On key press, we set the onboard_neopixel to the key's associated color - by indexing keyColors[] with the event.number. The current animation is frozen (stops updating on animations.animate() call in forever loop). We black out all 16 pixels and set the key's pixel to its keyColor.  Lastly, we blink the onboard pixel, which introduces a blocking delay.  May want to remove that for a responsive application.

    Nothing changes while the key is held down, but when it is released, we set the current_animation to the key's associated keyAnimation. It will be run on then next animate() call in forever loop.

    Pretty simple. Fairly straight forward to modify for future functions.  Like, maybe, send a midi note like the Astral TV rPi Pico midi remote did?

    # doKey() will be called when button events are received
    def doKey(event):
        print("\nKeyEvent: ", str(event), " event number",event.number, " edge:",event.edge)
        if event.edge == NeoTrellis.EDGE_RISING:
            # pressed: toggle, stop/freeze current animation, color my pixel
            onboard_neopixel.on_board_neopixel[0] = keyColors[event.number]
            # freeze current animation, set all to black, just the one to its keyColor
            __trellis.pixels[event.number] = keyColors[event.number]
            print("pixel color", hex(keyColors[event.number]))
            # blink onboard with same color
        # start animationwhen a falling edge is detected
        elif event.edge == NeoTrellis.EDGE_FALLING:
            onboard_neopixel.on_board_neopixel[0] = Color.BLACK
            #trellis.pixels[event.number] = Color.BLACK
            print("new animation", neotrellis_animations.current_animation)
    Read more »

  • Walkthru module

    Jerry Isdale07/19/2022 at 08:27 0 comments

    TThe file (module) encasulates the neopixels in the 4x4 key pad matrix.  Note these are NOT the conventional NeoPixel class.  They are accessed through the Adafruite_seesaw.neopixel version.  At time of project (and this writeup), the release version of adafruit_seesaw package (library) has its own independent NeoPixel class. Of particular annoyance is this class does not derive from Adafruit_CircuitPython_PixelBuf class, which means it cannot be used as a direct replacement for Adafruit_CircuitPython_NeoPixel, especially in the rather nice Adafruit_led_animations package. As mentioned in main project details, Neradoc has a pull request for their version of seesaw that does derive its NeoPixel class from PixelBuf.  The github's lib folder contains a snapshot of that fork of the seesaw package.

    The module starts off with the imports and a few module global variables. We pull in all 13 of the current animations.

    from adafruit_led_animation.animation.blink import Blink
    from adafruit_led_animation.animation.sparklepulse import SparklePulse
    from adafruit_led_animation.animation.comet import Comet
    from import Chase
    from adafruit_led_animation.animation.pulse import Pulse
    from adafruit_led_animation.animation.sparkle import Sparkle
    from adafruit_led_animation.animation.rainbowchase import RainbowChase
    from adafruit_led_animation.animation.rainbowsparkle import RainbowSparkle
    from adafruit_led_animation.animation.rainbowcomet import RainbowComet
    from adafruit_led_animation.animation.solid import Solid
    from adafruit_led_animation.animation.colorcycle import ColorCycle
    from adafruit_led_animation.animation.rainbow import Rainbow
    from adafruit_led_animation.animation.customcolorchase import CustomColorChase
    from adafruit_led_animation.sequence import AnimationSequence
    from adafruit_led_animation import helper
    import adafruit_led_animation.color as Color
    from onboard_neopixel import blinkOnBoardPixel
    # a local reference to the neotrellis.pixels PixelBuf derived NeoPixel
    __trellis_pixels = None
    __allBlack = None
    __trellis_pixel_columns = None
    __trellis_pixel_rows = None

     Next up we define a list, rainbowPalette, that has 16 somewhat contrasting colors. These will later be available to map to default key colors. Selecting these colors could be a whole huge discussion.

    # we have Color.BLACK as well as the other color names (and colorwheel() ) from led_animations
    # we need a 16 color palette that does NOT contain black - one for each button in keypad
    rainbowPalette = [
        0xa0002, 0x80004, 0x50007, 0x30009,
        0xb, 0x10b, 0x308, 0x606,
        0x804, 0xb01, 0xb00, 0x30900,
        0x50700, 0x70400, 0xa0200, 0xc0000
    def print_rainbowPalette():
        print("Rainbow Palette:", rainbowPalette)
        print("Rainbow Palette: ", end=" ")
        for clr in rainbowPalette:
            print(hex(clr), end=", ")
    def show_rainbowPalette():
        global __trellis_pixels
        print("RainbowPalete blinking on board pixel")
        for clr in rainbowPalette:
            print("clr ", hex(clr))
            BLINK_COLOR = clr

     Then we define a couple more public module variables. current_animation references the currently active animation. trellisAnimations[] is a list of all the locally created animations. The first 16 entries will be used over in our neotrellis_keypad module.  They are associated by index order with the 16 keys.

    # define some module variables
    current_animation = None
    trellisAnimations = []

    the setup_animations(neotrellis):method comes next. It takes a neotrellis object parameter and defines a number  of  animations. The code is almost directly from the Adafruit_led_animations.examples.led_animation_all_animations demonstration program.

    setup_animations() is the main entry. It takes a NeoTrellis as a parameter and fills in the globals.  The animations are created as local named objects in setup_animations(),...

    Read more »

  • Walkthru module

    Jerry Isdale07/19/2022 at 08:08 0 comments

    The module encapsulates functions that access the single pixel neopixel strip that is on some CircuitPython boards. Most of the Adafruit link include these.  If the current board has one of these, the board module will include a member NEOPIXEL.  We import the required modules, define some module globals, and then define the setup_onboard_neopixel() function.

    This function checks for existence of the neopixel, and if supported, creates an instance of the usual NeoPixel class with a single pixel on the member variable "on_board_neopixel". It prints some status to the console and then invokes a local function to blink the pixel in the default BLINK_COLOR

    import board
    import neopixel
    import time
    import adafruit_led_animation.color as Color
    BLINK_COLOR = (100, 50, 150)  # color to blink, allow change
    DELAY = 0.25  # blink rate in seconds
    on_board_neopixel = None
    def setup_onboard_neopixel():
        global on_board_neopixel
        # Create the NeoPixel object for the onBoard pixel,
        # check if processor board supports it
        # note this will be regular NeoPixel strip of length 1, not a seesaw.NeoPixel like on neotrellis
        if 'NEOPIXEL' in dir(board):
            on_board_neopixel = neopixel.NeoPixel(board.NEOPIXEL, 1, pixel_order=neopixel.GRB)
            print("Board have onboard NEOPIXEL", on_board_neopixel)
            print("Board does NOT have onboard NEOPIXEL")
        # blink it once to show we here

     That blinkOnBoardPixel() function is a basic neopixel blink() using time.sleep(). Clients can specify the blink color, or use the default.

    def blinkOnBoardPixel(color=BLINK_COLOR):
        global on_board_neopixel
        if on_board_neopixel:
            on_board_neopixel[0] = (0,0,0)
            print("No onBoardPixel to blink")

     Lastly we define toggleOnBoardPixel() which toggles between the blink color and black on successive calls.

    __onboardStatus = False
    def toggleOnBoardPixel(color=BLINK_COLOR):
        global __onboardStatus
        global on_board_neopixel
        if on_board_neopixel:
            if __onboardStatus:
                on_board_neopixel.fill((0, 0, 0))
                __onboardStatus = False
                __onboardStatus = True

     Thats a pretty simple module.  Function names are not the standard lower_case_with_underscore Python convention but I'm more used to CamelCase style.

    The module should work with other processor boards and other projects. I'll have to buy/build some to find out.

  • Refactored code_NeotrellisExplorations and

    Jerry Isdale07/19/2022 at 07:52 0 comments

    The examples folder has single application ( that runs the full set of animations tied to key presses.  That code irked me as hard to explain.  I like clean modular code.  So I split that program up into 4 files:

    • is the main program. It does the setup and has the infinite loop.   copy it to to run on a Circuit Python board
    • handles the single pixel Neopixel on many CircuitPython boards, like the Adafruit Feather M4 Express. It should work ok if run on a board that doesn't have such pixel (eg. Raspberry Pi Pico)
    • encapsulates the animations that run across the 4x4 matrix
    • encapsulates the key handling

    This log will go over the main file with subsequent project logs discussing the other modules.

    The first bit of is the bit discussed earlier to disable the autoreload feature of CircuitPython. Then it imports the required system and local modules

    import time
    import board
    import busio
    from adafruit_neotrellis.neotrellis import NeoTrellis
    # local modules
    import onboard_neopixel
    import neotrellis_animations
    import neotrellis_keypad

     we then make a call setup the onboard_neopixel

    # setup single pixel strip if board has it, blink it once

    Then initialize the neotrellis board:

    # create the i2c object for the trellis
    # note the use of busio.I2C() instead of board.I2C()
    # apparently this is an issue for M4 (rPi Pico too?)
    i2c_bus = busio.I2C(board.SCL, board.SDA)
    trellis = NeoTrellis(i2c_bus)

     Two simple calls setup the animations and keypad handling

    # setup animation and keypad modules

    The remainder of is the infinite loop. Basically this updates the animation and then handles keypad activity with trellis.sync().  There is a bunch of print stuff so we can see that program continues to run when reading the REPL (serial link) in MU Editor

    print("Setup Complete enter forever loop ", neotrellis_animations.current_animation)
    i = 0
    while True:
        # tell animation to update
        # call the sync function call any triggered callbacks
        # the trellis can only be read every 17 milliseconds or so
        # really? the neopixel _getItem() could be an issue for i2c/seesaw connected neopixels
        # print out something so debug console watcher knows program is running
        # also might give keyboard interrupt (Ctrl C) a chance to pause CircuitPython
        i +=1
        if i%50 == 0:
            print(i, end='.')
        if i%10000 == 0:
            print(i, "reset")

    Using modules for the app makes the top level easier to read and understand. Hopefully.

View all 6 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates