- New flash layout (partitions.csv): factory = recovery app, ota_0/ota_1 = clock firmware; esp32_usb uploads to ota_0 so it never overwrites it - src/recovery: WiFi + OTA + web page only. Opens the password protected 'FlipDot-Setup' AP with a captive portal when WiFi fails, lets you pick another WiFi (stored in NVS, used by both apps), start the clock or stay - Recovery is started when the clock firmware can't connect to WiFi, crashes 3 times, after 3 quick power cycles, from a new Home Assistant button, or by the bootloader when no clock firmware works - Shared NVS store and boot switching in src/esp32/system - Clock firmware keeps working on the old flash layout (falls back to OTA-only safe mode) - README: recovery app, one-time USB installation; tests: 65 total Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.8 KiB
ESP32: controller
PlatformIO environments: esp32 (OTA) and esp32_usb (USB). Tests: test_esp32.
The ESP32 decides what is shown on the display. It never touches the panel itself. The
BROSE9323 driver is built with -D ESPSERIAL, so every GFX call is sent to the Nano over
Serial2 (UART2, GPIO 17 TX / GPIO 16 RX, 115200 baud). Serial (USB) is only used for
debug output. Wiring is described in the main README.
Files
| File | Contents |
|---|---|
main.cpp |
Creates the objects, setup() and loop() |
config.h |
All settings: pins, timings, timezone, Home Assistant device id |
credentials.h |
Includes secrets.h (WiFi and MQTT credentials, gitignored); template: secrets.example.h |
logic/ |
Pure logic without Arduino code, tested on the PC |
logic/display_controller.* |
Decides what is shown and when the display is powered (DisplayController) |
logic/schedule.* |
When the clock is shown (night, clock, standby) |
logic/power_timer.* |
Countdown to switching the display off |
logic/status_led.h |
Blink pattern of the built-in LED |
logic/boot_policy.* |
How to start after a reset: normal, OTA-only window, safe mode |
system/ |
Keeping OTA alive (system_store and boot_target are shared with the recovery app) |
system/firmware_guard.* |
Rollback of bad OTA firmware, watchdog, crash counter, power-cycle trigger, status for Home Assistant |
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/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) |
nano_link/nano_flash_bridge.* |
WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
net/ |
Network |
net/wifi_ota.* |
WiFi (with a restart when it stays down), ESP32 OTA updates, OTA-only mode |
net/home_assistant.* |
MQTT and the Home Assistant entities (two switches, firmware sensor, recovery button) |
DisplayController only talks to two small interfaces, DisplayPower (implemented by
NanoLink) and Frames (implemented by FlipdotFrames). The tests replace both with
fakes.
Keeping OTA alive
The ESP32 must stay reachable over OTA whatever the firmware does, so it never needs USB again. Several layers take care of that:
| Problem | What happens |
|---|---|
| A new OTA firmware crashes, hangs or can't connect to WiFi | Rollback. A new firmware is only marked as good after it has run for 60 s with WiFi. If it restarts before that, the bootloader starts the previous firmware again. |
The firmware hangs in loop() |
Watchdog. If loop() doesn't run for 30 s, the ESP32 restarts. That counts as a crash. |
| The firmware crashes once or twice | OTA window. After a crash the ESP32 runs only WiFi and OTA for 15 s (LED blinks fast), then starts the main code again. |
| The firmware keeps crashing (3 times in a row) | Recovery app for 10 minutes (see the main README). Then the main code gets one more try; one more crash brings the recovery app back. Running 5 minutes without a crash clears the counter. Without the recovery app installed: only WiFi and OTA run, for 10 minutes. |
| WiFi doesn't connect at boot (30 s), or stays down for 10 minutes | Recovery app, which keeps trying and opens the FlipDot-Setup WiFi with a setup page to choose another WiFi. Without the recovery app: restart. |
| The firmware runs, but its OTA is broken | Recovery app through the Home Assistant button, or by switching the power on and off 3 times quickly. |
So in every case you can flash a fix with pio run -e esp32 -t upload, at the latest
after waiting for a safe-mode cycle.
The FlipDot Display Firmware sensor in Home Assistant shows the state, e.g.
2.1.0, ok, 0 crashes:
| State | Meaning |
|---|---|
testing |
New OTA firmware, not yet marked as good (first 60 s) |
ok |
Firmware marked as good |
ok, no rollback in bootloader |
The bootloader on the ESP32 can't roll back. The other layers still work. The one-time USB install of the recovery app also installs a bootloader that can. |
usb |
Flashed over USB, so there is no previous firmware to roll back to |
Rules for changing the code:
- Keep
otaLoop()andfirmwareGuardLoop()at the top ofloop(). - Never block
loop()for 30 s or longer. - Change
FIRMWARE_VERSIONinconfig.hwith each release, so the sensor shows which version is running.
Features
-
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.
-
OTA updates through ArduinoOTA. Hostname:
FlipDotDisplay. -
Robust OTA: rollback, watchdog, OTA window after a crash and safe mode, see Keeping OTA alive.
-
NTP time with timezone
CET-1CEST,M3.5.0,M10.5.0/3(Europe/Amsterdam). Until the time is synced, the display shows "Connecting...". -
Home Assistant through MQTT (
home-assistant-integrationlibrary) with two switches. Each switch reports its new state back, so Home Assistant always shows the real state.FlipDot Display Clock: on = clock; off = Nintendo logo while the display is on, then power off after 30 sFlipDot Display Power: off = clear the display and power it off after 1 s
Plus the FlipDot Display Firmware sensor (see Keeping OTA alive) and the FlipDot Display Recovery button, which restarts into the recovery app for 10 minutes.
-
Relay on GPIO 13 switches the power for both the panels and the Nano.
LOWturns it on.- On: the ESP waits for the Nano's
READYmessage, at most 3 s (NANO_BOOT_TIMEOUT), before it sends frames. - Off: the ESP releases its TX pin (
Serial2.end()and the pin set to input). An idle UART line is high and would otherwise power the Nano through its RX pin.
- On: the ESP waits for the Nano's
-
Nano flash bridge:
pio run -e nano_ota -t uploadflashes the Nano through the ESP32. While a flash session runs, the clock is paused. See the main README. -
Status LED (built-in): off while the display is off, slow blink with WiFi, fast blink without WiFi.
Schedule
| Time | Display |
|---|---|
| 08:00 – 17:00 | Pikachu and the clock, updated every minute |
| 17:00 – 22:00 | Yawning Pikachu and the clock |
| 22:00 – 23:30 | Clock shown only during the minutes :00, :15, :30 and :45. In between the power is off, and the last time stays visible (flipdots keep their image without power). |
| 23:31 – 08:00 | Screen cleared, power off after 30 s |
The times are in logic/schedule.cpp.
Tests
pio test -e test_esp32 runs test/test_esp32 on
the PC: the schedule boundaries, the power timer (including millis() overflow), the LED
pattern, the boot policy (crash counting, safe mode, rollback timing) and the display
controller (power on/off, redraws, both Home Assistant switches).
Adding bitmaps
Convert an image with image2cpp to "Arduino code",
horizontal, 1 bit per pixel. Paste the array into display/bitmaps.h and draw it in
display/frames.cpp with _display.drawBitmap(x, y, bitmap, w, h, 1). The display is
84 × 19 pixels.