Add ESP32 recovery app with WiFi setup portal

- New flash layout (partitions.csv): factory = recovery app, ota_0/ota_1 =
  clock firmware; esp32_usb uploads to ota_0 so it never overwrites it
- src/recovery: WiFi + OTA + web page only. Opens the password protected
  'FlipDot-Setup' AP with a captive portal when WiFi fails, lets you pick
  another WiFi (stored in NVS, used by both apps), start the clock or stay
- Recovery is started when the clock firmware can't connect to WiFi,
  crashes 3 times, after 3 quick power cycles, from a new Home Assistant
  button, or by the bootloader when no clock firmware works
- Shared NVS store and boot switching in src/esp32/system
- Clock firmware keeps working on the old flash layout (falls back to
  OTA-only safe mode)
- README: recovery app, one-time USB installation; tests: 65 total

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 01:21:04 +02:00
1 parent ddbdf7e65f
commit 4749f9e596
25 files changed
+979 -29

No files matched your search

+13 -6
View File
@@ -20,8 +20,10 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
| `logic/power_timer.*` | Countdown to switching the display off |
| `logic/status_led.h` | Blink pattern of the built-in LED |
| `logic/boot_policy.*` | How to start after a reset: normal, OTA-only window, safe mode |
| **`system/`** | **Keeping OTA alive** |
| `system/firmware_guard.*` | Rollback of bad OTA firmware, watchdog, crash counter, status for Home Assistant |
| **`system/`** | **Keeping OTA alive** (`system_store` and `boot_target` are shared with the recovery app) |
| `system/firmware_guard.*` | Rollback of bad OTA firmware, watchdog, crash counter, power-cycle trigger, status for Home Assistant |
| `system/boot_target.*` | Switching between the clock firmware and the recovery app |
| `system/system_store.*` | Settings in NVS: WiFi, recovery reason, firmware slot and version, power-on count |
| **`display/`** | **What is drawn** |
| `display/frames.*` | The screens: clock, logo, blank, text (`FlipdotFrames`) |
| `display/bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
@@ -30,7 +32,7 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
| `nano_link/nano_flash_bridge.*` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
| **`net/`** | **Network** |
| `net/wifi_ota.*` | WiFi (with a restart when it stays down), ESP32 OTA updates, OTA-only mode |
| `net/home_assistant.*` | MQTT and the two Home Assistant switches |
| `net/home_assistant.*` | MQTT and the Home Assistant entities (two switches, firmware sensor, recovery button) |
`DisplayController` only talks to two small interfaces, `DisplayPower` (implemented by
`NanoLink`) and `Frames` (implemented by `FlipdotFrames`). The tests replace both with
@@ -46,8 +48,9 @@ again. Several layers take care of that:
| A new OTA firmware crashes, hangs or can't connect to WiFi | **Rollback.** A new firmware is only marked as good after it has run for 60 s with WiFi. If it restarts before that, the bootloader starts the previous firmware again. |
| The firmware hangs in `loop()` | **Watchdog.** If `loop()` doesn't run for 30 s, the ESP32 restarts. That counts as a crash. |
| The firmware crashes once or twice | **OTA window.** After a crash the ESP32 runs only WiFi and OTA for 15 s (LED blinks fast), then starts the main code again. |
| The firmware keeps crashing (3 times in a row) | **Safe mode.** Only WiFi and OTA run, for 10 minutes. Then the main code gets one more try; one more crash brings safe mode back. Running 5 minutes without a crash clears the counter. |
| WiFi stays down | After 10 minutes without WiFi, the ESP32 restarts, in case the WiFi stack is stuck. |
| The firmware keeps crashing (3 times in a row) | **Recovery app** for 10 minutes (see the [main README](../../README.md#recovery-app)). Then the main code gets one more try; one more crash brings the recovery app back. Running 5 minutes without a crash clears the counter. Without the recovery app installed: only WiFi and OTA run, for 10 minutes. |
| WiFi doesn't connect at boot (30 s), or stays down for 10 minutes | **Recovery app**, which keeps trying and opens the **FlipDot-Setup** WiFi with a setup page to choose another WiFi. Without the recovery app: restart. |
| The firmware runs, but its OTA is broken | **Recovery app** through the Home Assistant button, or by switching the power on and off 3 times quickly. |
So in every case you can flash a fix with `pio run -e esp32 -t upload`, at the latest
after waiting for a safe-mode cycle.
@@ -59,7 +62,7 @@ The **FlipDot Display Firmware** sensor in Home Assistant shows the state, e.g.
|-------|---------|
| `testing` | New OTA firmware, not yet marked as good (first 60 s) |
| `ok` | Firmware marked as good |
| `ok, no rollback in bootloader` | The bootloader on the ESP32 can't roll back. The other layers still work. |
| `ok, no rollback in bootloader` | The bootloader on the ESP32 can't roll back. The other layers still work. The one-time USB install of the recovery app also installs a bootloader that can. |
| `usb` | Flashed over USB, so there is no previous firmware to roll back to |
Rules for changing the code:
@@ -84,6 +87,10 @@ Rules for changing the code:
- `FlipDot Display Clock`: on = clock; off = Nintendo logo while the display is on,
then power off after 30 s
- `FlipDot Display Power`: off = clear the display and power it off after 1 s
Plus the **FlipDot Display Firmware** sensor (see
[Keeping OTA alive](#keeping-ota-alive)) and the **FlipDot Display Recovery** button,
which restarts into the recovery app for 10 minutes.
- **Relay on GPIO 13** switches the power for both the panels and the Nano. `LOW` turns it
on.
- **On:** the ESP waits for the Nano's `READY` message, at most 3 s
+10
View File
@@ -74,6 +74,16 @@ const uint32_t FIRMWARE_VALID_TIME = 60UL * 1000;
const uint32_t CRASH_FREE_TIME = 5UL * 60 * 1000;
// loop() must not block longer than this, or the ESP32 restarts (counts as a crash)
const uint32_t WATCHDOG_TIMEOUT_S = 30;
// Recovery app (src/recovery): started by the triggers in logic/boot_policy.h
const uint8_t POWER_CYCLES_FOR_RECOVERY = 3; // power ons in a row ...
const uint32_t POWER_CYCLE_WINDOW = 10000; // ... each switched off within this time
const uint32_t RECOVERY_TIME = 10UL * 60 * 1000; // then back to the clock firmware
// Stay at least this long, also when the WiFi works right away (limits switching back and
// forth, which wears the otadata flash sector)
const uint32_t RECOVERY_MIN_TIME = 60UL * 1000;
#define SETUP_AP_SSID "FlipDot-Setup" // password: SETUP_AP_PASSWORD in secrets.h
const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails this long
// WiFi: first connection at boot, and restart when WiFi stays down this long
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
+4
View File
@@ -6,3 +6,7 @@
#else
#error "Missing src/esp32/secrets.h - copy src/esp32/secrets.example.h to secrets.h and fill it in"
#endif
#ifndef SETUP_AP_PASSWORD
#error "Add SETUP_AP_PASSWORD to src/esp32/secrets.h (see secrets.example.h)"
#endif
+33
View File
@@ -35,3 +35,36 @@ bool shouldClearCrashCount(uint32_t running_ms)
{
return running_ms >= CRASH_FREE_TIME;
}
const char *recoveryReasonText(RecoveryReason reason)
{
switch (reason)
{
case RecoveryReason::Wifi:
return "the clock firmware could not connect to WiFi";
case RecoveryReason::Crash:
return "the clock firmware crashed repeatedly";
case RecoveryReason::Manual:
return "requested from Home Assistant";
case RecoveryReason::PowerCycle:
return "the power was switched on and off quickly";
case RecoveryReason::None:
default:
return "no working clock firmware";
}
}
bool powerCyclesRequestRecovery(uint8_t power_on_count)
{
return power_on_count >= POWER_CYCLES_FOR_RECOVERY;
}
bool recoveryShouldStartMain(RecoveryReason reason, uint32_t running_ms, bool wifi_connected,
bool main_available)
{
if (!main_available)
return false;
if (reason == RecoveryReason::Wifi)
return wifi_connected && running_ms >= RECOVERY_MIN_TIME;
return running_ms >= RECOVERY_TIME;
}
+28 -1
View File
@@ -9,8 +9,12 @@
// Software reset normal start (e.g. after an OTA update)
// Crash (panic, watchdog) crash counter + 1, then:
// < SAFE_MODE_CRASHES OTA-only for OTA_PANIC_WINDOW, then normal
// >= SAFE_MODE_CRASHES safe mode: OTA only for SAFE_MODE_TIME
// >= SAFE_MODE_CRASHES safe mode: the recovery app, or OTA
// only for SAFE_MODE_TIME without it
// Other (brownout, ...) normal start, counter kept
//
// The recovery app (src/recovery) uses the same file to decide when to start the clock
// firmware again.
enum class ResetKind
{
@@ -43,3 +47,26 @@ bool shouldMarkFirmwareValid(uint32_t running_ms, bool wifi_connected);
// Running this long without a crash clears the crash counter
bool shouldClearCrashCount(uint32_t running_ms);
// Why the recovery app was started (stored in NVS, shared by both apps)
enum class RecoveryReason : uint8_t
{
None = 0, // no firmware in ota_0/ota_1, or the bootloader fell back to recovery
Wifi = 1, // the clock firmware couldn't connect to WiFi
Crash = 2, // the clock firmware crashed SAFE_MODE_CRASHES times in a row
Manual = 3, // Home Assistant button
PowerCycle = 4, // switched on POWER_CYCLES_FOR_RECOVERY times quickly
};
const char *recoveryReasonText(RecoveryReason reason);
// Power-on count: each power on within POWER_CYCLE_WINDOW of the previous one counts.
// True when the next start should be the recovery app.
bool powerCyclesRequestRecovery(uint8_t power_on_count);
// Recovery app: time to start the clock firmware again?
// Wifi reason: as soon as WiFi is connected again (but not before RECOVERY_MIN_TIME)
// other reasons: after RECOVERY_TIME (so a manual visit has time for an OTA upload)
// never when there is no clock firmware
bool recoveryShouldStartMain(RecoveryReason reason, uint32_t running_ms, bool wifi_connected,
bool main_available);
+5 -2
View File
@@ -13,6 +13,7 @@
#include "nano_link/nano_link.h"
#include "net/home_assistant.h"
#include "net/wifi_ota.h"
#include "system/boot_target.h"
#include "system/firmware_guard.h"
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
@@ -41,9 +42,11 @@ void setup()
if (boot_mode == BootMode::SafeMode)
{
// Crashed too often: only WiFi and OTA, then try the main code once more
otaOnlyMode(SAFE_MODE_TIME, LED_PIN);
// Crashed too often: the recovery app, or without it only WiFi and OTA here,
// then try the main code once more
firmwareGuardSafeModeEnded();
rebootToRecovery(RecoveryReason::Crash);
otaOnlyMode(SAFE_MODE_TIME, LED_PIN);
ESP.restart();
}
if (boot_mode == BootMode::PanicWindow)
+6
View File
@@ -6,6 +6,7 @@
#include "config.h"
#include "credentials.h"
#include "system/boot_target.h"
static WiFiClient client;
static HADevice device(HA_DEVICE_ID, sizeof(HA_DEVICE_ID));
@@ -15,6 +16,7 @@ static HAMqtt mqtt(client, device);
static HASwitch clockSwitch("FlipDotDisplayClock");
static HASwitch powerSwitch("FlipDotDisplayPower");
static HASensor firmwareSensor("FlipDotDisplayFirmware");
static HAButton recoveryButton("FlipDotDisplayRecovery");
static char firmware_status[64] = "";
static bool firmware_status_sent = false;
@@ -51,6 +53,10 @@ void homeAssistantBegin(DisplayController &controller)
powerSwitch.setCurrentState(controller.powerEnabled());
powerSwitch.onCommand(onPowerCommand);
recoveryButton.setName("FlipDot Display Recovery");
recoveryButton.setIcon("mdi:lifebuoy");
recoveryButton.onCommand([](HAButton *) { rebootToRecovery(RecoveryReason::Manual); });
firmwareSensor.setName("FlipDot Display Firmware");
firmwareSensor.setIcon("mdi:chip");
// The sensor doesn't resend its value on reconnect by itself
+2 -1
View File
@@ -5,7 +5,8 @@
// Home Assistant integration over MQTT (with discovery):
// FlipDot Display Clock switch: on = clock, off = Nintendo logo
// FlipDot Display Power switch: off = clear the display and switch it off
// FlipDot Display Firmware sensor: version and health, e.g. "2.1.0, ok, 0 crashes"
// FlipDot Display Firmware sensor: version and health, e.g. "2.1.0, ok, 0 crashes, ..."
// FlipDot Display Recovery button: restart into the recovery app (for 10 minutes)
void homeAssistantBegin(DisplayController &controller);
void homeAssistantLoop();
+11 -4
View File
@@ -5,7 +5,9 @@
#include "config.h"
#include "credentials.h"
#include "system/boot_target.h"
#include "system/firmware_guard.h"
#include "system/system_store.h"
static uint32_t wifi_lost_at = 0;
static bool wifi_lost = false;
@@ -15,11 +17,15 @@ void wifiBegin()
WiFi.setHostname(HOSTNAME);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.println("Connecting to WiFi");
String ssid, password;
loadWifiCredentials(ssid, password);
WiFi.begin(ssid.c_str(), password.c_str());
Serial.printf("Connecting to WiFi %s\n", ssid.c_str());
if (WiFi.waitForConnectResult(WIFI_CONNECT_TIMEOUT) != WL_CONNECTED)
{
Serial.println("WiFi connection failed, restarting");
// The recovery app keeps trying and offers a setup page to pick another WiFi
Serial.println("WiFi connection failed");
rebootToRecovery(RecoveryReason::Wifi);
ESP.restart();
}
Serial.print("Connected, IP ");
@@ -45,7 +51,8 @@ void wifiWatchdog()
}
else if (millis() - wifi_lost_at >= WIFI_RESTART_TIME)
{
Serial.println("WiFi down for too long, restarting");
Serial.println("WiFi down for too long");
rebootToRecovery(RecoveryReason::Wifi);
ESP.restart();
}
}
+4
View File
@@ -9,3 +9,7 @@
#define BROKER_ADDR IPAddress(192, 168, 1, 10)
#define MQTT_USER "mqtt-user"
#define MQTT_PASSWORD "mqtt-password"
// Password of the "FlipDot-Setup" WiFi that the recovery app opens when it can't connect
// (WPA2, at least 8 characters)
#define SETUP_AP_PASSWORD "change-me-please"
+92
View File
@@ -0,0 +1,92 @@
#include "system/boot_target.h"
#include <Arduino.h>
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include "system/system_store.h"
static const esp_partition_t *appPartition(esp_partition_subtype_t subtype)
{
return esp_partition_find_first(ESP_PARTITION_TYPE_APP, subtype, NULL);
}
// The partition starts with a firmware image (full check happens in esp_ota_set_boot_partition)
static bool holdsFirmware(const esp_partition_t *partition)
{
esp_app_desc_t desc;
return partition && esp_ota_get_partition_description(partition, &desc) == ESP_OK;
}
bool recoveryAvailable()
{
return holdsFirmware(appPartition(ESP_PARTITION_SUBTYPE_APP_FACTORY));
}
void rebootToRecovery(RecoveryReason reason)
{
const esp_partition_t *recovery = appPartition(ESP_PARTITION_SUBTYPE_APP_FACTORY);
if (!holdsFirmware(recovery))
{
Serial.println("No recovery app on this flash");
return;
}
Serial.printf("Restarting into the recovery app: %s\n", recoveryReasonText(reason));
setRecoveryReason(reason);
// Selecting factory clears otadata; the recovery app sets the clock firmware again later
if (esp_ota_set_boot_partition(recovery) != ESP_OK)
{
Serial.println("Could not select the recovery app");
return;
}
delay(100);
ESP.restart();
}
const char *runningPartitionLabel()
{
return esp_ota_get_running_partition()->label;
}
bool mainFirmwareAvailable()
{
return holdsFirmware(appPartition(ESP_PARTITION_SUBTYPE_APP_OTA_0)) ||
holdsFirmware(appPartition(ESP_PARTITION_SUBTYPE_APP_OTA_1));
}
static bool startSlot(const esp_partition_t *partition)
{
if (!holdsFirmware(partition))
return false;
// Verifies the whole image; fails for a broken or half written firmware
if (esp_ota_set_boot_partition(partition) != ESP_OK)
return false;
Serial.printf("Starting the clock firmware in %s\n", partition->label);
delay(100);
ESP.restart();
return true;
}
void startMainFirmware()
{
const esp_partition_t *ota0 = appPartition(ESP_PARTITION_SUBTYPE_APP_OTA_0);
const esp_partition_t *ota1 = appPartition(ESP_PARTITION_SUBTYPE_APP_OTA_1);
bool last_was_ota1 = loadMainSlot() == "ota_1";
if (last_was_ota1)
startSlot(ota1) || startSlot(ota0);
else
startSlot(ota0) || startSlot(ota1);
Serial.println("No valid clock firmware found");
}
String slotDescription(const char *label)
{
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, label);
if (!holdsFirmware(partition))
return "empty";
// The app description holds the Arduino core's version, so use the one the clock
// firmware stored when it started
String version = loadSlotVersion(label);
return version.isEmpty() ? "firmware (version unknown)" : "version " + version;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <Arduino.h>
#include "logic/boot_policy.h"
// Switching between the clock firmware (ota_0 / ota_1) and the recovery app (factory).
// See partitions.csv.
// The flash has a recovery app (only with partitions.csv, after the one-time USB install)
bool recoveryAvailable();
// Restart into the recovery app. Returns only when there is no recovery app.
void rebootToRecovery(RecoveryReason reason);
// Label of the partition this firmware runs from ("factory", "ota_0", "ota_1")
const char *runningPartitionLabel();
// Recovery app: an ota slot holds a clock firmware
bool mainFirmwareAvailable();
// Recovery app: restart into the clock firmware (the last one that ran, else the other
// slot). Returns only when no slot holds a valid firmware.
void startMainFirmware();
// The firmware in an ota slot for the recovery page, e.g. "version 2.1.0" or "empty"
String slotDescription(const char *label);
+38 -4
View File
@@ -6,6 +6,8 @@
#include <esp_task_wdt.h>
#include "config.h"
#include "system/boot_target.h"
#include "system/system_store.h"
// Survives crashes and software resets, not a power loss
static const uint32_t RTC_MAGIC = 0xF11D0C0A;
@@ -16,7 +18,8 @@ static uint32_t main_start = 0; // millis() when the main code started
static bool watchdog_running = false;
static bool firmware_pending = false; // new OTA firmware, not yet marked as good
static bool crash_count_cleared = false;
static char status[64];
static bool power_on_count_cleared = false;
static char status[80];
// The Arduino core marks a new OTA firmware as good right at boot. Returning true here
// leaves that to us, so the bootloader can roll back a firmware that fails early.
@@ -70,16 +73,40 @@ static const char *firmwareState()
static void updateStatus()
{
snprintf(status, sizeof(status), "%s, %s, %u crashes", FIRMWARE_VERSION, firmwareState(),
rtc_magic == RTC_MAGIC ? rtc_crash_count : 0);
snprintf(status, sizeof(status), "%s, %s, %u crashes, %s", FIRMWARE_VERSION, firmwareState(),
rtc_magic == RTC_MAGIC ? rtc_crash_count : 0,
recoveryAvailable() ? "recovery ok" : "no recovery app");
}
// Switching the power on and off quickly a few times starts the recovery app. Checked
// before anything else, so it works even when the rest of this firmware is broken.
static void checkPowerCycles(ResetKind reset)
{
if (reset != ResetKind::PowerOn)
return;
uint8_t count = loadPowerOnCount() + 1;
if (powerCyclesRequestRecovery(count) && recoveryAvailable())
{
savePowerOnCount(0);
rebootToRecovery(RecoveryReason::PowerCycle);
}
savePowerOnCount(count);
}
BootMode firmwareGuardBegin()
{
ResetKind reset = resetKind();
checkPowerCycles(reset);
uint8_t count = rtc_magic == RTC_MAGIC ? rtc_crash_count : 0;
BootDecision decision = decideBoot(resetKind(), count);
BootDecision decision = decideBoot(reset, count);
setCrashCount(decision.crash_count);
// Tells the recovery app which slot to start and which version is in it
const char *label = runningPartitionLabel();
if (strncmp(label, "ota_", 4) == 0)
saveMainSlot(label, FIRMWARE_VERSION);
esp_ota_img_states_t state;
firmware_pending = esp_ota_get_state_partition(esp_ota_get_running_partition(), &state) == ESP_OK &&
state == ESP_OTA_IMG_PENDING_VERIFY;
@@ -126,6 +153,13 @@ void firmwareGuardLoop(bool wifi_connected)
Serial.println("New firmware marked as good");
}
// Running long enough after a power on: it was not a quick power cycle
if (!power_on_count_cleared && millis() >= POWER_CYCLE_WINDOW)
{
savePowerOnCount(0);
power_on_count_cleared = true;
}
if (!crash_count_cleared && shouldClearCrashCount(running))
{
setCrashCount(0);
+7 -4
View File
@@ -10,10 +10,13 @@
// - Watchdog: loop() must keep running; a hang longer than WATCHDOG_TIMEOUT_S restarts
// the ESP32 (and counts as a crash).
// - Crash counter (kept in RTC memory over resets): after a crash the ESP32 first runs
// only OTA for a while, after SAFE_MODE_CRASHES crashes in a row it goes to safe mode.
// The decisions are in logic/boot_policy.
// only OTA for a while, after SAFE_MODE_CRASHES crashes in a row it goes to safe mode
// (the recovery app). The decisions are in logic/boot_policy.
// - Power cycles: switching the power on POWER_CYCLES_FOR_RECOVERY times, each time for
// less than POWER_CYCLE_WINDOW, starts the recovery app.
// Call first in setup(): reads the reset reason and updates the crash counter
// Call first in setup(): reads the reset reason, updates the crash counter, and restarts
// into the recovery app after quick power cycles
BootMode firmwareGuardBegin();
// Safe mode is over: allow one more try of the main code
@@ -28,5 +31,5 @@ void firmwareGuardStopWatchdog();
// Call every loop(): feeds the watchdog, marks a new firmware as good, clears the counter
void firmwareGuardLoop(bool wifi_connected);
// Short status for Home Assistant, e.g. "2.1.0, ok, 0 crashes"
// Short status for Home Assistant, e.g. "2.1.0, ok, 0 crashes, recovery ok"
const char *firmwareGuardStatus();
+98
View File
@@ -0,0 +1,98 @@
#include "system/system_store.h"
#include <Preferences.h>
#include "credentials.h"
static const char *NAMESPACE = "flipdot";
void loadWifiCredentials(String &ssid, String &password)
{
Preferences prefs;
prefs.begin(NAMESPACE, true);
ssid = prefs.getString("wifi_ssid", "");
password = prefs.getString("wifi_pass", "");
prefs.end();
if (ssid.isEmpty())
{
ssid = WIFI_SSID;
password = WIFI_PASSWORD;
}
}
void saveWifiCredentials(const String &ssid, const String &password)
{
Preferences prefs;
prefs.begin(NAMESPACE, false);
prefs.putString("wifi_ssid", ssid);
prefs.putString("wifi_pass", password);
prefs.end();
}
void setRecoveryReason(RecoveryReason reason)
{
Preferences prefs;
prefs.begin(NAMESPACE, false);
prefs.putUChar("rec_reason", (uint8_t)reason);
prefs.end();
}
RecoveryReason takeRecoveryReason()
{
Preferences prefs;
prefs.begin(NAMESPACE, false);
uint8_t reason = prefs.getUChar("rec_reason", 0);
if (reason)
prefs.putUChar("rec_reason", 0);
prefs.end();
return reason <= (uint8_t)RecoveryReason::PowerCycle ? (RecoveryReason)reason : RecoveryReason::None;
}
void saveMainSlot(const char *label, const char *version)
{
String version_key = String("ver_") + label;
Preferences prefs;
prefs.begin(NAMESPACE, false);
if (prefs.getString("main_slot", "") != label)
prefs.putString("main_slot", label);
if (prefs.getString(version_key.c_str(), "") != version)
prefs.putString(version_key.c_str(), version);
prefs.end();
}
String loadMainSlot()
{
Preferences prefs;
prefs.begin(NAMESPACE, true);
String label = prefs.getString("main_slot", "");
prefs.end();
return label;
}
String loadSlotVersion(const char *label)
{
String version_key = String("ver_") + label;
Preferences prefs;
prefs.begin(NAMESPACE, true);
String version = prefs.getString(version_key.c_str(), "");
prefs.end();
return version;
}
uint8_t loadPowerOnCount()
{
Preferences prefs;
prefs.begin(NAMESPACE, true);
uint8_t count = prefs.getUChar("power_ons", 0);
prefs.end();
return count;
}
void savePowerOnCount(uint8_t count)
{
Preferences prefs;
prefs.begin(NAMESPACE, false);
if (prefs.getUChar("power_ons", 0) != count)
prefs.putUChar("power_ons", count);
prefs.end();
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <Arduino.h>
#include "logic/boot_policy.h"
// Settings in NVS, shared by the clock firmware and the recovery app (both read the same
// NVS partition). Survives power loss and firmware updates.
// WiFi set in the recovery web page, or WIFI_SSID / WIFI_PASSWORD from secrets.h
void loadWifiCredentials(String &ssid, String &password);
void saveWifiCredentials(const String &ssid, const String &password);
// Why the recovery app is started; take...() reads and clears it
void setRecoveryReason(RecoveryReason reason);
RecoveryReason takeRecoveryReason();
// The clock firmware that started last: its partition label ("ota_0" / "ota_1") and
// version, stored per slot for the recovery page
void saveMainSlot(const char *label, const char *version);
String loadMainSlot();
String loadSlotVersion(const char *label);
// Quick power ons in a row (see powerCyclesRequestRecovery)
uint8_t loadPowerOnCount();
void savePowerOnCount(uint8_t count);