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