Public Access
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:
1 parent
4749f9e596
commit
c9e0800e36
19 files changed
+331
-47
No files matched your search
@@ -110,9 +110,12 @@ See README.md for the overview.
|
|||||||
USB only), ota_0/ota_1 = clock firmware. `esp32_usb` uploads to 0x110000
|
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
|
(`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.
|
`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`
|
- 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
|
`esp32/system/boot_target.cpp`, `esp32/nano_link/nano_link.cpp`,
|
||||||
clock-firmware dependencies. Both apps share NVS namespace `flipdot`. RTC memory is
|
`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).
|
not shared between the apps (different layouts).
|
||||||
- `rebootToRecovery()` selects the factory partition, which erases otadata. The recovery
|
- `rebootToRecovery()` selects the factory partition, which erases otadata. The recovery
|
||||||
app later selects the clock slot again (`startMainFirmware()`), so the clock firmware
|
app later selects the clock slot again (`startMainFirmware()`), so the clock firmware
|
||||||
|
|||||||
@@ -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
|
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,
|
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
|
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
|
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
|
The setup page can also start the clock right away, or keep the recovery app running
|
||||||
("Stay in recovery").
|
("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
|
**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
|
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
|
`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 erase
|
||||||
pio run -e recovery -t upload
|
pio run -e recovery -t upload
|
||||||
```
|
```
|
||||||
The recovery app starts (LED double-blinks), connects to the WiFi from `secrets.h`, and
|
The recovery app starts (LED double-blinks, the display shows `Recovery` /
|
||||||
waits, because there is no clock firmware yet.
|
`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:
|
3. Install the clock firmware **over WiFi**. This also tests that the recovery app works:
|
||||||
```sh
|
```sh
|
||||||
pio run -e esp32 -t upload
|
pio run -e esp32 -t upload
|
||||||
|
|||||||
@@ -18,13 +18,10 @@ BROSE9323::BROSE9323(uint8_t w, uint8_t h, uint8_t pw, uint16_t ft) : Adafruit_G
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ESPSERIAL
|
#ifdef ESPSERIAL
|
||||||
|
// Only sets the serial port; the first frame is up to the caller
|
||||||
void BROSE9323::begin(Stream *s)
|
void BROSE9323::begin(Stream *s)
|
||||||
{
|
{
|
||||||
stream = s;
|
stream = s;
|
||||||
fillScreen(0);
|
|
||||||
setCursor(0, 4);
|
|
||||||
print("ESP Connected");
|
|
||||||
display();
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
void BROSE9323::begin(void)
|
void BROSE9323::begin(void)
|
||||||
|
|||||||
+5
-2
@@ -94,8 +94,11 @@ build_src_filter =
|
|||||||
+<esp32/logic/boot_policy.cpp>
|
+<esp32/logic/boot_policy.cpp>
|
||||||
+<esp32/system/system_store.cpp>
|
+<esp32/system/system_store.cpp>
|
||||||
+<esp32/system/boot_target.cpp>
|
+<esp32/system/boot_target.cpp>
|
||||||
build_flags = -I src/esp32
|
+<esp32/nano_link/nano_link.cpp>
|
||||||
lib_deps =
|
+<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_protocol = esptool
|
||||||
upload_port = COM11
|
upload_port = COM11
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -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/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 |
|
| `system/system_store.*` | Settings in NVS: WiFi, recovery reason, firmware slot and version, power-on count |
|
||||||
| **`display/`** | **What is drawn** |
|
| **`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, ...) |
|
| `display/bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
||||||
| **`nano_link/`** | **Connection to the Nano** |
|
| **`nano_link/`** | **Connection to the Nano** |
|
||||||
| `nano_link/nano_link.*` | Relay and serial link: power on (waits for `READY`), power off, bootloader start (`NanoLink`) |
|
| `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
|
## 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
|
- **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
|
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.
|
quiet in between, which also gives the Nano time for its periodic full redraw.
|
||||||
|
|||||||
@@ -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
|
// 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;
|
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)
|
// Power off this long after the display is no longer needed (night, standby, logo)
|
||||||
const uint32_t POWER_OFF_DELAY = 30000;
|
const uint32_t POWER_OFF_DELAY = 30000;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "display/bitmaps.h"
|
#include "display/bitmaps.h"
|
||||||
|
#include "display/text.h"
|
||||||
#include "logic/schedule.h"
|
#include "logic/schedule.h"
|
||||||
|
|
||||||
// Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display
|
// 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();
|
_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()
|
void FlipdotFrames::logo()
|
||||||
{
|
{
|
||||||
_display.fillScreen(0);
|
_display.fillScreen(0);
|
||||||
@@ -43,13 +52,3 @@ void FlipdotFrames::blank()
|
|||||||
_display.fillScreen(0);
|
_display.fillScreen(0);
|
||||||
_display.display();
|
_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();
|
|
||||||
}
|
|
||||||
@@ -11,11 +11,11 @@ public:
|
|||||||
|
|
||||||
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
||||||
void clock(int hour, int minute) override;
|
void clock(int hour, int minute) override;
|
||||||
|
// "No WiFi" with the time below it
|
||||||
|
void noWifi(int hour, int minute) override;
|
||||||
// Nintendo logo
|
// Nintendo logo
|
||||||
void logo() override;
|
void logo() override;
|
||||||
void blank() override;
|
void blank() override;
|
||||||
// A line of text, e.g. while connecting
|
|
||||||
void message(const char *text);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BROSE9323 &_display;
|
BROSE9323 &_display;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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)
|
bool powerCyclesRequestRecovery(uint8_t power_on_count)
|
||||||
{
|
{
|
||||||
return power_on_count >= POWER_CYCLES_FOR_RECOVERY;
|
return power_on_count >= POWER_CYCLES_FOR_RECOVERY;
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ enum class RecoveryReason : uint8_t
|
|||||||
};
|
};
|
||||||
|
|
||||||
const char *recoveryReasonText(RecoveryReason reason);
|
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.
|
// 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.
|
// True when the next start should be the recovery app.
|
||||||
|
|||||||
@@ -27,8 +27,17 @@ bool DisplayController::powerEnabled() const
|
|||||||
return _power_enabled;
|
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
|
// Switched off elsewhere (power timer, Nano flashing): the Nano lost its frame
|
||||||
if (!_power.isOn())
|
if (!_power.isOn())
|
||||||
_screen = Screen::Unknown;
|
_screen = Screen::Unknown;
|
||||||
@@ -81,7 +90,7 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
|
|||||||
_power.on();
|
_power.on();
|
||||||
_screen = Screen::Unknown;
|
_screen = Screen::Unknown;
|
||||||
}
|
}
|
||||||
_showClock(hour, minute);
|
_showClock(hour, minute, show_no_wifi);
|
||||||
break;
|
break;
|
||||||
case ScheduleMode::Standby:
|
case ScheduleMode::Standby:
|
||||||
// Flipdots keep their image without power, so the last time stays visible
|
// 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;
|
return;
|
||||||
|
if (no_wifi)
|
||||||
|
_frames.noWifi(hour, minute);
|
||||||
|
else
|
||||||
_frames.clock(hour, minute);
|
_frames.clock(hour, minute);
|
||||||
_screen = Screen::Clock;
|
_screen = screen;
|
||||||
_shown_hour = hour;
|
_shown_hour = hour;
|
||||||
_shown_minute = minute;
|
_shown_minute = minute;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class Frames
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual void clock(int hour, int minute) = 0;
|
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 logo() = 0;
|
||||||
virtual void blank() = 0;
|
virtual void blank() = 0;
|
||||||
};
|
};
|
||||||
@@ -40,13 +42,15 @@ public:
|
|||||||
bool powerEnabled() const;
|
bool powerEnabled() const;
|
||||||
|
|
||||||
// Call often (every loop). time_valid is false until the time is synced over NTP.
|
// 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:
|
private:
|
||||||
enum class Screen
|
enum class Screen
|
||||||
{
|
{
|
||||||
Unknown, // nothing sent since the last power on
|
Unknown, // nothing sent since the last power on
|
||||||
Clock,
|
Clock,
|
||||||
|
NoWifi,
|
||||||
Logo,
|
Logo,
|
||||||
Blank,
|
Blank,
|
||||||
};
|
};
|
||||||
@@ -62,7 +66,10 @@ private:
|
|||||||
int _shown_hour = -1;
|
int _shown_hour = -1;
|
||||||
int _shown_minute = -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 _showLogo();
|
||||||
void _showBlank();
|
void _showBlank();
|
||||||
};
|
};
|
||||||
+13
-8
@@ -7,6 +7,7 @@
|
|||||||
#include "BROSE9323.h"
|
#include "BROSE9323.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "display/frames.h"
|
#include "display/frames.h"
|
||||||
|
#include "display/status_screen.h"
|
||||||
#include "logic/display_controller.h"
|
#include "logic/display_controller.h"
|
||||||
#include "logic/status_led.h"
|
#include "logic/status_led.h"
|
||||||
#include "nano_link/nano_flash_bridge.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);
|
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
||||||
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
||||||
FlipdotFrames frames(display);
|
FlipdotFrames frames(display);
|
||||||
|
StatusScreen status(display, nano);
|
||||||
DisplayController controller(nano, frames);
|
DisplayController controller(nano, frames);
|
||||||
|
|
||||||
// Flashing the Nano over WiFi: restart it into its bootloader, switch it off afterwards.
|
// Flashing the Nano over WiFi: restart it into its bootloader, switch it off afterwards.
|
||||||
@@ -32,12 +34,14 @@ void setup()
|
|||||||
{
|
{
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
pinMode(LED_PIN, OUTPUT);
|
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
|
// Decided first, before anything that could crash
|
||||||
BootMode boot_mode = firmwareGuardBegin();
|
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();
|
otaBegin();
|
||||||
|
|
||||||
if (boot_mode == BootMode::SafeMode)
|
if (boot_mode == BootMode::SafeMode)
|
||||||
@@ -46,19 +50,20 @@ void setup()
|
|||||||
// then try the main code once more
|
// then try the main code once more
|
||||||
firmwareGuardSafeModeEnded();
|
firmwareGuardSafeModeEnded();
|
||||||
rebootToRecovery(RecoveryReason::Crash);
|
rebootToRecovery(RecoveryReason::Crash);
|
||||||
|
status.show("Crashed 3x", "OTA only");
|
||||||
otaOnlyMode(SAFE_MODE_TIME, LED_PIN);
|
otaOnlyMode(SAFE_MODE_TIME, LED_PIN);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
if (boot_mode == BootMode::PanicWindow)
|
if (boot_mode == BootMode::PanicWindow)
|
||||||
|
{
|
||||||
|
status.show("Crashed!", "OTA open 15s");
|
||||||
otaOnlyMode(OTA_PANIC_WINDOW, LED_PIN);
|
otaOnlyMode(OTA_PANIC_WINDOW, LED_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
flashBridge.begin();
|
flashBridge.begin();
|
||||||
|
|
||||||
nano.on();
|
// Shown until the time is synced over NTP
|
||||||
display.begin(&nano.stream());
|
status.show("Getting", "the time...", true);
|
||||||
display.setTiming(FLIP_TIME);
|
|
||||||
// Shown until the time is synced
|
|
||||||
frames.message("Connecting...");
|
|
||||||
|
|
||||||
configTzTime(TIMEZONE, NTP_SERVER);
|
configTzTime(TIMEZONE, NTP_SERVER);
|
||||||
homeAssistantBegin(controller);
|
homeAssistantBegin(controller);
|
||||||
@@ -82,7 +87,7 @@ void loop()
|
|||||||
|
|
||||||
struct tm now = {};
|
struct tm now = {};
|
||||||
bool time_valid = getLocalTime(&now, 0);
|
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()));
|
digitalWrite(LED_PIN, statusLedLevel(millis(), wifiConnected(), nano.isOn()));
|
||||||
}
|
}
|
||||||
+49
-5
@@ -1,16 +1,19 @@
|
|||||||
// Recovery app (factory partition): WiFi setup portal and OTA, nothing else. Started when
|
// 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,
|
// 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.
|
// 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 <Arduino.h>
|
||||||
#include <ArduinoOTA.h>
|
#include <ArduinoOTA.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
|
#include "BROSE9323.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "credentials.h"
|
#include "credentials.h"
|
||||||
|
#include "display/status_screen.h"
|
||||||
#include "logic/boot_policy.h"
|
#include "logic/boot_policy.h"
|
||||||
|
#include "nano_link/nano_link.h"
|
||||||
#include "recovery_state.h"
|
#include "recovery_state.h"
|
||||||
#include "system/boot_target.h"
|
#include "system/boot_target.h"
|
||||||
#include "system/system_store.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
|
// Close the setup AP this long after the stored WiFi works again
|
||||||
static const uint32_t AP_CLOSE_DELAY = 120000;
|
static const uint32_t AP_CLOSE_DELAY = 120000;
|
||||||
static const uint32_t RECOVERY_WATCHDOG_S = 60;
|
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;
|
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_lost_since = 0;
|
||||||
static uint32_t station_connected_since = 0;
|
static uint32_t station_connected_since = 0;
|
||||||
static bool watchdog_running = false;
|
static bool watchdog_running = false;
|
||||||
@@ -79,6 +98,32 @@ static void updateWifi()
|
|||||||
connectStation();
|
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
|
// Double blink: recovery app is running
|
||||||
static void updateLed()
|
static void updateLed()
|
||||||
{
|
{
|
||||||
@@ -91,11 +136,8 @@ void setup()
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("FlipDot recovery app");
|
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);
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
nano.begin(); // display off between status messages
|
||||||
|
|
||||||
recovery.reason = takeRecoveryReason();
|
recovery.reason = takeRecoveryReason();
|
||||||
main_available = mainFirmwareAvailable();
|
main_available = mainFirmwareAvailable();
|
||||||
@@ -123,6 +165,7 @@ void setup()
|
|||||||
ArduinoOTA.begin();
|
ArduinoOTA.begin();
|
||||||
|
|
||||||
portalBegin();
|
portalBegin();
|
||||||
|
updateDisplay();
|
||||||
|
|
||||||
// A hang restarts the ESP32; the bootloader then starts this recovery app again
|
// A hang restarts the ESP32; the bootloader then starts this recovery app again
|
||||||
esp_task_wdt_init(RECOVERY_WATCHDOG_S, true);
|
esp_task_wdt_init(RECOVERY_WATCHDOG_S, true);
|
||||||
@@ -138,6 +181,7 @@ void loop()
|
|||||||
ArduinoOTA.handle();
|
ArduinoOTA.handle();
|
||||||
portalLoop();
|
portalLoop();
|
||||||
updateWifi();
|
updateWifi();
|
||||||
|
updateDisplay();
|
||||||
updateLed();
|
updateLed();
|
||||||
|
|
||||||
bool wifi_connected = WiFi.status() == WL_CONNECTED;
|
bool wifi_connected = WiFi.status() == WL_CONNECTED;
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ public:
|
|||||||
snprintf(s, sizeof(s), "clock %02d:%02d", hour, minute);
|
snprintf(s, sizeof(s), "clock %02d:%02d", hour, minute);
|
||||||
shown.push_back(s);
|
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 logo() override { shown.push_back("logo"); }
|
||||||
void blank() override { shown.push_back("blank"); }
|
void blank() override { shown.push_back("blank"); }
|
||||||
|
|
||||||
@@ -63,6 +69,7 @@ static FakePower power;
|
|||||||
static FakeFrames frames;
|
static FakeFrames frames;
|
||||||
static DisplayController *controller;
|
static DisplayController *controller;
|
||||||
static uint32_t now;
|
static uint32_t now;
|
||||||
|
static bool wifi;
|
||||||
|
|
||||||
void setUp()
|
void setUp()
|
||||||
{
|
{
|
||||||
@@ -71,6 +78,7 @@ void setUp()
|
|||||||
delete controller;
|
delete controller;
|
||||||
controller = new DisplayController(power, frames);
|
controller = new DisplayController(power, frames);
|
||||||
now = 1000;
|
now = 1000;
|
||||||
|
wifi = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void tearDown() {}
|
void tearDown() {}
|
||||||
@@ -81,7 +89,7 @@ static void run(int hour, int minute, uint32_t ms = 0)
|
|||||||
uint32_t end = now + ms;
|
uint32_t end = now + ms;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
controller->update(now, true, hour, minute);
|
controller->update(now, true, hour, minute, wifi);
|
||||||
now += 100;
|
now += 100;
|
||||||
} while ((int32_t)(end - now) >= 0);
|
} 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()
|
void test_every_recovery_reason_has_a_text()
|
||||||
{
|
{
|
||||||
for (uint8_t r = 0; r <= (uint8_t)RecoveryReason::PowerCycle; r++)
|
for (uint8_t r = 0; r <= (uint8_t)RecoveryReason::PowerCycle; r++)
|
||||||
|
{
|
||||||
TEST_ASSERT_TRUE(strlen(recoveryReasonText((RecoveryReason)r)) > 0);
|
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),
|
TEST_ASSERT_TRUE(strcmp(recoveryReasonText(RecoveryReason::Wifi),
|
||||||
recoveryReasonText(RecoveryReason::Crash)) != 0);
|
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()
|
void test_nothing_happens_until_time_is_synced()
|
||||||
{
|
{
|
||||||
power.is_on = true; // showing "Connecting..."
|
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, power.off_count);
|
||||||
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
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());
|
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()
|
int main()
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
@@ -501,6 +564,11 @@ int main()
|
|||||||
RUN_TEST(test_clock_switch_off_never_powers_on);
|
RUN_TEST(test_clock_switch_off_never_powers_on);
|
||||||
RUN_TEST(test_clock_switch_on_again_shows_clock);
|
RUN_TEST(test_clock_switch_on_again_shows_clock);
|
||||||
RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere);
|
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();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user