diff --git a/firmware/main/Kconfig.projbuild b/firmware/main/Kconfig.projbuild index 0fb7d5a..050ee17 100644 --- a/firmware/main/Kconfig.projbuild +++ b/firmware/main/Kconfig.projbuild @@ -83,4 +83,16 @@ menu "ESPresso Frame Configuration" server was unreachable or the fetch/display failed, instead of waiting the full FRAME_SLEEP_INTERVAL_S. + config FRAME_REPROVISION_AFTER_FAILURES + int "Consecutive server-unreachable wakes before falling back to provisioning" + default 12 + help + If the tools server is unreachable for this many consecutive + wakes in a row (at FRAME_RETRY_INTERVAL_S apart -- ~1 hour at + the defaults), the device gives up retrying silently and drops + back into provisioning mode instead, so a moved/renamed server + can be reconfigured without a USB connection. Resets to 0 on + any successful contact with the server, or on fresh + provisioning. + endmenu diff --git a/firmware/main/frame_client.c b/firmware/main/frame_client.c index 244282d..81a3ff7 100644 --- a/firmware/main/frame_client.c +++ b/firmware/main/frame_client.c @@ -286,10 +286,30 @@ void frame_client_run(const frame_config_t *cfg) * compounding flashing. */ ESP_LOGW(TAG, "Tools server not reachable, retrying sooner"); sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S; + bool give_up = frame_config_record_server_failure(); if (have_display) { status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED); } + if (give_up) { + /* Unreachable for too many wakes in a row -- likely moved or + * reconfigured. Clear the stored config and reboot rather than + * calling wifi_provisioning_start() directly here: that would + * mean initializing the display driver a second time in this + * same session (frame_client_run already did once), which is + * the same class of double-init bug hit earlier with WiFi. + * The next boot's frame_config_load() will report "not + * provisioned" and route through the existing, already-tested + * provisioning path with a single, fresh epd_init(). */ + ESP_LOGW(TAG, "Server unreachable for %d consecutive wakes, falling back to provisioning", + CONFIG_FRAME_REPROVISION_AFTER_FAILURES); + frame_config_clear(); + if (have_display) { + epd_sleep(); + } + esp_restart(); + } } else { + frame_config_reset_server_failures(); if (first_connection && have_display) { status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK); } diff --git a/firmware/main/main.c b/firmware/main/main.c index 5e03290..18e5dab 100644 --- a/firmware/main/main.c +++ b/firmware/main/main.c @@ -1,5 +1,6 @@ #include "esp_event.h" #include "esp_log.h" +#include "esp_system.h" #include "nvs_flash.h" #include "esp_netif.h" @@ -26,6 +27,22 @@ void app_main(void) } ESP_ERROR_CHECK(nvs_err); + /* ESP32-C6 (like most ESP32 variants) can't electrically distinguish + * the RST/EN button from a genuine power-on -- both report as + * ESP_RST_POWERON, since there's no separate external-reset-pin signal + * (see ESP_RST_EXT's doc comment). The device's only *normal* restart + * path is ESP_RST_DEEPSLEEP (its own scheduled timer wake); crash-type + * resets (brownout/watchdog/panic) report their own distinct reasons, + * not POWERON. So treating POWERON as "user wants to reconfigure" is + * safe: it won't fire on a scheduled wake or a transient crash, only + * on an actual power cycle or RST press -- which for this device + * plausibly means it's being moved/redeployed anyway. */ + if (esp_reset_reason() == ESP_RST_POWERON) { + ESP_LOGI(TAG, "Power-on/RST reset -- entering provisioning to allow reconfiguration"); + wifi_provisioning_start(); + return; + } + frame_config_t cfg; esp_err_t cfg_err = frame_config_load(&cfg); if (cfg_err == ESP_OK) { diff --git a/firmware/main/wifi_provisioning.c b/firmware/main/wifi_provisioning.c index feae0a9..c011ef3 100644 --- a/firmware/main/wifi_provisioning.c +++ b/firmware/main/wifi_provisioning.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -93,9 +94,13 @@ esp_err_t frame_config_save(const frame_config_t *cfg) } if (err == ESP_OK) { /* Fresh (re)provisioning -- the next successful connection should - * show the status screen again. */ + * show the status screen again, and any stale failure streak from + * before this reprovisioning no longer applies. */ err = nvs_set_u8(handle, "connected_once", 0); } + if (err == ESP_OK) { + err = nvs_set_u8(handle, "fail_count", 0); + } if (err == ESP_OK) { err = nvs_commit(handle); } @@ -130,6 +135,54 @@ void frame_config_mark_connected_once(void) nvs_close(handle); } +bool frame_config_record_server_failure(void) +{ + nvs_handle_t handle; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) { + return false; + } + + uint8_t count = 0; + nvs_get_u8(handle, "fail_count", &count); /* leaves count=0 if not yet set */ + if (count < UINT8_MAX) { + count++; + } + nvs_set_u8(handle, "fail_count", count); + nvs_commit(handle); + nvs_close(handle); + + return count >= CONFIG_FRAME_REPROVISION_AFTER_FAILURES; +} + +void frame_config_reset_server_failures(void) +{ + nvs_handle_t handle; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) { + return; + } + nvs_set_u8(handle, "fail_count", 0); + nvs_commit(handle); + nvs_close(handle); +} + +esp_err_t frame_config_clear(void) +{ + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + + /* Best-effort: a key simply not existing yet is fine here. */ + nvs_erase_key(handle, "sta_ssid"); + nvs_erase_key(handle, "sta_pass"); + nvs_erase_key(handle, "toolsserver"); + + err = nvs_commit(handle); + nvs_close(handle); + return err; +} + static void generate_ap_password(char *out, size_t out_size) { size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1); diff --git a/firmware/main/wifi_provisioning.h b/firmware/main/wifi_provisioning.h index 6e1798e..7848b85 100644 --- a/firmware/main/wifi_provisioning.h +++ b/firmware/main/wifi_provisioning.h @@ -36,6 +36,28 @@ bool frame_config_has_connected_once(void); /** Marks the status screen as having been shown for the current WiFi config. */ void frame_config_mark_connected_once(void); +/** + * Increments the persisted count of consecutive wakes where the tools + * server was unreachable. Returns true once it reaches + * CONFIG_FRAME_REPROVISION_AFTER_FAILURES, signaling the caller should + * fall back to provisioning instead of just retrying again. + */ +bool frame_config_record_server_failure(void); + +/** Resets the consecutive-server-failure count. Call on any successful + * contact with the tools server. */ +void frame_config_reset_server_failures(void); + +/** + * Erases the saved home-network config (SSID/password/toolsserver) so the + * next boot's frame_config_load() reports "not provisioned" and falls + * back into provisioning. Doesn't touch the AP password or other + * device-identity state. Follow with esp_restart() -- doesn't itself + * bring up provisioning, since that would mean initializing the display + * driver a second time in a session that may have already done so. + */ +esp_err_t frame_config_clear(void); + /** * Returns this device's provisioning AP identity: a fixed SSID (from * Kconfig) and a password that's generated once on first use and persisted