8cbd035708
Flip APP_BASE_URL and the on-screen "go to <domain>/setup/..." text in the rendered setup_bg images from pictureframe.edholm.me to wevisto.com. Per the dual-domain migration plan (Option C — server keeps both alive indefinitely), this only affects newly-flashed units; field devices on the old URL keep working against the same backend. Regenerated both panels' setup_bg.bin via gen_screens*.py so the embedded URL in the on-screen QR overlay text matches the firmware's runtime poll URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
467 lines
22 KiB
Python
467 lines
22 KiB
Python
#!/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 qrcode
|
||
import os, sys
|
||
|
||
# URL for the user manual QR baked into Step 1/2. Static on purpose —
|
||
# changing it requires regenerating the bg images + uploadfs.
|
||
MANUAL_URL = "https://wevisto.com/help"
|
||
|
||
# ── 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)
|
||
# WiFi-join QR — drawn at runtime by firmware. Cell shrunk from 5 to 4
|
||
# (148 px instead of 185 px) to leave room for a second QR below it that
|
||
# opens Safari → forces iOS captive UI.
|
||
AP_QR_CELL = 4
|
||
AP_QR_MODS = 37 # version 5, ECC_LOW
|
||
AP_QR_PX = AP_QR_MODS * AP_QR_CELL # 148
|
||
|
||
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
|
||
|
||
# Stacked layout: WiFi QR (top) + URL QR (bottom). Each has a label
|
||
# above it. AP_QR is dynamic — firmware overlays it at runtime. URL QR
|
||
# is static (always http://192.168.4.1/) and baked into the bg image.
|
||
AP_QR_X = RIGHT_CX - AP_QR_PX // 2 # centered horizontally
|
||
AP_QR_Y = 100 # below STEP 1 label
|
||
URL_QR_BOX = 4 # cell size in px
|
||
URL_QR_TARGET_Y = 320 # below STEP 2 label
|
||
|
||
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 up_arrow(draw, cx, cy, half_w=12, h=22, color=BK):
|
||
"""Solid filled triangle pointing up, centered on (cx, cy)."""
|
||
draw.polygon([
|
||
(cx, cy - h // 2), # tip
|
||
(cx - half_w, cy + h // 2), # base left
|
||
(cx + half_w, cy + h // 2), # base right
|
||
], fill=color)
|
||
|
||
|
||
def left_arrow(draw, cx, cy, half_h=12, w=22, color=BK):
|
||
"""Solid filled triangle pointing left, centered on (cx, cy)."""
|
||
draw.polygon([
|
||
(cx - w // 2, cy), # tip
|
||
(cx + w // 2, cy - half_h), # base top
|
||
(cx + w // 2, cy + half_h), # base bottom
|
||
], fill=color)
|
||
|
||
|
||
def orientation_diagrams(draw, accent, show_active_ls=True):
|
||
"""Draw both orientation diagrams in the centre panel.
|
||
|
||
The accent / show_active_ls parameters are kept for call-site
|
||
compatibility but no longer drive any colour decisions — both
|
||
diagrams render in black so neither orientation looks privileged.
|
||
The supported orientation is communicated by the up-arrow inside
|
||
the landscape screen instead."""
|
||
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, BK)
|
||
|
||
draw.rectangle([ls_x, ls_y, ls_x+ls_w-1, ls_y+ls_h-1], outline=BK, width=3)
|
||
draw.rectangle([ls_x, ls_y+ls_h, ls_x+rib_w-1, ls_y+ls_h+rib_h-1], fill=BK)
|
||
|
||
# Up arrow inside both diagrams — shows the user which edge is "up"
|
||
# when they hang the frame, regardless of orientation.
|
||
up_arrow(draw, ls_x + ls_w // 2, ls_y + ls_h // 2, half_w=14, h=26)
|
||
|
||
# Thin separator
|
||
sep_y = ls_y + ls_h + rib_h + 14
|
||
draw.rectangle([CTR_X+10, sep_y, CTR_X+CTR_W-10, sep_y], fill=(180,180,175))
|
||
|
||
# ── Portrait — drawn 90° CCW from upright so it rotates back to a
|
||
# correct portrait view (tall rect, ribbon on left, arrow pointing
|
||
# up) when the user tilts the frame 90° CW from landscape. In the
|
||
# source/landscape view it appears as a wide rect with the ribbon
|
||
# on the bottom and the arrow pointing left — looks "rotated"
|
||
# because it is, on purpose.
|
||
pt_w, pt_h = 106, 64
|
||
pr_w, pr_h = 106, 10
|
||
pt_x = CTR_X + (CTR_W - pt_w) // 2
|
||
pt_y = sep_y + 14
|
||
|
||
text_center(draw, cx, pt_y-14, "PORTRAIT", F_LABEL, BK)
|
||
|
||
draw.rectangle([pt_x, pt_y, pt_x+pt_w-1, pt_y+pt_h-1], outline=BK, width=3)
|
||
draw.rectangle([pt_x, pt_y+pt_h, pt_x+pr_w-1, pt_y+pt_h+pr_h-1], fill=BK)
|
||
|
||
left_arrow(draw, pt_x + pt_w // 2, pt_y + pt_h // 2, half_h=12, w=22)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 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 — frames ship with battery unplugged to preserve shelf life
|
||
# (idle setup-screen polling is non-trivial draw on e-ink), so the
|
||
# very first prompt is "Plug in the frame". Step 2 is unlock-phone
|
||
# because iOS won't fire the captive UI from a locked-phone scan.
|
||
# Two-QR flow because iOS in recent versions doesn't auto-open the
|
||
# captive portal even after CNA detects it; scanning the second QR
|
||
# opens a browser which forces the portal to render.
|
||
# NEW WORDING 2026-05-09 — beta tester called the prior copy
|
||
# "Chinglish." Tighter, plainer, no Safari-specific reference.
|
||
steps = [
|
||
("Plug in the frame", ""),
|
||
("Unlock your phone", ""),
|
||
("Scan QR 1", "joins your phone to PictureFrame"),
|
||
("Scan QR 2", "opens the setup page"),
|
||
("Type your home WiFi password", "and tap Connect"),
|
||
]
|
||
sy = BODY_Y + 95
|
||
step_pitch = 32
|
||
for i, (l1, l2) in enumerate(steps):
|
||
bx, by = 28, sy + i*step_pitch
|
||
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)
|
||
if l2:
|
||
draw.text((62, by+17), l2, font=F_STEP, fill=BK)
|
||
|
||
# Manual QR + side label — bottom of left panel.
|
||
# The manual covers the captive-portal-didn't-open fallback (192.168.4.1)
|
||
# plus general setup/troubleshooting, so we drop the inline footnote
|
||
# and rely on the QR/manual to deliver the longer story.
|
||
qr_bottom_y = BODY_Y + 95 + len(steps)*step_pitch + 12 # below last step
|
||
qr = qrcode.QRCode(
|
||
version=None,
|
||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
box_size=4,
|
||
border=2,
|
||
)
|
||
qr.add_data(MANUAL_URL)
|
||
qr.make(fit=True)
|
||
qr_img = qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
|
||
qr_w, qr_h = qr_img.size
|
||
img.paste(qr_img, (28, qr_bottom_y))
|
||
|
||
# Side label, vertically centered against the QR
|
||
label_x = 28 + qr_w + 14
|
||
label_y = qr_bottom_y + (qr_h - 56) // 2
|
||
draw.text((label_x, label_y), "Need help?", font=F_STEP_B, fill=BK)
|
||
draw.text((label_x, label_y + 18), "Scan for setup", font=F_STEP, fill=BK)
|
||
draw.text((label_x, label_y + 32), "& troubleshooting", font=F_STEP, fill=BK)
|
||
|
||
# ── Centre panel ─────────────────────────────────────────────
|
||
orientation_diagrams(draw, accent, show_active_ls=True)
|
||
|
||
# ── Right panel ──────────────────────────────────────────────
|
||
# Stacked: STEP 1 (WiFi join QR, dynamic) above STEP 2 (URL QR, static
|
||
# → http://192.168.4.1/, baked into bg). The URL QR is the trick that
|
||
# forces iOS to open the captive portal: scanning a URL QR launches
|
||
# Safari, Safari hits 192.168.4.1, iOS sees the request go to a
|
||
# captive network and renders the portal instead of fighting whether
|
||
# to auto-show CNA.
|
||
cx = RIGHT_CX
|
||
label_color = accent if accent != YL else BK
|
||
|
||
# Step 1 — WiFi join (dynamic QR, overlaid by firmware)
|
||
text_center(draw, cx, AP_QR_Y - 22, "STEP 1 — JOIN WIFI", F_BIG, label_color)
|
||
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_white(draw, qx, qy, qp)
|
||
text_center(draw, cx, qy+qp+8, qr_label, F_FOOT, label_color)
|
||
|
||
# Step 2 — URL QR (static, baked here so we don't need a second
|
||
# firmware render path; URL is fixed at http://192.168.4.1/).
|
||
url_qr = qrcode.QRCode(
|
||
version=None,
|
||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
box_size=URL_QR_BOX,
|
||
border=2,
|
||
)
|
||
url_qr.add_data("http://" + "192.168.4.1" + "/")
|
||
url_qr.make(fit=True)
|
||
url_img = url_qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
|
||
url_w, url_h = url_img.size
|
||
url_x = cx - url_w // 2
|
||
url_y = URL_QR_TARGET_Y
|
||
|
||
text_center(draw, cx, url_y - 22, "STEP 2 — OPEN PAGE", F_BIG, label_color)
|
||
# Decorative border around the static URL QR — same accent treatment
|
||
# as the WiFi QR for visual consistency.
|
||
draw.rectangle([url_x-6, url_y-6, url_x+url_w+5, url_y+url_h+5], outline=accent, width=3)
|
||
draw.rectangle([url_x-3, url_y-3, url_x+url_w+2, url_y+url_h+2], outline=BK, width=3)
|
||
img.paste(url_img, (url_x, url_y))
|
||
|
||
text_center(draw, cx, url_y + url_h + 8, "http://192.168.4.1/", F_FOOT, label_color)
|
||
|
||
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 the QR to link this frame", font=F_STEP, fill=(80,80,75))
|
||
draw.text((28, BODY_Y+110), "to your account.", font=F_STEP, fill=(80,80,75))
|
||
|
||
# NEW WORDING 2026-05-09 — beta-tester feedback. Step 2 used to break
|
||
# mid-domain ("pictureframe / .edholm.me") which read as broken text;
|
||
# also tightened step 1 and 3.
|
||
steps = [
|
||
("Scan the QR with your phone's", "camera"),
|
||
("Sign in or create an account", ""),
|
||
("Name your frame and pick", "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), "wevisto.com/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}")
|