Public Access
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:
1 parent
ddbdf7e65f
commit
4749f9e596
25 files changed
+979
-29
No files matched your search
+13
-6
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user