Setup¶
Hardware¶
Adafruit Feather ESP32-S3 (4 MB flash, 2 MB PSRAM)
MAX17048 fuel gauge (on the Feather: I²C 0x36, SDA GPIO 3, SCL GPIO 4, powered via GPIO 7)
Waveshare 4.2” e-Paper Module (SKU 13353, 400×300, b/w, SSD1683), see Wiring
A LiPo battery (2000 mAh here) on the Feather’s JST connector
Flashing MicroPython¶
MicroPython v1.29.0 (ESP32_GENERIC_S3); the standard variant fits 4 MB of flash:
curl -o firmware/ESP32_GENERIC_S3-20260824-v1.29.0.bin \
https://micropython.org/resources/firmware/ESP32_GENERIC_S3-20260824-v1.29.0.bin
# ROM bootloader: hold BOOT, press Reset, release BOOT
uvx esptool --chip esp32s3 erase-flash
uvx esptool --chip esp32s3 write-flash 0x0 firmware/ESP32_GENERIC_S3-20260824-v1.29.0.bin
# then press Reset once
Debian’s esptool 4.7 package is broken for the ESP32-S3 (missing stub), hence uvx esptool.
Configuration¶
The settings come in two parts: src/defaults.py holds all defaults and is versioned;
src/config.py belongs to your installation and is ignored by git. It starts with
from defaults import * and adds the credentials (WLAN, Home Assistant, MQTT) and whatever
should differ from the defaults.
cp src/config_example.py src/config.py # fill it in; it is never committed
make deploy
make watch
WIFI_NETWORKS may list several networks (at home and on a boat, say). The list order is the
priority: the first network in range wins, hidden networks (not in the scan) come last. If the
first network in the list worked last time (remembered in wlan_last.txt), the board connects
without a scan; otherwise it scans on every connect, so that at home it returns to the home
network instead of, for example, a travel router brought along.
Development tools¶
Requires uv; uv sync installs the dependencies (pytest, ruff,
mpremote, mpy-cross, …).
make check # ruff + pytest (including the CPython/MicroPython comparison)
make format # format the code
make docs # build this documentation into docs/_build/html
make deploy # compile the modules (.mpy), copy them to the board via USB, restart
make deploy-wlan # upload changed files over the WLAN and restart (HOST=…)
make monitor # print the battery state every 5 s via USB (Ctrl+C ends)
make watch # query the battery state every 5 s via WLAN (HOST=epaper.local)
make fonts # regenerate the bitmap fonts from DejaVu Sans
make icons # regenerate the weather icons (Phosphor Icons Bold)
make micropython # build the MicroPython unix port (for the comparison in make check)
uv run tools/preview.py preview.png [--live] [--charging] [--link URL] [--alerts ID …]
uv run tools/board_tiles.py pull|push # tile settings as a local file (tiles.json)
uv run tools/serve_ui.py # try the web interface on the PC (http://localhost:8080/)
uv run tools/qr_sticker.py sticker.png [--size-mm 30] # QR code to print
uv run tools/battery_forecast.py [--since …] # discharge rate and runtime from Home Assistant
When deploying, all modules except boot.py, main.py and config.py are precompiled with
mpy-cross; that saves import time on every wake (the large fonts load in 0.2 instead of
1.1 s). Replaced .py files are deleted on the board.
Versions¶
Versions follow Semantic Versioning and are git tags (v0.2.0). When
building the firmware, tools/firmware.py adds version.mpy with the output of
git describe: 0.2.0 for a release, 0.2.0-3-g6ec6236 three commits later, with -dirty
for uncommitted changes. The board shows it in the boot sequence, in the web interface and as
firmware version of the device in Home Assistant; the docs show the same version.
To release, tag and push:
git tag -a v0.2.0 -m "Version 0.2.0"
git push origin main v0.2.0
Updates over the WLAN¶
make deploy-wlan works at any time. If the board is asleep, it switches on the
“Maintenance mode” switch via MQTT and waits until the board sees it at its next data
update (during the day within 10 minutes, at night or with a low battery up to an hour) and stays
awake; afterwards it switches it off again. --no-reset skips the restart after the upload, for
instance during a battery measurement.
The update endpoints are protected with UPDATE_PASSWORD from config.py (or
WEBREPL_PASSWORD as a fallback). A watchdog restarts the board if main.py hangs for longer
than WATCHDOG_S, even when interrupted via mpremote.