Restructure ESP32 firmware into modules, fix bugs, add tests

Structure (src/esp32/):
- logic/: DisplayController, schedule, power timer, LED pattern; pure C++
- display/: FlipdotFrames and bitmaps
- nano_link/: NanoLink (relay, serial, READY, bootloader) and flash bridge
- net/: WiFi/OTA/panic window, Home Assistant
- config.h with all settings; libs.h (globals in a header) removed

Bug fixes:
- Home Assistant switches now report their state back and start as ON
- loop() no longer blocks: WiFi.waitForConnectResult() (up to 60 s
  without WiFi) and getLocalTime() (5 s before NTP sync) were called
  every iteration
- Power switch off now powers off after 1 s even when a 30 s countdown
  is already running
- millis() overflow safe power-off timer; no frame catch-up bursts
- No garbage time string before NTP sync; 'Connecting...' stays instead
- Frames are only sent when the screen changes instead of every second
- Reset reasons are logged once instead of every 100 ms
- Removed dead code (unused fonts, WiFiMulti, rotated bitmap, ...)

Tests: new test_esp32 environment (26 tests); native renamed to test_nano

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 00:56:32 +02:00
1 parent cefb76c1ad
commit 683a47c055
29 files changed
+1289 -755

No files matched your search

+52 -23
View File
@@ -1,6 +1,6 @@
# ESP32: controller
PlatformIO environments: `esp32` (OTA) and `esp32_usb` (USB)
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
@@ -11,35 +11,55 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
| File | Contents |
|------|----------|
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
| `nano_flash_bridge.h/.cpp` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
| `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 |
| **`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, ESP32 OTA updates and the OTA panic window |
| `net/home_assistant.*` | MQTT and the two Home Assistant switches |
`DisplayController` only talks to two small interfaces, `DisplayPower` (implemented by
`NanoLink`) and `Frames` (implemented by `FlipdotFrames`). The tests replace both with
fakes.
## 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`.
- **OTA panic window:** after a crash or watchdog reset, the ESP32 runs only the OTA
handler for 15 seconds before it starts the main code. A firmware that crashes on boot
can still be replaced over the air.
- **NTP time** with timezone `CET-1CEST,M3.5.0,M10.5.0/3` (Europe/Amsterdam).
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches:
- `FlipDotDisplayClock`: clock on, or the Nintendo logo when off
- `FlipDotDisplayPower`: turns the display off through the relay
handler for 15 seconds before it starts the main code, with the LED blinking fast. A
firmware that crashes on boot can still be replaced over the air.
- **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-integration` library) 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 s
- `FlipDot Display Power`: off = clear the display and power it off after 1 s
- **Relay on GPIO 13** switches the power for both the panels and the Nano. `LOW` turns it
on.
- **On:** the ESP waits for the Nano's `READY` message, at most 3 s
(`NANO_BOOT_TIMEOUT`), before it sends frames. The relay stays on while it is needed;
the ESP only waits when it actually switches the relay on.
- **Off:** the ESP stops sending and releases its TX pin (`Serial.end()` and the pin set
to input). An idle UART line is high and would otherwise power the Nano through its RX
pin.
(`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.
- **Nano flash bridge:** `pio run -e nano_ota -t upload` flashes the Nano through the ESP32.
While a flash session runs, the clock is paused. See the
[main README](../../README.md#flashing-the-nano-over-wifi).
- **Status LED** (built-in) blinks fast when WiFi is down and slowly when connected. It is
off at night or when the display is powered off.
- **Status LED** (built-in): off while the display is off, slow blink with WiFi, fast blink
without WiFi.
## Schedule
@@ -47,11 +67,20 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|------|---------|
| 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; power is off in between |
| 23:31 – 08:00 | Screen cleared, power off |
| 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](../../test/test_esp32/test_main.cpp) on
the PC: the schedule boundaries, the power timer (including `millis()` overflow), the LED
pattern and the display controller (power on/off, redraws, both Home Assistant switches).
## Adding bitmaps
Convert an image with [image2cpp](https://javl.github.io/image2cpp/) to "Arduino code",
horizontal, 1 bit per pixel. Paste the array into `bitmaps.h` and draw it with
`display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is 84 × 19 pixels.
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.