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:
+18
-14
@@ -122,19 +122,23 @@
|
|||||||
#define APP_BASE_URL "https://pictureframe.edholm.me"
|
#define APP_BASE_URL "https://pictureframe.edholm.me"
|
||||||
#define AP_IP "192.168.4.1"
|
#define AP_IP "192.168.4.1"
|
||||||
#define WIFI_TIMEOUT_MS 30000
|
#define WIFI_TIMEOUT_MS 30000
|
||||||
#ifndef FETCH_INTERVAL_MS
|
// Server's X-Interval-Ms is the primary schedule — driven by the user's
|
||||||
// TODO(post-dev): drop the 60s cap. Today this value is used in
|
// rotationIntervalMinutes / wakeTimes settings. The constants below are
|
||||||
// operation.h as BOTH the no-header fallback AND the upper bound that
|
// only safety nets:
|
||||||
// clamps the server-provided X-Interval-Ms. While we're iterating on the
|
// - FALLBACK is used when the server omits the header (shouldn't happen
|
||||||
// firmware we want the frame to poll every minute so changes land fast,
|
// in normal operation, but guards against a rolling deploy or hand-
|
||||||
// but in production we want to honor whatever the app sends (e.g.,
|
// crafted response).
|
||||||
// rotationIntervalMinutes=60 → 1 hour, or wakeHour set → ~24 h sleep).
|
// - CLAMP_MIN/MAX bound the server value to sane physical limits — no
|
||||||
// When the firmware stabilizes, split this into two constants:
|
// runaway polling on a malformed 0/negative, no week-long naps on a
|
||||||
// - FETCH_INTERVAL_MS_FALLBACK (used when no X-Interval-Ms header)
|
// misconfigured 999 days. CLAMP_MAX is just past 24 h to give DST
|
||||||
// - SLEEP_CLAMP_MIN_MS / SLEEP_CLAMP_MAX_MS (sanity bounds, not the
|
// transitions and edge-case wake-time math a little slack.
|
||||||
// primary schedule)
|
#ifndef FETCH_INTERVAL_MS_FALLBACK
|
||||||
// and let server values flow through. See operation.h:138 for the cap
|
#define FETCH_INTERVAL_MS_FALLBACK 60000ULL // 1 min, used only when no X-Interval-Ms header
|
||||||
// site, and tests FW-09/FW-10 for the assertions that will need updating.
|
#endif
|
||||||
#define FETCH_INTERVAL_MS 60000 // 1 min deep sleep between polls (DEV value)
|
#ifndef SLEEP_CLAMP_MIN_MS
|
||||||
|
#define SLEEP_CLAMP_MIN_MS 30000ULL // 30 s — protect against runaway polls
|
||||||
|
#endif
|
||||||
|
#ifndef SLEEP_CLAMP_MAX_MS
|
||||||
|
#define SLEEP_CLAMP_MAX_MS (25ULL * 60ULL * 60ULL * 1000ULL) // 25 h — past 24h with DST slack
|
||||||
#endif
|
#endif
|
||||||
#define IMAGE_PATH "/img.bin"
|
#define IMAGE_PATH "/img.bin"
|
||||||
|
|||||||
+13
-11
@@ -131,20 +131,22 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
|
|||||||
http.collectHeaders(collectHeaders, 3);
|
http.collectHeaders(collectHeaders, 3);
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
|
|
||||||
// TODO(post-dev): trust the server's X-Interval-Ms instead of capping
|
// Honor the server's X-Interval-Ms — that's the user's configured
|
||||||
// it at FETCH_INTERVAL_MS. The cap is here so the dev unit polls
|
// rotationIntervalMinutes / wakeTimes schedule, computed in
|
||||||
// every minute regardless of what the app's rotationIntervalMinutes /
|
// DeviceImageController::computeIntervalMs. Clamp to sane physical
|
||||||
// wakeHour settings say — fast iteration. Once the firmware is stable
|
// limits so a malformed 0/garbage value doesn't burn the battery
|
||||||
// and we want real battery life on V2, the line below should become
|
// (CLAMP_MIN) and a misconfigured "every 999 days" doesn't strand the
|
||||||
// simply `sleepMs = v;` plus a sanity clamp (e.g. min 30 s, max 25 h).
|
// device for a week (CLAMP_MAX). When no header is present (server
|
||||||
// Tests FW-09 and FW-10 in test_normal_operation/test_main.cpp lock
|
// bug, mid-deploy), fall back to FETCH_INTERVAL_MS_FALLBACK.
|
||||||
// the current behavior — update them when removing the cap. See the
|
uint64_t sleepMs = FETCH_INTERVAL_MS_FALLBACK;
|
||||||
// matching note in config.h on FETCH_INTERVAL_MS.
|
|
||||||
uint64_t sleepMs = FETCH_INTERVAL_MS;
|
|
||||||
String intervalHdr = http.header("X-Interval-Ms");
|
String intervalHdr = http.header("X-Interval-Ms");
|
||||||
if (intervalHdr.length() > 0) {
|
if (intervalHdr.length() > 0) {
|
||||||
uint64_t v = strtoull(intervalHdr.c_str(), nullptr, 10);
|
uint64_t v = strtoull(intervalHdr.c_str(), nullptr, 10);
|
||||||
if (v > 0) sleepMs = std::min<uint64_t>(v, (uint64_t)FETCH_INTERVAL_MS);
|
if (v > 0) {
|
||||||
|
sleepMs = v;
|
||||||
|
if (sleepMs < SLEEP_CLAMP_MIN_MS) sleepMs = SLEEP_CLAMP_MIN_MS;
|
||||||
|
if (sleepMs > SLEEP_CLAMP_MAX_MS) sleepMs = SLEEP_CLAMP_MAX_MS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool displayInitialized = false;
|
bool displayInitialized = false;
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,9 @@
|
|||||||
#define NVS_KEY_IMG_ID "img_id"
|
#define NVS_KEY_IMG_ID "img_id"
|
||||||
#define NVS_KEY_DRAW_NEEDED "draw"
|
#define NVS_KEY_DRAW_NEEDED "draw"
|
||||||
#define IMAGE_PATH "/img.bin"
|
#define IMAGE_PATH "/img.bin"
|
||||||
#define FETCH_INTERVAL_MS 60000ULL
|
#define FETCH_INTERVAL_MS_FALLBACK 60000ULL
|
||||||
|
#define SLEEP_CLAMP_MIN_MS 30000ULL
|
||||||
|
#define SLEEP_CLAMP_MAX_MS (25ULL * 60ULL * 60ULL * 1000ULL)
|
||||||
#define WIFI_TIMEOUT_MS 30000
|
#define WIFI_TIMEOUT_MS 30000
|
||||||
#define RESET_HOLD_MS 5000
|
#define RESET_HOLD_MS 5000
|
||||||
#define AP_IP "192.168.4.1"
|
#define AP_IP "192.168.4.1"
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ void tearDown() {}
|
|||||||
|
|
||||||
// FW-01: 200 response — file written, epd_draw called, NVS saved, deep sleep started
|
// FW-01: 200 response — file written, epd_draw called, NVS saved, deep sleep started
|
||||||
void test_fw01_200_response_happy_path() {
|
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-Image-Id"] = "42";
|
||||||
g_http_response_headers["X-Interval-Ms"] = "30000";
|
g_http_response_headers["X-Interval-Ms"] = "30000";
|
||||||
g_http_body = "BINDATA";
|
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
|
// FW-03: 304 — no epd draw, no init, deep sleep started
|
||||||
void test_fw03_304_no_redraw() {
|
void test_fw03_304_no_redraw() {
|
||||||
g_http_get_code = 304;
|
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";
|
g_http_response_headers["X-Interval-Ms"] = "30000";
|
||||||
|
|
||||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
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());
|
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() {
|
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";
|
g_http_response_headers["X-Image-Id"] = "1";
|
||||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
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.
|
// FW-10: server interval > SLEEP_CLAMP_MAX_MS → clamped at MAX.
|
||||||
// TODO(post-dev): when the cap in operation.h is removed (so the device
|
// Protects against a misconfigured "every 999 days" stranding the device.
|
||||||
// honors the app's rotationIntervalMinutes / wakeHour settings), this
|
void test_fw10_server_interval_clamped_to_max() {
|
||||||
// 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() {
|
|
||||||
g_http_response_headers["X-Interval-Ms"] = "999999999";
|
g_http_response_headers["X-Interval-Ms"] = "999999999";
|
||||||
g_http_response_headers["X-Image-Id"] = "1";
|
g_http_response_headers["X-Image-Id"] = "1";
|
||||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
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
|
// FW-10b: server interval < SLEEP_CLAMP_MIN_MS → clamped UP to MIN.
|
||||||
void test_fw11_default_interval_when_absent() {
|
// 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";
|
g_http_response_headers["X-Image-Id"] = "1";
|
||||||
// no X-Interval-Ms set
|
// no X-Interval-Ms set
|
||||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
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)
|
// 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_fw07_current_image_id_sent_when_saved);
|
||||||
RUN_TEST(test_fw08_no_current_image_id_when_default);
|
RUN_TEST(test_fw08_no_current_image_id_when_default);
|
||||||
RUN_TEST(test_fw09_server_interval_honored);
|
RUN_TEST(test_fw09_server_interval_honored);
|
||||||
RUN_TEST(test_fw10_server_interval_capped);
|
RUN_TEST(test_fw10_server_interval_clamped_to_max);
|
||||||
RUN_TEST(test_fw11_default_interval_when_absent);
|
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_fw12_ap_ssid_from_mac_aabbcc);
|
||||||
RUN_TEST(test_fw13_ap_ssid_from_real_mac);
|
RUN_TEST(test_fw13_ap_ssid_from_real_mac);
|
||||||
RUN_TEST(test_fw14_304_skips_epd_sleep);
|
RUN_TEST(test_fw14_304_skips_epd_sleep);
|
||||||
|
|||||||
Reference in New Issue
Block a user