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:
@@ -127,6 +127,15 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
|
||||
http.addHeader("X-Current-Image-Id", String(currentImgId));
|
||||
}
|
||||
|
||||
// Tell the server how we got here. The server uses this to honor a
|
||||
// power-cycle as a deliberate "force resync" — a poll that arrives with
|
||||
// X-Boot-Reason: cold gets a fresh rotation even outside configured wake
|
||||
// times, so unplugging and replugging the frame works as a manual refresh.
|
||||
// Timer wakes (the normal case) keep their schedule-gated semantics.
|
||||
const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||
http.addHeader("X-Boot-Reason",
|
||||
cause == ESP_SLEEP_WAKEUP_TIMER ? "timer" : "cold");
|
||||
|
||||
const char* collectHeaders[] = { "X-Interval-Ms", "X-Image-Id", "X-Image-Sha256" };
|
||||
http.collectHeaders(collectHeaders, 3);
|
||||
int code = http.GET();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user