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>
139 lines
5.1 KiB
Python
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()
|