2026-10-13 01:21:04 +02:00
|
|
|
// Recovery app (factory partition): WiFi setup portal and OTA, nothing else. Started when
|
|
|
|
|
// the clock firmware can't connect to WiFi, crashes repeatedly, after quick power cycles,
|
|
|
|
|
// from the Home Assistant button, or by the bootloader when no clock firmware works.
|
2026-10-13 01:34:04 +02:00
|
|
|
// It only shows short status messages on the display. See README.md "Recovery app".
|
2026-10-13 01:21:04 +02:00
|
|
|
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <ArduinoOTA.h>
|
|
|
|
|
#include <WiFi.h>
|
|
|
|
|
#include <esp_task_wdt.h>
|
|
|
|
|
|
2026-10-13 01:34:04 +02:00
|
|
|
#include "BROSE9323.h"
|
2026-10-13 01:21:04 +02:00
|
|
|
#include "config.h"
|
|
|
|
|
#include "credentials.h"
|
2026-10-13 01:34:04 +02:00
|
|
|
#include "display/status_screen.h"
|
2026-10-13 01:21:04 +02:00
|
|
|
#include "logic/boot_policy.h"
|
2026-10-13 01:34:04 +02:00
|
|
|
#include "nano_link/nano_link.h"
|
2026-10-13 01:21:04 +02:00
|
|
|
#include "recovery_state.h"
|
|
|
|
|
#include "system/boot_target.h"
|
|
|
|
|
#include "system/system_store.h"
|
|
|
|
|
#include "web_portal.h"
|
|
|
|
|
|
|
|
|
|
// Retry the stored WiFi this often while it is not connected
|
|
|
|
|
static const uint32_t STATION_RETRY_TIME = 30000;
|
|
|
|
|
// Close the setup AP this long after the stored WiFi works again
|
|
|
|
|
static const uint32_t AP_CLOSE_DELAY = 120000;
|
|
|
|
|
static const uint32_t RECOVERY_WATCHDOG_S = 60;
|
2026-10-13 01:34:04 +02:00
|
|
|
// Show a new status at most this often (each message power cycles the display)
|
|
|
|
|
static const uint32_t STATUS_MIN_INTERVAL = 30000;
|
2026-10-13 01:21:04 +02:00
|
|
|
|
|
|
|
|
RecoveryState recovery;
|
|
|
|
|
|
2026-10-13 01:34:04 +02:00
|
|
|
static NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
|
|
|
|
static BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
|
|
|
|
static StatusScreen status(display, nano);
|
|
|
|
|
|
|
|
|
|
enum class Shown
|
|
|
|
|
{
|
|
|
|
|
Nothing,
|
|
|
|
|
Started, // "Recovery" + reason
|
|
|
|
|
SetupWifi, // name of the setup WiFi
|
|
|
|
|
Connected, // IP address
|
|
|
|
|
};
|
|
|
|
|
static Shown shown = Shown::Nothing;
|
|
|
|
|
static uint32_t last_status = 0;
|
|
|
|
|
|
2026-10-13 01:21:04 +02:00
|
|
|
static uint32_t station_lost_since = 0;
|
|
|
|
|
static uint32_t station_connected_since = 0;
|
|
|
|
|
static bool watchdog_running = false;
|
|
|
|
|
static bool main_available = false;
|
|
|
|
|
|
|
|
|
|
void connectStation()
|
|
|
|
|
{
|
|
|
|
|
String ssid, password;
|
|
|
|
|
loadWifiCredentials(ssid, password);
|
|
|
|
|
Serial.printf("Connecting to WiFi %s\n", ssid.c_str());
|
|
|
|
|
WiFi.disconnect();
|
|
|
|
|
WiFi.begin(ssid.c_str(), password.c_str());
|
|
|
|
|
recovery.station_attempt = millis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void openAccessPoint()
|
|
|
|
|
{
|
|
|
|
|
WiFi.mode(WIFI_AP_STA);
|
|
|
|
|
WiFi.softAP(SETUP_AP_SSID, SETUP_AP_PASSWORD);
|
|
|
|
|
portalStartCaptiveDns();
|
|
|
|
|
recovery.ap_running = true;
|
|
|
|
|
Serial.printf("Setup WiFi \"%s\" open at %s\n", SETUP_AP_SSID, WiFi.softAPIP().toString().c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void closeAccessPoint()
|
|
|
|
|
{
|
|
|
|
|
portalStopCaptiveDns();
|
|
|
|
|
WiFi.softAPdisconnect(true);
|
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
|
recovery.ap_running = false;
|
|
|
|
|
Serial.println("Setup WiFi closed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void updateWifi()
|
|
|
|
|
{
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
if (WiFi.status() == WL_CONNECTED)
|
|
|
|
|
{
|
|
|
|
|
station_lost_since = now;
|
|
|
|
|
if (recovery.ap_running && now - station_connected_since >= AP_CLOSE_DELAY &&
|
|
|
|
|
WiFi.softAPgetStationNum() == 0)
|
|
|
|
|
closeAccessPoint();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
station_connected_since = now;
|
|
|
|
|
|
|
|
|
|
if (!recovery.ap_running && now - station_lost_since >= SETUP_AP_DELAY)
|
|
|
|
|
openAccessPoint();
|
|
|
|
|
|
|
|
|
|
// Connection attempts disturb the access point, so don't retry while someone uses it
|
|
|
|
|
bool ap_in_use = recovery.ap_running && WiFi.softAPgetStationNum() > 0;
|
|
|
|
|
if (!ap_in_use && now - recovery.station_attempt >= STATION_RETRY_TIME)
|
|
|
|
|
connectStation();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:34:04 +02:00
|
|
|
// Show what is going on, so you know what to do without a computer. The display is
|
|
|
|
|
// switched off after each message; the dots keep showing it.
|
|
|
|
|
static void updateDisplay()
|
|
|
|
|
{
|
|
|
|
|
Shown wanted = WiFi.status() == WL_CONNECTED ? Shown::Connected
|
|
|
|
|
: recovery.ap_running ? Shown::SetupWifi
|
|
|
|
|
: Shown::Started;
|
|
|
|
|
if (wanted == shown || (shown != Shown::Nothing && millis() - last_status < STATUS_MIN_INTERVAL))
|
|
|
|
|
return;
|
|
|
|
|
shown = wanted;
|
|
|
|
|
last_status = millis();
|
|
|
|
|
|
|
|
|
|
switch (wanted)
|
|
|
|
|
{
|
|
|
|
|
case Shown::Connected:
|
|
|
|
|
status.show("Recovery", WiFi.localIP().toString().c_str());
|
|
|
|
|
break;
|
|
|
|
|
case Shown::SetupWifi:
|
|
|
|
|
status.show("WiFi setup:", SETUP_AP_SSID);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
status.show("Recovery", recoveryReasonShort(recovery.reason));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 01:21:04 +02:00
|
|
|
// Double blink: recovery app is running
|
|
|
|
|
static void updateLed()
|
|
|
|
|
{
|
|
|
|
|
uint32_t t = millis() % 1500;
|
|
|
|
|
digitalWrite(LED_PIN, t < 100 || (t >= 250 && t < 350));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setup()
|
|
|
|
|
{
|
|
|
|
|
Serial.begin(115200);
|
|
|
|
|
Serial.println("FlipDot recovery app");
|
|
|
|
|
|
|
|
|
|
pinMode(LED_PIN, OUTPUT);
|
2026-10-13 01:34:04 +02:00
|
|
|
nano.begin(); // display off between status messages
|
2026-10-13 01:21:04 +02:00
|
|
|
|
|
|
|
|
recovery.reason = takeRecoveryReason();
|
|
|
|
|
main_available = mainFirmwareAvailable();
|
|
|
|
|
Serial.printf("Reason: %s\n", recoveryReasonText(recovery.reason));
|
|
|
|
|
|
|
|
|
|
WiFi.persistent(false);
|
|
|
|
|
WiFi.setAutoReconnect(false); // retries are done in updateWifi()
|
|
|
|
|
WiFi.setHostname(HOSTNAME);
|
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
|
connectStation();
|
|
|
|
|
|
|
|
|
|
ArduinoOTA.setHostname(HOSTNAME);
|
|
|
|
|
ArduinoOTA.onStart([]() {
|
|
|
|
|
// The update blocks loop() until it is done
|
|
|
|
|
if (watchdog_running)
|
|
|
|
|
esp_task_wdt_delete(NULL);
|
|
|
|
|
watchdog_running = false;
|
|
|
|
|
Serial.println("OTA update started");
|
|
|
|
|
});
|
|
|
|
|
ArduinoOTA.onEnd([]() { Serial.println("OTA update done, starting the new firmware"); });
|
|
|
|
|
ArduinoOTA.onError([](ota_error_t error) {
|
|
|
|
|
Serial.printf("OTA error %u, restarting\n", error);
|
|
|
|
|
ESP.restart();
|
|
|
|
|
});
|
|
|
|
|
ArduinoOTA.begin();
|
|
|
|
|
|
|
|
|
|
portalBegin();
|
2026-10-13 01:34:04 +02:00
|
|
|
updateDisplay();
|
2026-10-13 01:21:04 +02:00
|
|
|
|
|
|
|
|
// A hang restarts the ESP32; the bootloader then starts this recovery app again
|
|
|
|
|
esp_task_wdt_init(RECOVERY_WATCHDOG_S, true);
|
|
|
|
|
esp_task_wdt_add(NULL);
|
|
|
|
|
watchdog_running = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop()
|
|
|
|
|
{
|
|
|
|
|
if (watchdog_running)
|
|
|
|
|
esp_task_wdt_reset();
|
|
|
|
|
|
|
|
|
|
ArduinoOTA.handle();
|
|
|
|
|
portalLoop();
|
|
|
|
|
updateWifi();
|
2026-10-13 01:34:04 +02:00
|
|
|
updateDisplay();
|
2026-10-13 01:21:04 +02:00
|
|
|
updateLed();
|
|
|
|
|
|
|
|
|
|
bool wifi_connected = WiFi.status() == WL_CONNECTED;
|
|
|
|
|
bool automatic = !recovery.stay &&
|
|
|
|
|
recoveryShouldStartMain(recovery.reason, millis(), wifi_connected, main_available);
|
|
|
|
|
if (recovery.start_main || automatic)
|
|
|
|
|
{
|
|
|
|
|
recovery.start_main = false;
|
|
|
|
|
startMainFirmware(); // returns only when no slot holds a valid firmware
|
|
|
|
|
recovery.stay = true;
|
|
|
|
|
}
|
|
|
|
|
delay(2);
|
|
|
|
|
}
|