PM_K-1 firmware: smooth AA fonts, full set list, LED-off-on-stop, touch edge-detect
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>
This commit is contained in:
parent
f34f7291de
commit
df6e70473c
3 changed files with 307 additions and 177 deletions
Binary file not shown.
110
pico/gen_font.py
Normal file
110
pico/gen_font.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#!/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")
|
||||
364
pico/main.py
364
pico/main.py
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue