PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI
From on-board feedback (works well; minor tweaks):
- Pad grid uses circles now: big circle on each beat (division), small on the
subdivisions (vectorio.Circle — native, no extra cost), coloured/lit as before.
- Lane labels use a new small font (font_s.bin, ~12px via gen_font.py) so they're
half-size and show more of the voice name (e.g. 'hatClos').
- LED was blinding -> LED_BRIGHTNESS scale (default 0.15) applied on every write.
- Residual tearing -> SPI back to 62.5 MHz (vendor speed; smaller tear window on a
panel with no tearing-effect pin). Both are CONFIG flags.
Verified by rendering the full scene headless. font_s.bin added to gen_font.py + bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6421e525da
commit
ec43c694a1
8 changed files with 32 additions and 13 deletions
2
build.sh
2
build.sh
|
|
@ -39,7 +39,7 @@ pathlib.Path("dist/pico-main.py").write_text(pathlib.Path("pico/main.py").read_t
|
||||||
print("copied pico-main.py")
|
print("copied pico-main.py")
|
||||||
import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY)
|
import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY)
|
||||||
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
for f in ("code.py", "programs.json", "font_m.bin", "font_l.bin", "README.md"):
|
for f in ("code.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", "README.md"):
|
||||||
z.write("pico-cp/" + f, f)
|
z.write("pico-cp/" + f, f)
|
||||||
print("zipped pm_k1_circuitpy.zip")
|
print("zipped pm_k1_circuitpy.zip")
|
||||||
PY
|
PY
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ same program‑string language as <https://metronome.varasys.io>.
|
||||||
2. **Copy everything from the bundle** onto `CIRCUITPY` (drag‑and‑drop — it's a normal drive now):
|
2. **Copy everything from the bundle** onto `CIRCUITPY` (drag‑and‑drop — it's a normal drive now):
|
||||||
- `code.py` (this firmware — runs on boot)
|
- `code.py` (this firmware — runs on boot)
|
||||||
- `programs.json` (your grooves)
|
- `programs.json` (your grooves)
|
||||||
- `font_m.bin`, `font_l.bin` (the anti‑aliased fonts — kept as files to save RAM)
|
- `font_s.bin`, `font_m.bin`, `font_l.bin` (the anti‑aliased fonts — kept as files to save RAM)
|
||||||
3. It starts immediately. Editing `programs.json` (or re‑saving it from the editor) makes CircuitPython
|
3. It starts immediately. Editing `programs.json` (or re‑saving it from the editor) makes CircuitPython
|
||||||
**auto‑reload** with the new tracks.
|
**auto‑reload** with the new tracks.
|
||||||
|
|
||||||
|
|
@ -45,6 +45,9 @@ Each `prog` is a program string from the web editor. Add/replace entries and sav
|
||||||
- **Taps land wrong:** set `TOUCH_DEBUG = True`, watch the serial output, then set
|
- **Taps land wrong:** set `TOUCH_DEBUG = True`, watch the serial output, then set
|
||||||
`TOUCH_SWAP_XY` / `TOUCH_INVERT_X` / `TOUCH_INVERT_Y`.
|
`TOUCH_SWAP_XY` / `TOUCH_INVERT_X` / `TOUCH_INVERT_Y`.
|
||||||
- **Joystick reversed:** toggle `JOY_INVERT_X` / `JOY_INVERT_Y`.
|
- **Joystick reversed:** toggle `JOY_INVERT_X` / `JOY_INVERT_Y`.
|
||||||
|
- **LED too bright / too dim:** change `LED_BRIGHTNESS` (0..1, default 0.15).
|
||||||
|
- **Screen tearing:** the SPI panel has no tearing-effect sync; `SPI_BAUD` (default 62.5 MHz) is pushed fast
|
||||||
|
to minimise it — lower it only if the display is unstable.
|
||||||
- **Screen blank / garbled:** the panel lot may differ; drop `SPI_BAUD`, and if it's a 240×320 ILI9341
|
- **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).
|
instead of the 320×480 ST7796, the init/size need changing (this targets the 320×480 you have).
|
||||||
- **RGB LED** is driven by the core `neopixel_write` module — no library to install. If it stays dark,
|
- **RGB LED** is driven by the core `neopixel_write` module — no library to install. If it stays dark,
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -28,7 +28,8 @@ except ImportError:
|
||||||
neopixel_write = None
|
neopixel_write = None
|
||||||
|
|
||||||
# ============================== CONFIG (tweak if needed) ==============================
|
# ============================== CONFIG (tweak if needed) ==============================
|
||||||
SPI_BAUD = 40_000_000
|
SPI_BAUD = 62_500_000 # faster SPI = smaller tearing window; drop to 40_000_000 if unstable
|
||||||
|
LED_BRIGHTNESS = 0.15 # WS2812 sits right next to you — keep it dim (0..1)
|
||||||
WIDTH, HEIGHT = 320, 480
|
WIDTH, HEIGHT = 320, 480
|
||||||
MADCTL = 0x48 # portrait; 0x48 swaps R/B for this BGR panel (cyan reads cyan). Use 0x40 if reversed.
|
MADCTL = 0x48 # portrait; 0x48 swaps R/B for this BGR panel (cyan reads cyan). Use 0x40 if reversed.
|
||||||
INVERT_COLORS = True # most ST7796 modules need inversion ON; set False if colours look negative
|
INVERT_COLORS = True # most ST7796 modules need inversion ON; set False if colours look negative
|
||||||
|
|
@ -75,7 +76,8 @@ class RGB:
|
||||||
self.buf = bytearray(3)
|
self.buf = bytearray(3)
|
||||||
def set(self, r, g, b):
|
def set(self, r, g, b):
|
||||||
if not self.ok: return
|
if not self.ok: return
|
||||||
self.buf[0] = g; self.buf[1] = r; self.buf[2] = b # WS2812 wants GRB order
|
# WS2812 wants GRB order; scale down so it isn't blinding
|
||||||
|
self.buf[0] = int(g * LED_BRIGHTNESS); self.buf[1] = int(r * LED_BRIGHTNESS); self.buf[2] = int(b * LED_BRIGHTNESS)
|
||||||
try: neopixel_write.neopixel_write(self.io, self.buf)
|
try: neopixel_write.neopixel_write(self.io, self.buf)
|
||||||
except Exception: self.ok = False
|
except Exception: self.ok = False
|
||||||
|
|
||||||
|
|
@ -91,8 +93,9 @@ def load_font(path):
|
||||||
glyphs[cp] = (w, h, xoff, top, adv, pixoff); pixoff += (w * h + 1) // 2
|
glyphs[cp] = (w, h, xoff, top, adv, pixoff); pixoff += (w * h + 1) // 2
|
||||||
return (glyphs, blob)
|
return (glyphs, blob)
|
||||||
|
|
||||||
FONT_M = load_font("/font_m.bin")
|
FONT_S = load_font("/font_s.bin") # small — pad-grid lane labels
|
||||||
FONT_L = load_font("/font_l.bin")
|
FONT_M = load_font("/font_m.bin") # labels / buttons
|
||||||
|
FONT_L = load_font("/font_l.bin") # big BPM
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
def _blend(bg, fg, i):
|
def _blend(bg, fg, i):
|
||||||
|
|
@ -456,16 +459,18 @@ class App:
|
||||||
while len(self.g_grid): self.g_grid.pop()
|
while len(self.g_grid): self.g_grid.pop()
|
||||||
self.lane_pads = []; self.lane_lit = []
|
self.lane_pads = []; self.lane_lit = []
|
||||||
n = min(len(self.lanes), MAXLANES)
|
n = min(len(self.lanes), MAXLANES)
|
||||||
top = 140; rowh = min(38, (294 - top) // max(1, n))
|
top = 140; rowh = min(40, (296 - top) // max(1, n))
|
||||||
for li in range(n):
|
for li in range(n):
|
||||||
L = self.lanes[li]; y = top + li * rowh
|
L = self.lanes[li]; y = top + li * rowh; cy = y + rowh // 2
|
||||||
tg, w, h = make_text((L.get('sound', '') or '?')[:4], FONT_M, C_MUTE, C_BG)
|
tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG)
|
||||||
tg.x = 10; tg.y = y + 2; self.g_grid.append(tg)
|
tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg)
|
||||||
steps = L['steps']; px0 = 66; pw = (WIDTH - 10 - px0) // steps; ph = max(8, rowh - 10)
|
steps = L['steps']; sub = L['sub']; px0 = 60; pw = (WIDTH - 8 - px0) // steps
|
||||||
|
r_big = max(3, min((rowh - 4) // 2, (pw - 2) // 2)); r_sml = max(2, r_big * 6 // 10)
|
||||||
pads = []
|
pads = []
|
||||||
for s in range(steps):
|
for s in range(steps):
|
||||||
r = vectorio.Rectangle(pixel_shader=self.pad_pal, width=max(2, pw - 1), height=ph, x=px0 + s*pw, y=y)
|
rad = r_big if (s % sub == 0) else r_sml # big = beat (division), small = subdivision
|
||||||
r.color_index = self._padbase(L, s); self.g_grid.append(r); pads.append(r)
|
c = vectorio.Circle(pixel_shader=self.pad_pal, radius=rad, x=px0 + s*pw + pw//2, y=cy)
|
||||||
|
c.color_index = self._padbase(L, s); self.g_grid.append(c); pads.append(c)
|
||||||
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
def _move_playhead(self, li, step):
|
def _move_playhead(self, li, step):
|
||||||
|
|
|
||||||
BIN
pico-cp/font_s.bin
Normal file
BIN
pico-cp/font_s.bin
Normal file
Binary file not shown.
1
pico/_font_l.b64
Normal file
1
pico/_font_l.b64
Normal file
File diff suppressed because one or more lines are too long
1
pico/_font_m.b64
Normal file
1
pico/_font_m.b64
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -20,6 +20,7 @@ ASCII = "".join(chr(c) for c in range(0x20, 0x7F))
|
||||||
SYMBOLS = "◀▶■" # ◀ ▶ ■ (prev / play / stop, anti-aliased)
|
SYMBOLS = "◀▶■" # ◀ ▶ ■ (prev / play / stop, anti-aliased)
|
||||||
M_CHARS = ASCII + SYMBOLS
|
M_CHARS = ASCII + SYMBOLS
|
||||||
L_CHARS = "0123456789 "
|
L_CHARS = "0123456789 "
|
||||||
|
S_CHARS = ASCII # small font for the pad-grid lane labels (CircuitPython)
|
||||||
|
|
||||||
def build(size, chars):
|
def build(size, chars):
|
||||||
font = ImageFont.truetype(FONT, size)
|
font = ImageFont.truetype(FONT, size)
|
||||||
|
|
@ -88,12 +89,20 @@ def render(draw_img, font, s, x, y, fg, bg):
|
||||||
pen += adv
|
pen += adv
|
||||||
return pen
|
return pen
|
||||||
|
|
||||||
|
blob_s = build(12, S_CHARS)
|
||||||
blob_m = build(22, M_CHARS)
|
blob_m = build(22, M_CHARS)
|
||||||
blob_l = build(78, L_CHARS)
|
blob_l = build(78, L_CHARS)
|
||||||
|
# base64 (baked into the MicroPython firmware, pico/main.py — FONT_M / FONT_L)
|
||||||
(HERE / "_font_m.b64").write_text(base64.b64encode(blob_m).decode())
|
(HERE / "_font_m.b64").write_text(base64.b64encode(blob_m).decode())
|
||||||
(HERE / "_font_l.b64").write_text(base64.b64encode(blob_l).decode())
|
(HERE / "_font_l.b64").write_text(base64.b64encode(blob_l).decode())
|
||||||
print("FONT_M %d bytes -> %d b64" % (len(blob_m), len(base64.b64encode(blob_m))))
|
print("FONT_M %d bytes -> %d b64" % (len(blob_m), len(base64.b64encode(blob_m))))
|
||||||
print("FONT_L %d bytes -> %d b64" % (len(blob_l), len(base64.b64encode(blob_l))))
|
print("FONT_L %d bytes -> %d b64" % (len(blob_l), len(base64.b64encode(blob_l))))
|
||||||
|
# binary blobs (read at boot by the CircuitPython firmware, pico-cp/code.py)
|
||||||
|
CP = HERE.parent / "pico-cp"
|
||||||
|
(CP / "font_s.bin").write_bytes(blob_s)
|
||||||
|
(CP / "font_m.bin").write_bytes(blob_m)
|
||||||
|
(CP / "font_l.bin").write_bytes(blob_l)
|
||||||
|
print("wrote pico-cp/font_{s,m,l}.bin (%d / %d / %d bytes)" % (len(blob_s), len(blob_m), len(blob_l)))
|
||||||
|
|
||||||
# verification image on a dark bg (565 colours like the firmware)
|
# verification image on a dark bg (565 colours like the firmware)
|
||||||
def c565(r, g, b): return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
def c565(r, g, b): return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue