Split the README into docs/ pages, add web UI screenshots
- README is the overview; web UI, pages, Home Assistant, how it works, wiring, Nano over WiFi and the recovery app have their own page in docs/. - docs/web-ui.md has a screenshot of every tab, made with headless Edge by tools/make_webui_screenshots.py. - The web UI opens a page in the editor with #pages/<name>. Version 2.7.2. - The reply wire is fitted (flashing the Nano over WiFi works). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -38,6 +38,10 @@ See README.md for the overview.
|
||||
`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/G/N/E as commands.
|
||||
- Open hardware TODOs are listed in the README `TODO` section. Update it when one is done.
|
||||
- The README is the overview; the documentation is in `docs/` (web UI, pages, Home
|
||||
Assistant, how it works, wiring, Nano over WiFi, recovery app). Screenshots:
|
||||
`tools/make_webui_screenshots.py` (headless Edge) and `tools/make_page_screenshots.py`
|
||||
write `Pictures/web-ui/` and `Pictures/pages/`; run them again after UI changes.
|
||||
- `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
|
||||
@@ -65,8 +69,8 @@ See README.md for the overview.
|
||||
- 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.
|
||||
acknowledgement. The Nano's TX is wired to the ESP's RX (flashing over WiFi works), but
|
||||
every wait for a reply still 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
|
||||
|
||||
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 158 KiB |
@@ -23,7 +23,7 @@ too, and it boots again each time the relay switches on. Only the ESP32 runs all
|
||||
| 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 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](#web-ui), exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates. |
|
||||
| **ESP32** | [src/esp32](src/esp32/README.md) | 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](docs/web-ui.md), exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates. |
|
||||
| **ESP32 recovery app** | [src/recovery](src/recovery/README.md) | Small second app: WiFi setup page and OTA, so the ESP32 stays reachable when the clock firmware fails. |
|
||||
| **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. |
|
||||
|
||||
@@ -31,6 +31,20 @@ Because the driver is an `Adafruit_GFX` subclass on both boards, the ESP32 can u
|
||||
normal GFX API (`print`, `drawBitmap`, `fillScreen`, ...). Each call is sent to the
|
||||
Nano and drawn there.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | About |
|
||||
|------|-------|
|
||||
| [Web UI](docs/web-ui.md) | The web page of the display, with screenshots of every tab |
|
||||
| [Pages](docs/pages.md) | Pictures of the dynamic pages |
|
||||
| [Home Assistant](docs/home-assistant.md) | Entities, values on pages, rules |
|
||||
| [How it works](docs/how-it-works.md) | Boot, the clock and the schedule, the Nano |
|
||||
| [Wiring](docs/wiring.md) | Between the ESP32, the Nano and the relay |
|
||||
| [Flashing the Nano over WiFi](docs/nano-ota.md) | `nano_ota`, bootloaders, troubleshooting |
|
||||
| [Recovery app](docs/recovery.md) | The ESP32's second app: WiFi setup and OTA, one-time USB installation |
|
||||
| [Nano firmware](src/nano/README.md) | The serial protocol and the Nano's behaviour |
|
||||
| [ESP32 firmware](src/esp32/README.md) | The files, keeping OTA alive, tests |
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
@@ -45,7 +59,10 @@ partitions.csv ESP32 flash layout (recovery app + two clock firmware sl
|
||||
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
|
||||
tools/ uploader for flashing the Nano over WiFi, font and picture scripts
|
||||
docs/ documentation (see below)
|
||||
Pictures/ photos, pictures of the pages and screenshots of the web UI
|
||||
examples/ example pages to import in the web UI
|
||||
archive/ old, unused sketches kept for reference
|
||||
CLAUDE.md project notes for Claude Code
|
||||
```
|
||||
@@ -63,10 +80,10 @@ this folder.
|
||||
| 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). |
|
||||
| `nano_ota` | Arduino Nano (ATmega328P) | Over WiFi through the ESP32 (`10.1.1.198`). See [Flashing the Nano over WiFi](docs/nano-ota.md). |
|
||||
| `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](#recovery-app) |
|
||||
| `recovery` | ESP32 DevKit v1 | USB (`COM11`), one-time install of the [recovery app](docs/recovery.md) |
|
||||
|
||||
```sh
|
||||
pio run # build nano + esp32
|
||||
@@ -87,266 +104,10 @@ In VS Code you can also pick the environment in the PlatformIO status bar.
|
||||
> 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; shown as a number with multiplier, decimals, auto unit `1257 W` → `1.3 kW` and optionally without minus sign, as a duration `2h 21m` / `2:21:00` / `2:21` / `2 uur`, as a time from an HA timestamp `23:07` / `do 23:07` / `15-10 23:07` / `over 34 min`, or with own texts per state `printing=Bezig, standby=Klaar`), 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. |
|
||||
| **Schedule** | What is shown when, every day: blocks from a start time until the next block, each with up to 6 pages or bitmaps that rotate (each for its own time, 5 s to 1 hour), switching with the refresh effect or instantly. Rules: a Home Assistant state (entity or attribute, `=`, `is not`, `above`, `below`) that shows a page while it is true, adds it to the rotation while it is true, or shows it for a while when it becomes true; each rule is read at its own interval (2-600 s) and can be limited to some of the blocks. A page chosen by hand (Show tab, HA "Mode") is shown until the next block starts; "Back to schedule" ends it earlier. The display schedule (Settings) still decides when the display is on. |
|
||||
| **Stopwatch** | Start, stop and reset the stopwatch that pages can show. |
|
||||
| **Bitmaps** | All bitmaps with a preview (the ones that pages or rules use in their own subsection): 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](#one-time-installation-usb);
|
||||
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.
|
||||
|
||||
## Pages
|
||||
|
||||
The dynamic pages on the display, drawn by the ESP32 itself (`python
|
||||
tools/make_page_screenshots.py` makes these pictures again). The Clock page is built in;
|
||||
the others are the example pages in [examples/home-pages.json](examples/home-pages.json)
|
||||
(import them in the web UI, System tab; they use this home's Home Assistant entities).
|
||||
|
||||
| Page | |
|
||||
|---|---|
|
||||
| **Clock** (built in), day and from 17:00 |   |
|
||||
| **Weer**: clock, weather icon by state, humidity, temperature |  |
|
||||
| **Dag en nacht**: sun from 07:00, moon and stars from 19:00, date |   |
|
||||
| **Energie**: net power now, solar today, clock |  |
|
||||
| **3D printer**: progress, time left, status message (scrolls), nozzle temperature |  |
|
||||
| **Binnen**: room and outside temperature, CO2, air quality |  |
|
||||
| **Verbruik**: water and energy used today, solar today |  |
|
||||
| **Grote klok**: big clock, inverted from 22:00 |   |
|
||||
| **Datum**: weekday and date |  |
|
||||
| **Aftellen**: countdown to Christmas |  |
|
||||
| **Stopwatch** |  |
|
||||
|
||||
## 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.
|
||||
|
||||
## Recovery app
|
||||
|
||||
The ESP32 has a small second app, the **recovery app** ([src/recovery](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:
|
||||
```sh
|
||||
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:
|
||||
```sh
|
||||
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](#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](src/esp32/README.md#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.
|
||||
- `FlipDot Display Shown page` sensor: what the display shows now (schedule, rule or
|
||||
chosen page). `FlipDot Display Back to schedule` button: ends a page chosen by hand.
|
||||
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
|
||||
|
||||
- [X] **Install the recovery app** (one-time USB flash, see
|
||||
[One-time installation](#one-time-installation-usb)). Until then the ESP32 runs the old
|
||||
[One-time installation](docs/recovery.md#one-time-installation-usb)). 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".
|
||||
- [X] **Test the web UI on the real hardware**: live view, uploading and drawing a bitmap,
|
||||
@@ -360,8 +121,8 @@ The serial protocol is described in [src/nano/README.md](src/nano/README.md).
|
||||
- [X] **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,
|
||||
[Wiring between ESP32 and Nano](docs/wiring.md).
|
||||
- [X] **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`).
|
||||
- [X] **Test flashing over WiFi on the real hardware.** The uploader is tested against a
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Home Assistant
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
The ESP32 connects to Home Assistant in two ways:
|
||||
|
||||
- **MQTT** (with discovery): the display's own switches, dropdowns, sensors and buttons.
|
||||
Broker, user and password are in the web UI, Home Assistant tab (an empty broker uses
|
||||
`secrets.h`). Changing them restarts the ESP32.
|
||||
- **REST API**: values of any entity for the dynamic pages and the trigger rules. Set the
|
||||
URL (empty: `http://<broker>:8123`, plain http only, use the IP address) and a
|
||||
long-lived access token (Home Assistant, your profile, Security tab) in the same tab.
|
||||
|
||||
## Entities
|
||||
|
||||
- `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.
|
||||
- `FlipDot Display Shown page` sensor: what the display shows now (schedule, rule or
|
||||
chosen page). `FlipDot Display Back to schedule` button: ends a page chosen by hand.
|
||||
- `FlipDot Display Firmware` sensor: version and health, e.g. `2.7.2, ok, 0 crashes, recovery ok`.
|
||||
- `FlipDot Display Recovery` button: restarts into the [recovery app](recovery.md) for 10
|
||||
minutes.
|
||||
|
||||
The unique ids of these entities never change, so automations and dashboards keep working
|
||||
after firmware updates.
|
||||
|
||||
## Values on dynamic pages
|
||||
|
||||
A Home Assistant value element on a [page](pages.md) shows an entity's state or one of
|
||||
its attributes, as a number (multiplier, decimals, auto unit, without minus sign), a
|
||||
duration, a time from a timestamp, or with own texts per state (see the
|
||||
[web UI](web-ui.md#pages)).
|
||||
|
||||
**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.
|
||||
|
||||
## Rules
|
||||
|
||||
The [schedule](web-ui.md#schedule) can bring a page to the front from a Home Assistant
|
||||
state, e.g. the 3D printer page while `sensor.mainsailos_current_print_state` is
|
||||
`printing`. Each rule is read at its own interval; when Home Assistant can't be reached it
|
||||
is read once a minute at most, so the display keeps running.
|
||||
@@ -0,0 +1,26 @@
|
||||
# How it works
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
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](../src/esp32/README.md#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: the chosen page (by default the built-in Clock page,
|
||||
Pikachu with a thought bubble showing `HH:MM`, yawning from 17:00), or what the
|
||||
[schedule and rules](web-ui.md#schedule) pick.
|
||||
- **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**: switches, dropdowns and sensors over MQTT, see
|
||||
[Home Assistant](home-assistant.md).
|
||||
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).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Flashing the Nano over WiFi
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Pages
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
The dynamic pages on the display, drawn by the ESP32 itself (`python
|
||||
tools/make_page_screenshots.py` makes these pictures again). The Clock page is built in;
|
||||
the others are the example pages in [examples/home-pages.json](../examples/home-pages.json)
|
||||
(import them in the web UI, System tab; they use this home's Home Assistant entities).
|
||||
|
||||
| Page | |
|
||||
|---|---|
|
||||
| **Clock** (built in), day and from 17:00 |   |
|
||||
| **Weer**: clock, weather icon by state, humidity, temperature |  |
|
||||
| **Dag en nacht**: sun from 07:00, moon and stars from 19:00, date |   |
|
||||
| **Energie**: net power now, solar today, clock |  |
|
||||
| **3D printer**: progress, time left, status message (scrolls), nozzle temperature |  |
|
||||
| **Binnen**: room and outside temperature, CO2, air quality |  |
|
||||
| **Verbruik**: water and energy used today, solar today |  |
|
||||
| **Grote klok**: big clock, inverted from 22:00 |   |
|
||||
| **Datum**: weekday and date |  |
|
||||
| **Aftellen**: countdown to Christmas |  |
|
||||
| **Stopwatch** |  |
|
||||
@@ -0,0 +1,82 @@
|
||||
# Recovery app
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
The ESP32 has a small second app, the **recovery app** ([src/recovery](../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:
|
||||
```sh
|
||||
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:
|
||||
```sh
|
||||
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`.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Web UI
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
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. Each tab
|
||||
has its own address, e.g. `http://10.1.1.198/#schedule`; `#pages/<page name>` opens a page
|
||||
in the editor.
|
||||
|
||||
The screenshots are made with `python tools/make_webui_screenshots.py` (Edge or Chrome in
|
||||
headless mode).
|
||||
|
||||
## 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, and what is left on the dots when the display
|
||||
switches off. With a [schedule](#schedule), a page chosen here (or with the Home Assistant
|
||||
"Mode" dropdown) is shown until the next block starts; "Back to schedule" ends it earlier.
|
||||
Bitmaps that pages or rules use are in their own subsection.
|
||||
|
||||

|
||||
|
||||
## 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. The preview is drawn by the ESP32, so it
|
||||
is exactly what the display shows; drag the elements on it, or select one and use the
|
||||
arrow keys. Built-in pages can be copied, not changed. See the [pictures of the pages](pages.md).
|
||||
|
||||
| Element | Options |
|
||||
|---------|---------|
|
||||
| Clock | `08:05`, `8:05`, optionally with seconds |
|
||||
| Date | 7 formats, Dutch or English (`do 15 okt`, `15 oktober`, `donderdag`, ...) |
|
||||
| Home Assistant value | An entity or attribute, text before and after. Shown as a number (multiplier, decimals, auto unit `1257 W` → `1.3 kW`, without minus sign), a duration (`2h 21m`, `2:21:00`, `2:21`, `2 uur`), a time from an HA timestamp (`23:07`, `do 23:07`, `15-10 23:07`, `over 34 min`) or with own texts per state (`printing=Bezig, standby=Klaar`) |
|
||||
| Text | Up to 32 characters |
|
||||
| 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 | `4:05`, `0:04`, `0:04:05`; started in the [Stopwatch](#stopwatch) tab or Home Assistant |
|
||||
| Image | A bitmap at a position, fixed or chosen by a Home Assistant state (`weer ` + `cloudy` = the bitmap `weer cloudy`) |
|
||||
|
||||
Every element has a position, alignment, font and overlay (on top, box that clears the
|
||||
dots behind it, inverted). Texts can scroll in a window at a chosen speed and can contain
|
||||
the degree sign. 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.
|
||||
|
||||

|
||||
|
||||
## Schedule
|
||||
|
||||
What is shown when, every day: blocks from a start time until the next block, each with up
|
||||
to 6 pages or bitmaps that rotate (each for its own time, 5 s to 1 hour), switching with
|
||||
the refresh effect or instantly.
|
||||
|
||||
Rules bring a page to the front from a Home Assistant state (an entity or attribute, `=`,
|
||||
`is not`, `above`, `below`): they show it while the state is true, add it to the rotation
|
||||
while it is true, or show it for a while when it becomes true. Each rule is read at its own
|
||||
interval (2-600 s) and can be limited to some of the blocks. The first rule that shows a
|
||||
page wins. The display schedule ([Settings](#settings)) still decides when the display is on.
|
||||
|
||||

|
||||
|
||||
## Stopwatch
|
||||
|
||||
Start, stop and reset the stopwatch that pages can show (also in Home Assistant).
|
||||
|
||||

|
||||
|
||||
## Bitmaps
|
||||
|
||||
All bitmaps with a preview: show one as the page, send it live, open it in the editor,
|
||||
export or delete it. The built-in ones (Nintendo, Pikachu, Pikachu yawning) can't be
|
||||
deleted, nor can a bitmap that a page, the schedule or a rule uses.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
The display 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
|
||||
|
||||
MQTT: 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 and the rules (REST API, plain http; empty URL: `http://<broker>:8123`). See
|
||||
[Home Assistant](home-assistant.md).
|
||||
|
||||

|
||||
|
||||
## System
|
||||
|
||||
Firmware status, uptime, WiFi, storage; restart; start the [recovery app](recovery.md).
|
||||
|
||||
**Backup:** export everything (settings, the schedule and rules, 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, e.g. to share one page (with the bitmaps and fonts it uses).
|
||||
|
||||

|
||||
|
||||
## Good to know
|
||||
|
||||
**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](recovery.md#one-time-installation-usb);
|
||||
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 only the dots that changed, not faster than the Nano can flip them: with 3
|
||||
pulses and a 300 µs gap a 1-dot scroll step of a line of text takes about 0.1-0.2 s.
|
||||
|
||||
**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.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Wiring between ESP32 and Nano
|
||||
|
||||
[← Back to the README](../README.md)
|
||||
|
||||
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](nano-ota.md#bootloaders). |
|
||||
| GND | GND | Common ground |
|
||||
| GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) |
|
||||
@@ -5,7 +5,7 @@ PlatformIO environments: `esp32` (OTA) and `esp32_usb` (USB). Tests: `test_esp32
|
||||
The ESP32 decides what is shown on the display. It never touches the panel itself. The
|
||||
BROSE9323 driver is built with `-D ESPSERIAL`, so every GFX call is sent to the Nano over
|
||||
`Serial2` (UART2, GPIO 17 TX / GPIO 16 RX, 115200 baud). `Serial` (USB) is only used for
|
||||
debug output. Wiring is described in the [main README](../../README.md#wiring-between-esp32-and-nano).
|
||||
debug output. Wiring is described in the [documentation](../../docs/wiring.md).
|
||||
|
||||
## Files
|
||||
|
||||
@@ -53,7 +53,7 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
||||
| `net/wifi_ota.*` | WiFi (with a restart when it stays down), ESP32 OTA updates, OTA-only mode |
|
||||
| `net/home_assistant.*` | MQTT and the Home Assistant entities (power switch, mode and off image dropdowns, IP and firmware sensors, recovery button) |
|
||||
| `net/ha_api.*` | Reads Home Assistant entity values over the REST API for the dynamic pages |
|
||||
| **`web/`** | **Web UI** (see the [main README](../../README.md#web-ui)) |
|
||||
| **`web/`** | **Web UI** (see the [documentation](../../docs/web-ui.md)) |
|
||||
| `web/web_ui.*` | Web server and JSON API (the endpoints are listed in `web_ui.h`) |
|
||||
| `web/page_json.*` | Dynamic pages as JSON |
|
||||
| `web/schedule_json.*` | The page schedule and rules as JSON |
|
||||
@@ -73,7 +73,7 @@ again. Several layers take care of that:
|
||||
| A new OTA firmware crashes, hangs or can't connect to WiFi | **Rollback.** A new firmware is only marked as good after it has run for 60 s with WiFi. If it restarts before that, the bootloader starts the previous firmware again. |
|
||||
| The firmware hangs in `loop()` | **Watchdog.** If `loop()` doesn't run for 30 s, the ESP32 restarts. That counts as a crash. |
|
||||
| The firmware crashes once or twice | **OTA window.** After a crash the ESP32 runs only WiFi and OTA for 15 s (LED blinks fast), then starts the main code again. |
|
||||
| The firmware keeps crashing (3 times in a row) | **Recovery app** for 10 minutes (see the [main README](../../README.md#recovery-app)). Then the main code gets one more try; one more crash brings the recovery app back. Running 5 minutes without a crash clears the counter. Without the recovery app installed: only WiFi and OTA run, for 10 minutes. |
|
||||
| The firmware keeps crashing (3 times in a row) | **Recovery app** for 10 minutes (see the [documentation](../../docs/recovery.md)). Then the main code gets one more try; one more crash brings the recovery app back. Running 5 minutes without a crash clears the counter. Without the recovery app installed: only WiFi and OTA run, for 10 minutes. |
|
||||
| WiFi doesn't connect at boot (30 s), or stays down for 10 minutes | **Recovery app**, which keeps trying and opens the **FlipDot-Setup** WiFi with a setup page to choose another WiFi. Without the recovery app: restart. |
|
||||
| The firmware runs, but its OTA is broken | **Recovery app** through the Home Assistant button, or by switching the power on and off 3 times quickly. |
|
||||
|
||||
@@ -102,7 +102,7 @@ Rules for changing the code:
|
||||
- **Status on the display:** `Connecting to WiFi...` and `Getting the time...` at boot,
|
||||
`Crashed!` messages in the OTA-only modes, and `No WiFi` with the time when WiFi is down
|
||||
for more than a minute. See the table in the
|
||||
[main README](../../README.md#recovery-app).
|
||||
[documentation](../../docs/recovery.md).
|
||||
|
||||
- **Frames only when something changes:** a new frame is sent when the minute changes, the
|
||||
screen changes (clock, page, off image) or the Nano was just powered on. The serial line is
|
||||
@@ -134,7 +134,7 @@ Rules for changing the code:
|
||||
idle UART line is high and would otherwise power the Nano through its RX pin.
|
||||
- **Nano flash bridge:** `pio run -e nano_ota -t upload` flashes the Nano through the ESP32.
|
||||
While a flash session runs, the clock is paused. See the
|
||||
[main README](../../README.md#flashing-the-nano-over-wifi).
|
||||
[documentation](../../docs/nano-ota.md).
|
||||
- **Status LED** (built-in): off while the display is off, slow blink with WiFi, fast blink
|
||||
without WiFi.
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi
|
||||
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
|
||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
||||
|
||||
#define FIRMWARE_VERSION "2.7.1"
|
||||
#define FIRMWARE_VERSION "2.7.2"
|
||||
|
||||
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
||||
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||
|
||||
@@ -549,6 +549,9 @@ function collect(){const s=JSON.parse(JSON.stringify(settings));
|
||||
if($('ha_pass').value)s.home_assistant.password=$('ha_pass').value;if($('ha_token').value)s.home_assistant.token=$('ha_token').value.trim();return s}
|
||||
async function saveSettings(){const r=await post('/api/settings',collect());settings=r.settings;fill();msg(r.restart?'Saved, restarting...':'Settings saved')}
|
||||
async function saveHa(){await saveSettings()}
|
||||
async function start(){settings=await api('/api/settings');fill();tab((location.hash||'#show').slice(1));pollScreen();pollStatus();loadLists()}
|
||||
// #tab, or #pages/<name> to open a page in the editor
|
||||
async function start(){settings=await api('/api/settings');fill();const[t,arg]=decodeURIComponent((location.hash||'#show').slice(1)).split('/');
|
||||
tab(t);pollScreen();pollStatus();await loadLists();
|
||||
if(t=='pages'&&arg){const pg=pageList.find(p=>same(p.name,arg));if(pg)peOpen(pg,pg.builtin)}}
|
||||
start();
|
||||
</script></body></html>)rawliteral";
|
||||
@@ -8,7 +8,7 @@ the clock firmware never change it. Its job: the ESP32 stays reachable, whatever
|
||||
to the clock firmware, so it never needs USB again.
|
||||
|
||||
Installing it and using it as a user is described in the
|
||||
[main README](../../README.md#recovery-app). This file is about how it works.
|
||||
[documentation](../../docs/recovery.md). This file is about how it works.
|
||||
|
||||
## Files
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Saves screenshots of the web UI tabs in Pictures/web-ui/, with Microsoft Edge (or Chrome)
|
||||
in headless mode.
|
||||
|
||||
python tools/make_webui_screenshots.py [ESP32 address, default 10.1.1.198]
|
||||
|
||||
Each tab is opened by its address (http://<ESP32>/#settings); the page editor with
|
||||
#pages/<page name>. Edge runs with its own temporary profile. Only the Python standard
|
||||
library is used.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
ESP = 'http://' + (sys.argv[1] if len(sys.argv) > 1 else '10.1.1.198')
|
||||
OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'Pictures', 'web-ui')
|
||||
BROWSERS = [
|
||||
r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
r'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
|
||||
r'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||||
'microsoft-edge', 'google-chrome', 'chromium',
|
||||
]
|
||||
WIDTH = 1000
|
||||
# File name, address part, window height
|
||||
SHOTS = [
|
||||
('show', 'show', 2150),
|
||||
('pages', 'pages/Weer', 3000), # the page list and the editor (tall: nothing has to scroll)
|
||||
('schedule', 'schedule', 1150),
|
||||
('stopwatch', 'stopwatch', 560),
|
||||
('bitmaps', 'bitmaps', 1600),
|
||||
('draw', 'draw', 820),
|
||||
('settings', 'settings', 1550),
|
||||
('home-assistant', 'ha', 780),
|
||||
('system', 'system', 1080),
|
||||
]
|
||||
|
||||
|
||||
def browser():
|
||||
for b in BROWSERS:
|
||||
path = shutil.which(b) or (b if os.path.exists(b) else None)
|
||||
if path:
|
||||
return path
|
||||
sys.exit('No Edge or Chrome found')
|
||||
|
||||
|
||||
def read_ha_values():
|
||||
"""Reads the Home Assistant values of all pages, so the previews show them"""
|
||||
with urllib.request.urlopen(ESP + '/api/pages', timeout=20) as r:
|
||||
pages = json.loads(r.read())['pages']
|
||||
for entity in {(e['entity'], e.get('attribute', '')) for p in pages for e in p['page']['elements'] if e.get('entity')}:
|
||||
query = urllib.parse.urlencode({'entity': entity[0], 'attribute': entity[1]})
|
||||
try:
|
||||
urllib.request.urlopen(ESP + '/api/ha/value?' + query, timeout=10).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def shoot(exe, profile, path, address, height):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
subprocess.run([exe, '--headless=new', '--disable-gpu', '--hide-scrollbars', '--user-data-dir=' + profile,
|
||||
'--window-size=%d,%d' % (WIDTH, height), '--virtual-time-budget=20000',
|
||||
'--screenshot=' + path, address],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=90)
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
pass # the browser hung: try again
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
exe = browser()
|
||||
read_ha_values()
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
profile = tempfile.mkdtemp(prefix='flipdot-edge-')
|
||||
try:
|
||||
for name, hash_part, height in SHOTS:
|
||||
path = os.path.join(OUT, name + '.png')
|
||||
ok = shoot(exe, profile, path, ESP + '/#' + hash_part, height)
|
||||
print(name, os.path.getsize(path) if ok and os.path.exists(path) else 'failed')
|
||||
finally:
|
||||
shutil.rmtree(profile, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||