Four firmware improvements (web simulator already had these):
- Smooth lettering: replace the upscaled 8x8 bitmap font (and the 7-seg BPM) with
baked anti-aliased proportional fonts (DejaVuSans-Bold 22 + 78), rendered by
blending fg over bg through a 16-entry alpha LUT. Generator: pico/gen_font.py.
- All grooves: PROGRAMS now carries the full web set list (23 grooves), not 5.
- Fix: the RGB LED stayed lit when stopped — the stop path now zeroes the colour
and the fade only runs while playing (led_off()).
- Fix: the on-screen play button played one beep then stopped — a finger held past
the old lockout re-fired the toggle. Touch is now edge-detected like the hardware
buttons (fires once on finger-down, ignores held).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.7 KiB
Python
110 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# Generate the anti-aliased bitmap fonts baked into main.py.
|
|
#
|
|
# Each font is one base64 blob: a small metrics table + 4-bit-alpha glyph pixels.
|
|
# main.py decodes it at boot (binascii.a2b_base64) and renders text by blending the
|
|
# foreground over the background per pixel via a 16-entry LUT (smooth, no upscaling).
|
|
#
|
|
# Re-run after changing sizes/charset: python3 pico/gen_font.py
|
|
# It writes pico/_font_m.b64 + pico/_font_l.b64 and /tmp/font_verify.png (eyeball it). Then
|
|
# inject the two base64 strings into main.py's FONT_M_B64 / FONT_L_B64 (replace the existing
|
|
# values, or re-add the @@FONT_M@@ / @@FONT_L@@ placeholders first and substitute them in).
|
|
|
|
import base64, pathlib
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
|
HERE = pathlib.Path(__file__).parent
|
|
|
|
ASCII = "".join(chr(c) for c in range(0x20, 0x7F))
|
|
SYMBOLS = "◀▶■" # ◀ ▶ ■ (prev / play / stop, anti-aliased)
|
|
M_CHARS = ASCII + SYMBOLS
|
|
L_CHARS = "0123456789 "
|
|
|
|
def build(size, chars):
|
|
font = ImageFont.truetype(FONT, size)
|
|
recs = [] # (cp, w, h, xoff, top, adv)
|
|
pixels = bytearray()
|
|
for ch in chars:
|
|
cp = ord(ch)
|
|
adv = round(font.getlength(ch))
|
|
bbox = font.getbbox(ch)
|
|
if not bbox or bbox[2] - bbox[0] <= 0 or bbox[3] - bbox[1] <= 0:
|
|
recs.append((cp, 0, 0, 0, 0, adv)); continue
|
|
l, t, r, b = bbox
|
|
w, h = r - l, b - t
|
|
img = Image.new("L", (w, h), 0)
|
|
ImageDraw.Draw(img).text((-l, -t), ch, fill=255, font=font)
|
|
px = img.load()
|
|
# pack 4-bit alpha, row-major, two pixels per byte (first = high nibble)
|
|
nib = []
|
|
for y in range(h):
|
|
for x in range(w):
|
|
nib.append(px[x, y] >> 4)
|
|
if len(nib) % 2: nib.append(0)
|
|
for i in range(0, len(nib), 2):
|
|
pixels.append((nib[i] << 4) | nib[i + 1])
|
|
recs.append((cp, w, h, l & 0xFF, t & 0xFF, adv & 0xFF))
|
|
header = bytearray([len(recs)])
|
|
for cp, w, h, xoff, top, adv in recs:
|
|
header += bytes([cp >> 8, cp & 0xFF, w, h, xoff, top, adv])
|
|
return bytes(header) + bytes(pixels)
|
|
|
|
# ---- reference decoder + renderer (must match main.py exactly) ----
|
|
def load_font(blob):
|
|
count = blob[0]; p = 1; pixoff = 1 + count * 7; glyphs = {}
|
|
for _ in range(count):
|
|
cp = (blob[p] << 8) | blob[p + 1]; w = blob[p + 2]; h = blob[p + 3]
|
|
xoff = blob[p + 4]; xoff = xoff - 256 if xoff > 127 else xoff
|
|
top = blob[p + 5]; adv = blob[p + 6]; p += 7
|
|
glyphs[cp] = (w, h, xoff, top, adv, pixoff)
|
|
pixoff += (w * h + 1) // 2
|
|
return glyphs, blob
|
|
|
|
def lut(fg, bg):
|
|
def unp(c): return ((c >> 11) & 0x1F, (c >> 5) & 0x3F, c & 0x1F)
|
|
fr, fgc, fb = unp(fg); br, bgc, bb = unp(bg); out = []
|
|
for a in range(16):
|
|
t = a * 17
|
|
r = (br * (255 - t) + fr * t) // 255
|
|
g = (bgc * (255 - t) + fgc * t) // 255
|
|
b = (bb * (255 - t) + fb * t) // 255
|
|
out.append((r << 11) | (g << 5) | b)
|
|
return out
|
|
|
|
def render(draw_img, font, s, x, y, fg, bg):
|
|
glyphs, blob = font; L = lut(fg, bg); pen = x
|
|
for ch in s:
|
|
g = glyphs.get(ord(ch))
|
|
if not g: continue
|
|
w, h, xoff, top, adv, off = g
|
|
for j in range(h):
|
|
for i in range(w):
|
|
k = j * w + i; byte = blob[off + (k >> 1)]
|
|
nibv = (byte >> 4) if (k & 1) == 0 else (byte & 0xF)
|
|
col = L[nibv]
|
|
r = (col >> 11) & 0x1F; gg = (col >> 5) & 0x3F; b = col & 0x1F
|
|
draw_img.putpixel((pen + xoff + i, y + top + j), (r << 3, gg << 2, b << 3))
|
|
pen += adv
|
|
return pen
|
|
|
|
blob_m = build(22, M_CHARS)
|
|
blob_l = build(78, L_CHARS)
|
|
(HERE / "_font_m.b64").write_text(base64.b64encode(blob_m).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_L %d bytes -> %d b64" % (len(blob_l), len(base64.b64encode(blob_l))))
|
|
|
|
# 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)
|
|
BG = c565(6, 9, 14); CYAN = c565(10, 179, 247); TXT = c565(199, 208, 219); GREEN = c565(47, 224, 122)
|
|
img = Image.new("RGB", (320, 220), (6, 9, 14))
|
|
fm = load_font(blob_m); fl = load_font(blob_l)
|
|
render(img, fm, "PM_K-1 KIT", 12, 10, CYAN, BG)
|
|
render(img, fl, "120", 150, 40, TXT, BG)
|
|
render(img, fm, "BPM", 12, 70, TXT, BG)
|
|
render(img, fm, "▶ RUN", 12, 120, GREEN, BG)
|
|
render(img, fm, "Four-on-the-floor", 12, 150, TXT, BG)
|
|
render(img, fm, "◀◀ ▶ ▶▶ - TAP +", 12, 185, TXT, BG)
|
|
img.resize((640, 440), Image.NEAREST).save("/tmp/font_verify.png")
|
|
print("wrote /tmp/font_verify.png")
|