| **Arduino Nano** | [src/nano](src/nano/README.md) | Drives the panel's row and column electronics directly. Keeps a frame buffer and flips only the dots that changed. Takes simple text commands over serial. |
| **ESP32** | [src/esp32](src/esp32/README.md) | Connects to WiFi, gets the time over NTP, shows dynamic pages (a background per time of day with a clock, the date, Home Assistant values or text) or your own bitmaps, has a [web UI](#web-ui), exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates. |
| **ESP32 recovery app** | [src/recovery](src/recovery/README.md) | Small second app: WiFi setup page and OTA, so the ESP32 stays reachable when the clock firmware fails. |
| **BROSE9323 driver** | [lib/BROSE9323](lib/BROSE9323/README.md) | Shared `Adafruit_GFX` display driver. On the Nano it toggles GPIO pins. On the ESP32 (`-D ESPSERIAL`) it turns every drawing call into a serial command for the Nano. |
Because the driver is an `Adafruit_GFX` subclass on both boards, the ESP32 can use the
normal GFX API (`print`, `drawBitmap`, `fillScreen`, ...). Each call is sent to the
Nano and drawn there.
## Repository layout
```
platformio.ini one PlatformIO project, multiple environments
lib/BROSE9323/ shared flipdot driver (used by both targets)
| `nano_ota` | Arduino Nano (ATmega328P) | Over WiFi through the ESP32 (`10.1.1.198`). See [Flashing the Nano over WiFi](#flashing-the-nano-over-wifi). |
| **Show** | Choose what the display shows during the day: a dynamic page (e.g. the built-in Clock: Pikachu with the time) or any bitmap. The schedule still decides when the display is on. |
| **Pages** | Dynamic pages: a background bitmap per time of day (e.g. Pikachu from 00:00, yawning Pikachu from 17:00) with up to 8 elements on top: clock (`08:05`, `8:05`, optionally with seconds), date (7 formats, Dutch or English), Home Assistant value (entity or attribute, text before and after; shown as a number with multiplier, decimals, auto unit `1257 W` → `1.3 kW` and optionally without minus sign, as a duration `2h 21m` / `2:21:00` / `2:21` / `2 uur`, as a time from an HA timestamp `23:07` / `do 23:07` / `15-10 23:07` / `over 34 min`, or with own texts per state `printing=Bezig, standby=Klaar`), text, countdown to a date and time (smart `12 dagen` / `5 uur` / `34 min` / `0:45`, days, `12d 04:05`, with seconds; a text when it has passed), stopwatch (start, stop and reset in the Show tab, the editor and Home Assistant) and image (a bitmap at a position, fixed or chosen by a Home Assistant state, e.g. `weather ` + `cloudy`). Texts can scroll in a window at a chosen speed, and can contain the degree sign. Each element has a position (drag it on the preview), alignment, font and overlay (on top, box that clears the dots behind it, inverted). The preview is drawn by the ESP32, so it is exactly what the display shows. Built-in pages can be copied, not changed. Fonts: Small (3x5), Normal (5x7), Large (5x7 doubled), Full height (19 dot digits), and your own: upload a TTF/OTF, choose the height in dots, the browser converts it. |
| **Schedule** | What is shown when, every day: blocks from a start time until the next block, each with up to 6 pages or bitmaps that rotate (each for its own time, 5 s to 1 hour), switching with the refresh effect or instantly. Rules: a Home Assistant state (entity or attribute, `=`, `is not`, `above`, `below`) that shows a page while it is true, adds it to the rotation while it is true, or shows it for a while when it becomes true; each rule is read at its own interval (2-600 s). A page chosen by hand (Show tab, HA "Mode") is shown until the next block starts; "Back to schedule" ends it earlier. The display schedule (Settings) still decides when the display is on. |
| **Stopwatch** | Start, stop and reset the stopwatch that pages can show. |
| **Bitmaps** | All bitmaps with a preview (the ones that pages or rules use in their own subsection): show one as the page, send it live, open it in the editor, delete it. The built-in ones (Nintendo, Pikachu, Pikachu yawning) can't be deleted, nor can a bitmap that a page uses as background. |
| **Draw / Live** | Draw on an 84 x 19 canvas (click or drag), or load an image: it is converted in the browser (threshold, invert, dithering, fit / fill / stretch). "Show on display now" sends it to the display right away (*live mode*); with "live while drawing" every change appears on the dots. Save it as a bitmap with a name. |
| **Settings** | Schedule on/off, when the display is on, the late-evening quarter-hour mode, when it switches off; the "No WiFi" screen; the flip time, the pause after each pulse, pulses per dot and the refresh effect (instant, sparkle, wipe, rain, from the middle out, diagonal, spiral, flip all then reveal, push, random; optionally only for small changes like the clock's minutes); test patterns; the status LED. |
| **Home Assistant** | On/off, broker, port, user, password (changing these restarts the ESP32; an empty broker uses `secrets.h`). The URL and long-lived access token for the values on dynamic pages (REST API, plain http; empty URL: `http://<broker>:8123`). |
| **System** | Firmware status, uptime, WiFi, storage; restart; start the recovery app. Backup: export everything (settings, your bitmaps, pages and fonts; the MQTT password and HA token only when ticked) to one JSON file, and import a file: choose which items and which parts of the settings. Imported items are added (a taken name gets a number) unless you give them an existing name. Pages and bitmaps also have their own Export button. |
**Moving pages** (scrolling text, seconds, a running stopwatch) send a frame whenever the content moves, without the refresh effect (`D1`, needs the Nano firmware from version 2.5.0), and not faster than the Nano can flip the dots: with 3 pulses and a 300 us gap a 1-dot scroll step of a line of text takes about 0.1-0.2 s.
| `can't connect to ...` | Wrong IP address, or the ESP32 is offline |
| `the ESP32 flash bridge didn't accept the request` | The ESP32 runs older firmware without the flash bridge. Flash it first (`esp32` or `esp32_usb`). |
| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot and the reset wire is missing |
| `bad reply to command ...` / `verify failed` | Connection problem. Try again. A failed upload can leave the Nano without working firmware, but the bootloader itself is never overwritten, so flashing again (over WiFi or USB) always fixes it. |
The ESP32 ends the session after 10 seconds without data, so the clock always resumes,
even if the uploader is interrupted.
> **Security:** anyone on your network can connect to port 2323 and flash the Nano, just
> like the ESP32's own OTA has no password. Keep the display on a trusted network.
| 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_PIN` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |