fix(13e6): partition + SPI corruption + bootstrap stay-awake
Three problems surfaced during the first 13.3" end-to-end run: 1) LittleFS IntegerDivideByZero on 200 → write /img.bin. Cause: the ~3.5 MB SPIFFS in default_16MB.csv can't fit three 960 KB setup screens + a 960 KB cached image (~3.84 MB). Switching to a custom partitions_13e6.csv with 24 MB LittleFS on the 32 MB flash. 2) Yellow wash across the panel on long SPI bursts. Cause: SPI DMA from a PSRAM-backed scratch buffer hits a cache-coherency window — the CPU's writes hadn't reached PSRAM yet when DMA read it. Push each half in 8 KB chunks through an internal-SRAM (DMA-coherent) scratch, and drop the bus clock to 4 MHz to match the 7.3" production speed. 3) Bootstrap window (no image yet) was deep-sleeping for 15 s between polls — each cycle a ~5 s ROM-boot + Wi-Fi reconnect, so the user waited ~20 s × N retries between scanning the setup QR and seeing their first photo land. Now normal_operation_impl returns early during bootstrap and main.cpp's normal_operation loops with a 2 s delay, keeping Wi-Fi up. Once the first image arrives, the normal scheduled deep sleep takes over. Also fixes a related bug Matt called out: a transient TLS hiccup during bootstrap was hitting the 5xx fallback path and painting a full yellow fill over the green setup QR, leaving the user with no claim path. Criterion is now "does /img.bin exist?" (panel has something worth showing with a border) rather than "is currentImgId set?", so a fresh device with no cached image preserves the setup screen through transient network errors. Diagnostic prints in the panel driver + [op] start/code lines in normal_operation_impl that proved invaluable during bringup; leaving them in for now. Tests updated for the new bootstrap semantics (deep sleep no longer arms on bootstrap-cycle 204/404/5xx); 43/43 native tests pass, 7.3" production build stays byte-identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,9 @@ void test_fw04_204_no_prior_image_does_not_redraw() {
|
||||
"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);
|
||||
// Bootstrap: no deep sleep. Caller (main.cpp normal_operation loop) keeps
|
||||
// WiFi up and retries on a short BOOTSTRAP_RETRY_INTERVAL_MS timer.
|
||||
TEST_ASSERT_FALSE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-04b: 204 after a real image was previously displayed — panel holds the
|
||||
@@ -157,14 +159,15 @@ void test_fw04b_204_with_prior_image_does_not_redraw() {
|
||||
TEST_ASSERT_EQUAL(0, g_epd_fill_count);
|
||||
}
|
||||
|
||||
// FW-05: 404 — same logic as 204; panel keeps whatever's there.
|
||||
// FW-05: 404 — same logic as 204; panel keeps whatever's there. Without a
|
||||
// prior image, the bootstrap path also skips deep sleep (caller retries).
|
||||
void test_fw05_404_does_not_redraw() {
|
||||
g_http_get_code = 404;
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
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);
|
||||
TEST_ASSERT_FALSE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-06a: 5xx error WITH a cached image → preserve last image and overlay a
|
||||
@@ -185,18 +188,24 @@ void test_fw06a_error_with_cache_draws_border_not_fill() {
|
||||
TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
|
||||
}
|
||||
|
||||
// FW-06b: 5xx error with NO cached image → fall back to full yellow fill so
|
||||
// the user still sees a sync-fail signal on a fresh device.
|
||||
void test_fw06b_error_without_cache_falls_back_to_fill() {
|
||||
// FW-06b: 5xx error during the bootstrap window (no cached image) MUST NOT
|
||||
// paint anything — the panel is holding the green setup-claim QR, which the
|
||||
// user still needs to scan. A transient TLS hiccup or server blip wiping it
|
||||
// to yellow leaves the recipient stranded with no path to claim the frame.
|
||||
// err_border MUST NOT be set either, since there's nothing to "recover" from
|
||||
// on the next healthy response.
|
||||
void test_fw06b_error_without_cache_preserves_setup_screen() {
|
||||
g_http_get_code = 500;
|
||||
// LittleFS has no IMAGE_PATH entry
|
||||
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
|
||||
TEST_ASSERT_EQUAL(0, g_epd_draw_border_count);
|
||||
TEST_ASSERT_EQUAL(1, g_epd_fill_count);
|
||||
TEST_ASSERT_EQUAL(COLOR_YELLOW, g_epd_fill_last_color);
|
||||
TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
|
||||
TEST_ASSERT_EQUAL_MESSAGE(0, g_epd_fill_count,
|
||||
"5xx during bootstrap must NOT paint yellow over the setup QR");
|
||||
// err_border NOT set — default sentinel (-1) means key wasn't written.
|
||||
TEST_ASSERT_EQUAL(-1, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
|
||||
TEST_ASSERT_FALSE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-06c: 304 with err_border flag set (sync recovered after a previous
|
||||
@@ -389,17 +398,20 @@ void test_fw_deep_sleep_arms_ext0_button_wakeup() {
|
||||
}
|
||||
|
||||
// FW-FIRST-IMG-A: device has never received an image (img_id = -1) AND the
|
||||
// poll didn't deliver one (e.g. 204 because no images approved yet). Sleep
|
||||
// must be the 15s bootstrap interval, NOT whatever the server's schedule
|
||||
// says. Without this, a fresh device on a noon-daily schedule sits dark
|
||||
// for up to 24 h before the first photo lands.
|
||||
void test_fw_first_image_bootstrap_polls_at_15s_when_no_image_yet() {
|
||||
// poll didn't deliver one (e.g. 204 because no images approved yet). The
|
||||
// bootstrap path must SKIP deep sleep entirely — the caller (main.cpp's
|
||||
// normal_operation loop) keeps WiFi alive and re-invokes the function on
|
||||
// a short BOOTSTRAP_RETRY_INTERVAL_MS timer so the user doesn't watch a
|
||||
// ~5 s deep-sleep + wifi-reconnect on every "no image yet" cycle.
|
||||
// Without this contract, a fresh device on a noon-daily schedule would
|
||||
// deep-sleep for the server's 6-hour interval and sit dark all day.
|
||||
void test_fw_first_image_bootstrap_skips_deep_sleep() {
|
||||
g_http_get_code = 204;
|
||||
// Server tries to set a 6-hour interval for the user's noon-daily schedule.
|
||||
g_http_response_headers["X-Interval-Ms"] = String((unsigned long)(6ULL * 60 * 60 * 1000)).c_str();
|
||||
// No img_id in NVS → device has never seen an image.
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
TEST_ASSERT_EQUAL_UINT64(FIRST_IMAGE_POLL_INTERVAL_MS * 1000ULL, g_sleep_us);
|
||||
TEST_ASSERT_FALSE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-FIRST-IMG-B: once we've persisted an image (200 path wrote img_id), the
|
||||
@@ -542,7 +554,7 @@ int main(int argc, char** argv) {
|
||||
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_fw06b_error_without_cache_preserves_setup_screen);
|
||||
RUN_TEST(test_fw06c_304_after_error_repaints_clean);
|
||||
RUN_TEST(test_fw06d_304_steady_state_does_not_fill_yellow);
|
||||
RUN_TEST(test_fw06e_200_after_error_clears_flag);
|
||||
@@ -560,7 +572,7 @@ int main(int argc, char** argv) {
|
||||
RUN_TEST(test_fw_no_flag_means_no_header);
|
||||
RUN_TEST(test_fw_X_Claimed_response_clears_flag);
|
||||
RUN_TEST(test_fw_no_X_Claimed_response_keeps_flag);
|
||||
RUN_TEST(test_fw_first_image_bootstrap_polls_at_15s_when_no_image_yet);
|
||||
RUN_TEST(test_fw_first_image_bootstrap_skips_deep_sleep);
|
||||
RUN_TEST(test_fw_first_image_bootstrap_clears_after_200);
|
||||
RUN_TEST(test_fw_first_image_just_arrived_uses_server_interval);
|
||||
RUN_TEST(test_fw_deep_sleep_arms_ext0_button_wakeup);
|
||||
|
||||
Reference in New Issue
Block a user