Files
pictureFrame-firmware/scripts/gen_screens.py
T
football2801 fb4c5ff5d3 fix(provisioning): stop redrawing the QR on every poll, add WiFi-fail retry screen
Two related fixes that together let the post-WiFi-setup window be quiet:

1. operation.h 204/404: skip the panel redraw entirely. The panel already
   holds the right thing — setup QR if no image has ever been painted
   (img_id == -1), or a real photo if img_id >= 0. Redrawing the QR every
   15s during the bootstrap claim window put the e-ink into a perpetual
   ~20s mid-refresh loop and risked ghosting. Tests updated to assert
   no redraw on either sub-case.

2. main.cpp WiFi-fail path: drop the epd_fill(RED) + 3s delay + AP
   re-redraw sequence (~43s of e-ink work that destroyed the QR mid-flow)
   and replace with a single repaint of a new "Connection Failed — try
   again" Step 1/2 screen with red accents. gen_screens.py grows a
   gen_ap_retry() variant that recolors yellow → red and swaps the
   header/QR labels; the result is shipped as ap_bg_retry.bin alongside
   ap_bg.bin in LittleFS. epd.h exposes epd_draw_ap_screen_retry().
2026-05-08 23:43:59 -04:00

370 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Generate ap_bg.bin and setup_bg.bin — 800×480 4bpp backgrounds for the
pictureFrame e-ink device. QR overlay areas are left WHITE so the
firmware can render the actual QR code at runtime.
Run from the firmware/ directory:
python3 scripts/gen_screens.py
Constants exported (copy to epd.cpp):
AP_QR_X, AP_QR_Y, AP_QR_CELL, AP_QR_PX
SETUP_QR_X, SETUP_QR_Y, SETUP_QR_CELL, SETUP_QR_PX
"""
from PIL import Image, ImageDraw, ImageFont
import os, sys
# ── Display ──────────────────────────────────────────────────────────────────
W, H = 800, 480
# ── EPD palette ───────────────────────────────────────────────────────────────
BLACK = 0x0; BK = (26, 26, 26 )
WHITE = 0x1; WH = (245, 245, 240)
YELLOW = 0x2; YL = (240, 208, 0 )
RED = 0x3; RD = (192, 48, 32 )
BLUE = 0x5; BL = (24, 64, 192)
GREEN = 0x6; GR = (16, 160, 64 )
PALETTE_RGB = {BLACK: BK, WHITE: WH, YELLOW: YL, RED: RD, BLUE: BL, GREEN: GR}
def nearest(r, g, b):
best, best_d = WHITE, float("inf")
for n, (pr, pg, pb) in PALETTE_RGB.items():
d = (r-pr)**2 + (g-pg)**2 + (b-pb)**2
if d < best_d: best, best_d = n, d
return best
def pack(img):
"""Convert RGB PIL image → 4bpp packed bytearray."""
px = img.load()
out = bytearray()
for y in range(H):
for x in range(0, W, 2):
hi = nearest(*px[x, y])
lo = nearest(*px[x+1, y])
out.append((hi << 4) | lo)
return out
# ── Fonts ─────────────────────────────────────────────────────────────────────
FONT_DIR = "/usr/share/fonts/truetype/dejavu"
def ttf(name, size):
try: return ImageFont.truetype(os.path.join(FONT_DIR, name), size)
except: return ImageFont.load_default()
F_HEAD = ttf("DejaVuSans-Bold.ttf", 26)
F_BAR = ttf("DejaVuSans-Bold.ttf", 13)
F_STEP = ttf("DejaVuSans.ttf", 13)
F_STEP_B= ttf("DejaVuSans-Bold.ttf", 13)
F_STEPN = ttf("DejaVuSans-Bold.ttf", 13)
F_LABEL = ttf("DejaVuSans-Bold.ttf", 11)
F_TINY = ttf("DejaVuSans-Bold.ttf", 10)
F_FOOT = ttf("DejaVuSans.ttf", 12)
F_CHIP = ttf("DejaVuSans-Bold.ttf", 12)
F_SUB = ttf("DejaVuSans.ttf", 14)
F_BIG = ttf("DejaVuSans-Bold.ttf", 14)
# ── Layout constants ──────────────────────────────────────────────────────────
BAR_H = 52
BODY_Y = BAR_H # 52
LEFT_X = 0; LEFT_W = 310
DIV1_X = 310; DIV_W = 2
CTR_X = 312; CTR_W = 196
DIV2_X = 508
RIGHT_X = 510; RIGHT_W = 290 # 800-510
# QR positions (MUST match epd.cpp constants)
AP_QR_CELL = 5
AP_QR_MODS = 37 # version 5, ECC_LOW
AP_QR_PX = AP_QR_MODS * AP_QR_CELL # 185
SETUP_QR_CELL = 5
SETUP_QR_MODS = 41 # version 6, ECC_LOW
SETUP_QR_PX = SETUP_QR_MODS * SETUP_QR_CELL # 205
# Centre of right panel
RIGHT_CX = RIGHT_X + RIGHT_W // 2 # 655
BODY_CY = BODY_Y + (H - BODY_Y) // 2 # 266
AP_QR_X = RIGHT_CX - AP_QR_PX // 2 # 563
AP_QR_Y = BODY_CY - AP_QR_PX // 2 # 174 (will nudge down below label)
AP_QR_Y = 185 # nudge down a bit for "SCAN TO CONNECT" label above
SETUP_QR_X = RIGHT_CX - SETUP_QR_PX // 2 # 553
SETUP_QR_Y = BODY_CY - SETUP_QR_PX // 2 # 164
SETUP_QR_Y = 175 # nudge for label
def leave_qr_white(draw, qr_x, qr_y, qr_px):
"""Blank the QR overlay region so firmware can write the real QR."""
draw.rectangle([qr_x, qr_y, qr_x+qr_px-1, qr_y+qr_px-1], fill=WH)
def text_center(draw, cx, y, text, font, fill):
bb = draw.textbbox((0,0), text, font=font)
tw = bb[2]-bb[0]
draw.text((cx - tw//2, y), text, font=font, fill=fill)
def orientation_diagrams(draw, accent, show_active_ls=True):
"""Draw both orientation diagrams in the centre panel.
accent = RGB colour for the active / ribbon highlights."""
cx = CTR_X + CTR_W // 2 # 410
# ── Section title ─────────────────────────────────────────────
text_center(draw, cx, BODY_Y+15, "FRAME", F_TINY, BK)
text_center(draw, cx, BODY_Y+27, "ORIENTATION", F_TINY, BK)
# ── Landscape ──────────────────────────────────────────────────
ls_x, ls_y, ls_w, ls_h = CTR_X+43, BODY_Y+52, 110, 66
rib_w, rib_h = 110, 10
text_center(draw, cx, ls_y-14, "LANDSCAPE", F_LABEL, accent if show_active_ls else BK)
ls_border = accent if show_active_ls else BK
draw.rectangle([ls_x, ls_y, ls_x+ls_w-1, ls_y+ls_h-1], outline=ls_border, width=3)
rib_rgb = accent if show_active_ls else BK
draw.rectangle([ls_x, ls_y+ls_h, ls_x+rib_w-1, ls_y+ls_h+rib_h-1], fill=rib_rgb)
if show_active_ls:
# check badge
bx, by = cx-9, ls_y+ls_h+rib_h+5
draw.rectangle([bx, by, bx+18, by+18], fill=accent)
text_center(draw, bx+9, by+3, "", F_CHIP, BK)
# Thin separator
sep_y = ls_y + ls_h + rib_h + (30 if show_active_ls else 14)
draw.rectangle([CTR_X+10, sep_y, CTR_X+CTR_W-10, sep_y], fill=(180,180,175))
# ── Portrait ──────────────────────────────────────────────────
pt_x, pt_y = CTR_X+56, sep_y+14
pt_w, pt_h = 64, 106
pr_w, pr_h = 10, 106
text_center(draw, cx, pt_y-14, "PORTRAIT", F_LABEL, BK)
draw.rectangle([pt_x-pr_w, pt_y, pt_x-1, pt_y+pr_h-1], fill=BK)
draw.rectangle([pt_x, pt_y, pt_x+pt_w-1, pt_y+pt_h-1], outline=BK, width=3)
# ═══════════════════════════════════════════════════════════════════════════════
# AP SCREEN — accent-colored, WiFi credentials
# Pass accent=YL/header="SETUP MODE — STEP 1 OF 2"/qr_label="SCAN TO CONNECT"
# for the normal first-attempt screen, or accent=RD/header="CONNECTION FAILED
# — TRY AGAIN"/qr_label="Connection Failed — try again" for the post-WiFi-fail
# retry screen. Same layout either way so the panel diff is just color +
# header/label text.
# ═══════════════════════════════════════════════════════════════════════════════
def gen_ap(accent=YL, header="SETUP MODE — STEP 1 OF 2", qr_label="SCAN TO CONNECT"):
img = Image.new("RGB", (W, H), WH)
draw = ImageDraw.Draw(img)
# ── Status bar ────────────────────────────────────────────────
draw.rectangle([0, 0, W-1, BAR_H-1], fill=accent)
draw.text((24, 18), header, font=F_BAR, fill=BK)
# Right chip: black box with device SSID
chip_x, chip_y = 498, 11
chip_text = "PictureFrame-91F8"
bb = draw.textbbox((0,0), chip_text, font=F_CHIP)
chip_w = bb[2]-bb[0] + 22
chip_x2 = chip_x + chip_w
draw.rectangle([chip_x, chip_y, chip_x2, BAR_H-12], fill=BK)
draw.text((chip_x+11, chip_y+7), chip_text, font=F_CHIP, fill=accent)
# ── Panel dividers ────────────────────────────────────────────
draw.rectangle([DIV1_X, BODY_Y, DIV1_X+1, H-1], fill=BK)
draw.rectangle([DIV2_X, BODY_Y, DIV2_X+1, H-1], fill=BK)
# ── Left panel ────────────────────────────────────────────────
# Heading
draw.text((28, BODY_Y+20), "Connect to", font=F_HEAD, fill=BK)
draw.text((28, BODY_Y+52), "WiFi", font=F_HEAD, fill=BK)
bb = draw.textbbox((0,0), "WiFi", font=F_HEAD)
draw.rectangle([28, BODY_Y+82, 28+bb[2]+2, BODY_Y+85], fill=accent)
# Steps
steps = [
("Scan the QR code →", "Phone joins PictureFrame-91F8"),
("Browser opens — enter", "your home WiFi password"),
("Tap Connect and watch", "for the QR code to change"),
]
sy = BODY_Y + 105
for i, (l1, l2) in enumerate(steps):
bx, by = 28, sy + i*46
draw.rectangle([bx, by, bx+24, by+24], fill=BK)
text_center(draw, bx+12, by+6, str(i+1), F_STEPN, accent)
draw.text((62, by+3), l1, font=F_STEP, fill=BK)
draw.text((62, by+17), l2, font=F_STEP, fill=BK)
# Divider + footnote
draw.rectangle([28, BODY_Y+254, 283, BODY_Y+255], fill=BK)
draw.text((28, BODY_Y+262), "Page didn't open?", font=F_FOOT, fill=BK)
draw.text((28, BODY_Y+276), "Go to 192.168.4.1", font=F_FOOT, fill=BK)
# ── Centre panel ─────────────────────────────────────────────
orientation_diagrams(draw, accent, show_active_ls=True)
# ── Right panel ──────────────────────────────────────────────
cx = RIGHT_CX
# QR label — accent-colored on retry so the failure is unmistakable.
label_color = accent if accent != YL else BK
text_center(draw, cx, AP_QR_Y - 26, qr_label, F_BIG, label_color)
# QR border: accent outer, black inner
qx, qy, qp = AP_QR_X, AP_QR_Y, AP_QR_PX
draw.rectangle([qx-6, qy-6, qx+qp+5, qy+qp+5], outline=accent, width=3)
draw.rectangle([qx-3, qy-3, qx+qp+2, qy+qp+2], outline=BK, width=3)
# Leave QR area white for firmware overlay
leave_qr_white(draw, qx, qy, qp)
# "Encodes WIFI:..." label below
text_center(draw, cx, qy+qp+10, "WIFI:S:PictureFrame-91F8;T:nopass;;", F_FOOT, (100,100,95))
return img
def gen_ap_retry():
"""Step 1/2 with red accents + 'Connection Failed — try again' label,
served after a failed WiFi connection attempt."""
return gen_ap(
accent=RD,
header="CONNECTION FAILED — TRY AGAIN",
qr_label="Connection Failed — try again",
)
# ═══════════════════════════════════════════════════════════════════════════════
# SETUP SCREEN — green accent, account link
# ═══════════════════════════════════════════════════════════════════════════════
def gen_setup():
img = Image.new("RGB", (W, H), WH)
draw = ImageDraw.Draw(img)
# ── Status bar ────────────────────────────────────────────────
draw.rectangle([0, 0, W-1, BAR_H-1], fill=GR)
# WiFi bars icon
bars = [(0, 8), (0, 13), (0, 18), (0, 22)]
bx = 24
for i, (_, bh) in enumerate(bars):
draw.rectangle([bx + i*8, BAR_H//2 - bh//2, bx+i*8+5, BAR_H//2 + bh//2], fill=WH)
draw.text((bx+38, 18), "WIFI CONNECTED — STEP 2 OF 2", font=F_BAR, fill=WH)
# Right IP chip
ip_text = "192.168.x.x"
bb = draw.textbbox((0,0), ip_text, font=F_CHIP)
chip_w = bb[2]-bb[0] + 22
chip_x = W - chip_w - 20
draw.rectangle([chip_x, 11, chip_x+chip_w, BAR_H-12], fill=WH)
draw.text((chip_x+11, 18), ip_text, font=F_CHIP, fill=GR)
# ── Panel dividers ────────────────────────────────────────────
draw.rectangle([DIV1_X, BODY_Y, DIV1_X+1, H-1], fill=BK)
draw.rectangle([DIV2_X, BODY_Y, DIV2_X+1, H-1], fill=BK)
# ── Left panel ────────────────────────────────────────────────
draw.text((28, BODY_Y+20), "Almost", font=F_HEAD, fill=BK)
draw.text((28, BODY_Y+52), "ready.", font=F_HEAD, fill=BK)
bb = draw.textbbox((0,0), "ready.", font=F_HEAD)
draw.rectangle([28, BODY_Y+82, 28+bb[2]+2, BODY_Y+85], fill=GR)
draw.text((28, BODY_Y+96), "Scan to name this frame and", font=F_STEP, fill=(80,80,75))
draw.text((28, BODY_Y+110), "link it to your account.", font=F_STEP, fill=(80,80,75))
steps = [
("Scan the QR with your phone", "camera or QR app"),
("Sign in at pictureframe", ".edholm.me"),
("Name the frame, choose", "orientation — done."),
]
sy = BODY_Y + 136
for i, (l1, l2) in enumerate(steps):
bx, by = 28, sy + i*46
draw.rectangle([bx, by, bx+24, by+24], fill=GR)
text_center(draw, bx+12, by+6, str(i+1), F_STEPN, WH)
draw.text((62, by+3), l1, font=F_STEP, fill=BK)
draw.text((62, by+17), l2, font=F_STEP, fill=BK)
# URL bar
url_y = BODY_Y + 278
draw.rectangle([28, url_y, 284, url_y+32], fill=BK)
draw.text((38, url_y+4), "URL", font=F_TINY, fill=GR)
draw.text((38, url_y+16), "pictureframe.edholm.me/setup/...", font=ttf("DejaVuSans.ttf", 10), fill=WH)
# Progress track
prog_y = BODY_Y + 328
draw.text((28, prog_y), "SETUP PROGRESS", font=F_TINY, fill=(140,140,135))
seg_y = prog_y + 14
segs = [("WiFi", GR), ("Account", BK), ("Frame ready", (200,200,195))]
seg_w = (284 - 28 - 8) // 3 # ~82px each
for i, (label, color) in enumerate(segs):
sx = 28 + i*(seg_w+4)
draw.rectangle([sx, seg_y, sx+seg_w, seg_y+6], fill=color)
text_center(draw, sx+seg_w//2, seg_y+10, label, ttf("DejaVuSans.ttf", 9), BK)
# ── Centre panel ─────────────────────────────────────────────
orientation_diagrams(draw, GR, show_active_ls=True)
# ── Right panel ──────────────────────────────────────────────
cx = RIGHT_CX
text_center(draw, cx, SETUP_QR_Y - 26, "SCAN TO FINISH", F_BIG, BK)
qx, qy, qp = SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX
draw.rectangle([qx-6, qy-6, qx+qp+5, qy+qp+5], outline=GR, width=3)
draw.rectangle([qx-3, qy-3, qx+qp+2, qy+qp+2], outline=BK, width=3)
leave_qr_white(draw, qx, qy, qp)
# MAC chip below QR
mac = "1C:C3:AB:D1:91:F8"
bb = draw.textbbox((0,0), mac, font=F_CHIP)
mw = bb[2]-bb[0]+20
mx = cx - mw//2
draw.rectangle([mx, qy+qp+8, mx+mw, qy+qp+26], fill=BK)
draw.text((mx+10, qy+qp+11), mac, font=ttf("DejaVuSans-Bold.ttf", 10), fill=WH)
return img
# ── Save ──────────────────────────────────────────────────────────────────────
def save_bin(img, path, preview_path):
data = pack(img)
with open(path, "wb") as f: f.write(data)
print(f"Written {len(data):,} bytes → {os.path.abspath(path)}")
# Reconstruct preview from packed data for verification
prev = Image.new("RGB", (W, H))
px = prev.load()
for y in range(H):
for x in range(0, W, 2):
byte = data[y*(W//2) + x//2]
px[x, y] = PALETTE_RGB.get(byte >> 4, (128,128,128))
px[x+1, y] = PALETTE_RGB.get(byte & 0xF, (128,128,128))
prev.save(preview_path)
print(f"Preview → {os.path.abspath(preview_path)}")
if __name__ == "__main__":
# Per-panel output directory: data/{vendor}-v{N}/. Defaults to the V1
# 7.3" panel since that's the only one in production; pass --panel to
# target a different one once new panels exist.
panel = "waveshare73-v1"
if "--panel" in sys.argv:
panel = sys.argv[sys.argv.index("--panel") + 1]
out_dir = os.path.join(os.path.dirname(__file__), f"../data/{panel}")
os.makedirs(out_dir, exist_ok=True)
print(f"Generating AP screen for {panel}")
save_bin(gen_ap(), f"{out_dir}/ap_bg.bin", f"{out_dir}/ap_bg_preview.png")
print()
print(f"Generating AP retry screen for {panel}")
save_bin(gen_ap_retry(), f"{out_dir}/ap_bg_retry.bin", f"{out_dir}/ap_bg_retry_preview.png")
print()
print(f"Generating setup screen for {panel}")
save_bin(gen_setup(), f"{out_dir}/setup_bg.bin", f"{out_dir}/setup_bg_preview.png")
print()
print("QR overlay constants for epd.cpp:")
print(f" AP_QR_X={AP_QR_X}, AP_QR_Y={AP_QR_Y}, AP_QR_CELL={AP_QR_CELL}, AP_QR_PX={AP_QR_PX}")
print(f" SETUP_QR_X={SETUP_QR_X}, SETUP_QR_Y={SETUP_QR_Y}, SETUP_QR_CELL={SETUP_QR_CELL}, SETUP_QR_PX={SETUP_QR_PX}")