# eink-screen — HelmLog remote e-paper display
Firmware for a **LilyGo T5 ePaper S3 PRO** (the phone-shaped ESP32-S3 device
with GPS/LoRa/touch, 4.7" 960×540 e-paper, MagSafe back) that acts as a cockpit
**instrument repeater** for [HelmLog](../helmlog).
The panel is a thin frame-buffer client: every cycle it fetches a pre-rendered,
pre-packed 4-bit frame from HelmLog's `GET /api/display.raw` and blits it. All
layout and formatting live in HelmLog (Python/Pillow), so restyling the display
never requires reflashing — and the refresh cadence is controlled from HelmLog.
```
LilyGo T5-S3 PRO ──GET /api/display.raw (Bearer key)──▶ HelmLog (Pi)
960×540 ◀── 259200 packed 4-bit bytes ─── renders STW, VMG,
e-paper (epdiy) ◀── X-Refresh-Seconds header ─── TWA/TWS/AWA/AWS
```
## What it shows
STW, VMG (`STW·cos(TWA)`), TWA, TWS, AWA, AWS, plus a clock and a
LIVE / NO DATA badge — rendered server-side (`helmlog/src/helmlog/
display_render.py`).
## Display stack (important)
This board is driven by **epdiy** with the **V7 board driver** (`epd_board_v7`),
vendored under `lib/epdiy`. That driver enables the panel's 15–22 V supply via
an on-board **PCA9555 I/O expander over I2C_NUM_0**.
- Do **not** use the plain `LilyGo-EPD47` library — it does not power this
board's panel (WiFi/CPU run, but the screen stays blank).
- Do **not** call `Wire.begin()` — it claims `I2C_NUM_0` and makes epdiy's own
I2C init fail, so panel power never turns on. Let `epd_init()` own I2C.
## Setup
1. **Install [PlatformIO Core](https://platformio.org/install/cli)** (`pio`).
2. **Configure** — copy the template and fill in your values:
```sh
cp include/config.example.h include/config.h
$EDITOR include/config.h
```
- `NETWORKS[]` — one entry per place the boat/Pi lives (marina, home, …),
each `{ ssid, password, host, port }`. `host` may be a LAN IP or an mDNS
name (`corvopi-live.local`, resolved on-device). Port is HelmLog's
`WEB_PORT` (default 3002).
- `HELMLOG_API_KEY` — a HelmLog **device API key** (admin → Devices), scoped
to `GET /api/display.*`. Sent as `Authorization: Bearer <key>`.
`include/config.h` is gitignored — credentials never get committed.
The firmware tries the networks in order and **locks onto the first where
HelmLog actually answers** (TCP probe), so a known AP that's in range but
doesn't have the Pi on it (e.g. the marina AP reachable from home) is skipped
automatically. If HelmLog stops responding it re-selects. The ESP32 can't
join Tailscale, so each `host` must be reachable on that LAN — not a `100.x`
tailnet address.
3. **Build & flash** (USB-C):
```sh
pio run --target upload && pio device monitor
```
- If upload can't find/open the port, the native-USB port sometimes drops
after a reset. Put the board in **download mode** — hold **BOOT**, tap
**RST**, release **BOOT** — or unplug/replug USB-C, then retry.
- Target a specific port with `--upload-port /dev/cu.usbmodemXXXX` if
auto-detect grabs the wrong device.
## Testing the server from your Mac (Tailscale)
You don't need the device — or the boat LAN — to see what the panel will show.
If your Mac and the Pi are both on Tailscale:
```sh
KEY=your-device-api-key
curl -s -H "Authorization: Bearer $KEY" \
http://corvopi-live.<your-tailnet>.ts.net:3002/api/display.png -o preview.png
open preview.png
```
`display.png` is the human-viewable render; `display.raw` is the byte-identical
frame the device blits. The device can't use this Tailscale name — it's a dev
convenience only.
## Refresh cadence
Controlled entirely from HelmLog — no reflashing:
- `DISPLAY_REFRESH_SECONDS` (default 5) — how often the panel refetches/redraws.
- `DISPLAY_FULL_REFRESH_SECONDS` (default 300) — de-ghost cadence.
Set these in HelmLog's admin Settings page. They ride along in the
`X-Refresh-Seconds` / `X-Full-Refresh-Seconds` response headers; the `*_DEFAULT`
values in `config.h` are only fallbacks. Each update flashes the panel, so
20–30 s is easier on the eyes and the panel than 5 s.
## Power off / sleep
There's no mechanical power switch, and **e-paper keeps its last image with no
power** — so you can't tell on/off by looking at the screen. To make it
unambiguous:
- **Hold the BOOT button (~3 s).** The panel draws a persistent **"DISPLAY OFF —
press BOOT or RST to wake"** screen, then the device deep-sleeps. That
message staying on screen *is* your "it's off" confirmation (instruments = on,
"DISPLAY OFF" = asleep).
- **Wake** with a **BOOT** press or **RST** — it cold-boots back into the live
display.
On battery this drops to microamps. Plugged into USB it can't fully power down
(USB keeps it alive), but the fetch/redraw loop stops until you wake it. For
true battery-disconnect during long storage, unplug USB while it's in DISPLAY
OFF (a BQ25896 ship-mode trigger could be added later).
## Hardware notes
- Board: LilyGo T5 ePaper S3 PRO (ESP32-S3, 16 MB flash / 8 MB PSRAM).
- Pinned to `espressif32@6.5.0` (matches LilyGo's own PRO project). The panel
runs at a reduced pixel clock on Arduino (32 B cache line) — expected.
- Restore the factory launcher from `Xinyuan-LilyGO/T5S3-4.7-e-paper-PRO`
(`examples/factory` / prebuilt `firmware/`).