feat(firmware): honor server X-Interval-Ms instead of capping at 60s

The dev-only cap that forced every-1-min polling regardless of the app's
schedule is removed. The device now sleeps for whatever X-Interval-Ms
the server hands back (driven by rotationIntervalMinutes / wakeTimes),
clamped to [30s, 25h] as a safety net against malformed values.

Renamed FETCH_INTERVAL_MS to FETCH_INTERVAL_MS_FALLBACK — it's now
*only* used when the header is absent (rare; rolling deploy / hand-
crafted response). Added SLEEP_CLAMP_MIN/MAX for the bounds.

Tests FW-09 and FW-10 flipped to lock the new behavior; added FW-10b
covering sub-MIN clamping (battery protection if server sends 1000ms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 15:34:20 -04:00
parent 8915a3d1f4
commit 988759f738
4 changed files with 61 additions and 44 deletions
+27 -18
View File
@@ -83,7 +83,7 @@ void tearDown() {}
// FW-01: 200 response — file written, epd_draw called, NVS saved, deep sleep started
void test_fw01_200_response_happy_path() {
// Use an interval < FETCH_INTERVAL_MS so server value is honored
// 30 s — at SLEEP_CLAMP_MIN_MS, well under MAX, so honored as-is
g_http_response_headers["X-Image-Id"] = "42";
g_http_response_headers["X-Interval-Ms"] = "30000";
g_http_body = "BINDATA";
@@ -108,7 +108,7 @@ void test_fw02_headers_read_before_end_regression() {
// FW-03: 304 — no epd draw, no init, deep sleep started
void test_fw03_304_no_redraw() {
g_http_get_code = 304;
// Use an interval < FETCH_INTERVAL_MS so server value is honored
// 30 s — at SLEEP_CLAMP_MIN_MS, well under MAX, so honored as-is
g_http_response_headers["X-Interval-Ms"] = "30000";
normal_operation_impl(String("mac"), http, String("url"), prefs);
@@ -263,33 +263,41 @@ void test_fw08_no_current_image_id_when_default() {
TEST_ASSERT_TRUE(g_http_request_headers.find("X-Current-Image-Id") == g_http_request_headers.end());
}
// FW-09: server interval < FETCH_INTERVAL_MS → server value used
// FW-09: server interval within clamp range → exact server value used.
// 5 min sits well between SLEEP_CLAMP_MIN_MS and SLEEP_CLAMP_MAX_MS.
void test_fw09_server_interval_honored() {
g_http_response_headers["X-Interval-Ms"] = "30000";
g_http_response_headers["X-Interval-Ms"] = "300000"; // 5 min
g_http_response_headers["X-Image-Id"] = "1";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us);
TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us);
}
// FW-10: server interval > FETCH_INTERVAL_MS → capped at ceiling.
// TODO(post-dev): when the cap in operation.h is removed (so the device
// honors the app's rotationIntervalMinutes / wakeHour settings), this
// test should flip to assert sleepMs == 999999999 (or whatever the
// server-side max is, e.g. 25 h clamp). See operation.h:~140 and the
// matching TODO in config.h on FETCH_INTERVAL_MS.
void test_fw10_server_interval_capped() {
// FW-10: server interval > SLEEP_CLAMP_MAX_MS → clamped at MAX.
// Protects against a misconfigured "every 999 days" stranding the device.
void test_fw10_server_interval_clamped_to_max() {
g_http_response_headers["X-Interval-Ms"] = "999999999";
g_http_response_headers["X-Image-Id"] = "1";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS * 1000ULL, g_sleep_us);
TEST_ASSERT_EQUAL_UINT64(SLEEP_CLAMP_MAX_MS * 1000ULL, g_sleep_us);
}
// FW-11: no X-Interval-Ms → default ceiling used
void test_fw11_default_interval_when_absent() {
// FW-10b: server interval < SLEEP_CLAMP_MIN_MS → clamped UP to MIN.
// Protects the battery against a runaway poll if the server sends 1000 ms
// (or anything malformed-but-positive that survives strtoull).
void test_fw10b_server_interval_clamped_to_min() {
g_http_response_headers["X-Interval-Ms"] = "1000";
g_http_response_headers["X-Image-Id"] = "1";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_UINT64(SLEEP_CLAMP_MIN_MS * 1000ULL, g_sleep_us);
}
// FW-11: no X-Interval-Ms → fallback used. Should not happen in normal
// operation but guards against rolling deploys / hand-crafted responses.
void test_fw11_fallback_used_when_header_absent() {
g_http_response_headers["X-Image-Id"] = "1";
// no X-Interval-Ms set
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS * 1000ULL, g_sleep_us);
TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS_FALLBACK * 1000ULL, g_sleep_us);
}
// FW-14: 304 — epd_sleep NOT called (display already in hardware deep sleep)
@@ -392,8 +400,9 @@ int main(int argc, char** argv) {
RUN_TEST(test_fw07_current_image_id_sent_when_saved);
RUN_TEST(test_fw08_no_current_image_id_when_default);
RUN_TEST(test_fw09_server_interval_honored);
RUN_TEST(test_fw10_server_interval_capped);
RUN_TEST(test_fw11_default_interval_when_absent);
RUN_TEST(test_fw10_server_interval_clamped_to_max);
RUN_TEST(test_fw10b_server_interval_clamped_to_min);
RUN_TEST(test_fw11_fallback_used_when_header_absent);
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);