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>
This commit is contained in:
parent
591fd8cfe5
commit
dec6c61fce
8 changed files with 251 additions and 23 deletions
2
build.sh
2
build.sh
|
|
@ -42,7 +42,7 @@ print("copied pico-cp-app.py")
|
|||
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:
|
||||
for f in ("code.py", "app.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin",
|
||||
"README.md", "protect-firmware.sh"):
|
||||
"logo.bin", "midi.bin", "usb.bin", "README.md", "protect-firmware.sh"):
|
||||
z.write("pico-cp/" + f, f)
|
||||
z.write("dist/editor.html", "editor.html") # offline copy of the editor, on the drive
|
||||
print("zipped pm_k1_circuitpy.zip")
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ from the web editor over USB‑MIDI**, and plays through your **computer's speak
|
|||
(<https://circuitpython.org/board/raspberry_pi_pico/> — Pico 2 / W builds also fine). A `CIRCUITPY`
|
||||
drive appears.
|
||||
2. **Copy the whole bundle** onto `CIRCUITPY`: `boot.py`, `code.py` (loader) + `app.py` (the application),
|
||||
`programs.json`, `font_s.bin` / `font_m.bin` / `font_l.bin`, `editor.html` (offline editor), and the
|
||||
helper scripts. (`code.py` is a tiny stable loader; `app.py` is what firmware updates replace.)
|
||||
`programs.json`, `font_s.bin` / `font_m.bin` / `font_l.bin`, `logo.bin` / `midi.bin` / `usb.bin` (the
|
||||
on-screen logo + MIDI/USB status icons), `editor.html` (offline editor), and the helper scripts.
|
||||
(`code.py` is a tiny stable loader; `app.py` is what firmware updates replace. The `.bin` assets — like
|
||||
the fonts — ride in the bundle, since the one-click updater only pushes `app.py`; if a `.bin` is missing
|
||||
the firmware just falls back to text and never fails to boot.)
|
||||
3. **Power‑cycle** (so `boot.py` takes effect). It boots into appliance mode and runs.
|
||||
|
||||
## Program it from the web (push over USB‑MIDI)
|
||||
|
|
|
|||
Binary file not shown.
126
pico-cp/app.py
126
pico-cp/app.py
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
|
||||
supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart
|
||||
APP_VERSION = "0.0.1" # firmware version (the A/B updater pushes/compares this)
|
||||
APP_VERSION = "0.0.2" # firmware version (the A/B updater pushes/compares this)
|
||||
try:
|
||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
||||
except ImportError:
|
||||
|
|
@ -85,10 +85,13 @@ SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare
|
|||
GM_DEFAULT = 37
|
||||
MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost
|
||||
MAXLANES = 5 # lanes shown on the pad grid (extras still play)
|
||||
GRID_TOP = 150 # top of the pad grid (leaves room for stopwatch/bar)
|
||||
LOG_TOP, LOG_ROWH, LOG_ROWS = 302, 16, 9 # practice-history log area (below the pad grid)
|
||||
MIN_LOG_SEC = 5 # don't log plays shorter than this
|
||||
PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost
|
||||
PAD_LIT = (0x39414D, 0x0AB3F7, 0xFF9B2E, 0x967BFF) # playhead pad: mute / normal / accent / ghost
|
||||
C_GRID = 0x1A2330 # faint vertical beat gridlines (beats line up across lanes)
|
||||
PAD_OUTLINE = 0x33414F # off-beat hollow-square border when idle
|
||||
|
||||
# WS2812 RGB LED - self-contained via the core neopixel_write module (no external library)
|
||||
class RGB:
|
||||
|
|
@ -162,6 +165,31 @@ def make_text(s, font, fg, bg):
|
|||
pen += adv
|
||||
return displayio.TileGrid(bmp, pixel_shader=pal), w, h
|
||||
|
||||
# ---- single-image alpha assets (logo, status icons) — blit like a one-off glyph; see gen_assets.py ----
|
||||
def load_alpha(path):
|
||||
try:
|
||||
with open(path, "rb") as f: blob = f.read()
|
||||
return (blob[0], blob[1], blob) # (w, h, bytes); pixels start at offset 2
|
||||
except Exception:
|
||||
return None # missing/corrupt -> caller falls back to text (no crash)
|
||||
def make_glyph(asset, fg, bg):
|
||||
w, h, blob = asset
|
||||
gc.collect()
|
||||
bmp = displayio.Bitmap(w, h, 16); pal = displayio.Palette(16)
|
||||
for i in range(16): pal[i] = _blend(bg, fg, i)
|
||||
for k in range(w * h):
|
||||
byte = blob[2 + (k >> 1)]
|
||||
nib = (byte >> 4) if (k & 1) == 0 else (byte & 0xF)
|
||||
if nib: bmp[k % w, k // w] = nib
|
||||
return displayio.TileGrid(bmp, pixel_shader=pal), pal, w, h
|
||||
def _recolor(pal, fg, bg): # re-tint a stored asset palette in place (tear-free)
|
||||
for i in range(16): pal[i] = _blend(bg, fg, i)
|
||||
|
||||
LOGO = load_alpha("/logo.bin") # VARASYS wordmark (no tagline)
|
||||
ICON_MIDI = load_alpha("/midi.bin") # DIN-5: green when a MIDI host is listening
|
||||
ICON_USB = load_alpha("/usb.bin") # trident: lit when USB-connected to a computer
|
||||
gc.collect()
|
||||
|
||||
# ============================== POLYMETER ENGINE (same semantics as the web/MicroPython) ==============================
|
||||
PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0}
|
||||
PRIO = {2: 3, 1: 2, 3: 1}
|
||||
|
|
@ -312,9 +340,13 @@ class App:
|
|||
self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.rgb = (0, 0, 0)
|
||||
self.programs = load_programs()
|
||||
self.dirty = True
|
||||
self.pad_pal = displayio.Palette(8)
|
||||
self.pad_pal = displayio.Palette(10) # 0-3 idle levels, 4-7 lit levels, 8 off-beat border, 9 hollow bg
|
||||
for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i]
|
||||
self.pad_pal[8] = PAD_OUTLINE; self.pad_pal[9] = C_BG
|
||||
self.lane_pads = []; self.lane_lit = []
|
||||
self.usb_conn = False; self._m_steps = 0 # USB-connected state; master-lane steps (for the bar counter)
|
||||
self._uiNext = 0.0; self._lastTs = None; self._lastBs = None # throttle the stopwatch/bar redraw
|
||||
self.ic_midi_pal = None; self.ic_usb_pal = None
|
||||
# practice history - persisted to /history.json (next to programs.json) when we own the filesystem
|
||||
self.can_write = self._probe_write()
|
||||
self.log = self._load_log()
|
||||
|
|
@ -322,7 +354,7 @@ class App:
|
|||
self._armed = None; self.log_rows = []
|
||||
self._build_scene()
|
||||
self.load(0)
|
||||
self.draw_log()
|
||||
self.draw_log(); self.draw_icons(); self.draw_meters()
|
||||
|
||||
def _btn(self, pin):
|
||||
d = digitalio.DigitalInOut(pin); d.direction = digitalio.Direction.INPUT; d.pull = digitalio.Pull.UP
|
||||
|
|
@ -332,13 +364,23 @@ class App:
|
|||
def _build_scene(self):
|
||||
root = displayio.Group(); self.display.root_group = root
|
||||
root.append(rect(0, 0, WIDTH, HEIGHT, C_BG))
|
||||
tg, w, h = make_text("PM_K-1 KIT", FONT_M, C_CYAN, C_BG); tg.x = 12; tg.y = 8; root.append(tg)
|
||||
# header: VARASYS logo (left, no tagline) + MIDI / USB status icons (right)
|
||||
if LOGO:
|
||||
tg, _p, lw, lh = make_glyph(LOGO, C_CYAN, C_BG); tg.x = 10; tg.y = 9; root.append(tg)
|
||||
else:
|
||||
tg, w, h = make_text("VARASYS", FONT_M, C_CYAN, C_BG); tg.x = 10; tg.y = 8; root.append(tg)
|
||||
x = WIDTH - 12
|
||||
for asset, attr in ((ICON_USB, "ic_usb_pal"), (ICON_MIDI, "ic_midi_pal")):
|
||||
if asset:
|
||||
tg, pal, w, h = make_glyph(asset, C_DIM, C_BG); x -= w; tg.x = x; tg.y = 8; x -= 8
|
||||
root.append(tg); setattr(self, attr, pal)
|
||||
root.append(rect(0, 38, WIDTH, 2, C_PANEL))
|
||||
# dynamic groups
|
||||
self.g_bpm = displayio.Group(); root.append(self.g_bpm) # big tempo (right)
|
||||
self.g_run = displayio.Group(); root.append(self.g_run) # RUN / STOP (left)
|
||||
self.g_time = displayio.Group(); root.append(self.g_time) # stopwatch (m:ss, left)
|
||||
self.g_bar = displayio.Group(); root.append(self.g_bar) # bar counter (right)
|
||||
self.g_name = displayio.Group(); root.append(self.g_name) # item index + name
|
||||
self.g_midi = displayio.Group(); root.append(self.g_midi) # "MIDI" indicator (top-right) when a host is listening
|
||||
self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes x step pads
|
||||
root.append(rect(0, LOG_TOP - 6, WIDTH, 2, C_PANEL)) # divider above the history log
|
||||
self.g_log = displayio.Group(); root.append(self.g_log) # practice history (tap a row to delete)
|
||||
|
|
@ -376,6 +418,7 @@ class App:
|
|||
now = time.monotonic_ns()
|
||||
for L in self.lanes:
|
||||
L['next'] = now; L['step'] = -1
|
||||
self._m_steps = 0 # restart the bar count
|
||||
|
||||
# ---------- audio + light ----------
|
||||
def click(self, level):
|
||||
|
|
@ -428,6 +471,7 @@ class App:
|
|||
adv = False
|
||||
while now >= L['next']:
|
||||
L['step'] = (L['step'] + 1) % L['steps']
|
||||
if li == 0: self._m_steps += 1 # count master-lane steps -> bars
|
||||
lvl = 0 if L['mute'] else L['levels'][L['step']]
|
||||
if lvl > 0:
|
||||
fired.append(lvl)
|
||||
|
|
@ -482,7 +526,10 @@ class App:
|
|||
if host != self.midi_host:
|
||||
self.midi_host = host
|
||||
if host: self.buz.duty_cycle = 0 # silence the buzzer when the computer takes over
|
||||
self.led_off(); self.draw_midi()
|
||||
self.led_off(); self.draw_icons()
|
||||
uc = bool(getattr(supervisor.runtime, "usb_connected", True)) # connected to a computer?
|
||||
if uc != self.usb_conn:
|
||||
self.usb_conn = uc; self.draw_icons()
|
||||
|
||||
# ---------- drawing ----------
|
||||
def draw_bpm(self):
|
||||
|
|
@ -491,42 +538,78 @@ class App:
|
|||
self._place(self.g_run, "RUN" if self.running else "STOP", 12, 48,
|
||||
C_GREEN if self.running else C_MUTE, C_BG, FONT_M)
|
||||
self._place(self.g_name, "%d/%d %s" % (self.idx+1, len(self.programs), self.name[:18]),
|
||||
12, 112, C_TXT, C_BG, FONT_M)
|
||||
def draw_midi(self):
|
||||
self._place(self.g_midi, "MIDI" if self.midi_host else "", 0, 12, C_GREEN, C_BG, FONT_M, right_edge=WIDTH-12)
|
||||
12, 120, C_TXT, C_BG, FONT_M)
|
||||
def draw_icons(self): # recolor the MIDI/USB icons by state (tear-free palette swap)
|
||||
if self.ic_midi_pal is not None:
|
||||
_recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG)
|
||||
if self.ic_usb_pal is not None:
|
||||
_recolor(self.ic_usb_pal, C_CYAN if self.usb_conn else C_DIM, C_BG)
|
||||
self.dirty = True
|
||||
def draw_meters(self): # stopwatch (m:ss) + bar counter; called ~4x/s from run()
|
||||
if self.running and self.play_start is not None:
|
||||
el = int(time.monotonic() - self.play_start)
|
||||
ts = "%d:%02d" % (el // 60, el % 60)
|
||||
mlen = self.lanes[0]['steps'] if self.lanes else 1
|
||||
bs = "bar %d" % (self._m_steps // max(1, mlen) + 1)
|
||||
else:
|
||||
ts = "0:00"; bs = "bar -"
|
||||
if ts != self._lastTs:
|
||||
self._place(self.g_time, ts, 12, 86, C_TXT, C_BG, FONT_M); self._lastTs = ts
|
||||
if bs != self._lastBs:
|
||||
self._place(self.g_bar, bs, 0, 92, C_MUTE, C_BG, FONT_M, right_edge=WIDTH-12); self._lastBs = bs
|
||||
|
||||
# ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ----------
|
||||
def _padbase(self, L, s):
|
||||
return 0 if L['mute'] else L['levels'][s]
|
||||
def _sq(self, cx, cy, side, ci): # a centred square pad sharing pad_pal
|
||||
r = vectorio.Rectangle(pixel_shader=self.pad_pal, width=side, height=side, x=cx - side // 2, y=cy - side // 2)
|
||||
r.color_index = ci; return r
|
||||
def build_grid(self):
|
||||
while len(self.g_grid): self.g_grid.pop()
|
||||
self.lane_pads = []; self.lane_lit = []
|
||||
n = min(len(self.lanes), MAXLANES)
|
||||
top = 140; rowh = min(40, (296 - top) // max(1, n))
|
||||
top = GRID_TOP; rowh = min(40, ((LOG_TOP - 10) - top) // max(1, n))
|
||||
px0 = 60; usable = WIDTH - 8 - px0 - 12; gridh = n * rowh
|
||||
# vertical gridlines at the master lane's beats, full height -> beats line up across lanes
|
||||
m = self.lanes[0]; mbeats = max(1, m['steps'] // max(1, m['sub']))
|
||||
for bcol in range(mbeats):
|
||||
self.g_grid.append(rect(px0 + 6 + (bcol * usable) // mbeats, top, 1, gridh, C_GRID))
|
||||
for li in range(n):
|
||||
L = self.lanes[li]; y = top + li * rowh; cy = y + rowh // 2
|
||||
tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG)
|
||||
tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg)
|
||||
steps = L['steps']; sub = L['sub']; px0 = 60
|
||||
usable = WIDTH - 8 - px0 - 12; stepw = max(1, usable // steps)
|
||||
r_big = max(2, min(6, stepw // 2, (rowh - 8) // 2)); r_sml = max(2, r_big - 2)
|
||||
steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps)
|
||||
side = max(5, min(15, stepw - 1, rowh - 6)); inner = max(2, side - 4)
|
||||
pads = []
|
||||
for s in range(steps):
|
||||
rad = r_big if (s % sub == 0) else r_sml # big = beat (division), small = subdivision
|
||||
base = self._padbase(L, s)
|
||||
cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes
|
||||
c = vectorio.Circle(pixel_shader=self.pad_pal, radius=rad, x=cxp, y=cy)
|
||||
c.color_index = self._padbase(L, s); self.g_grid.append(c); pads.append(c)
|
||||
if s % sub == 0: # main beat -> filled square
|
||||
sq = self._sq(cxp, cy, side, base); self.g_grid.append(sq)
|
||||
pads.append(("fill", (sq,), base))
|
||||
else: # off-beat -> hollow outline square
|
||||
out = self._sq(cxp, cy, side, base if base else 8)
|
||||
ins = self._sq(cxp, cy, inner, 9) # bg fill = the hollow centre
|
||||
self.g_grid.append(out); self.g_grid.append(ins)
|
||||
pads.append(("out", (out, ins), base))
|
||||
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
||||
self.dirty = True
|
||||
def _pad_idle(self, pad):
|
||||
kind, shapes, base = pad
|
||||
if kind == "fill": shapes[0].color_index = base
|
||||
else: shapes[0].color_index = base if base else 8; shapes[1].color_index = 9 # ring + hollow centre
|
||||
def _pad_lit(self, pad):
|
||||
kind, shapes, base = pad
|
||||
for sh in shapes: sh.color_index = base + 4 # fill the square (lit level) regardless of shape
|
||||
def _move_playhead(self, li, step):
|
||||
pads = self.lane_pads[li]; prev = self.lane_lit[li]
|
||||
if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev)
|
||||
if step < len(pads): pads[step].color_index = self._padbase(self.lanes[li], step) + 4
|
||||
if 0 <= prev < len(pads): self._pad_idle(pads[prev])
|
||||
if step < len(pads): self._pad_lit(pads[step])
|
||||
self.lane_lit[li] = step; self.dirty = True
|
||||
def reset_playheads(self):
|
||||
for li, pads in enumerate(self.lane_pads):
|
||||
prev = self.lane_lit[li]
|
||||
if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev)
|
||||
if 0 <= prev < len(pads): self._pad_idle(pads[prev])
|
||||
self.lane_lit[li] = -1
|
||||
self.dirty = True
|
||||
|
||||
|
|
@ -635,7 +718,10 @@ class App:
|
|||
except OSError: committed = True
|
||||
while True:
|
||||
self.tick(); self.poll()
|
||||
if not committed and time.monotonic() - boot > 5: # booted & ran fine for 5s -> confirm the update
|
||||
tnow = time.monotonic()
|
||||
if tnow >= self._uiNext: # ~4x/s: tick the stopwatch + bar counter
|
||||
self._uiNext = tnow + 0.25; self.draw_meters()
|
||||
if not committed and tnow - boot > 5: # booted & ran fine for 5s -> confirm the update
|
||||
try: os.remove("/trial")
|
||||
except Exception: pass
|
||||
committed = True
|
||||
|
|
|
|||
139
pico-cp/gen_assets.py
Normal file
139
pico-cp/gen_assets.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/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()
|
||||
BIN
pico-cp/logo.bin
Normal file
BIN
pico-cp/logo.bin
Normal file
Binary file not shown.
BIN
pico-cp/midi.bin
Normal file
BIN
pico-cp/midi.bin
Normal file
Binary file not shown.
BIN
pico-cp/usb.bin
Normal file
BIN
pico-cp/usb.bin
Normal file
Binary file not shown.
Loading…
Reference in a new issue