metronome/pico-cp/gen_assets.py
Me Here dec6c61fce PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter
Device screen redesign (CircuitPython app.py), built proportional to WIDTH/HEIGHT
so it scales to other panels (one adaptive firmware, per-panel config — not a fork):

- gen_assets.py bakes logo.bin (VARASYS wordmark, no tagline), midi.bin (DIN-5),
  usb.bin (trident) as 4-bit-alpha bitmaps (same packing as the fonts).
- Header: VARASYS logo (brand cyan) replaces the "PM_K-1 KIT" text; MIDI icon goes
  green when a host is listening, USB icon lights when supervisor.runtime.usb_connected.
  load_alpha/make_glyph are non-fatal — a missing .bin falls back to text, never a
  black screen (addresses the corrupt-file failure mode we just hit).
- Pad grid: filled squares on main beats, hollow outline squares (outer+inner rect) on
  off-beats; playhead fills the lit pad. Vertical gridlines at the master lane's beats
  (full height) so beats line up across lanes.
- Stopwatch (m:ss) + bar counter (master-lane cycles), refreshed ~4x/s only on change.

The .bin assets ship in the drive bundle (the A/B updater only pushes app.py), so a
one-time re-copy is needed to pick them up. APP_VERSION -> 0.0.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:56:45 -05:00

139 lines
5.1 KiB
Python

#!/usr/bin/env python3
# Generate the on-screen bitmap assets the CircuitPython firmware (app.py) blits:
# logo.bin - the VARASYS wordmark (no tagline), tinted brand cyan at the top
# midi.bin - a 5-pin DIN icon (lights green when a MIDI host is listening)
# usb.bin - the USB "trident" icon (lights when the device is USB-connected)
#
# Each file is a single 4-bit-alpha image with a 2-byte header (matches the font packing
# in gen_font.py, just without the glyph metrics table):
# byte 0 = width, byte 1 = height, then ((w*h+1)//2) bytes of 4-bit alpha,
# row-major, two pixels per byte (first pixel = high nibble).
# app.py's load_alpha()/make_glyph() decode it and blend bg->fg per pixel (smooth).
#
# Re-run after changing the logo/icons: python3 pico-cp/gen_assets.py
# Writes pico-cp/{logo,midi,usb}.bin and /tmp/assets_verify.png (eyeball it).
import math, pathlib
from PIL import Image, ImageDraw
HERE = pathlib.Path(__file__).parent
LOGO_PNG = pathlib.Path.home() / "src/varasys_logo/For using with dark colored background/VARASYS Limited.png"
SS = 6 # supersample factor for the drawn icons (downscaled -> anti-aliased)
def pack(coverage_img):
"""coverage_img: 'L' image (0..255 alpha). -> bytes(w, h, packed 4-bit alpha)."""
w, h = coverage_img.size
assert w <= 255 and h <= 255, "asset too large for the 1-byte dims (%dx%d)" % (w, h)
px = coverage_img.load()
nib = []
for y in range(h):
for x in range(w):
nib.append(px[x, y] >> 4) # 8-bit -> 4-bit alpha
if len(nib) % 2:
nib.append(0)
out = bytearray([w, h])
for i in range(0, len(nib), 2):
out.append((nib[i] << 4) | nib[i + 1])
return bytes(out)
def make_logo(target_w=156):
img = Image.open(LOGO_PNG).convert("RGBA")
r, g, b, a = img.split()
# Coverage = alpha if the PNG is genuinely transparent, else brightness (cyan on a flat bg).
if a.getextrema()[0] < 250:
cov = a
else:
cov = img.convert("L")
bbox = cov.getbbox()
if bbox:
cov = cov.crop(bbox)
w, h = cov.size
th = max(1, round(target_w * h / w))
cov = cov.resize((target_w, th), Image.LANCZOS)
return pack(cov)
def make_midi(size=22):
cw = size * SS
img = Image.new("L", (cw, cw), 0)
d = ImageDraw.Draw(img)
cx = cy = cw / 2
R = cw * 0.44
lw = max(2, int(cw * 0.07))
d.ellipse([cx - R, cy - R, cx + R, cy + R], outline=255, width=lw) # connector shell
# 5 pins in the DIN fan (one top-centre, a flanking pair above, a wider pair below)
ring = R * 0.60
pr = cw * 0.075
pins = [(0.0, -1.0), (-0.72, -0.55), (0.72, -0.55), (-0.95, 0.18), (0.95, 0.18)]
for fx, fy in pins:
x = cx + fx * ring
y = cy + fy * ring
d.ellipse([x - pr, y - pr, x + pr, y + pr], fill=255)
return pack(img.resize((size, size), Image.LANCZOS))
def make_usb(size=22):
cw = size * SS
img = Image.new("L", (cw, cw), 0)
d = ImageDraw.Draw(img)
cx = cw / 2
lw = max(2, int(cw * 0.075))
top, bot = cw * 0.10, cw * 0.90
d.line([(cx, top + cw * 0.10), (cx, bot)], fill=255, width=lw) # shaft
# arrowhead at the top
ah = cw * 0.13
d.polygon([(cx, top), (cx - ah, top + ah * 1.4), (cx + ah, top + ah * 1.4)], fill=255)
# base plug (filled circle at the bottom)
br = cw * 0.10
d.ellipse([cx - br, bot - br, cx + br, bot + br], fill=255)
# left branch -> small filled circle
ly = cw * 0.46
lx = cx - cw * 0.26
d.line([(cx, ly + cw * 0.10), (lx, ly)], fill=255, width=lw)
cr = cw * 0.085
d.ellipse([lx - cr, ly - cr, lx + cr, ly + cr], fill=255)
# right branch -> small filled square
ry = cw * 0.34
rx = cx + cw * 0.26
d.line([(cx, ry + cw * 0.10), (rx, ry)], fill=255, width=lw)
sq = cw * 0.08
d.rectangle([rx - sq, ry - sq, rx + sq, ry + sq], fill=255)
return pack(img.resize((size, size), Image.LANCZOS))
def unpack_to_img(blob, fg=(10, 179, 247), bg=(6, 9, 14)):
"""Decode like app.py would, for the verify sheet."""
w, h = blob[0], blob[1]
im = Image.new("RGB", (w, h), bg)
for k in range(w * h):
byte = blob[2 + (k >> 1)]
nib = (byte >> 4) if (k & 1) == 0 else (byte & 0xF)
t = nib * 17
col = tuple((bg[i] * (255 - t) + fg[i] * t) // 255 for i in range(3))
im.putpixel((k % w, k // w), col)
return im
def main():
assets = {"logo": make_logo(), "midi": make_midi(), "usb": make_usb()}
for name, blob in assets.items():
(HERE / (name + ".bin")).write_bytes(blob)
print("wrote %s.bin %dx%d %d bytes" % (name, blob[0], blob[1], len(blob)))
# verify sheet on a dark panel-like background
pad = 12
imgs = [unpack_to_img(b) for b in assets.values()]
W = max(i.width for i in imgs) + pad * 2
H = sum(i.height for i in imgs) + pad * (len(imgs) + 1)
sheet = Image.new("RGB", (W, H), (6, 9, 14))
y = pad
for i in imgs:
sheet.paste(i, (pad, y))
y += i.height + pad
sheet.save("/tmp/assets_verify.png")
print("verify -> /tmp/assets_verify.png")
if __name__ == "__main__":
main()