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));
|
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" };
|
const char* collectHeaders[] = { "X-Interval-Ms", "X-Image-Id", "X-Image-Sha256" };
|
||||||
http.collectHeaders(collectHeaders, 3);
|
http.collectHeaders(collectHeaders, 3);
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
|
|||||||
@@ -4,5 +4,19 @@
|
|||||||
extern uint64_t g_sleep_us;
|
extern uint64_t g_sleep_us;
|
||||||
extern bool g_deep_sleep_started;
|
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_sleep_enable_timer_wakeup(uint64_t us) { g_sleep_us = us; }
|
||||||
inline void esp_deep_sleep_start() { g_deep_sleep_started = true; }
|
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;
|
uint64_t g_sleep_us;
|
||||||
bool g_deep_sleep_started;
|
bool g_deep_sleep_started;
|
||||||
|
esp_sleep_wakeup_cause_t g_wakeup_cause;
|
||||||
|
|
||||||
// Globals for new mocks
|
// Globals for new mocks
|
||||||
int g_show_setup_qr_count;
|
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_epd_draw_border_count = g_epd_draw_border_last_color = g_epd_draw_border_last_thickness = 0;
|
||||||
g_sleep_us = 0;
|
g_sleep_us = 0;
|
||||||
g_deep_sleep_started = false;
|
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_show_setup_qr_count = 0;
|
||||||
g_millis_value = 0;
|
g_millis_value = 0;
|
||||||
g_digital_read_value = HIGH; // button not pressed by default
|
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);
|
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
|
// FW-11: no X-Interval-Ms → fallback used. Should not happen in normal
|
||||||
// operation but guards against rolling deploys / hand-crafted responses.
|
// operation but guards against rolling deploys / hand-crafted responses.
|
||||||
void test_fw11_fallback_used_when_header_absent() {
|
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_fw10_server_interval_clamped_to_max);
|
||||||
RUN_TEST(test_fw10b_server_interval_clamped_to_min);
|
RUN_TEST(test_fw10b_server_interval_clamped_to_min);
|
||||||
RUN_TEST(test_fw11_fallback_used_when_header_absent);
|
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_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