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

+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();
}
}