# 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)