# Flipdot display: notes for Claude A PlatformIO project with two firmware targets for an 84×19 Brose flipdot clock. See README.md for the overview. ## Layout and build - `platformio.ini` has the environments `nano`, `nano_ota`, `esp32` (OTA to 10.1.1.198), `esp32_usb` (COM11), and the PC test environments `test_nano` and `test_esp32`. The sources are split with `build_src_filter` (`src/nano/`, `src/esp32/`). - `lib/BROSE9323/` is the shared driver. Its behaviour depends on build flags set in `platformio.ini`, not in the header: - `ESPSERIAL` (ESP32): draw calls become serial commands. - `FLIPDOT_PLCC_ADAPTER` (Nano, optional): alternative pinout. - PlatformIO CLI is not on PATH. Run `~/.platformio/penv/Scripts/pio.exe run` (Git Bash) or `$env:USERPROFILE\.platformio\penv\Scripts\pio.exe run` (PowerShell). - If an ESP32 build fails with `No module named 'intelhex'`, run `~/.platformio/penv/Scripts/python.exe -m pip install intelhex`. - `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP) talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends `FLASH \n`. The ESP power cycles the Nano (plus an optional reset pulse on `NANO_RESET_PIN`), answers `OK\n`, then passes the bytes through at that baud rate. `--baud=auto` tries 57600 (old ATmegaBOOT, which also starts after a power cycle), then 115200 (Optiboot, which only starts after a reset-pin reset, so it needs `NANO_RESET_PIN`). avrdude 6.3's `net:` port is broken on Windows (`ser_drain()` fails), which is why the uploader is a Python script. The uploader only uses the Python standard library. Test it against a simulated bootloader before changing it. - After any change, build both targets (`pio run`) and run the tests (`pio test -e test_nano -e test_esp32`). ## Constraints - **Nano RAM is tight** (ATmega328P, 2 KB). The frame buffers are allocated with `calloc`. Avoid `String`, large stack buffers and new globals in Nano code. Put constant data in `PROGMEM`. - The ESP32 talks to the Nano on `Serial2` (owned by `NanoLink`, GPIO 17 TX / GPIO 16 RX). `Serial` (UART0/USB) is free for debug output. Never write debug text to `Serial2`: the Nano runs lines that start with s/S/F/D/P/B/R/T as commands. - Open hardware TODOs are listed in the README `TODO` section. Update it when one is done. - `src/esp32/secrets.h` holds the real credentials and is gitignored. Never commit it. Add new secrets to `secrets.example.h` as placeholders. - The relay (ESP32 GPIO 13, `LOW` = on) powers both the panels and the Nano. Each time it switches on, the Nano cold-boots and then sends `READY`. `NanoLink::on()` waits for that message, at most `NANO_BOOT_TIMEOUT`. The timeout also covers the case where the Nano's TX isn't wired to the ESP's RX. - `NanoLink::off()` ends `Serial2` and sets its TX pin to input. Otherwise the idle-high TX line powers the Nano through its RX pin. Never send display commands while `NanoLink::isOn()` is false (`DisplayController` takes care of this). - The Nano keeps no state over a power cycle except the flip time in EEPROM. The driver forces a full redraw on the first `display()` after boot (`_force_next`). - Hardware is not available here. Builds can be checked, but runtime behaviour can't. ## Nano serial parser (`src/nano/main.cpp`) - Files: `main.cpp` (setup/loop, refresh), `config.h` (all constants and debug defines), `command_parser.*` (protocol, one method per command), `settings.*` (EEPROM). - Line based and non-blocking: bytes are collected into a 64-character buffer and a command runs when `\n` arrives. Bitmap (`B`) pixel data is the exception. It is drawn while it arrives, so it can be longer than the buffer. Keep `loop()` free of busy-waits. - A full redraw (`display(true)`) blocks for about 0.5 s, which overflows the 64-byte serial receive buffer. Only run it when no data is expected: right after a `D`, or after 2 s with no serial data. The ESP only sends a frame (ending with `D`) when the screen changes, mostly once a minute. - Flip time is stored in EEPROM as a magic word at address 0 and a `uint16_t` at address 2. Values outside 50–2000 µs are rejected. - The Nano only replies `READY` (at boot) and `ERR: ...`. There is no per-frame acknowledgement. Whether the Nano's TX is wired to the ESP's RX is unconfirmed, so every wait for a reply needs a timeout. - Unit tests: `pio test -e test_nano` runs `test/test_nano/` on the PC with the fakes in `test/mocks/` (Arduino, EEPROM, and a `BROSE9323` that records calls). It needs gcc on PATH (Strawberry Perl provides it at `C:\Strawberry\c\bin`). Add a test for every protocol change. The mocks only implement what the Nano code uses, so extend them when new Arduino or driver functions are called. - The board environments have `test_ignore = *`, so test firmware is never uploaded. Plain `pio test` runs nothing; always pass `-e test_nano -e test_esp32`. ## ESP32 structure (`src/esp32/`) - Folders: `logic/` (pure C++, no Arduino includes, tested in `test/test_esp32`), `display/` (frames and bitmaps), `nano_link/` (relay, serial, flash bridge), `net/` (WiFi/OTA, Home Assistant). Includes are relative to `src/esp32` (`-I src/esp32`), e.g. `#include "logic/schedule.h"`. - `DisplayController` holds the display logic and only uses the `DisplayPower` and `Frames` interfaces. Put new behaviour there, behind those interfaces, so it stays testable. `logic/` must not include Arduino headers, or `test_esp32` stops building. - All settings are in `config.h`. Keep it free of Arduino types for the same reason. - Frames are only sent when the screen changes. The schedule uses 08:00 as the start of the day on purpose (that is what the old code did, and the README documents it). - Home Assistant switches are not optimistic: callbacks must call `sender->setState()`. The unique ids `FlipDotDisplayClock` / `FlipDotDisplayPower` and `HA_DEVICE_ID` must stay the same, or Home Assistant creates new entities. ## Git - Remote: https://mangavalk.com/mangavalk/Flipdot-display.git, branch `main`.