-
Official Seesaw Updated
07/25/2022 at 19:39 • 0 commentsThe 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
07/20/2022 at 02:33 • 0 commentsSo far the neotrellis is a somewhat pretty blinkie toy. We can make it a bit more useful by turning it into a 16 button Midi keypad. That is 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 the Astral TV midi controller project that inspired this one.
The new modular design makes it really easy to implement.using the adafruit_midi and usb_midi libraries. We added a module to our project called neotrellis_midi.py. It 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 # https://www.kilpatrickaudio.com/apps/midiview/ # 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") else: 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") else: __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 neotrellis_keypad.py module.
First add the import at the top
import neotrellis_midi
Then in setup_keypad(), call the setup_midi()
neotrellis_midi.setup_midi()
Then down in doKey(), we add lines to send NoteOn and NoteOff. In the NeoTrellis.EDGE_RISING block, we add
neotrellis_midi.send_note_on(event.number)
and then in the NeoTrellis.EDGE_FALLING block, we add
neotrellis_midi.send_note_off(event.number)
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 https://www.kilpatrickaudio.com/apps/midiview/
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
07/19/2022 at 19:27 • 0 commentsThe neotrellis_keypad.py 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=", ") print("")
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] #toggleOnBoardPixel() # freeze current animation, set all to black, just the one to its keyColor neotrellis_animations.freeze() __trellis.pixels.fill(Color.BLACK) __trellis.pixels[event.number] = keyColors[event.number] print("pixel color", hex(keyColors[event.number])) __trellis.pixels.show() # blink onboard with same color onboard_neopixel.blinkOnBoardPixel(keyColors[event.number]) # start animationwhen a falling edge is detected elif event.edge == NeoTrellis.EDGE_FALLING: #toggleOnBoardPixel() onboard_neopixel.on_board_neopixel[0] = Color.BLACK __trellis.pixels.fill(Color.BLACK) #trellis.pixels[event.number] = Color.BLACK neotrellis_animations.set_animation_byIndex(event.number) neotrellis_animations.current_animation.resume() print("new animation", neotrellis_animations.current_animation)
-
Walkthru neotrellis_animations.py module
07/19/2022 at 08:27 • 0 commentsTThe file (module) neotrellis_animations.py 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 adafruit_led_animation.animation.chase 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=", ") print() def show_rainbowPalette(): global __trellis_pixels print_rainbowPalette() print("RainbowPalete blinking on board pixel") for clr in rainbowPalette: print("clr ", hex(clr)) BLINK_COLOR = clr __trellis_pixels.fill(clr) blinkOnBoardPixel()
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(), with references hidden away in trellisAnimations[]. We do keep a module reference to the Solid Black animation in __allBlack. There is likely a better organization of references, but that will really depend on how the code gets reused for a real neotreillis product.
# this init function creates a bunch of animations in the trellisAnimations[] def setup_animations(neotrellis): global __allBlack global __trellis_pixels global __trellis_pixel_columns global __trellis_pixel_rows __trellis_pixels = neotrellis.pixels __trellis_pixels.fill(Color.WHITE) #trellis.pixels.fill(Color.BLACK) blink = Blink(__trellis_pixels, speed=0.5, color=Color.JADE) color_cycle = ColorCycle(__trellis_pixels, speed=0.4, colors=[Color.MAGENTA, Color.ORANGE]) comet = Comet(__trellis_pixels, speed=0.1, color=Color.PURPLE, tail_length=4, bounce=True) chase = Chase(__trellis_pixels, speed=0.1, size=3, spacing=6, color=Color.WHITE) pulse = Pulse(__trellis_pixels, speed=0.1, period=3, color=Color.AMBER) sparkle = Sparkle(__trellis_pixels, speed=0.1, color=Color.PURPLE, num_sparkles=6) solid = Solid(__trellis_pixels, color=Color.JADE) rainbow = Rainbow(__trellis_pixels, speed=0.1, period=2) sparkle_pulse = SparklePulse(__trellis_pixels, speed=0.1, period=3, color=Color.JADE) rainbow_comet = RainbowComet(__trellis_pixels, speed=0.1, tail_length=16, bounce=True) rainbow_chase = RainbowChase(__trellis_pixels, speed=0.1, size=3, spacing=2, step=8) rainbow_sparkle = RainbowSparkle(__trellis_pixels, speed=0.1, num_sparkles=4) custom_color_chase = CustomColorChase( __trellis_pixels, speed=0.1, size=2, spacing=3, colors=[Color.ORANGE, Color.WHITE, Color.JADE] ) # thats only 13 built in! need 16 to have one each key allWhite = Solid(__trellis_pixels, color=Color.WHITE) allBlack = Solid(__trellis_pixels, color=Color.BLACK) __allBlack = allBlack allGray = Solid(__trellis_pixels, color=(8,8,2)) allOrange = Solid(__trellis_pixels, color=Color.ORANGE) allRed= Solid(__trellis_pixels, color=Color.RED) allBlue = Solid(__trellis_pixels, color=Color.BLUE) allGold = Solid(__trellis_pixels, color=Color.GOLD)
Halfway thru setup_animations() we come to the rather nice PixelMaps. From the adafruit learn on led_animations: " The
PixelMap
helper enables you to treat a strip or strips of LEDs as a grid for animation purposes. It also works great with LED matrices that are actually a strip of LEDs arranged in a matrix, such as the NeoPixel FeatherWing." [and the NeoTrellis]We create two PixelMaps, one to reference Columns (vertical) keys and one to reference Rows (horizontal) keys. These are then used to create animations that run across/up/down the keypad.
At the end of setup_animiations(), the trellisAnimations[] list is filled and we set the current_animation to be Solid Gray.
# create some pixelMaps for rows and columns __trellis_pixel_columns = helper.PixelMap.vertical_lines( __trellis_pixels, 4, 4, helper.horizontal_strip_gridmap(4, alternating=False) ) __trellis_pixel_rows = helper.PixelMap.horizontal_lines( __trellis_pixels, 4, 4, helper.horizontal_strip_gridmap(4, alternating=False) ) # and build some animations using PixelMaps (from example, mod for size) comet_h = Comet(__trellis_pixel_rows, speed=0.1, color=Color.PURPLE, tail_length=3, bounce=True) comet_v = Comet(__trellis_pixel_columns, speed=0.1, color=Color.AMBER, tail_length=6, bounce=True) chase_h = Chase(__trellis_pixel_rows, speed=0.1, size=3, spacing=6, color=Color.JADE) rainbow_v = Rainbow(__trellis_pixel_columns, speed=0.1, period=2) rainbow_chase_v = RainbowChase(__trellis_pixel_columns, speed=0.1, size=3, spacing=2, step=8) rainbow_chase_h = RainbowChase(__trellis_pixel_rows, speed=0.1, size=3, spacing=3) rainbow_comet_v = RainbowComet(__trellis_pixel_columns, speed=0.1, tail_length=7, bounce=True) # build an array of all those animation, first 16 will be tied to keypad actions later global trellisAnimations trellisAnimations = [ # first 16 will match to key pad index 0-15 blink, color_cycle, comet, chase, pulse, sparkle, comet_h, rainbow, sparkle_pulse, rainbow_comet, rainbow_chase, rainbow_sparkle, custom_color_chase, rainbow_chase_v, rainbow_chase_h, rainbow_comet_v, # any more are just filler here allWhite, allBlack, allRed, allBlue, allGold, allOrange, allGray, chase_h, comet_v, rainbow_v, ] # start with simple solid Gray animation global current_animation current_animation = allGray print ("animations setup current_animation: ", current_animation)
Lastly the module defines some functions to act on the current_animation.
def set_all_black_animation(): global current_animation global __allBlack current_animation = __allBlack current_animation = __allBlack def set_animation_byIndex(idx): global current_animation if idx in range(len(trellisAnimations)): current_animation = trellisAnimations[idx] def freeze(): global current_animation current_animation.freeze() def resume(): global current_animation print ("resume current_animation: ", current_animation) current_animation.resume()
-
Walkthru onboard_neopixel.py module
07/19/2022 at 08:08 • 0 commentsThe module onboard_neopixel.py 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) else: print("Board does NOT have onboard NEOPIXEL") # blink it once to show we here blinkOnBoardPixel()
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.fill(color) on_board_neopixel.show() time.sleep(DELAY) on_board_neopixel[0] = (0,0,0) time.sleep(DELAY) on_board_neopixel.show() else: 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 else: on_board_neopixel.fill(color) __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 code.py
07/19/2022 at 07:52 • 0 commentsThe examples folder has single application (code_animKey_PixelMaps.py) 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:
- code_NeotrellisExplorations.py is the main program. It does the setup and has the infinite loop. copy it to code.py to run on a Circuit Python board
- onboard_neopixel.py 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)
- neotrellis_animations.py encapsulates the animations that run across the 4x4 matrix
- neotrellis_keypad.py encapsulates the key handling
This log will go over the main file with subsequent project logs discussing the other modules.
The first bit of code_NeotrellisExplorations.py 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 onboard_neopixel.setup_onboard_neopixel()
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 neotrellis_animations.setup_animations(trellis) neotrellis_keypad.setup_keypad(trellis)
The remainder of code.py 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 neotrellis_animations.current_animation.animate() # call the sync function call any triggered callbacks trellis.sync() # the trellis can only be read every 17 milliseconds or so # really? the neopixel _getItem() could be an issue for i2c/seesaw connected neopixels time.sleep(0.02) # 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") i=0
Using modules for the app makes the top level easier to read and understand. Hopefully.
- code_NeotrellisExplorations.py is the main program. It does the setup and has the infinite loop. copy it to code.py to run on a Circuit Python board