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

+12 -9
View File
@@ -39,7 +39,8 @@ src/nano/ Arduino Nano firmware
src/esp32/ ESP32 firmware
secrets.example.h template for WiFi / MQTT credentials
test/test_nano/ unit tests for the Nano firmware (run on the PC)
test/mocks/ fake Arduino, EEPROM and display driver for the tests
test/test_esp32/ unit tests for the ESP32 logic (run on the PC)
test/mocks/ fake Arduino, EEPROM and display driver for the Nano tests
tools/nano_net_upload.py uploader for flashing the Nano over WiFi
archive/ old, unused sketches kept for reference
CLAUDE.md project notes for Claude Code
@@ -69,7 +70,7 @@ pio run -e nano_ota -t upload # flash the Nano over WiFi, through the ESP32
pio run -e esp32 -t upload # flash the ESP32 over the air
pio run -e esp32_usb -t upload # flash the ESP32 over USB
pio device monitor -e nano # serial monitor
pio test -e native # run the Nano unit tests on the PC
pio test -e test_nano -e test_esp32 # run the unit tests on the PC
```
The unit tests need `gcc`/`g++` on your PATH. On Windows you can get it from
@@ -126,8 +127,8 @@ By default (`--baud=auto` in `upload_flags`) the uploader tries 57600 first and
If you ever replace the bootloader with Optiboot:
- Fit the reset wire: ESP32 GPIO → 1 kΩ → base of an NPN transistor (e.g. 2N2222 or
BC547). Connect the collector to Nano RESET and the emitter to GND. Set `nano_reset` in
`src/esp32/libs.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V
BC547). Connect the collector to Nano RESET and the emitter to GND. Set `NANO_RESET_PIN` in
`src/esp32/config.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V
reset line away from the 3.3 V ESP32 pin.
- For uploads over USB, change `board` in the `nano` environment to `nanoatmega328new`.
@@ -136,8 +137,8 @@ If you ever replace the bootloader with Optiboot:
- ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You
can add a port number: `10.1.1.198:2323`. For a one-time different address, use
`pio run -e nano_ota -t upload --upload-port 10.1.1.50`.
- Port, reset pin and timings: `NANO_FLASH_PORT`, `nano_reset`, `NANO_POWER_OFF_TIME` and
`NANO_RESET_DELAY` in `src/esp32/libs.h`.
- Port, reset pin and timings: `NANO_FLASH_PORT`, `NANO_RESET_PIN`, `NANO_POWER_OFF_TIME`
and `NANO_RESET_DELAY` in `src/esp32/config.h`.
**If it fails**
@@ -163,7 +164,7 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
|-------|------|-------|
| GPIO 17 (TX2) | D0 (RX) | Display commands |
| GPIO 16 (RX2) | D1 (TX) | `READY` and `ERR` replies, and bootloader answers when flashing over WiFi. Use a 5 V → 3.3 V voltage divider (e.g. 1 kΩ / 2 kΩ), because the ESP32 is not 5 V tolerant. Without it, the ESP waits a fixed 3 s after power-on, and `nano_ota` doesn't work. |
| GPIO set in `nano_reset` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |
| GPIO set in `NANO_RESET_PIN` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |
| GND | GND | Common ground |
| GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) |
@@ -172,8 +173,10 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
1. **ESP32 boot.** The ESP32 connects to WiFi and starts ArduinoOTA. After a crash or
unexpected reset it stays in an *OTA panic window* for 15 seconds, so you can flash a
fix before the main code runs again. It then switches the display relay on, waits for
the Nano's `READY` message (at most 3 s), starts NTP and connects to MQTT.
2. **Clock loop (every second).**
the Nano's `READY` message (at most 3 s), shows "Connecting...", starts NTP and
connects to MQTT.
2. **Clock.** Once the time is synced, the ESP32 sends a new frame whenever the screen
changes, mostly once a minute:
- **08:00–22:00:** Pikachu with a thought bubble showing `HH:MM`. A yawning Pikachu is
shown from 17:00.
- **22:00–23:30:** the clock is only shown during the minutes :00, :15, :30 and :45.