Drum rack and lights firmware examples #1

Open
flpvsk wants to merge 3 commits from flpvsk/neogrid:example/drum-rack into main
4 changed files with 235 additions and 0 deletions
Showing only changes of commit 9c57c04a00 - Show all commits

Drum Rack example

Andrey Salomatin 2026-04-11 00:50:28 +02:00

View file

@ -16,6 +16,7 @@ Custom micropython build for the Olimex RP2350B-XL. Comes with some features ena
9. Run `make BOARD=OLIMEX_PICO2_XL submodules` to install dependencies
10. Run `make BOARD=OLIMEX_PICO2_XL clean` to remove previous build artefacts (if any)
11. Run `make BOARD=OLIMEX_PICO2_XL` to build the firmware
12. To rebuild: `make BOARD=OLIMEX_PICO2_XL clean && make BOARD=OLIMEX_PICO2_XL`
## flashing

View file

@ -0,0 +1,20 @@
# MIDI Drum Rack example
Press to send a MIDI note in the range from C1 to D#2. Buttons are
ordered in the same way the pads are in Ableton Drum Rack: with the USB
port poiting from the player left-to-right, bottom-to-top.
## build
1. Make a micropython build with the libraries added:
```bash
cd $MICROPYTHON_PATH/ports/rp2
make BOARD=OLIMEX_PICO2_XL clean && make V=1 BOARD=OLIMEX_PICO2_XL FROZEN_MANIFEST={path-to-neogrid-project}/firmware/examples/nanogrid-drum-rack/manifest.py
```
2. Flush the board with the resulting uf2 (see
[firmware/README.md](/firmware/README.md))
3. Copy the `main.py` to the board

View file

@ -0,0 +1,210 @@
# MicroPython USB MIDI example
#
# This example demonstrates creating a custom MIDI device.
#
# To run this example:
#
# 1. Make sure `usb-device-midi` is installed via: mpremote mip install usb-device-midi
#
# 2. Run the example via: mpremote run midi_example.py
#
# 3. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the MIDI interface present. At this point, the example is running.
#
# 4. To see output from the example, re-connect: mpremote connect PORTNAME
#
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import usb.device
from usb.device.midi import MIDIInterface
import time
from machine import Pin
import array, time
import rp2
NUM_COL = 4
NUM_ROW = 4
NUM_LEDS = NUM_COL * NUM_ROW
PIN_NUM = 16
brightness = 0.2
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
T1 = 2
T2 = 5
T3 = 3
wrap_target()
label("bitloop")
out(x, 1) .side(1) [T3 - 1]
jmp(not_x, "do_zero") .side(0) [T1 - 1]
jmp("bitloop") .side(0) [T2 - 1]
label("do_zero")
nop() .side(1) [T2 - 1]
wrap()
def init_leds():
# Create the StateMachine with the ws2812 program, outputting on pin
sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM))
# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)
# Display a pattern on the LEDs via an array of LED RGB values.
ar = array.array("I", [0 for _ in range(NUM_LEDS)])
##########################################################################
def show_all():
dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)])
for i, c in enumerate(ar):
r = int(((c >> 8) & 0xFF) * brightness)
g = int(((c >> 16) & 0xFF) * brightness)
b = int((c & 0xFF) * brightness)
dimmer_ar[i] = (g<<16) + (r<<8) + b
sm.put(dimmer_ar, 8)
def set_led(i, color):
row = i // NUM_COL
col = i - row * NUM_COL
new_col = NUM_COL - col - 1
new_row = NUM_ROW - row - 1 if new_col % 2 == 0 else row
ar[new_col * NUM_ROW + new_row] = (color[1]<<16) + (color[0]<<8) + color[2]
return (set_led, show_all)
class MIDIExample(MIDIInterface):
# Very simple example event handler functions, showing how to receive note
# and control change messages sent from the host to the device.
#
# If you need to send MIDI data to the host, then it's fine to instantiate
# MIDIInterface class directly.
def on_open(self):
super().on_open()
print("Device opened by host")
def on_note_on(self, channel, pitch, vel):
print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}")
def on_note_off(self, channel, pitch, vel):
print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}")
def on_control_change(self, channel, controller, value):
print(f"RX Control channel {channel} controller {controller} value {value}")
m = MIDIExample()
# Remove builtin_driver=True if you don't want the MicroPython serial REPL available.
usb.device.get().init(m, builtin_driver=True)
print("Waiting for USB host to configure the interface...")
while not m.is_open():
time.sleep_ms(100)
# TX constants
CHANNEL = 0
PITCH_C1 = 37
CONTROLLER = 64
control_val = 0
def init_btns():
row_pins = [
Pin(0, Pin.OUT),
Pin(1, Pin.OUT),
Pin(2, Pin.OUT),
Pin(3, Pin.OUT),
]
col_pins = [
Pin(32, Pin.IN, Pin.PULL_UP),
Pin(33, Pin.IN, Pin.PULL_UP),
Pin(34, Pin.IN, Pin.PULL_UP),
Pin(35, Pin.IN, Pin.PULL_UP),
]
pressed_keys = set()
# reset cols
for pin in row_pins:
pin.value(1)
def loop():
pressed_keys.clear()
for row, row_pin in enumerate(row_pins):
# set only active pin low, keep rest high
row_pin.value(0)
for col, col_pin in enumerate(col_pins):
key = (
(NUM_ROW - row - 1) +
(NUM_COL - col - 1) * NUM_ROW
)
if not col_pin.value():
pressed_keys.add(key)
row_pin.value(1)
return set(pressed_keys)

This is a (minor) memory leak allocation inefficiency as it will create a new set object on every loop iteration. It's fine to return pressed_keys directly.

This is a (minor) memory ~~leak~~ allocation inefficiency as it will create a new `set` object on every loop iteration. It's fine to return `pressed_keys` directly.

Yes, makes sense to remove it. Out of curiousity, shouldn't python take care of cleaning up that memory once the references to the new set go out of scope? just saw your correction :)

Also, not sure if it's overoptimizing things, but we could use a single integer instead of a set here. Each bit representing an on/off state of a btn. We'd have to deal with bitwise operations, but the memory footprint would likely be smaller. Maybe the amount of cycles too?

E.g.

# no btns pressed
pressed_keys = 0

# btn 3 is pressed
pressed_keys = pressed_keys | (1 << 3)

# btn 4 is not pressed
pressed_keys = pressed_keys & ~(1 << 4)

# check if btn 2 is pressed
(pressed_keys >> 2) & 1

# set intersection
pressed_keys_1 & pressed_keys_2

# set difference
pressed_keys_1 & ~pressed_keys_2

# get all the pressed btns
pressed_list = []
for i in range(NUM_BTNS):
  if (pressed_keys >> i) & 1:
    pressed_list.append(i)
Yes, makes sense to remove it. ~Out of curiousity, shouldn't python take care of cleaning up that memory once the references to the new set go out of scope?~ just saw your correction :) Also, not sure if it's overoptimizing things, but we could use a single integer instead of a set here. Each bit representing an on/off state of a btn. We'd have to deal with bitwise operations, but the memory footprint would likely be smaller. Maybe the amount of cycles too? E.g. ```python # no btns pressed pressed_keys = 0 # btn 3 is pressed pressed_keys = pressed_keys | (1 << 3) # btn 4 is not pressed pressed_keys = pressed_keys & ~(1 << 4) # check if btn 2 is pressed (pressed_keys >> 2) & 1 # set intersection pressed_keys_1 & pressed_keys_2 # set difference pressed_keys_1 & ~pressed_keys_2 # get all the pressed btns pressed_list = [] for i in range(NUM_BTNS): if (pressed_keys >> i) & 1: pressed_list.append(i) ```
return loop
btn_loop = init_btns()
turned_on = set()
(set_led, show_all) = init_leds()
# for i in range(NUM_LEDS):
# set_led(i, CYAN)
# for j in range(NUM_LEDS):
# if i != j:
# set_led(j, BLACK)
# show_all()
# time.sleep(2)
#
CYAN = (0, 255, 255)
BLACK = (0, 0, 0)
print("Starting loop...")
while m.is_open():
time.sleep_ms(5)
# print(f"TX Note On channel {CHANNEL} pitch {PITCH_C1}")
# m.note_on(CHANNEL, PITCH_C1) # Velocity is an optional third argument
# time.sleep(0.5)
# print(f"TX Note Off channel {CHANNEL} pitch {PITCH_C1}")
# m.note_off(CHANNEL, PITCH_C1)
# time.sleep(1)
# print(f"TX Control channel {CHANNEL} controller {CONTROLLER} value {control_val}")
# m.control_change(CHANNEL, CONTROLLER, control_val)
# control_val += 1
# if control_val == 0x7F:
# control_val = 0
# time.sleep(1)
pressed = btn_loop()
to_turn_on = pressed.difference(turned_on)
to_turn_off = turned_on.difference(pressed)
if len(to_turn_on):
print("to_turn_on", to_turn_on)
if len(to_turn_off):
print("to_turn_off", to_turn_off)
for key in to_turn_on:
m.note_on(CHANNEL, PITCH_C1 + key - 1)
set_led(key, CYAN)
turned_on.add(key)
for key in to_turn_off:
m.note_off(CHANNEL, PITCH_C1 + key - 1)
turned_on.remove(key)
set_led(key, BLACK)
show_all()
print("USB host has reset device, example done.")

View file

@ -0,0 +1,4 @@
metadata(version="0.1.0")
include("$(PORT_DIR)/boards/manifest.py")
require("usb-device")
require("usb-device-midi")