feat(operation): poll every 15s until first image lands

A freshly-claimed device on a noon-daily schedule would otherwise sit
dark for up to 24 h after WiFi setup waiting for its first image. The
schedule kicks in only AFTER an image has actually been displayed.

Mechanism: at the bottom of normal_operation_impl, re-read NVS_KEY_IMG_ID
to see whether any successful 200-with-integrity-OK persisted an image
id this cycle (or any prior). If still -1, override sleepMs to
FIRST_IMAGE_POLL_INTERVAL_MS (15 s) — bypassing the schedule and the
clamp range, since SLEEP_CLAMP_MIN_MS is about runaway protection in
steady state and the bootstrap window is naturally bounded by "first
image arrives."

Tests:
  - FW-FIRST-IMG-A: 204 with no img_id in NVS → 15s override fires
    even when server says 6 hours.
  - FW-FIRST-IMG-B: img_id pre-set, 200 cycle → server interval honored
    (override doesn't trap the device in 15s forever).
  - FW-FIRST-IMG-C: first 200 ever (img_id was -1, now persisted) →
    server interval applies starting THIS cycle, no extra 15s nap.

Also patched FW-03 (304 sleep timing) to pre-set img_id so the test
exercises what it claims; 304 in production only happens when the
device already holds the image, so the override would never fire there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:13:53 -04:00
parent a0dc4e0115
commit 2df2a14df6
4 changed files with 71 additions and 0 deletions
+50
View File
@@ -112,6 +112,11 @@ void test_fw03_304_no_redraw() {
g_http_get_code = 304;
// 30 s — at SLEEP_CLAMP_MIN_MS, well under MAX, so honored as-is
g_http_response_headers["X-Interval-Ms"] = "30000";
// 304 only ever happens when the device already holds the image, so
// pre-set img_id to match what the server would have served. Without
// this the FIRST_IMAGE_POLL bootstrap override would (correctly)
// shorten sleep to 15 s — and that's not what this test exercises.
prefs.ints[NVS_KEY_IMG_ID] = 1;
normal_operation_impl(String("mac"), http, String("url"), prefs);
@@ -347,6 +352,48 @@ void test_fw_X_Claimed_response_clears_flag() {
TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_JUST_PROVISIONED, -1));
}
// 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() {
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);
}
// FW-FIRST-IMG-B: once we've persisted an image (200 path wrote img_id), the
// server's schedule wins again. Without this assertion the override would
// trap the device in 15s polling forever and burn the battery.
void test_fw_first_image_bootstrap_clears_after_200() {
// Pre-set NVS as if we'd received an image previously.
prefs.ints[NVS_KEY_IMG_ID] = 42;
g_http_get_code = 200;
g_http_response_headers["X-Image-Id"] = "42";
g_http_response_headers["X-Interval-Ms"] = "300000"; // 5 min (well above 15s)
g_http_body = "BINDATA";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us);
}
// FW-FIRST-IMG-C: receiving the FIRST image (200 with previously -1 img_id)
// must let the server's schedule take over starting from this very poll —
// no extra "one more 15s sleep" cycle.
void test_fw_first_image_just_arrived_uses_server_interval() {
// img_id starts at -1 (default, no prior image).
g_http_get_code = 200;
g_http_response_headers["X-Image-Id"] = "1";
g_http_response_headers["X-Interval-Ms"] = "300000";
g_http_body = "BINDATA";
normal_operation_impl(String("mac"), http, String("url"), prefs);
// The 200 path persists img_id=1 BEFORE sleep is computed.
TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us);
}
// FW-PROV-D: server omits X-Claimed (e.g. stale-binding 204) → flag stays
// set so the device keeps signaling "I'm freshly provisioned" until a
// later poll lands on a fresh-binding response.
@@ -476,6 +523,9 @@ 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_clears_after_200);
RUN_TEST(test_fw_first_image_just_arrived_uses_server_interval);
RUN_TEST(test_fw12_ap_ssid_from_mac_aabbcc);
RUN_TEST(test_fw13_ap_ssid_from_real_mac);
RUN_TEST(test_fw14_304_skips_epd_sleep);