metronome/wokwi/main.py
Me Here 1186e61588 Add Wokwi (Pi Pico) simulation of the Micro metronome
MicroPython sim that runs on https://wokwi.com/pi-pico: KY-040 encoder stands in
for the thumb-roller (rotate=tempo, press=start/stop, hold+rotate=track), an
SSD1306 OLED for the display, and a piezo buzzer for the click. Files:
diagram.json, main.py, ssd1306.py + README with the (manual) setup steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:04:53 -05:00

119 lines
3.9 KiB
Python

# VARASYS PM-u "Micro" — metronome, simulated on a Raspberry Pi Pico (RP2040).
#
# A functional stand-in for the real inline practice bar, using parts Wokwi has:
# * KY-040 rotary encoder -> the clickable thumb-roller
# rotate = tempo
# press (SW) = start / stop
# hold SW + rotate = switch track
# * SSD1306 OLED -> the amber 14-segment display (shows BPM / track name)
# * Piezo buzzer -> the click (accent beat = higher, longer beep)
#
# Run it at https://wokwi.com/pi-pico (MicroPython).
# Files in this project: diagram.json, main.py, ssd1306.py (see README.md).
from machine import Pin, I2C, PWM
import ssd1306, time
# ---- display: SSD1306 128x64 on I2C0 (SDA=GP0, SCL=GP1) ----
i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=400_000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
# ---- KY-040 rotary encoder (CLK=GP2, DT=GP3, SW=GP4) ----
clk = Pin(2, Pin.IN, Pin.PULL_UP)
dt = Pin(3, Pin.IN, Pin.PULL_UP)
sw = Pin(4, Pin.IN, Pin.PULL_UP)
# ---- piezo buzzer (GP5) ----
buz = PWM(Pin(5)); buz.duty_u16(0)
# ---- built-in "tracks": (name, bpm, accent pattern over the bar) ; 2 = accent, 1 = normal ----
TRACKS = [
("ROCK", 120, (2, 1, 1, 1)),
("FUNK", 100, (2, 1, 2, 1)),
("SWING", 140, (2, 1, 1, 2)),
("WALTZ", 90, (2, 1, 1)), # 3/4
("BCKBT", 96, (1, 2, 1, 2)), # backbeat
]
ti = 0
bpm = TRACKS[0][1]
pat = TRACKS[0][2]
running = False
mode = "bpm" # "bpm" | "track"
preview = 0
beat = 0
next_beat = time.ticks_ms()
def load(i):
global ti, bpm, pat, beat
ti = i % len(TRACKS)
bpm = TRACKS[ti][1]
pat = TRACKS[ti][2]
beat = 0
def show():
oled.fill(0)
oled.text("PM-u MICRO", 0, 0)
oled.text("RUN" if running else "stop", 96, 0)
oled.hline(0, 12, 128, 1)
if mode == "track":
oled.text("TRACK", 0, 24)
oled.text(TRACKS[preview][0], 0, 42)
oled.text("#%d/%d" % (preview + 1, len(TRACKS)), 70, 42)
else:
oled.text("TEMPO (BPM)", 0, 24)
oled.text("%d" % bpm, 0, 42)
oled.text(TRACKS[ti][0], 64, 42)
oled.show()
def click(accent):
buz.freq(2000 if accent else 1200)
buz.duty_u16(22000)
time.sleep_ms(18 if accent else 11)
buz.duty_u16(0)
load(0); show()
last_clk = clk.value()
sw_down = None
held = False
while True:
now = time.ticks_ms()
# --- metronome beat ---
if running and time.ticks_diff(now, next_beat) >= 0:
click(pat[beat % len(pat)] >= 2)
beat = (beat + 1) % len(pat)
next_beat = time.ticks_add(next_beat, int(60000 / bpm))
# --- encoder rotation: one detent per CLK falling edge ---
c = clk.value()
if c == 0 and last_clk == 1:
step = 1 if dt.value() else -1
if held: # hold + rotate -> preview track
mode = "track"
preview = (preview + step) % len(TRACKS)
else: # rotate -> tempo
mode = "bpm"
bpm = max(30, min(300, bpm + step))
show()
last_clk = c
# --- button: quick press = start/stop ; hold (~350 ms) = enter track mode ---
if sw.value() == 0 and sw_down is None:
sw_down = now; held = False; preview = ti
if sw_down is not None and not held and time.ticks_diff(now, sw_down) > 350:
held = True; mode = "track"; show()
if sw.value() == 1 and sw_down is not None:
if held: # release after hold+rotate -> commit track
load(preview); mode = "track"; show()
time.sleep_ms(800); mode = "bpm"; show()
else: # quick tap -> start / stop
running = not running
if running:
beat = 0; next_beat = time.ticks_ms()
show()
sw_down = None; held = False
time.sleep_ms(2)