| **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
| `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). |
> 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**.
2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. This
restarts the Nano, and its bootloader starts.
3. The ESP32 switches its link to the bootloader's 57600 baud 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).
- The Nano must have the **old bootloader** (the `nanoatmega328` board, 57600 baud), which
also runs at power-on. The newer Optiboot bootloader only runs after a reset-pin reset,
so the relay power cycle would not start it.
**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, baud rate and power-off time: `NANO_FLASH_PORT`, `NANO_BOOTLOADER_BAUD` and
`NANO_POWER_OFF_TIME` in `src/esp32/libs.h`.
**If it fails**
| Message | Cause |
|---------|-------|
| `can't connect to ...` | Wrong IP address, or the ESP32 is offline or running older firmware |
| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot |
| `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. |