// 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. // It only shows short status messages on the display. See README.md "Recovery app". #include #include #include #include #include "BROSE9323.h" #include "config.h" #include "credentials.h" #include "display/status_screen.h" #include "logic/boot_policy.h" #include "nano_link/nano_link.h" #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; // Show a new status at most this often (each message power cycles the display) static const uint32_t STATUS_MIN_INTERVAL = 30000; RecoveryState recovery; 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; 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(); } // 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; } } // 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); nano.begin(); // display off between status messages 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(); updateDisplay(); // 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(); updateDisplay(); 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); }