mangavalkandClaude Opus 5.5 fc6fa1a180 Add scrolling, countdown, stopwatch, images, seconds, multiplier and degree sign
- Text elements can scroll in a window at a set speed; clock, countdown and
  stopwatch can show seconds. Moving pages are sent as instant frames (new
  Nano command D1) and paced by the estimated flip time.
- Countdown to a date and time (smart, days, days + time, with seconds),
  stopwatch (web UI, HA switch and reset button), image element (fixed or
  by Home Assistant state), multiplier for HA values, degree sign (char 127)
  in all fonts and the font converter.
- Page format version 2; version 1 pages are converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:01:14 +02:00

Flipdot Display

Firmware for an 84 × 19 Brose flipdot display (three 28 × 19 panels) that runs as a Home Assistant–controlled clock.

The system uses two microcontrollers:

 ┌──────────────┐   WiFi / MQTT   ┌────────────────┐   UART 115200   ┌──────────────┐   GPIO   ┌──────────────┐
 │ Home         │ ◄─────────────► │ ESP32          │ ──────────────► │ Arduino Nano │ ───────► │ Brose        │
 │ Assistant    │                 │ (controller)   │  text commands  │ (driver)     │          │ flipdot      │
 └──────────────┘                 └───────┬────────┘                 └──────▲───────┘          │ panels 84x19 │
                                          │ GPIO 13                         │                  └──────▲───────┘
                                          ▼                                 │     power               │
                                   ┌─────────────┐                          │                         │
                                   │ Relay       │ ─────────────────────────┴─────────────────────────┘
                                   └─────────────┘

The relay powers both the panels and the Nano. When the relay is off, the Nano is off too, and it boots again each time the relay switches on. Only the ESP32 runs all the time.

Part Folder What it does
Arduino Nano src/nano 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 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, exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates.
ESP32 recovery app src/recovery Small second app: WiFi setup page and OTA, so the ESP32 stays reachable when the clock firmware fails.
BROSE9323 driver lib/BROSE9323 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)
src/nano/               Arduino Nano firmware
src/esp32/              ESP32 clock firmware
  secrets.example.h     template for WiFi / MQTT credentials
  web/                  the web UI
src/recovery/           ESP32 recovery app: WiFi setup page + OTA
partitions.csv          ESP32 flash layout (recovery app + two clock firmware slots)
test/test_nano/         unit tests for the Nano firmware (run on the PC)
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

Building and flashing

Install PlatformIO (the VS Code extension is easiest) and open this folder.

  1. Copy src/esp32/secrets.example.h to src/esp32/secrets.h and fill in your WiFi and MQTT details. secrets.h is gitignored.
  2. Check the COM ports and the OTA IP in platformio.ini.
  3. Build or upload:
Environment Board Upload method
nano Arduino Nano (ATmega328P) USB (COM10)
nano_ota Arduino Nano (ATmega328P) Over WiFi through the ESP32 (10.1.1.198). See Flashing the Nano over WiFi.
esp32 ESP32 DevKit v1 OTA over WiFi (10.1.1.198)
esp32_usb ESP32 DevKit v1 USB (COM11), into ota_0. Normally not needed.
recovery ESP32 DevKit v1 USB (COM11), one-time install of the recovery app
pio run                          # build nano + esp32
pio run -e nano -t upload        # flash the Nano over USB
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 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 MSYS2 or Strawberry Perl.

In VS Code you can also pick the environment in the PlatformIO status bar.

Tip: The ESP32 → Nano wire goes to the Nano's RX pin, which the Nano's USB chip also uses. Disconnect that wire before you flash the Nano over USB, or the upload may fail. Flashing over WiFi (nano_ota) doesn't have this problem.

Web UI

Open http://<ESP32 IP>/ (e.g. http://10.1.1.198/) in a browser. At the top is a live view of the display: it shows what the dots show right now, updated every 1.5 s.

Tab What you can do
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, multiplier, decimals), 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.
Bitmaps All bitmaps with a preview: 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.

Live mode shows the image at once, also at night or before the time is known. It ends with "Back to normal", or 30 minutes after the last image. The Home Assistant Power switch still wins: when it is off, nothing is shown.

Bitmaps, pages and fonts are stored on the ESP32 (LittleFS, 320 KB, room for well over a thousand bitmaps, 50 pages, 20 fonts). Pages and bitmaps share their names. They need the new flash layout from the one-time installation; before that, only the built-in ones are available.

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.

Home Assistant values on the shown page are read every minute (one entity per loop, at most about 3 s). An unknown or unreachable value shows --. The display only changes when the shown text changes, so round values with the decimals setting.

Settings are stored in the ESP32's NVS. The defaults are the original behaviour. There is no login: anyone on your WiFi can use the page, as with OTA.

Flashing the Nano over WiFi

The ESP32 can flash the Nano for you, so you don't need a USB cable at the display:

pio run -e nano_ota -t upload

What happens:

  1. The uploader (tools/nano_net_upload.py) connects to the ESP32 on TCP port 2323 and tells it the bootloader's baud rate.
  2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. If the reset wire is fitted, it also pulses the Nano's RESET pin. This restarts the Nano, and its bootloader starts.
  3. The ESP32 switches its link to the bootloader's baud rate and passes bytes between WiFi and the Nano.
  4. The uploader writes the new firmware and reads it back to verify it.
  5. When the uploader disconnects, the ESP32 switches the relay off. The clock powers the Nano on again with the new firmware and continues.

The display goes dark for the duration of the upload, which takes a few seconds.

Requirements

  • The reply wire must be connected: Nano D1 (TX) → voltage divider → ESP32 GPIO 16. The uploader needs the bootloader's answers.
  • The ESP32 must be running firmware with the flash bridge (this version or newer).

Bootloaders

Flashing the Nano's firmware never changes its bootloader. Both common Nano bootloaders are supported:

Bootloader Baud Starts after a power cycle Reset wire
Old bootloader (ATmegaBOOT), the one this Nano has now 57600 Yes Not needed
Optiboot ("new bootloader") 115200 No, only after a reset-pin reset Required

By default (--baud=auto in upload_flags) the uploader tries 57600 first and then 115200. Set --baud=57600 or --baud=115200 to skip the other attempt.

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_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.

Settings

  • 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_PIN, NANO_POWER_OFF_TIME and NANO_RESET_DELAY in src/esp32/config.h.

If it fails

Message Cause
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.

Recovery app

The ESP32 has a small second app, the recovery app (src/recovery). It has its own place on the flash and is never changed by OTA updates. It contains only WiFi, a setup web page and OTA, so the ESP32 can always be reached, even when the clock firmware is broken. It shows what is going on as a short message on the display, and the built-in LED double-blinks.

flash (4 MB):  bootloader | nvs | otadata | factory: recovery | ota_0: clock | ota_1: clock | bitmaps

When it starts

Trigger Back to the clock
The clock firmware can't connect to WiFi (30 s at boot, or 10 min later on) As soon as the WiFi works again (after at least 1 minute)
The clock firmware crashed 3 times in a row After 10 minutes
FlipDot Display Recovery button in Home Assistant After 10 minutes
Switching the power on and off 3 times, each time within 10 s After 10 minutes
There is no working clock firmware (the bootloader falls back to it) Never by itself

The setup page can also start the clock right away, or keep the recovery app running ("Stay in recovery").

What the display shows

Message Meaning
Connecting / to WiFi... The clock firmware is starting
Getting / the time... Connected, waiting for the time from the internet (NTP)
No WiFi / 12:34 The clock firmware lost WiFi for more than a minute; the time keeps running
Crashed! / OTA open 15s The clock firmware crashed and waits 15 s for an OTA update before it starts again
Crashed 3x / OTA only Repeated crashes without the recovery app installed: only OTA for 10 minutes
Recovery / reason The recovery app started (no WiFi, crashed, requested, power cycled, no firmware)
WiFi setup: / FlipDot-Setup Connect to this WiFi to choose another network, see below
Recovery / IP address The recovery app is connected; open this address for the setup page

The recovery app switches the display off after each message; the dots keep showing it without power.

WiFi setup. The recovery app keeps trying the stored WiFi. If that fails for 20 s, it also opens its own WiFi: FlipDot-Setup, with the password SETUP_AP_PASSWORD from src/esp32/secrets.h. Connect a phone to it; the setup page opens by itself (otherwise go to http://192.168.4.1). Pick a network, enter the password and save. The new WiFi is stored in the ESP32's NVS and used by both apps from then on (instead of WIFI_SSID from secrets.h). The setup page is also reachable on your normal WiFi, at the ESP32's IP address.

Updating the clock firmware from the recovery app. Upload as usual, pio run -e esp32 -t upload. On the setup WiFi, add --upload-port 192.168.4.1. The recovery app writes the firmware to ota_0 and starts it.

Updating the recovery app itself needs USB (pio run -e recovery -t upload). It should rarely be necessary.

One-time installation (USB)

This changes the flash layout, so it can only be done over USB. It is the last USB flash the ESP32 needs. Best done together with the wiring TODOs, while the display is open anyway.

  1. Connect the ESP32 over USB (COM11, see platformio.ini).
  2. Erase the flash and install the recovery app:
    pio run -e recovery -t erase
    pio run -e recovery -t upload
    
    The recovery app starts (LED double-blinks, the display shows Recovery / no firmware), connects to the WiFi from secrets.h and shows its IP address. It waits there, because there is no clock firmware yet.
  3. Install the clock firmware over WiFi. This also tests that the recovery app works:
    pio run -e esp32 -t upload
    
  4. Check Home Assistant: the FlipDot Display Firmware sensor should show 2.1.0, testing, ..., and after a minute 2.1.0, ok, 0 crashes, recovery ok.

Erasing the flash also clears the NVS: WiFi settings made on the setup page are reset to the ones in secrets.h.

Wiring between ESP32 and Nano

The ESP32 talks to the Nano on UART2 (Serial2, 115200 baud). The ESP32's own Serial (UART0, USB) is free for debug output.

ESP32 Nano Notes
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_PIN RESET (through an NPN transistor) Optional, only needed to flash a Nano with Optiboot over WiFi. See Bootloaders.
GND GND Common ground
GPIO 13 Relay input Relay powers the panels and the Nano (LOW = on)

How it works

  1. ESP32 boot. The ESP32 connects to WiFi and starts ArduinoOTA. After a crash it runs only OTA for 15 seconds, and after repeated crashes it goes to safe mode. Bad OTA firmware is rolled back automatically, see Keeping OTA alive. It then switches the display relay on, waits for 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: the page, by default the built-in Clock page: Pikachu with a thought bubble showing HH:MM, a yawning Pikachu from 17:00 (its time slots).
    • 22:00–23:30: the clock is only shown during the minutes :00, :15, :30 and :45. 30 seconds after each of those minutes, the relay turns the display off.
    • 23:31–08:00: the screen is cleared (or shows the off image chosen in the web UI) and power is cut after 30 seconds.
  3. Home Assistant (MQTT discovery):
    • FlipDot Display Power switch: off clears the display and cuts power through the relay.
    • FlipDot Display Mode dropdown: what is shown during the day, a dynamic page ("Clock" first) or one of the bitmaps (the same choice as in the web UI).
    • FlipDot Display Off image dropdown: what is left on the dots when the display switches off (at night or with the Power switch): blank or one of the bitmaps.
    • FlipDot Display IP sensor: the ESP32's IP address.
    • FlipDot Display Stopwatch switch and FlipDot Display Stopwatch reset button: the stopwatch of the dynamic pages.
  4. Nano. The Nano reads commands from the serial line and updates its frame buffer. On D it flips only the dots that changed. Every 2 minutes it redraws the whole screen to fix dots that did not flip.

The serial protocol is described in src/nano/README.md.

TODO

  • Install the recovery app (one-time USB flash, see One-time installation). Until then the ESP32 runs the old flash layout: the clock firmware works, but without the recovery app; the Home Assistant sensor shows "no recovery app".

  • Test the web UI on the real hardware: live view, uploading and drawing a bitmap, live mode, choosing a page, settings, Home Assistant settings (restart).

  • Test the dynamic pages on the real hardware: the Clock page after the update (same as before, yawning from 17:00), a new page with date and Full height clock, an uploaded font, a Home Assistant value (token in the Home Assistant tab).

  • Test the recovery app on the real hardware: the Home Assistant button, the setup WiFi with a wrong WiFi password, 3 quick power cycles, and an OTA upload into it.

  • Rewire the Nano link to UART2. The firmware now uses Serial2 (GPIO 16/17) instead of Serial (GPIO 1/3). Move the ESP32 TX wire from GPIO 1 to GPIO 17 before you flash this firmware, or the display gets no data. See Wiring between ESP32 and Nano.

  • Add the reply wire: Nano D1 (TX) → voltage divider → ESP32 GPIO 16 (RX2). With it, the ESP sends the first frame as soon as the Nano is ready, instead of after 3 s, and the Nano can be flashed over WiFi (nano_ota).

  • Test flashing over WiFi on the real hardware. The uploader is tested against a simulated bootloader, not yet against the real Nano. Check especially that the old bootloader starts after the relay power cycle.

  • Tune the flip settings on the real hardware. Flash both the Nano and the ESP32 (the old Nano ignores the new G/N/E commands). With the test patterns (Settings), try flip gap 0/200/500 µs and 1 or 2 pulses, and check that the refresh effects look right and that the off image is complete before the power is cut.

  • Check the boot timing. Build the Nano with MEASURE_REFRESH to see how long a full redraw takes. If the reply wire isn't fitted, check that 3 s (NANO_BOOT_TIMEOUT) is enough for the Nano to boot.

S
Description
No description provided
Readme
41 MiB
0 Stars 1 Watchers 0 Forks
Languages
C++ 71.6%
C 23.4%
Python 5%