Files
Flipdot-display/CLAUDE.md
T
mangavalkandClaude Opus 5.5 683a47c055 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>
2026-10-13 00:56:32 +02:00

6.0 KiB
Raw Blame History

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 <baud>\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