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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 22:42:46 +02:00
1 parent e991210ce6
commit 58a7cb03c2
23 files changed
+532 -273

No files matched your search

+5 -5
View File
@@ -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.
+1 -1
View File
@@ -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};
+4 -1
View File
@@ -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";