9829d1af37
Rename the AP broadcast SSID from PictureFrame-XXXX to WeVisto-XXXX
(operation.h:ap_ssid_from_mac + main.cpp:enter_provisioning). Tests
updated to match.
Setup screens (both panels):
- Top-right header chip replaced with a draw_logo_placeholder() box —
a 'WeVisto' text mark with a 'PLACEHOLDER' subtitle. When the real
brand asset lands, swap the function for a paste of the file at the
same coordinates; no layout change needed.
- Step list rewritten to Matt's spec (4 steps, not 5):
1. Turn on your WeVisto
2. Unlock your phone
3. Scan QR 1 — This will connect your phone to the WeVisto
4. Scan QR 2 — This will open the WeVisto setup page
Step 5 (type WiFi password) lived only in the on-panel guide; the
user does that on the phone via the captive portal, where the
prompt is already explicit.
- Regenerated both panels' setup_bg / ap_bg / ap_bg_retry assets via
the gen_screens scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
488 lines
21 KiB
Python
488 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Generate 1200×1600 portrait setup-screen backgrounds for the Waveshare
|
||
13.3" Spectra-6 panel.
|
||
|
||
Layout mirrors the 7.3" gen_screens.py (yellow header + instructions +
|
||
QR codes) but adapted for portrait: a single yellow header band on top,
|
||
then a two-column body — instructions + manual QR on the left, the two
|
||
runtime QR codes on the right. No orientation chooser (13.3" setup is
|
||
portrait-only — see project memory).
|
||
|
||
Run from the firmware/ directory:
|
||
python3 scripts/gen_screens_13e6.py
|
||
|
||
Exports the QR overlay constants the firmware uses to overlay the
|
||
runtime QR images on the static .bin backgrounds; the driver must hold
|
||
the same numbers.
|
||
"""
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
import qrcode
|
||
import os, sys
|
||
|
||
MANUAL_URL = "https://wevisto.com/help"
|
||
|
||
W, H = 1200, 1600
|
||
|
||
# ── Spectra-6 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):
|
||
"""RGB PIL → 4bpp packed bytearray, row-major panel-native order."""
|
||
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 Exception:
|
||
return ImageFont.load_default()
|
||
|
||
|
||
F_BAR = ttf("DejaVuSans-Bold.ttf", 34) # header band text
|
||
F_CHIP = ttf("DejaVuSans-Bold.ttf", 30) # SSID chip
|
||
F_HEAD = ttf("DejaVuSans-Bold.ttf", 78) # "Connect to WiFi" heading
|
||
F_TITLE = ttf("DejaVuSans-Bold.ttf", 60) # "Almost ready" on setup screen
|
||
F_LABEL = ttf("DejaVuSans-Bold.ttf", 36) # column section labels
|
||
F_STEPN = ttf("DejaVuSans-Bold.ttf", 32) # step number inside black box
|
||
F_STEP_B = ttf("DejaVuSans-Bold.ttf", 30) # bold step body
|
||
F_STEP = ttf("DejaVuSans.ttf", 30) # step body
|
||
F_FOOT = ttf("DejaVuSans.ttf", 26) # captions under QR
|
||
F_TINY = ttf("DejaVuSans-Bold.ttf", 22) # progress-bar labels
|
||
F_URL = ttf("DejaVuSans.ttf", 22) # URL bar mono
|
||
|
||
|
||
# ── Layout constants ─────────────────────────────────────────────────────────
|
||
HEADER_H = 130 # header band height
|
||
BODY_Y = HEADER_H
|
||
DIV_X = 600 # vertical divider between left + right columns
|
||
LEFT_X = 0; LEFT_W = 600
|
||
RIGHT_X = 602; RIGHT_W = W - RIGHT_X # 598
|
||
LEFT_PAD = 36
|
||
RIGHT_PAD = 36
|
||
|
||
|
||
# ── QR overlay regions — MUST match the panel driver ─────────────────────────
|
||
# Dynamic QRs (left as WHITE rectangles in the .bin so firmware can overlay).
|
||
AP_QR_MODS = 37 # version 5, ECC_LOW
|
||
AP_QR_CELL = 14 # 37 × 14 = 518 px
|
||
AP_QR_PX = AP_QR_MODS * AP_QR_CELL
|
||
AP_QR_X = RIGHT_X + (RIGHT_W - AP_QR_PX) // 2 # 642
|
||
AP_QR_Y = BODY_Y + 100 # 230
|
||
|
||
SETUP_QR_MODS = 41 # version 6, ECC_LOW
|
||
SETUP_QR_CELL = 16 # 41 × 16 = 656 px
|
||
SETUP_QR_PX = SETUP_QR_MODS * SETUP_QR_CELL
|
||
SETUP_QR_X = (W - SETUP_QR_PX) // 2 # 272
|
||
SETUP_QR_Y = BODY_Y + 360 # 490
|
||
|
||
|
||
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 leave_qr_white(draw, qr_x, qr_y, qr_px):
|
||
draw.rectangle([qr_x, qr_y, qr_x + qr_px - 1, qr_y + qr_px - 1], fill=WH)
|
||
|
||
|
||
def draw_qr_frame(draw, qx, qy, qp, accent):
|
||
"""Two-layer decorative border around a QR — accent outer + black inner."""
|
||
draw.rectangle([qx - 12, qy - 12, qx + qp + 11, qy + qp + 11], outline=accent, width=6)
|
||
draw.rectangle([qx - 4, qy - 4, qx + qp + 3, qy + qp + 3], outline=BK, width=4)
|
||
|
||
|
||
def draw_header(draw, accent, header_text):
|
||
"""Coloured band along the top with section title on the left. Logo
|
||
placeholder is rendered separately by draw_logo_placeholder() so the
|
||
brand mark sits in the top right of every setup screen."""
|
||
draw.rectangle([0, 0, W - 1, HEADER_H - 1], fill=accent)
|
||
draw.text((LEFT_PAD, 40), header_text, font=F_BAR, fill=BK)
|
||
|
||
|
||
# ── Logo placeholder ─────────────────────────────────────────────────────────
|
||
# Box dimensions for the top-right placeholder. When the real brand mark
|
||
# lands, replace draw_logo_placeholder() with a paste of assets/logo.png
|
||
# (or similar) at these same coordinates so the layout stays stable.
|
||
LOGO_W = 300
|
||
LOGO_H = 92
|
||
LOGO_X = W - LOGO_W - LEFT_PAD # 864
|
||
LOGO_Y = (HEADER_H - LOGO_H) // 2 # 19
|
||
|
||
def draw_logo_placeholder(img):
|
||
"""White rounded-look box with 'WeVisto' brand text in the top right of
|
||
the header. Stand-in for the real logo — same position will be reused."""
|
||
draw = ImageDraw.Draw(img)
|
||
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1], fill=WH)
|
||
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1],
|
||
outline=BK, width=4)
|
||
|
||
f_logo = ttf("DejaVuSans-Bold.ttf", 44)
|
||
bb = draw.textbbox((0, 0), "WeVisto", font=f_logo)
|
||
tw = bb[2] - bb[0]
|
||
draw.text((LOGO_X + (LOGO_W - tw) // 2, LOGO_Y + 12), "WeVisto",
|
||
font=f_logo, fill=BK)
|
||
|
||
f_hint = ttf("DejaVuSans-Bold.ttf", 14)
|
||
bb = draw.textbbox((0, 0), "PLACEHOLDER", font=f_hint)
|
||
pw = bb[2] - bb[0]
|
||
draw.text((LOGO_X + (LOGO_W - pw) // 2, LOGO_Y + LOGO_H - 22),
|
||
"PLACEHOLDER", font=f_hint, fill=BK)
|
||
|
||
|
||
def draw_divider(draw):
|
||
"""Vertical divider between the two body columns."""
|
||
draw.rectangle([DIV_X, BODY_Y + 20, DIV_X + 1, H - 30], fill=BK)
|
||
|
||
|
||
def up_arrow(draw, cx, cy, half_w=18, h=34, color=BK):
|
||
"""Solid filled triangle pointing up, centered on (cx, cy)."""
|
||
draw.polygon([
|
||
(cx, cy - h // 2),
|
||
(cx - half_w, cy + h // 2),
|
||
(cx + half_w, cy + h // 2),
|
||
], fill=color)
|
||
|
||
|
||
def left_arrow(draw, cx, cy, half_h=18, w=34, color=BK):
|
||
"""Solid filled triangle pointing left, centered on (cx, cy)."""
|
||
draw.polygon([
|
||
(cx - w // 2, cy),
|
||
(cx + w // 2, cy - half_h),
|
||
(cx + w // 2, cy + half_h),
|
||
], fill=color)
|
||
|
||
|
||
def paste_rotated_text(img, text, font, fill, anchor_xy, ccw_degrees=90):
|
||
"""
|
||
Render `text` horizontally onto a transparent overlay, rotate ccw, and
|
||
paste it onto `img` at `anchor_xy` (top-left of the rotated bounding
|
||
box). Used for vertical labels on orientation diagrams — PIL's
|
||
ImageDraw.text() can't rotate, so we render-then-rotate.
|
||
"""
|
||
bb = font.getbbox(text)
|
||
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
||
pad = 4
|
||
layer = Image.new("RGBA", (tw + pad * 2, th + pad * 2), (0, 0, 0, 0))
|
||
ImageDraw.Draw(layer).text((pad - bb[0], pad - bb[1]), text, font=font, fill=fill)
|
||
rotated = layer.rotate(ccw_degrees, expand=True, resample=Image.BILINEAR)
|
||
img.paste(rotated, anchor_xy, rotated)
|
||
|
||
|
||
def orientation_diagrams(img, cx, top_y, label_color=None):
|
||
"""
|
||
Side-by-side PORTRAIT / LANDSCAPE diagrams illustrating the two ways
|
||
the user can hang the frame. Drawn in current portrait-view coords:
|
||
|
||
PORTRAIT = upright tall rect, ribbon along the bottom short edge,
|
||
up-arrow inside.
|
||
LANDSCAPE = pre-rotated 90° CCW from upright landscape. The frame
|
||
rotation portrait→landscape is 90° CW (ribbon moves
|
||
bottom→left as viewed by the user); the CCW pre-rotation
|
||
cancels that, so when the user picks the frame up and
|
||
rotates it 90° CW into landscape the diagram lands
|
||
upright (wide rect, ribbon-left, up-arrow).
|
||
In the portrait rendering that means: tall rect, ribbon
|
||
along bottom edge (was the LEFT edge upright), LEFT-
|
||
pointing arrow (was UP upright), and the "LANDSCAPE"
|
||
label rotated 90° CCW so it runs up the long edge —
|
||
reads horizontally once the frame is mounted landscape.
|
||
"""
|
||
if label_color is None:
|
||
label_color = BK
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
# Section heading
|
||
text_center(draw, cx, top_y, "FRAME", F_TINY, label_color)
|
||
text_center(draw, cx, top_y + 28, "ORIENTATION", F_TINY, label_color)
|
||
|
||
# Same external dimensions for both diagrams (the LANDSCAPE is a 90°-CCW
|
||
# rotation of an upright wide rect — its bounding box is square'd to
|
||
# match portrait so the pair sits in a clean two-up grid).
|
||
diag_w, diag_h = 130, 200
|
||
ribbon_thick = 14
|
||
pair_gap = 100 # extra room so the rotated label doesn't crowd the divider
|
||
pair_w = diag_w * 2 + pair_gap
|
||
base_y = top_y + 100
|
||
|
||
pt_x = cx - pair_w // 2
|
||
ls_x = pt_x + diag_w + pair_gap
|
||
pt_y = base_y
|
||
ls_y = base_y
|
||
diag_bottom = base_y + diag_h
|
||
|
||
# PORTRAIT — upright tall rect, ribbon along bottom short edge.
|
||
draw.rectangle([pt_x, pt_y, pt_x + diag_w - 1, pt_y + diag_h - 1],
|
||
outline=BK, width=3)
|
||
draw.rectangle([pt_x, pt_y + diag_h - ribbon_thick,
|
||
pt_x + diag_w - 1, pt_y + diag_h - 1], fill=BK)
|
||
up_arrow(draw, pt_x + diag_w // 2,
|
||
pt_y + (diag_h - ribbon_thick) // 2)
|
||
text_center(draw, pt_x + diag_w // 2, diag_bottom + 14, "PORTRAIT", F_TINY, BK)
|
||
|
||
# LANDSCAPE — pre-rotated 90° CCW from upright.
|
||
# Upright landscape: wide rect, ribbon along LEFT long edge, UP arrow.
|
||
# After 90° CCW (in portrait rendering): tall rect, ribbon along BOTTOM
|
||
# short edge, LEFT-pointing arrow. Label runs up the LEFT long edge,
|
||
# rotated 90° CCW so it reads L→R once the frame is rotated to landscape.
|
||
draw.rectangle([ls_x, ls_y, ls_x + diag_w - 1, ls_y + diag_h - 1],
|
||
outline=BK, width=3)
|
||
draw.rectangle([ls_x, ls_y + diag_h - ribbon_thick,
|
||
ls_x + diag_w - 1, ls_y + diag_h - 1], fill=BK)
|
||
left_arrow(draw, ls_x + diag_w // 2,
|
||
ls_y + (diag_h - ribbon_thick) // 2)
|
||
# Rotated label, anchored just left of the diagram's left long edge.
|
||
label_text = "LANDSCAPE"
|
||
bb = F_TINY.getbbox(label_text)
|
||
label_w = bb[2] - bb[0]
|
||
label_h = bb[3] - bb[1]
|
||
# Rotated label is `label_w` tall, `label_h` wide. Centred vertically
|
||
# against the rect, sitting just to its left.
|
||
rotated_x = ls_x - label_h - 16
|
||
rotated_y = ls_y + (diag_h - label_w) // 2
|
||
paste_rotated_text(img, label_text, F_TINY, BK, (rotated_x, rotated_y),
|
||
ccw_degrees=90)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# AP SCREEN — yellow (or red retry) accent, Step 1 of 2
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
def gen_ap(accent=YL,
|
||
header_text="SETUP MODE — STEP 1 OF 2",
|
||
qr_label="SCAN TO CONNECT"):
|
||
img = Image.new("RGB", (W, H), WH)
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
draw_header(draw, accent, header_text)
|
||
draw_logo_placeholder(img)
|
||
draw_divider(draw)
|
||
|
||
# ── LEFT COLUMN ──────────────────────────────────────────────────────────
|
||
# Big "Connect to WiFi" heading with accent underline.
|
||
head_y = BODY_Y + 30
|
||
draw.text((LEFT_PAD, head_y), "Connect to", font=F_HEAD, fill=BK)
|
||
draw.text((LEFT_PAD, head_y + 90), "WiFi", font=F_HEAD, fill=BK)
|
||
bb = draw.textbbox((0, 0), "WiFi", font=F_HEAD)
|
||
underline_y = head_y + 90 + bb[3] + 6
|
||
draw.rectangle([LEFT_PAD, underline_y,
|
||
LEFT_PAD + bb[2] + 4, underline_y + 6], fill=accent)
|
||
|
||
# 5 numbered steps — same instructions as the 7.3" but with the larger
|
||
# type that fits comfortably on a 13.3" portrait body.
|
||
steps = [
|
||
("Turn on your WeVisto", ""),
|
||
("Unlock your phone", ""),
|
||
("Scan QR 1", "This will connect your phone to the WeVisto"),
|
||
("Scan QR 2", "This will open the WeVisto setup page"),
|
||
]
|
||
step_y0 = head_y + 240
|
||
step_pitch = 92
|
||
box = 50 # numbered black box size
|
||
for i, (l1, l2) in enumerate(steps):
|
||
by = step_y0 + i * step_pitch
|
||
draw.rectangle([LEFT_PAD, by, LEFT_PAD + box, by + box], fill=BK)
|
||
text_center(draw, LEFT_PAD + box // 2, by + 8,
|
||
str(i + 1), F_STEPN, accent)
|
||
draw.text((LEFT_PAD + box + 22, by - 4), l1, font=F_STEP_B, fill=BK)
|
||
if l2:
|
||
draw.text((LEFT_PAD + box + 22, by + 32), l2, font=F_STEP, fill=BK)
|
||
|
||
# Orientation diagrams — tucked between the steps and the manual QR so
|
||
# the user sees both possible hanging positions before they commit.
|
||
orientation_diagrams(img, LEFT_X + LEFT_W // 2, step_y0 + len(steps) * step_pitch + 60)
|
||
|
||
# Manual QR bottom-left — covers "captive portal didn't open" + general
|
||
# troubleshooting. Same intent as the 7.3" but bigger box, more room.
|
||
manual_qr = qrcode.QRCode(
|
||
version=None,
|
||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
box_size=7,
|
||
border=2,
|
||
)
|
||
manual_qr.add_data(MANUAL_URL)
|
||
manual_qr.make(fit=True)
|
||
manual_img = manual_qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
|
||
mw, mh = manual_img.size
|
||
manual_x = LEFT_PAD
|
||
manual_y = H - mh - 60
|
||
img.paste(manual_img, (manual_x, manual_y))
|
||
|
||
# Side label aligned with manual QR.
|
||
label_x = manual_x + mw + 28
|
||
label_y = manual_y + (mh - 80) // 2
|
||
draw.text((label_x, label_y), "Need help?", font=F_STEP_B, fill=BK)
|
||
draw.text((label_x, label_y + 38), "Scan for setup", font=F_STEP, fill=BK)
|
||
draw.text((label_x, label_y + 70), "& troubleshoot", font=F_STEP, fill=BK)
|
||
|
||
# ── RIGHT COLUMN ─────────────────────────────────────────────────────────
|
||
rcx = RIGHT_X + RIGHT_W // 2
|
||
|
||
# Step 1 — WiFi join QR (dynamic, firmware overlay)
|
||
text_center(draw, rcx, AP_QR_Y - 56, "STEP 1 — JOIN WIFI", F_LABEL, BK)
|
||
draw_qr_frame(draw, AP_QR_X, AP_QR_Y, AP_QR_PX, accent)
|
||
leave_qr_white(draw, AP_QR_X, AP_QR_Y, AP_QR_PX)
|
||
text_center(draw, rcx, AP_QR_Y + AP_QR_PX + 16, qr_label, F_FOOT, BK)
|
||
|
||
# Step 2 — URL QR (static "http://192.168.4.1/", baked into bg). Scanning
|
||
# this in iOS Safari forces the captive portal to render — works around
|
||
# iOS's reluctance to auto-launch CNA from a QR-scan WiFi join.
|
||
url_qr = qrcode.QRCode(
|
||
version=None,
|
||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
box_size=10,
|
||
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")
|
||
uw, uh = url_img.size
|
||
url_x = rcx - uw // 2
|
||
url_y = H - uh - 100
|
||
|
||
text_center(draw, rcx, url_y - 56, "STEP 2 — OPEN PAGE", F_LABEL, BK)
|
||
draw.rectangle([url_x - 12, url_y - 12, url_x + uw + 11, url_y + uh + 11],
|
||
outline=accent, width=6)
|
||
draw.rectangle([url_x - 4, url_y - 4, url_x + uw + 3, url_y + uh + 3],
|
||
outline=BK, width=4)
|
||
img.paste(url_img, (url_x, url_y))
|
||
text_center(draw, rcx, url_y + uh + 12, "http://192.168.4.1/", F_FOOT, BK)
|
||
|
||
return img
|
||
|
||
|
||
def gen_ap_retry():
|
||
"""Red-accented retry screen, served after a failed WiFi-join attempt."""
|
||
return gen_ap(
|
||
accent=RD,
|
||
header_text="CONNECTION FAILED — TRY AGAIN",
|
||
qr_label="Connection failed — try again",
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# SETUP SCREEN — green accent, post-WiFi setup-claim QR
|
||
# Single-column centred layout: heading + numbered steps + big setup QR + MAC.
|
||
# No two-column split here because the QR is much bigger (16-px cells × 41
|
||
# modules = 656 px) and a side column would crowd it.
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
def gen_setup():
|
||
img = Image.new("RGB", (W, H), WH)
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
draw_header(draw, GR, "WIFI CONNECTED — STEP 2 OF 2")
|
||
draw_logo_placeholder(img)
|
||
|
||
# Centered heading
|
||
text_center(draw, W // 2, BODY_Y + 30, "Almost ready", F_HEAD, BK)
|
||
bb = draw.textbbox((0, 0), "Almost ready", font=F_HEAD)
|
||
underline_w = bb[2] + 4
|
||
text_w = bb[2] - bb[0]
|
||
underline_x = (W - text_w) // 2
|
||
underline_y = BODY_Y + 30 + bb[3] + 6
|
||
draw.rectangle([underline_x, underline_y,
|
||
underline_x + text_w, underline_y + 6], fill=GR)
|
||
|
||
text_center(draw, W // 2, BODY_Y + 170, "Scan the QR to link this frame", F_STEP_B, BK)
|
||
text_center(draw, W // 2, BODY_Y + 210, "to your wevisto.com account.", F_STEP, BK)
|
||
|
||
# Setup QR with decorative border + green/black double frame
|
||
draw_qr_frame(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX, GR)
|
||
leave_qr_white(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX)
|
||
text_center(draw, W // 2,
|
||
SETUP_QR_Y - 56,
|
||
"SCAN TO FINISH",
|
||
F_LABEL, BK)
|
||
|
||
# MAC chip below QR — frame's identifier so the user knows which device
|
||
# they're claiming. Static placeholder until firmware writes text here.
|
||
mac = "1C:C3:AB:D1:91:F8"
|
||
bb = draw.textbbox((0, 0), mac, font=F_CHIP)
|
||
chip_w = bb[2] - bb[0] + 32
|
||
chip_x = (W - chip_w) // 2
|
||
chip_y = SETUP_QR_Y + SETUP_QR_PX + 30
|
||
draw.rectangle([chip_x, chip_y, chip_x + chip_w, chip_y + 50], fill=BK)
|
||
text_center(draw, W // 2, chip_y + 14, mac, F_CHIP, WH)
|
||
|
||
text_center(draw, W // 2, chip_y + 80,
|
||
"Your frame's MAC — handy for support",
|
||
F_FOOT, BK)
|
||
|
||
# Progress track at the bottom
|
||
track_y = H - 90
|
||
track_h = 14
|
||
seg_pad = 20
|
||
segs = [("WiFi", GR), ("Account", BK), ("Frame ready", (200, 200, 195))]
|
||
seg_w = (W - LEFT_PAD * 2 - seg_pad * 2) // 3
|
||
text_center(draw, W // 2, track_y - 36, "SETUP PROGRESS", F_TINY, BK)
|
||
for i, (label, color) in enumerate(segs):
|
||
sx = LEFT_PAD + i * (seg_w + seg_pad)
|
||
draw.rectangle([sx, track_y, sx + seg_w, track_y + track_h], fill=color)
|
||
text_center(draw, sx + seg_w // 2, track_y + track_h + 10, label, F_FOOT, BK)
|
||
|
||
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)}")
|
||
|
||
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__":
|
||
out_dir = os.path.join(os.path.dirname(__file__), "../data/waveshare13e6-v1")
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
|
||
print("Generating AP screen…")
|
||
save_bin(gen_ap(), f"{out_dir}/ap_bg.bin", f"{out_dir}/ap_bg_preview.png")
|
||
print()
|
||
print("Generating AP retry screen…")
|
||
save_bin(gen_ap_retry(), f"{out_dir}/ap_bg_retry.bin", f"{out_dir}/ap_bg_retry_preview.png")
|
||
print()
|
||
print("Generating setup screen…")
|
||
save_bin(gen_setup(), f"{out_dir}/setup_bg.bin", f"{out_dir}/setup_bg_preview.png")
|
||
print()
|
||
print("QR overlay constants — keep these in sync with epd_driver.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}, "
|
||
f"SETUP_QR_CELL={SETUP_QR_CELL}, SETUP_QR_PX={SETUP_QR_PX}")
|