From fb4c5ff5d3c91f339493b29bedf1aeb3fbeba576 Mon Sep 17 00:00:00 2001 From: Matt Edholm Date: Fri, 8 May 2026 23:43:59 -0400 Subject: [PATCH] fix(provisioning): stop redrawing the QR on every poll, add WiFi-fail retry screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(). --- data/waveshare73-v1/ap_bg_retry.bin | Bin 0 -> 192000 bytes data/waveshare73-v1/ap_bg_retry_preview.png | Bin 0 -> 10237 bytes scripts/gen_screens.py | 47 ++++++++++++++------ src/epd.h | 3 ++ src/main.cpp | 31 +++++++------ src/operation.h | 23 ++++++---- src/panels/waveshare73/v1/epd_driver.cpp | 6 +++ test/mocks/epd.h | 1 + test/test_normal_operation/test_main.cpp | 38 ++++++++++++---- 9 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 data/waveshare73-v1/ap_bg_retry.bin create mode 100644 data/waveshare73-v1/ap_bg_retry_preview.png diff --git a/data/waveshare73-v1/ap_bg_retry.bin b/data/waveshare73-v1/ap_bg_retry.bin new file mode 100644 index 0000000000000000000000000000000000000000..0a73343abacbd78c7a9e19cab331af528ac1120a GIT binary patch literal 192000 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

MDd5_>9%aCKZeCK_G&&vC+1?Ys5xq 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 literal 0 HcmV?d00001 diff --git a/scripts/gen_screens.py b/scripts/gen_screens.py index 19d1442..f5bceae 100644 --- a/scripts/gen_screens.py +++ b/scripts/gen_screens.py @@ -146,15 +146,20 @@ def orientation_diagrams(draw, accent, show_active_ls=True): # ═══════════════════════════════════════════════════════════════════════════════ -# AP SCREEN — yellow accent, WiFi credentials +# 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(): +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=YL) - draw.text((24, 18), "SETUP MODE — STEP 1 OF 2", font=F_BAR, fill=BK) + 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 @@ -163,7 +168,7 @@ def gen_ap(): 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=YL) + 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) @@ -174,7 +179,7 @@ def gen_ap(): 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=YL) + draw.rectangle([28, BODY_Y+82, 28+bb[2]+2, BODY_Y+85], fill=accent) # Steps steps = [ @@ -186,7 +191,7 @@ def gen_ap(): 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, YL) + 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) @@ -196,17 +201,18 @@ def gen_ap(): draw.text((28, BODY_Y+276), "Go to 192.168.4.1", font=F_FOOT, fill=BK) # ── Centre panel ───────────────────────────────────────────── - orientation_diagrams(draw, YL, show_active_ls=True) + orientation_diagrams(draw, accent, show_active_ls=True) # ── Right panel ────────────────────────────────────────────── cx = RIGHT_CX - # "SCAN TO CONNECT" label - text_center(draw, cx, AP_QR_Y - 26, "SCAN TO CONNECT", F_BIG, BK) + # 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: yellow outer, black inner + # 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=YL, width=3) + 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 @@ -218,6 +224,16 @@ def gen_ap(): 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 # ═══════════════════════════════════════════════════════════════════════════════ @@ -340,10 +356,13 @@ if __name__ == "__main__": 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") + 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") + 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}") diff --git a/src/epd.h b/src/epd.h index 21c1d46..ce26a01 100644 --- a/src/epd.h +++ b/src/epd.h @@ -20,4 +20,7 @@ void epd_draw_qr(QRCode* qr, uint8_t cellPx, uint8_t bg, uint8_t fg); // Draw the setup screen: pre-rendered background from LittleFS with QR overlaid. void epd_draw_ap_screen(QRCode* qr); +// Same layout as ap_screen but with red accents and a "Connection Failed — +// try again" label, served after a failed WiFi join attempt. +void epd_draw_ap_screen_retry(QRCode* qr); void epd_draw_setup_screen(QRCode* qr); diff --git a/src/main.cpp b/src/main.cpp index 3dba3ee..6a024ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,13 +25,17 @@ static String g_req_pass; // ── QR helpers ─────────────────────────────────────────────────────────────── -static void show_ap_qr(const String& apSsid) { +static void show_ap_qr(const String& apSsid, bool retry = false) { String content = "WIFI:S:" + apSsid + ";T:nopass;;"; QRCode qr; uint8_t buf[qrcode_getBufferSize(5)]; qrcode_initText(&qr, buf, 5, ECC_LOW, content.c_str()); - epd_draw_ap_screen(&qr); + if (retry) { + epd_draw_ap_screen_retry(&qr); + } else { + epd_draw_ap_screen(&qr); + } } static void show_setup_qr(const String& mac) { @@ -116,7 +120,7 @@ static void handle_captive() { // ── WiFi provisioning ───────────────────────────────────────────────────────── -static void enter_provisioning(const String& mac) { +static void enter_provisioning(const String& mac, bool retry = false) { // Derive AP name from last 4 hex chars of MAC (no colons) String suffix = mac; suffix.replace(":", ""); @@ -124,7 +128,7 @@ static void enter_provisioning(const String& mac) { suffix.toUpperCase(); String apSsid = "PictureFrame-" + suffix; - Serial.println("AP: " + apSsid); + Serial.println(retry ? "AP (retry): " + apSsid : "AP: " + apSsid); WiFi.disconnect(true); WiFi.mode(WIFI_AP); @@ -143,8 +147,11 @@ static void enter_provisioning(const String& mac) { server.onNotFound(handle_root); server.begin(); + // On retry, repaint with red accents + "Connection Failed — try again" + // label so the user has a clear visual signal that their last credential + // entry didn't work. On first entry, paint the standard yellow Step 1/2. epd_init(); - show_ap_qr(apSsid); + show_ap_qr(apSsid, retry); epd_sleep(); g_provisioning = true; @@ -267,13 +274,11 @@ void loop() { // serves an image — no need for an artificial display delay here. normal_operation(mac); } else { - // Connection failed — fill red, restart AP - epd_init(); - epd_fill(COLOR_RED); - epd_sleep(); - delay(3000); - - // Re-enter provisioning to retry - enter_provisioning(mac); + // Connection failed — go back into AP mode with the red retry screen + // so the user sees "Connection Failed — try again" and can rescan. + // No epd_fill(COLOR_RED) detour: that would obliterate the QR for + // ~20 s and force a second redraw to put it back. One repaint into + // the retry screen is faster and clearer. + enter_provisioning(mac, /*retry=*/true); } } diff --git a/src/operation.h b/src/operation.h index 81bc584..80979c7 100644 --- a/src/operation.h +++ b/src/operation.h @@ -263,16 +263,21 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre Serial.println("[op] recovery aborted: /img.bin not in LittleFS"); } } - } else if (code == 204) { + } else if (code == 204 || code == 404) { + // No image to serve. Don't touch the panel — whatever's already + // displayed is the right thing: + // • currentImgId == -1 → the setup QR is up (painted by + // enter_provisioning after WiFi save). The 15s bootstrap poll + // hits this branch every cycle until the user claims via + // /setup/{mac}; redrawing the QR each time would put the panel + // in a perpetual ~20s e-ink redraw loop and risk ghosting. + // • currentImgId >= 0 → a real photo is up (server hiccup, asset + // missing, image deleted). Don't paint the setup QR over the + // user's photo; leave the last-good image alone. + // displayInitialized stays false → epd_sleep() at the bottom is + // also skipped, since the display was already in sleep from the + // previous cycle. http.end(); - displayInitialized = true; - epd_init(); - show_setup_qr(mac); - } else if (code == 404) { - http.end(); - displayInitialized = true; - epd_init(); - show_setup_qr(mac); } else { // Sync failed (5xx, timeout, malformed). Per FR38, the last-good image // must persist; only the border indicates the error. epd_draw_image_with_border diff --git a/src/panels/waveshare73/v1/epd_driver.cpp b/src/panels/waveshare73/v1/epd_driver.cpp index 03ca2cf..63ef97d 100644 --- a/src/panels/waveshare73/v1/epd_driver.cpp +++ b/src/panels/waveshare73/v1/epd_driver.cpp @@ -179,6 +179,12 @@ void epd_draw_ap_screen(QRCode* qr) { draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 563, 185, 5); } +void epd_draw_ap_screen_retry(QRCode* qr) { + // Same QR coordinates — only the bg .bin differs (red accents, + // "Connection Failed — try again" label). + draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 563, 185, 5); +} + void epd_draw_setup_screen(QRCode* qr) { // SETUP_QR_X=553, SETUP_QR_Y=175, SETUP_QR_CELL=5 (must match gen_screens.py) draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 553, 175, 5); diff --git a/test/mocks/epd.h b/test/mocks/epd.h index a76a2d3..2353b67 100644 --- a/test/mocks/epd.h +++ b/test/mocks/epd.h @@ -14,4 +14,5 @@ inline void epd_sleep() { g_epd_sleep_count++; } inline void epd_draw_image_from_file(File& f) { g_epd_draw_image_count++; } inline void epd_fill(int color) { g_epd_fill_count++; g_epd_fill_last_color = color; } inline void epd_draw_ap_screen(void*) {} +inline void epd_draw_ap_screen_retry(void*) {} inline void epd_draw_setup_screen(void*) { g_epd_draw_setup_count++; } diff --git a/test/test_normal_operation/test_main.cpp b/test/test_normal_operation/test_main.cpp index 6addb74..d0d94d7 100644 --- a/test/test_normal_operation/test_main.cpp +++ b/test/test_normal_operation/test_main.cpp @@ -130,20 +130,41 @@ void test_fw03_304_no_redraw() { TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us); } -// FW-04: 204 — show_setup_qr called exactly once -void test_fw04_204_shows_setup_qr() { +// FW-04: 204 with no prior image — panel already shows the setup QR from +// provisioning; the firmware MUST NOT redraw it on every 15s bootstrap poll +// or the e-ink panel sits in a perpetual mid-refresh loop. +void test_fw04_204_no_prior_image_does_not_redraw() { g_http_get_code = 204; + // currentImgId defaults to -1 from prefs.clear() in reset_state() normal_operation_impl(String("mac"), http, String("url"), prefs); - TEST_ASSERT_EQUAL(1, g_show_setup_qr_count); + TEST_ASSERT_EQUAL_MESSAGE(0, g_show_setup_qr_count, + "204 must not redraw the QR — panel already holds it from provisioning"); + TEST_ASSERT_EQUAL(0, g_epd_init_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); + TEST_ASSERT_TRUE(g_deep_sleep_started); } -// FW-05: 404 — show_setup_qr called exactly once -void test_fw05_404_shows_setup_qr() { +// FW-04b: 204 after a real image was previously displayed — panel holds the +// photo; firmware MUST NOT paint the setup QR over it. +void test_fw04b_204_with_prior_image_does_not_redraw() { + g_http_get_code = 204; + prefs.ints[NVS_KEY_IMG_ID] = 7; // device has previously painted image #7 + normal_operation_impl(String("mac"), http, String("url"), prefs); + TEST_ASSERT_EQUAL_MESSAGE(0, g_show_setup_qr_count, + "204 must not paint the setup QR over a real photo"); + TEST_ASSERT_EQUAL(0, g_epd_init_count); + TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); + TEST_ASSERT_EQUAL(0, g_epd_fill_count); +} + +// FW-05: 404 — same logic as 204; panel keeps whatever's there. +void test_fw05_404_does_not_redraw() { g_http_get_code = 404; normal_operation_impl(String("mac"), http, String("url"), prefs); - TEST_ASSERT_EQUAL(1, g_show_setup_qr_count); + TEST_ASSERT_EQUAL(0, g_show_setup_qr_count); + TEST_ASSERT_EQUAL(0, g_epd_init_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); + TEST_ASSERT_TRUE(g_deep_sleep_started); } // FW-06a: 5xx error WITH a cached image → preserve last image and overlay a @@ -517,8 +538,9 @@ int main(int argc, char** argv) { RUN_TEST(test_fw01_200_response_happy_path); RUN_TEST(test_fw02_headers_read_before_end_regression); RUN_TEST(test_fw03_304_no_redraw); - RUN_TEST(test_fw04_204_shows_setup_qr); - RUN_TEST(test_fw05_404_shows_setup_qr); + RUN_TEST(test_fw04_204_no_prior_image_does_not_redraw); + RUN_TEST(test_fw04b_204_with_prior_image_does_not_redraw); + RUN_TEST(test_fw05_404_does_not_redraw); RUN_TEST(test_fw06a_error_with_cache_draws_border_not_fill); RUN_TEST(test_fw06b_error_without_cache_falls_back_to_fill); RUN_TEST(test_fw06c_304_after_error_repaints_clean);