Add two ways back into provisioning: RST press and repeated server failure

RST/power-on trigger: checks esp_reset_reason() at the very top of boot.
ESP32-C6 can't electrically distinguish the RST/EN button from a genuine
power-on (both report ESP_RST_POWERON -- confirmed against ESP-IDF's own
docs, ESP_RST_EXT is explicitly "not applicable"), so POWERON is treated
as "user wants to reconfigure" and routes straight to provisioning. Safe
because the device's only normal restart path is ESP_RST_DEEPSLEEP (its
own scheduled wake), and crash-type resets (brownout/watchdog/panic)
report their own distinct reasons, not POWERON -- so a flaky power supply
or transient crash won't get bounced into provisioning, only an actual
power cycle or RST press will (which plausibly means the frame is being
moved/redeployed anyway).

Auto-fallback: a new NVS-persisted consecutive-failure counter
(frame_config_record_server_failure/reset_server_failures) tracks wakes
where the tools server was unreachable. After
CONFIG_FRAME_REPROVISION_AFTER_FAILURES in a row (default 12, ~1hr at the
retry interval), the device clears its stored WiFi config and
esp_restart()s rather than calling wifi_provisioning_start() directly --
doing that inline would mean initializing the display driver a second
time in the same session (frame_client_run already did once), the same
class of double-init bug hit earlier with WiFi. The next boot's
frame_config_load() naturally reports "not provisioned" and routes
through the existing, already-tested provisioning path with a single
fresh epd_init(). Solves the "I moved the server to a new address" case
without needing USB access.

Both frame_config_save() (fresh provisioning) and any successful server
contact reset the failure counter.
This commit is contained in:
2026-07-18 16:10:03 -04:00
parent e7f096ac23
commit d32236d832
5 changed files with 125 additions and 1 deletions
+12
View File
@@ -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
+20
View File
@@ -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);
}
+17
View File
@@ -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) {
+54 -1
View File
@@ -1,4 +1,5 @@
#include <ctype.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <sys/param.h>
@@ -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);
+22
View File
@@ -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