Public Access
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>
209 lines
12 KiB
Markdown
209 lines
12 KiB
Markdown
# 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](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 a clock with Pikachu bitmaps, exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates. |
|
||
| **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)
|
||
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/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](https://platformio.org/) (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](#flashing-the-nano-over-wifi). |
|
||
| `esp32` | ESP32 DevKit v1 | OTA over WiFi (`10.1.1.198`) |
|
||
| `esp32_usb` | ESP32 DevKit v1 | USB (`COM11`). Use it for the first flash or for recovery. |
|
||
|
||
```sh
|
||
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](https://www.msys2.org/) 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.
|
||
|
||
## Flashing the Nano over WiFi
|
||
|
||
The ESP32 can flash the Nano for you, so you don't need a USB cable at the display:
|
||
|
||
```sh
|
||
pio run -e nano_ota -t upload
|
||
```
|
||
|
||
What happens:
|
||
|
||
1. The uploader ([tools/nano_net_upload.py](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.
|
||
|
||
## 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](#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 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), 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.
|
||
30 seconds after each of those minutes, the relay turns the display off.
|
||
- **23:31–08:00:** the screen is cleared and power is cut after 30 seconds.
|
||
3. **Home Assistant switches** (MQTT discovery):
|
||
- `FlipDot Display Clock`: off shows a Nintendo logo instead of the clock.
|
||
- `FlipDot Display Power`: off clears the display and cuts power through the relay.
|
||
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](src/nano/README.md).
|
||
|
||
## TODO
|
||
|
||
- [ ] **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](#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.
|
||
- [ ] **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.
|