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>
119 lines
3.9 KiB
Python
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)
|