feat(operation): send X-Boot-Reason so power-cycle is a force-resync

Distinguish a cold-boot poll (UNDEFINED wakeup cause = power-on, hard
reset, plug-cycle) from a normal timer wake. Encoded as the
X-Boot-Reason request header; server uses it to deliberately bypass
the schedule and rotate. Matches how users actually use the device:
unplug-and-replug as a manual refresh.

Tests: two new native cases asserting the header is "cold" on
UNDEFINED wakeup and "timer" on TIMER wakeup. esp_sleep mock now
exposes a settable wakeup_cause global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 12:18:32 -04:00
parent 988759f738
commit bbd5e84db0
3 changed files with 48 additions and 0 deletions
+14
View File
@@ -4,5 +4,19 @@
extern uint64_t g_sleep_us;
extern bool g_deep_sleep_started;
// Mirror of the ESP-IDF wakeup-cause enum that the firmware actually checks.
// Tests set g_wakeup_cause directly to simulate cold-boot vs timer-wake.
typedef enum {
ESP_SLEEP_WAKEUP_UNDEFINED = 0, // cold boot / power-on / hard reset
ESP_SLEEP_WAKEUP_EXT0 = 2,
ESP_SLEEP_WAKEUP_EXT1 = 3,
ESP_SLEEP_WAKEUP_TIMER = 4, // returned to userland after deep-sleep timer
ESP_SLEEP_WAKEUP_TOUCHPAD = 5,
ESP_SLEEP_WAKEUP_ULP = 6,
} esp_sleep_wakeup_cause_t;
extern esp_sleep_wakeup_cause_t g_wakeup_cause;
inline void esp_sleep_enable_timer_wakeup(uint64_t us) { g_sleep_us = us; }
inline void esp_deep_sleep_start() { g_deep_sleep_started = true; }
inline esp_sleep_wakeup_cause_t esp_sleep_get_wakeup_cause() { return g_wakeup_cause; }
+25
View File
@@ -34,6 +34,7 @@ int g_epd_draw_border_count, g_epd_draw_border_last_color, g_epd_draw_border_las
uint64_t g_sleep_us;
bool g_deep_sleep_started;
esp_sleep_wakeup_cause_t g_wakeup_cause;
// Globals for new mocks
int g_show_setup_qr_count;
@@ -66,6 +67,7 @@ void reset_state() {
g_epd_draw_border_count = g_epd_draw_border_last_color = g_epd_draw_border_last_thickness = 0;
g_sleep_us = 0;
g_deep_sleep_started = false;
g_wakeup_cause = ESP_SLEEP_WAKEUP_TIMER; // default to timer wake unless a test sets cold
g_show_setup_qr_count = 0;
g_millis_value = 0;
g_digital_read_value = HIGH; // button not pressed by default
@@ -291,6 +293,27 @@ void test_fw10b_server_interval_clamped_to_min() {
TEST_ASSERT_EQUAL_UINT64(SLEEP_CLAMP_MIN_MS * 1000ULL, g_sleep_us);
}
// FW-FORCE-RESYNC: a wakeup cause of UNDEFINED (cold boot, hard reset,
// power-cycle) MUST surface to the server as X-Boot-Reason: cold so that
// the server can force a rotation regardless of the wakeTimes schedule.
// This is what makes "unplug → replug" a manual refresh feature for users.
void test_fw_cold_boot_sends_X_Boot_Reason_cold() {
g_wakeup_cause = ESP_SLEEP_WAKEUP_UNDEFINED;
g_http_response_headers["X-Image-Id"] = "1";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_STRING("cold", g_http_request_headers["X-Boot-Reason"].c_str());
}
// Inverse of FW-FORCE-RESYNC: scheduled timer wakeups must not pretend to be
// power-cycles, or every poll would force a rotation and the schedule gating
// would be useless.
void test_fw_timer_wake_sends_X_Boot_Reason_timer() {
g_wakeup_cause = ESP_SLEEP_WAKEUP_TIMER;
g_http_response_headers["X-Image-Id"] = "1";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL_STRING("timer", g_http_request_headers["X-Boot-Reason"].c_str());
}
// 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() {
@@ -403,6 +426,8 @@ int main(int argc, char** argv) {
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_fw_cold_boot_sends_X_Boot_Reason_cold);
RUN_TEST(test_fw_timer_wake_sends_X_Boot_Reason_timer);
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);