diff --git a/platformio.ini b/platformio.ini index 96ec996..ddd4cb9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,6 +126,13 @@ build_flags = -DPIN_PWR=1 -DPANEL_ID=\"waveshare-13.3-spectra6\" -DBOARD_HAS_PSRAM + ; Route Serial through the S3's native USB-CDC. Without this the default + ; arduino-esp32 routing sends Serial to UART0, whose pins aren't wired to + ; either USB endpoint on the 13.3E6 board — making firmware logs invisible + ; over USB and forcing reliance on server-side telemetry alone. The CDC + ; endpoint enumerates as the "Espressif USB JTAG serial debug unit" ACM + ; port; it disappears when the chip deep-sleeps and re-enumerates on wake. + -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ricmoo/QRCode@^0.0.1 diff --git a/src/operation.h b/src/operation.h index 17f4f31..a7cfe14 100644 --- a/src/operation.h +++ b/src/operation.h @@ -160,6 +160,19 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre http.addHeader("X-Just-Provisioned", "1"); } + // Recovery handshake: drawNeeded survives a power-loss-during-draw + // because we set it before kicking the e-ink refresh and only clear + // it after the panel finishes. Telling the server about it lets it + // suppress rotation advancement (including the X-Boot-Reason: cold + // force-resync) so we get the SAME image back and can repaint it + // from the cached /img.bin, instead of chasing a fresh image every + // time a draw resets. Without this, cold-boot rotation defeats the + // 304-with-drawNeeded recovery branch and the device churns through + // images, leaving torn frames on the panel. + if (drawNeeded) { + http.addHeader("X-Draw-Pending", "1"); + } + const char* collectHeaders[] = { "X-Interval-Ms", "X-Image-Id", "X-Image-Sha256", "X-Claimed" }; http.collectHeaders(collectHeaders, 4); int code = http.GET(); diff --git a/test/test_normal_operation/test_main.cpp b/test/test_normal_operation/test_main.cpp index 21bb2dc..3a17cee 100644 --- a/test/test_normal_operation/test_main.cpp +++ b/test/test_normal_operation/test_main.cpp @@ -608,6 +608,41 @@ void test_fw_bootstrap_loop_iterates_when_no_image() { TEST_ASSERT_TRUE(g_deep_sleep_started); } +// FW-DRAWPENDING-A: when drawNeeded is set in NVS at boot, the poll must +// carry X-Draw-Pending: 1. Server uses this to suppress rotation advancement +// (incl. cold-boot force-resync) so the device gets the SAME image back and +// can retry the interrupted draw from the cached /img.bin. Without the +// header, cold-boot rotation defeats the 304-with-drawNeeded recovery branch. +void test_fw_draw_pending_header_when_draw_needed_set() { + prefs.begin(NVS_NAMESPACE, false); + prefs.putInt(NVS_KEY_DRAW_NEEDED, 1); + prefs.putInt(NVS_KEY_SCHEMA_V, NVS_SCHEMA_VERSION); // skip migration noise + prefs.end(); + g_http_response_headers["X-Image-Id"] = "1"; + + normal_operation_impl(String("mac"), http, String("url"), prefs); + + TEST_ASSERT_TRUE_MESSAGE( + g_http_request_headers.find("X-Draw-Pending") != g_http_request_headers.end(), + "X-Draw-Pending header must be present when drawNeeded is set"); + TEST_ASSERT_EQUAL_STRING("1", g_http_request_headers["X-Draw-Pending"].c_str()); +} + +// FW-DRAWPENDING-B: in the normal case (drawNeeded == 0), the header is +// absent. The server treats absence as "device is happy, you may rotate." +void test_fw_draw_pending_header_absent_when_no_draw_needed() { + prefs.begin(NVS_NAMESPACE, false); + prefs.putInt(NVS_KEY_SCHEMA_V, NVS_SCHEMA_VERSION); + prefs.end(); + g_http_response_headers["X-Image-Id"] = "1"; + + normal_operation_impl(String("mac"), http, String("url"), prefs); + + TEST_ASSERT_TRUE_MESSAGE( + g_http_request_headers.find("X-Draw-Pending") == g_http_request_headers.end(), + "X-Draw-Pending header must be absent when drawNeeded is clear"); +} + // FW-12/13: AP SSID derivation via ap_ssid_from_mac() void test_fw12_ap_ssid_from_mac_aabbcc() { String ssid = ap_ssid_from_mac(String("AA:BB:CC:DD:EE:FF")); @@ -661,5 +696,7 @@ int main(int argc, char** argv) { RUN_TEST(test_fw_panel_id_header_value_matches_macro); RUN_TEST(test_fw_bootstrap_loop_exits_after_deep_sleep); RUN_TEST(test_fw_bootstrap_loop_iterates_when_no_image); + RUN_TEST(test_fw_draw_pending_header_when_draw_needed_set); + RUN_TEST(test_fw_draw_pending_header_absent_when_no_draw_needed); return UNITY_END(); }