diff --git a/pico-cp/README.md b/pico-cp/README.md index 5c48e56..c1e5832 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -47,8 +47,8 @@ Each `prog` is a program string from the web editor. Add/replace entries and sav - **Joystick reversed:** toggle `JOY_INVERT_X` / `JOY_INVERT_Y`. - **Screen blank / garbled:** the panel lot may differ; drop `SPI_BAUD`, and if it's a 240×320 ILI9341 instead of the 320×480 ST7796, the init/size need changing (this targets the 320×480 you have). -- **No RGB LED:** the WS2812 needs the `neopixel` library on `CIRCUITPY/lib` (`circup install neopixel`) - — everything else works without it. +- **RGB LED** is driven by the core `neopixel_write` module — no library to install. If it stays dark, + your CircuitPython build is unusually missing that module (everything else still works). If `code.py` ever errors, CircuitPython prints the traceback **on the screen and over USB serial** — copy that to me and I'll fix it. diff --git a/pico-cp/__pycache__/code.cpython-312.pyc b/pico-cp/__pycache__/code.cpython-312.pyc index 851f8f3..dfa6936 100644 Binary files a/pico-cp/__pycache__/code.cpython-312.pyc and b/pico-cp/__pycache__/code.cpython-312.pyc differ diff --git a/pico-cp/code.py b/pico-cp/code.py index 02439b7..898f7c0 100644 --- a/pico-cp/code.py +++ b/pico-cp/code.py @@ -23,9 +23,9 @@ except ImportError: # CircuitPython 8.x from displayio import FourWire from displayio import Display as BusDisplay try: - import neopixel + import neopixel_write # core module on RP2040 — drives WS2812 with no external library except ImportError: - neopixel = None + neopixel_write = None # ============================== CONFIG (tweak if needed) ============================== SPI_BAUD = 40_000_000 @@ -63,6 +63,19 @@ C_CYAN, C_AMBER, C_GREEN, C_DIM = 0x0AB3F7, 0xFF9B2E, 0x2FE07A, 0x243240 C_BTN = 0x1C222C LEVEL_RGB = {2: (255, 110, 0), 1: (0, 150, 255), 3: (130, 70, 255)} +# WS2812 RGB LED — self-contained via the core neopixel_write module (no external library) +class RGB: + def __init__(self, pin): + self.ok = neopixel_write is not None + if self.ok: + self.io = digitalio.DigitalInOut(pin); self.io.direction = digitalio.Direction.OUTPUT + self.buf = bytearray(3) + def set(self, r, g, b): + if not self.ok: return + self.buf[0] = g; self.buf[1] = r; self.buf[2] = b # WS2812 wants GRB order + try: neopixel_write.neopixel_write(self.io, self.buf) + except Exception: self.ok = False + # ============================== ANTI-ALIASED FONTS (binary blobs on the drive; see pico/gen_font.py) ============================== def load_font(path): with open(path, "rb") as f: @@ -240,7 +253,7 @@ def make_display(): displayio.release_displays() spi = busio.SPI(clock=P_SCK, MOSI=P_MOSI) bus = FourWire(spi, command=P_DC, chip_select=P_CS, reset=P_RST, baudrate=SPI_BAUD) - return BusDisplay(bus, st7796_init(), width=WIDTH, height=HEIGHT, auto_refresh=True) + return BusDisplay(bus, st7796_init(), width=WIDTH, height=HEIGHT, auto_refresh=False) def solid(color): p = displayio.Palette(1); p[0] = color; return p @@ -254,7 +267,7 @@ class App: self.display = make_display() self.i2c = busio.I2C(scl=P_SCL, sda=P_SDA, frequency=400_000) self.touch = GT911(self.i2c) - self.np = neopixel.NeoPixel(P_RGB, 1, auto_write=True) if neopixel else None + self.led = RGB(P_RGB) self.buz = pwmio.PWMOut(P_BUZ, frequency=1600, variable_frequency=True, duty_cycle=0) self.buz_off = 0 self.btnA = self._btn(P_BTNA); self.btnB = self._btn(P_BTNB) @@ -265,6 +278,9 @@ class App: self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.rgb = (0, 0, 0) self.programs = load_programs() self.buttons = [] + self.dirty = True; self.dots = [] + self.dot_pal = displayio.Palette(3) + self.dot_pal[0] = C_DIM; self.dot_pal[1] = C_CYAN; self.dot_pal[2] = C_AMBER self._build_scene() self.load(0) @@ -300,12 +316,14 @@ class App: def _place(self, group, s, x, y, fg, bg, font, right_edge=None): while len(group): group.pop() + self.dirty = True if not s: return tg, w, h = make_text(s, font, fg, bg) tg.x = (right_edge - w) if right_edge is not None else x; tg.y = y; group.append(tg) def _center(self, group, s, cx, cy, fg, bg, font): while len(group): group.pop() tg, w, h = make_text(s, font, fg, bg); tg.x = cx - w//2; tg.y = cy - h//2; group.append(tg) + self.dirty = True def _label(self, key): sym = {"prev": "◀◀", "next": "▶▶", "minus": "-", "plus": "+", "tap": "TAP", "play": "■" if self.running else "▶"}[key] @@ -331,10 +349,10 @@ class App: self.buz_off = time.monotonic_ns() + 22_000_000 def flash(self, level): self.rgb = LEVEL_RGB.get(level, (0, 150, 255)) - if self.np: self.np[0] = self.rgb + self.led.set(*self.rgb) def led_off(self): self.rgb = (0, 0, 0) - if self.np: self.np[0] = (0, 0, 0) + self.led.set(0, 0, 0) # ---------- transport ---------- def toggle(self): @@ -380,7 +398,7 @@ class App: if self.rgb != (0, 0, 0): r, g, b = self.rgb; r = r*7//10; g = g*7//10; b = b*7//10 self.rgb = (r, g, b) if (r+g+b) > 12 else (0, 0, 0) - if self.np: self.np[0] = self.rgb + self.led.set(*self.rgb) # ---------- inputs ---------- def poll(self): @@ -432,17 +450,26 @@ class App: self._place(self.g_name, self.name[:26], 12, 272, C_TXT, C_BG, FONT_M) def draw_dots(self): m = self.master; bpb = max(1, m['steps'] // m['sub']) - while len(self.g_dots): self.g_dots.pop() - sz, sp = 18, 26; x0 = max(12, WIDTH - 12 - bpb*sp) - for i in range(bpb): + if len(self.dots) != bpb: # rebuild only when the beat count changes + while len(self.g_dots): self.g_dots.pop() + self.dots = []; sz, sp = 18, 26; x0 = max(12, WIDTH - 12 - bpb*sp) + for i in range(bpb): + r = vectorio.Rectangle(pixel_shader=self.dot_pal, width=sz, height=sz, x=x0 + i*sp, y=200) + self.g_dots.append(r); self.dots.append(r) + for i in range(bpb): # otherwise just recolour (cheap, no tearing) lvl = m['levels'][(i*m['sub']) % m['steps']]; on = self.running and i == self.beat - col = (C_AMBER if lvl == 2 else C_CYAN) if on else C_DIM - self.g_dots.append(rect(x0 + i*sp, 200, sz, sz, col)) + self.dots[i].color_index = (2 if lvl == 2 else 1) if on else 0 + self.dirty = True def run(self): if self.touch.addr is None: self._place(self.g_name, "touch: not found", 12, 272, C_AMBER, C_BG, FONT_M) while True: - self.tick(); self.poll(); time.sleep(0.0005) + self.tick(); self.poll() + # push a complete frame only when something changed (no mid-update tearing); + # capped at the display's refresh rate, so dirty regions stay small and quick + if self.dirty and self.display.refresh(): + self.dirty = False + time.sleep(0.0005) App().run()