From c9e0800e363618e46951d6b826f291244ecdf8f4 Mon Sep 17 00:00:00 2001 From: MangaValk Date: Tue, 13 Oct 2026 01:34:04 +0200 Subject: [PATCH] Show status messages on the display - Clock firmware: 'Connecting to WiFi...' and 'Getting the time...' at boot, 'Crashed!' in the OTA-only modes, and 'No WiFi' with the time when WiFi is down for over a minute (new DisplayController screen) - Recovery app: shows its reason, the setup WiFi name, or its IP address, then powers the display off (flipdots keep the image) - StatusScreen and drawTextLines shared by both apps - Driver: ESP begin() no longer draws 'ESP Connected' - Tests: No WiFi screen, short reason texts (70 total) Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 9 ++-- README.md | 24 +++++++-- lib/BROSE9323/BROSE9323.cpp | 5 +- platformio.ini | 7 ++- src/esp32/README.md | 9 +++- src/esp32/config.h | 5 ++ src/esp32/display/frames.cpp | 19 ++++--- src/esp32/display/frames.h | 4 +- src/esp32/display/status_screen.cpp | 31 +++++++++++ src/esp32/display/status_screen.h | 21 ++++++++ src/esp32/display/text.cpp | 33 ++++++++++++ src/esp32/display/text.h | 10 ++++ src/esp32/logic/boot_policy.cpp | 18 +++++++ src/esp32/logic/boot_policy.h | 2 + src/esp32/logic/display_controller.cpp | 25 ++++++--- src/esp32/logic/display_controller.h | 11 +++- src/esp32/main.cpp | 21 +++++--- src/recovery/main.cpp | 54 +++++++++++++++++-- test/test_esp32/test_main.cpp | 72 +++++++++++++++++++++++++- 19 files changed, 332 insertions(+), 48 deletions(-) create mode 100644 src/esp32/display/status_screen.cpp create mode 100644 src/esp32/display/status_screen.h create mode 100644 src/esp32/display/text.cpp create mode 100644 src/esp32/display/text.h diff --git a/CLAUDE.md b/CLAUDE.md index dcb6b5a..272216e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,9 +110,12 @@ See README.md for the overview. USB only), ota_0/ota_1 = clock firmware. `esp32_usb` uploads to 0x110000 (`board_upload.offset_address`) so it never overwrites the recovery app. Check with `pio run -e -t envdump` (`ESP32_APP_OFFSET`) after changing upload settings. -- The recovery app compiles `esp32/logic/boot_policy.cpp`, `esp32/system/system_store.cpp` - and `esp32/system/boot_target.cpp` (see its `build_src_filter`). Keep those free of - clock-firmware dependencies. Both apps share NVS namespace `flipdot`. RTC memory is +- The recovery app compiles `esp32/logic/boot_policy.cpp`, `esp32/system/system_store.cpp`, + `esp32/system/boot_target.cpp`, `esp32/nano_link/nano_link.cpp`, + `esp32/display/status_screen.cpp` and `esp32/display/text.cpp` (see its + `build_src_filter`). Keep those free of clock-firmware dependencies. +- Status messages (`StatusScreen`) are at most 14 characters per line, 2 lines. The + recovery app powers the display off after each message (flipdots keep the image). Both apps share NVS namespace `flipdot`. RTC memory is not shared between the apps (different layouts). - `rebootToRecovery()` selects the factory partition, which erases otadata. The recovery app later selects the clock slot again (`startMainFirmware()`), so the clock firmware diff --git a/README.md b/README.md index bdaec6d..9c705a5 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,8 @@ even if the uploader is interrupted. The ESP32 has a small second app, the **recovery app** ([src/recovery](src/recovery)). It has its own place on the flash and is never changed by OTA updates. It contains only WiFi, a setup web page and OTA, so the ESP32 can always be reached, even when the clock firmware -is broken. The display stays off while it runs, and the built-in LED double-blinks. +is broken. It shows what is going on as a short message on the display, and the built-in +LED double-blinks. ``` flash (4 MB): bootloader | nvs | otadata | factory: recovery app | ota_0: clock | ota_1: clock @@ -182,6 +183,22 @@ flash (4 MB): bootloader | nvs | otadata | factory: recovery app | ota_0: clock The setup page can also start the clock right away, or keep the recovery app running ("Stay in recovery"). +**What the display shows** + +| Message | Meaning | +|---------|---------| +| `Connecting` / `to WiFi...` | The clock firmware is starting | +| `Getting` / `the time...` | Connected, waiting for the time from the internet (NTP) | +| `No WiFi` / `12:34` | The clock firmware lost WiFi for more than a minute; the time keeps running | +| `Crashed!` / `OTA open 15s` | The clock firmware crashed and waits 15 s for an OTA update before it starts again | +| `Crashed 3x` / `OTA only` | Repeated crashes without the recovery app installed: only OTA for 10 minutes | +| `Recovery` / reason | The recovery app started (`no WiFi`, `crashed`, `requested`, `power cycled`, `no firmware`) | +| `WiFi setup:` / `FlipDot-Setup` | Connect to this WiFi to choose another network, see below | +| `Recovery` / IP address | The recovery app is connected; open this address for the setup page | + +The recovery app switches the display off after each message; the dots keep showing it +without power. + **WiFi setup.** The recovery app keeps trying the stored WiFi. If that fails for 20 s, it also opens its own WiFi: **FlipDot-Setup**, with the password `SETUP_AP_PASSWORD` from `src/esp32/secrets.h`. Connect a phone to it; the setup page opens by itself (otherwise go @@ -209,8 +226,9 @@ anyway. pio run -e recovery -t erase pio run -e recovery -t upload ``` - The recovery app starts (LED double-blinks), connects to the WiFi from `secrets.h`, and - waits, because there is no clock firmware yet. + The recovery app starts (LED double-blinks, the display shows `Recovery` / + `no firmware`), connects to the WiFi from `secrets.h` and shows its IP address. It waits + there, because there is no clock firmware yet. 3. Install the clock firmware **over WiFi**. This also tests that the recovery app works: ```sh pio run -e esp32 -t upload diff --git a/lib/BROSE9323/BROSE9323.cpp b/lib/BROSE9323/BROSE9323.cpp index 47c2b34..57d5fdc 100644 --- a/lib/BROSE9323/BROSE9323.cpp +++ b/lib/BROSE9323/BROSE9323.cpp @@ -18,13 +18,10 @@ BROSE9323::BROSE9323(uint8_t w, uint8_t h, uint8_t pw, uint16_t ft) : Adafruit_G } #ifdef ESPSERIAL +// Only sets the serial port; the first frame is up to the caller void BROSE9323::begin(Stream *s) { stream = s; - fillScreen(0); - setCursor(0, 4); - print("ESP Connected"); - display(); } #else void BROSE9323::begin(void) diff --git a/platformio.ini b/platformio.ini index 962f280..027cac2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -94,8 +94,11 @@ build_src_filter = + + + -build_flags = -I src/esp32 -lib_deps = + + + + + + +; ESPSERIAL: status messages are sent to the Nano like in the clock firmware +build_flags = -D ESPSERIAL -I src/esp32 upload_protocol = esptool upload_port = COM11 diff --git a/src/esp32/README.md b/src/esp32/README.md index dd0c7b1..67b1260 100644 --- a/src/esp32/README.md +++ b/src/esp32/README.md @@ -25,7 +25,9 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be | `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/frames.*` | The screens of the clock: clock, "No WiFi", logo, blank (`FlipdotFrames`) | +| `display/status_screen.*` | Two-line status messages at boot, after a crash and in the recovery app (`StatusScreen`) | +| `display/text.*` | Centered text lines on the 84 x 19 display | | `display/bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) | | **`nano_link/`** | **Connection to the Nano** | | `nano_link/nano_link.*` | Relay and serial link: power on (waits for `READY`), power off, bootloader start (`NanoLink`) | @@ -74,6 +76,11 @@ Rules for changing the code: ## Features +- **Status on the display:** `Connecting to WiFi...` and `Getting the time...` at boot, + `Crashed!` messages in the OTA-only modes, and `No WiFi` with the time when WiFi is down + for more than a minute. See the table in the + [main README](../../README.md#recovery-app). + - **Frames only when something changes:** a new frame is sent when the minute changes, the screen changes (clock, logo, blank) or the Nano was just powered on. The serial line is quiet in between, which also gives the Nano time for its periodic full redraw. diff --git a/src/esp32/config.h b/src/esp32/config.h index 03882ae..a8f9f7c 100644 --- a/src/esp32/config.h +++ b/src/esp32/config.h @@ -36,6 +36,11 @@ const uint32_t NANO_BOOT_TIMEOUT = 3000; // After a frame is cleared, wait this long before cutting the power, so the dots can flip const uint32_t CLEAR_BEFORE_OFF_TIME = 1000; +// Status messages (boot, crash, recovery): time for the Nano to flip all dots before the +// power is cut again. The message stays visible without power. +const uint32_t STATUS_FLIP_TIME = 2000; +// WiFi down this long while the clock runs: show "No WiFi" with the time instead +const uint32_t NO_WIFI_SHOW_TIME = 60000; // Power off this long after the display is no longer needed (night, standby, logo) const uint32_t POWER_OFF_DELAY = 30000; diff --git a/src/esp32/display/frames.cpp b/src/esp32/display/frames.cpp index f3accfd..113247f 100644 --- a/src/esp32/display/frames.cpp +++ b/src/esp32/display/frames.cpp @@ -3,6 +3,7 @@ #include #include "display/bitmaps.h" +#include "display/text.h" #include "logic/schedule.h" // Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display @@ -31,6 +32,14 @@ void FlipdotFrames::clock(int hour, int minute) _display.display(); } +void FlipdotFrames::noWifi(int hour, int minute) +{ + char time[6]; + snprintf(time, sizeof(time), "%02d:%02d", hour, minute); + drawTextLines(_display, "No WiFi", time); + _display.display(); +} + void FlipdotFrames::logo() { _display.fillScreen(0); @@ -43,13 +52,3 @@ void FlipdotFrames::blank() _display.fillScreen(0); _display.display(); } - -void FlipdotFrames::message(const char *text) -{ - _display.fillScreen(0); - _display.setTextSize(1); - _display.setTextWrap(true); - _display.setCursor(0, 0); - _display.print(text); - _display.display(); -} diff --git a/src/esp32/display/frames.h b/src/esp32/display/frames.h index 8eb3f67..27001e6 100644 --- a/src/esp32/display/frames.h +++ b/src/esp32/display/frames.h @@ -11,11 +11,11 @@ public: // Pikachu with a thought bubble showing HH:MM (yawning in the evening) void clock(int hour, int minute) override; + // "No WiFi" with the time below it + void noWifi(int hour, int minute) override; // Nintendo logo void logo() override; void blank() override; - // A line of text, e.g. while connecting - void message(const char *text); private: BROSE9323 &_display; diff --git a/src/esp32/display/status_screen.cpp b/src/esp32/display/status_screen.cpp new file mode 100644 index 0000000..62ba788 --- /dev/null +++ b/src/esp32/display/status_screen.cpp @@ -0,0 +1,31 @@ +#include "display/status_screen.h" + +#include + +#include "config.h" +#include "display/text.h" + +StatusScreen::StatusScreen(BROSE9323 &display, NanoLink &nano) : _display(display), _nano(nano) +{ +} + +void StatusScreen::show(const char *line1, const char *line2, bool keep_on) +{ + Serial.printf("Display: %s / %s\n", line1, line2 ? line2 : ""); + _nano.on(); + if (!_started) + { + _display.begin(&_nano.stream()); + _display.setTiming(FLIP_TIME); + _started = true; + } + drawTextLines(_display, line1, line2); + _display.display(); + + if (!keep_on) + { + // The first frame after power on flips every dot, give the Nano time for that + delay(STATUS_FLIP_TIME); + _nano.off(); + } +} diff --git a/src/esp32/display/status_screen.h b/src/esp32/display/status_screen.h new file mode 100644 index 0000000..5e2dab8 --- /dev/null +++ b/src/esp32/display/status_screen.h @@ -0,0 +1,21 @@ +#pragma once + +#include "BROSE9323.h" +#include "nano_link/nano_link.h" + +// Short messages for situations outside the normal clock: booting, crashes, recovery app. +// Used by both the clock firmware and the recovery app. +class StatusScreen +{ +public: + StatusScreen(BROSE9323 &display, NanoLink &nano); + + // Power the display on, show up to two lines (TEXT_LINE_CHARS each). Unless keep_on, + // power off again once the dots have flipped: flipdots keep their image without power. + void show(const char *line1, const char *line2, bool keep_on = false); + +private: + BROSE9323 &_display; + NanoLink &_nano; + bool _started = false; +}; diff --git a/src/esp32/display/text.cpp b/src/esp32/display/text.cpp new file mode 100644 index 0000000..0193f04 --- /dev/null +++ b/src/esp32/display/text.cpp @@ -0,0 +1,33 @@ +#include "display/text.h" + +#include + +static const int CHAR_WIDTH = 6; + +static void drawCentered(BROSE9323 &display, const char *text, int y) +{ + int len = strlen(text); + if (len > TEXT_LINE_CHARS) + len = TEXT_LINE_CHARS; + char line[TEXT_LINE_CHARS + 1]; + memcpy(line, text, len); + line[len] = '\0'; + + display.setCursor((display.width() - len * CHAR_WIDTH) / 2, y); + display.print(line); +} + +void drawTextLines(BROSE9323 &display, const char *line1, const char *line2) +{ + display.fillScreen(0); + display.setTextSize(1); + display.setTextWrap(false); + display.setTextColor(1); + if (line2 == nullptr || line2[0] == '\0') + { + drawCentered(display, line1, 6); // one line: vertically centered + return; + } + drawCentered(display, line1, 1); + drawCentered(display, line2, 10); +} diff --git a/src/esp32/display/text.h b/src/esp32/display/text.h new file mode 100644 index 0000000..d96315e --- /dev/null +++ b/src/esp32/display/text.h @@ -0,0 +1,10 @@ +#pragma once + +#include "BROSE9323.h" + +// Characters per line with the built-in 6x8 font on the 84 pixel wide display +const int TEXT_LINE_CHARS = 14; + +// Clear the screen and draw up to two centered lines of text (line2 may be empty). +// Does not call display(). +void drawTextLines(BROSE9323 &display, const char *line1, const char *line2); diff --git a/src/esp32/logic/boot_policy.cpp b/src/esp32/logic/boot_policy.cpp index 3da44de..eab2bab 100644 --- a/src/esp32/logic/boot_policy.cpp +++ b/src/esp32/logic/boot_policy.cpp @@ -54,6 +54,24 @@ const char *recoveryReasonText(RecoveryReason reason) } } +const char *recoveryReasonShort(RecoveryReason reason) +{ + switch (reason) + { + case RecoveryReason::Wifi: + return "no WiFi"; + case RecoveryReason::Crash: + return "crashed"; + case RecoveryReason::Manual: + return "requested"; + case RecoveryReason::PowerCycle: + return "power cycled"; + case RecoveryReason::None: + default: + return "no firmware"; + } +} + bool powerCyclesRequestRecovery(uint8_t power_on_count) { return power_on_count >= POWER_CYCLES_FOR_RECOVERY; diff --git a/src/esp32/logic/boot_policy.h b/src/esp32/logic/boot_policy.h index ae77931..7b5c8b2 100644 --- a/src/esp32/logic/boot_policy.h +++ b/src/esp32/logic/boot_policy.h @@ -59,6 +59,8 @@ enum class RecoveryReason : uint8_t }; const char *recoveryReasonText(RecoveryReason reason); +// At most 14 characters, for the flipdot display +const char *recoveryReasonShort(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. diff --git a/src/esp32/logic/display_controller.cpp b/src/esp32/logic/display_controller.cpp index e2614d1..7185fad 100644 --- a/src/esp32/logic/display_controller.cpp +++ b/src/esp32/logic/display_controller.cpp @@ -27,8 +27,17 @@ bool DisplayController::powerEnabled() const return _power_enabled; } -void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute) +void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected) { + if (wifi_connected) + _wifi_lost = false; + else if (!_wifi_lost) + { + _wifi_lost = true; + _wifi_lost_since = now; + } + bool show_no_wifi = _wifi_lost && now - _wifi_lost_since >= NO_WIFI_SHOW_TIME; + // Switched off elsewhere (power timer, Nano flashing): the Nano lost its frame if (!_power.isOn()) _screen = Screen::Unknown; @@ -81,7 +90,7 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu _power.on(); _screen = Screen::Unknown; } - _showClock(hour, minute); + _showClock(hour, minute, show_no_wifi); break; case ScheduleMode::Standby: // Flipdots keep their image without power, so the last time stays visible @@ -91,12 +100,16 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu } } -void DisplayController::_showClock(int hour, int minute) +void DisplayController::_showClock(int hour, int minute, bool no_wifi) { - if (_screen == Screen::Clock && _shown_hour == hour && _shown_minute == minute) + Screen screen = no_wifi ? Screen::NoWifi : Screen::Clock; + if (_screen == screen && _shown_hour == hour && _shown_minute == minute) return; - _frames.clock(hour, minute); - _screen = Screen::Clock; + if (no_wifi) + _frames.noWifi(hour, minute); + else + _frames.clock(hour, minute); + _screen = screen; _shown_hour = hour; _shown_minute = minute; } diff --git a/src/esp32/logic/display_controller.h b/src/esp32/logic/display_controller.h index b8685a0..e9f61f8 100644 --- a/src/esp32/logic/display_controller.h +++ b/src/esp32/logic/display_controller.h @@ -19,6 +19,8 @@ class Frames { public: virtual void clock(int hour, int minute) = 0; + // Shown instead of the clock while WiFi is down (the time keeps running without it) + virtual void noWifi(int hour, int minute) = 0; virtual void logo() = 0; virtual void blank() = 0; }; @@ -40,13 +42,15 @@ public: bool powerEnabled() const; // Call often (every loop). time_valid is false until the time is synced over NTP. - void update(uint32_t now, bool time_valid, int hour, int minute); + // While WiFi is down for NO_WIFI_SHOW_TIME, "No WiFi" is shown instead of the clock. + void update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected); private: enum class Screen { Unknown, // nothing sent since the last power on Clock, + NoWifi, Logo, Blank, }; @@ -62,7 +66,10 @@ private: int _shown_hour = -1; int _shown_minute = -1; - void _showClock(int hour, int minute); + bool _wifi_lost = false; + uint32_t _wifi_lost_since = 0; + + void _showClock(int hour, int minute, bool no_wifi); void _showLogo(); void _showBlank(); }; diff --git a/src/esp32/main.cpp b/src/esp32/main.cpp index 352ea25..f50ae4f 100644 --- a/src/esp32/main.cpp +++ b/src/esp32/main.cpp @@ -7,6 +7,7 @@ #include "BROSE9323.h" #include "config.h" #include "display/frames.h" +#include "display/status_screen.h" #include "logic/display_controller.h" #include "logic/status_led.h" #include "nano_link/nano_flash_bridge.h" @@ -19,6 +20,7 @@ NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN); BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH); FlipdotFrames frames(display); +StatusScreen status(display, nano); DisplayController controller(nano, frames); // Flashing the Nano over WiFi: restart it into its bootloader, switch it off afterwards. @@ -32,12 +34,14 @@ void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); - nano.begin(); // display off until the main code runs + nano.begin(); // display off until something is shown // Decided first, before anything that could crash BootMode boot_mode = firmwareGuardBegin(); - wifiBegin(); + if (boot_mode == BootMode::Normal) + status.show("Connecting", "to WiFi...", true); + wifiBegin(); // starts the recovery app when it fails otaBegin(); if (boot_mode == BootMode::SafeMode) @@ -46,19 +50,20 @@ void setup() // then try the main code once more firmwareGuardSafeModeEnded(); rebootToRecovery(RecoveryReason::Crash); + status.show("Crashed 3x", "OTA only"); otaOnlyMode(SAFE_MODE_TIME, LED_PIN); ESP.restart(); } if (boot_mode == BootMode::PanicWindow) + { + status.show("Crashed!", "OTA open 15s"); otaOnlyMode(OTA_PANIC_WINDOW, LED_PIN); + } flashBridge.begin(); - nano.on(); - display.begin(&nano.stream()); - display.setTiming(FLIP_TIME); - // Shown until the time is synced - frames.message("Connecting..."); + // Shown until the time is synced over NTP + status.show("Getting", "the time...", true); configTzTime(TIMEZONE, NTP_SERVER); homeAssistantBegin(controller); @@ -82,7 +87,7 @@ void loop() struct tm now = {}; bool time_valid = getLocalTime(&now, 0); - controller.update(millis(), time_valid, now.tm_hour, now.tm_min); + controller.update(millis(), time_valid, now.tm_hour, now.tm_min, wifiConnected()); digitalWrite(LED_PIN, statusLedLevel(millis(), wifiConnected(), nano.isOn())); } diff --git a/src/recovery/main.cpp b/src/recovery/main.cpp index 05463ab..1f556ea 100644 --- a/src/recovery/main.cpp +++ b/src/recovery/main.cpp @@ -1,16 +1,19 @@ // 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 never drives the display. See README.md "Recovery app". +// 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" @@ -21,9 +24,25 @@ 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; @@ -79,6 +98,32 @@ static void updateWifi() 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() { @@ -91,11 +136,8 @@ void setup() Serial.begin(115200); Serial.println("FlipDot recovery app"); - // Display off: relay off, and no high TX line that would power the Nano - digitalWrite(RELAY_PIN, HIGH); - pinMode(RELAY_PIN, OUTPUT); - pinMode(NANO_TX_PIN, INPUT); pinMode(LED_PIN, OUTPUT); + nano.begin(); // display off between status messages recovery.reason = takeRecoveryReason(); main_available = mainFirmwareAvailable(); @@ -123,6 +165,7 @@ void setup() 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); @@ -138,6 +181,7 @@ void loop() ArduinoOTA.handle(); portalLoop(); updateWifi(); + updateDisplay(); updateLed(); bool wifi_connected = WiFi.status() == WL_CONNECTED; diff --git a/test/test_esp32/test_main.cpp b/test/test_esp32/test_main.cpp index 8997d2d..f9ce504 100644 --- a/test/test_esp32/test_main.cpp +++ b/test/test_esp32/test_main.cpp @@ -53,6 +53,12 @@ public: snprintf(s, sizeof(s), "clock %02d:%02d", hour, minute); shown.push_back(s); } + void noWifi(int hour, int minute) override + { + char s[16]; + snprintf(s, sizeof(s), "nowifi %02d:%02d", hour, minute); + shown.push_back(s); + } void logo() override { shown.push_back("logo"); } void blank() override { shown.push_back("blank"); } @@ -63,6 +69,7 @@ static FakePower power; static FakeFrames frames; static DisplayController *controller; static uint32_t now; +static bool wifi; void setUp() { @@ -71,6 +78,7 @@ void setUp() delete controller; controller = new DisplayController(power, frames); now = 1000; + wifi = true; } void tearDown() {} @@ -81,7 +89,7 @@ static void run(int hour, int minute, uint32_t ms = 0) uint32_t end = now + ms; do { - controller->update(now, true, hour, minute); + controller->update(now, true, hour, minute, wifi); now += 100; } while ((int32_t)(end - now) >= 0); } @@ -285,7 +293,12 @@ void test_recovery_never_starts_missing_clock_firmware() void test_every_recovery_reason_has_a_text() { for (uint8_t r = 0; r <= (uint8_t)RecoveryReason::PowerCycle; r++) + { TEST_ASSERT_TRUE(strlen(recoveryReasonText((RecoveryReason)r)) > 0); + // fits on one line of the display + TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) > 0); + TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) <= 14); + } TEST_ASSERT_TRUE(strcmp(recoveryReasonText(RecoveryReason::Wifi), recoveryReasonText(RecoveryReason::Crash)) != 0); } @@ -318,7 +331,7 @@ void test_led_blinks_slow_with_wifi_and_fast_without() void test_nothing_happens_until_time_is_synced() { power.is_on = true; // showing "Connecting..." - controller->update(now, false, 0, 0); + controller->update(now, false, 0, 0, true); TEST_ASSERT_EQUAL(0, power.off_count); TEST_ASSERT_EQUAL(0, (int)frames.shown.size()); } @@ -452,6 +465,56 @@ void test_redraws_after_power_was_switched_off_elsewhere() TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } +void test_no_wifi_is_shown_after_a_minute_without_wifi() +{ + run(12, 0); + wifi = false; + run(12, 0, NO_WIFI_SHOW_TIME - 200); + TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); + run(12, 1, 300); + TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str()); + TEST_ASSERT_TRUE(power.is_on); +} + +void test_no_wifi_screen_keeps_the_time_running() +{ + wifi = false; + run(12, 0, NO_WIFI_SHOW_TIME + 100); + run(12, 1); + TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str()); + int frames_before = (int)frames.shown.size(); + run(12, 1, 5000); + TEST_ASSERT_EQUAL(frames_before, (int)frames.shown.size()); // only on change +} + +void test_clock_returns_when_wifi_is_back() +{ + wifi = false; + run(12, 0, NO_WIFI_SHOW_TIME + 100); + wifi = true; + run(12, 0); + TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); +} + +void test_short_wifi_drop_does_not_change_the_display() +{ + run(12, 0); + wifi = false; + run(12, 0, 10000); + wifi = true; + run(12, 0, NO_WIFI_SHOW_TIME); + TEST_ASSERT_EQUAL(1, (int)frames.shown.size()); +} + +void test_night_without_wifi_still_powers_off() +{ + run(23, 30); + wifi = false; + run(23, 31, POWER_OFF_DELAY + 200); + TEST_ASSERT_FALSE(power.is_on); + TEST_ASSERT_EQUAL_STRING("blank", frames.last().c_str()); +} + int main() { UNITY_BEGIN(); @@ -501,6 +564,11 @@ int main() RUN_TEST(test_clock_switch_off_never_powers_on); RUN_TEST(test_clock_switch_on_again_shows_clock); RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere); + RUN_TEST(test_no_wifi_is_shown_after_a_minute_without_wifi); + RUN_TEST(test_no_wifi_screen_keeps_the_time_running); + RUN_TEST(test_clock_returns_when_wifi_is_back); + RUN_TEST(test_short_wifi_drop_does_not_change_the_display); + RUN_TEST(test_night_without_wifi_still_powers_off); return UNITY_END(); }