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 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 01:34:04 +02:00
1 parent 4749f9e596
commit c9e0800e36
19 files changed
+331 -47

No files matched your search

+6 -3
View File
@@ -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 <env> -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
+21 -3
View File
@@ -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
+1 -4
View File
@@ -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)
+5 -2
View File
@@ -94,8 +94,11 @@ build_src_filter =
+<esp32/logic/boot_policy.cpp>
+<esp32/system/system_store.cpp>
+<esp32/system/boot_target.cpp>
build_flags = -I src/esp32
lib_deps =
+<esp32/nano_link/nano_link.cpp>
+<esp32/display/status_screen.cpp>
+<esp32/display/text.cpp>
; 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
+8 -1
View File
@@ -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.
+5
View File
@@ -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;
+9 -10
View File
@@ -3,6 +3,7 @@
#include <stdio.h>
#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();
}
+2 -2
View File
@@ -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;
+31
View File
@@ -0,0 +1,31 @@
#include "display/status_screen.h"
#include <Arduino.h>
#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();
}
}
+21
View File
@@ -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;
};
+33
View File
@@ -0,0 +1,33 @@
#include "display/text.h"
#include <string.h>
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);
}
+10
View File
@@ -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);
+18
View File
@@ -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;
+2
View File
@@ -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.
+18 -5
View File
@@ -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;
if (no_wifi)
_frames.noWifi(hour, minute);
else
_frames.clock(hour, minute);
_screen = Screen::Clock;
_screen = screen;
_shown_hour = hour;
_shown_minute = minute;
}
+9 -2
View File
@@ -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();
};
+13 -8
View File
@@ -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()));
}
+49 -5
View File
@@ -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 <Arduino.h>
#include <ArduinoOTA.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#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;
+70 -2
View File
@@ -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();
}