A TfL + weather + bedroom sensors dashboard for the reTerminal E1001, with a Cloudflare Worker API and full-screen 1-bit ePaper refresh.
The slow refresh of SenseCraft HMI simply wasn't cutting it for my ideal use case, and the absolute positioning of UI elements was also very annoying, as my fonts are not monospaced.
So I decided to rewrite everything in Arduino (not Rust!)...
Everything you need to run the dashboard is already in the repo — icon and font headers under arduino/dashboard/ are pre-generated. You do not need to run asset or font conversion scripts unless you are changing those source files (see Development).
Copy .env.example to .env and fill in your values, then sync derived config files:
cp .env.example .env
npm run sync-envThis writes:
| File | Purpose |
|---|---|
cloudflare/.dev.vars |
API keys for local wrangler dev |
cloudflare/wrangler.jsonc |
Worker vars (TfL stop, weather location, weather cache TTL) and KV namespace binding |
arduino/dashboard/config.h |
WORKER_URL, WiFi credentials, refresh intervals (gitignored) |
Re-run npm run sync-env whenever you change .env.
cd cloudflare
npm install
npm run deploySet production secrets once:
npx wrangler secret put TFL_API_KEY
npx wrangler secret put METOFFICE_API_KEYThe worker caches weather data in a KV namespace (CACHE binding in wrangler.jsonc) to stay within Met Office rate limits. TfL provides a much more generous rate limit, therefore responses are always fetched fresh (in-memory only per isolate for in-flight dedup and error fallback). Namespace IDs are safe to commit — they are resource identifiers, not secrets. If you deploy to your own Cloudflare account for the first time, create namespaces and update the IDs in wrangler.jsonc:
cd cloudflare
npx wrangler kv namespace create CACHE
npx wrangler kv namespace create CACHE --previewPut the deployed worker URL in .env as WORKER_URL, then run npm run sync-env again so the firmware picks it up.
For local worker development: npm run dev (http://localhost:8787). Local dev uses the preview KV namespace; API responses are not edge-cached (Cache-Control: private, no-store).
Libraries (Arduino Library Manager or manual install):
- Seeed_GFX — enable
#define SMOOTH_FONTinUser_Setup.h - ArduinoJson
- Sensirion I2C SHT4x
Board config: generate driver.h for the reTerminal E1001 using the Seeed GFX Configuration Tool (board combo 520).
Flash arduino/dashboard/dashboard.ino to the device. Ensure npm run sync-env has populated arduino/dashboard/config.h with your WiFi and worker URL.
These steps are only needed when changing source assets or fonts, not for normal setup.
npm run sync-env — propagates .env to Cloudflare and Arduino config. Run after any .env edit.
Source SVGs live in assets/ (icons, weather, TfL). Generated 1-bit PROGMEM headers are committed under arduino/dashboard/assets/.
If you add or edit an SVG:
npm run convert-assetsRe-flash the firmware after regenerating headers.
VLW font headers are committed under arduino/dashboard/fonts/. The firmware auto-enables Montserrat when those headers are present.
To regenerate fonts from assets/fonts/*.ttf:
npm run generate-fonts # writes manifest + instructionsFollow arduino/dashboard/fonts/README.md to produce new .h files with TFT_eSPI's Create_font.pde, then commit them.
For the browser preview only:
npm run setup-fonts # downloads TTF + woff2 into preview/fonts/files/Smoke-test the 800×480 layout in a browser — no device required (though not very accurate haha):
npm run previewOpen http://localhost:5173/preview/. The preview script downloads Montserrat web fonts automatically on first run.
I wanted TfL ETAs on the E1001 without the full-panel flicker of SenseCraft HMI's refresh cycle, which takes ~3 seconds per refresh and looks glitchy.
I originally wanted to recreate the dashboard with 1-bit partial updates for the maximum smoothness, but experienced multiple issues including but not limited to:
- Differential fading, where static elements fade in color over time.
- Ghosting artifacts, where previous text are not fully cleared by the partial updates.
- Ink bleeding, where the background turns grayish over time, no idea why this happens.
so this firmware uses 1-bit full-screen GC refresh (epaper.update()) on a fixed schedule instead of partial updates. With a 1-bit design, the refresh flicker is fast enough for me so that it actually looks like a refresh rather than a display glitch.
- On-device RTC (NTP, Europe/London timezone), battery, SHT4x temperature/humidity
- Met Office DataHub — site-specific forecast (via worker)
- TfL Unified API (via worker)
- Arduino + Seeed_GFX
- Cloudflare Worker — JSON API (
/,/tfl,/weather); weather cached in KV, TfL fetched fresh
| What | Interval |
|---|---|
| Display (full GC refresh) | 20 s |
| TfL fetch | 20 s |
| Bedroom sensors + battery | 3 min |
| Weather fetch (worker caches Met Office for 15 min) | 15 min |
| Date rollover | midnight |
Stale API data keeps the last good payload on screen; a hourglass icon indicates stale TfL data, a warning icon indicates disruption or non–Good Service.
{
"tfl": {
"line": "Elizabeth line",
"stop": "Ealing Broadway",
"direction": "Eastbound",
"status": "Good Service",
"disruption": false,
"etas": [{ "destination": "Shenfield", "minutes": 1 }, { "destination": "Abbey Wood", "minutes": 2 }]
},
"weather": {
"location": "Ealing, London",
"description": "Overcast",
"icon": "wi-cloudy",
"tempC": 28.9,
"feelsLikeC": 27.5,
"minC": 17.2,
"maxC": 29.0,
"precipMm": 0.0
}
}Credits: SenseCraft HMI, erikflowers/weather-icons.
