ux(provisioning): unlock-first step + manual QR on Step 1/2

Two changes to the yellow Step 1/2 (and red retry-twin) screens, both
in service of the locked-phone-scan failure mode where iOS joins the
AP but never opens the captive portal:

* Promote 'Unlock your phone first' to step 1 of a four-step list
  (was three steps starting with 'Scan the QR'). Tightens step pitch
  from 46→38 px to fit the new step. Surfacing the requirement
  visually beats discovering it by scanning, getting nothing, and
  giving up.

* Bake a manual-link QR into the bottom of the left panel pointing
  to https://pictureframe.edholm.me/help. Side label 'Need help? /
  Scan for setup / & troubleshooting'. Static URL → encoded directly
  into ap_bg.bin via the qrcode Python lib at gen time, no firmware
  QR-render changes needed. Retry twin (ap_bg_retry.bin) inherits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 11:06:30 -04:00
parent 4454e9a8a5
commit e089911cfa
5 changed files with 38 additions and 8 deletions
+38 -8
View File
@@ -13,8 +13,13 @@ Constants exported (copy to epd.cpp):
"""
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://pictureframe.edholm.me/help"
# ── Display ──────────────────────────────────────────────────────────────────
W, H = 800, 480
@@ -181,24 +186,49 @@ def gen_ap(accent=YL, header="SETUP MODE — STEP 1 OF 2", qr_label="SCAN TO C
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 — step 1 is the unlock-first prompt because iOS won't open
# the captive portal from a locked-phone scan, and we'd rather
# surface that requirement up front than have the user discover it
# by scanning, getting nothing, and giving up.
steps = [
("Unlock your phone first", ""),
("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
sy = BODY_Y + 95
step_pitch = 38
for i, (l1, l2) in enumerate(steps):
bx, by = 28, sy + i*46
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)
draw.text((62, by+17), l2, font=F_STEP, fill=BK)
if l2:
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)
# 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)