From ffededd05b1e7f9162285212d53f3d77b87456a5 Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 28 May 2026 21:43:48 -0500 Subject: [PATCH] PM_K-1 CircuitPython: self-contained RGB LED + fix screen tearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From on-board feedback (memory + colours now good): - LED: drive the WS2812 via the core neopixel_write module (no neopixel library to install) — a tiny RGB class. Self-contained: it works straight from the bundle. - Tearing: switch displayio to auto_refresh=False and push a complete frame only when the scene changed (dirty flag, capped at the panel's refresh rate) so updates are never shown mid-paint. Beat dots now recolour in place (vectorio color_index) instead of being rebuilt every beat, shrinking the dirty region. Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/README.md | 4 +- pico-cp/__pycache__/code.cpython-312.pyc | Bin 31772 -> 33689 bytes pico-cp/code.py | 53 +++++++++++++++++------ 3 files changed, 42 insertions(+), 15 deletions(-) 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 851f8f3990c6377620725a0d2efd2e431cfea3a3..dfa69362d7e62a95b4c428072e3dcc4b5da8bdb3 100644 GIT binary patch delta 8573 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 delta 7265 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 A6951J 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()