A Build-Your-Own-Server backend for a custom TRMNL e-ink device. It speaks the TRMNL device API, renders an 800×480 dashboard itself, and serves it to the device as a 1-bit BMP — no TRMNL cloud involved.
Point your device's firmware at this server (custom server URL) and it will fetch its screens from here.
- Implements the TRMNL device endpoints:
/api/setup,/api/display,/api/log. - Renders a dashboard with satori (layout →
SVG) + resvg (SVG → raster), then encodes
a 1-bit
800×480BMP — the format the TRMNL panel expects. - Auto-registers devices by MAC on first contact (in-memory; see below).
| Method & path | Purpose |
|---|---|
GET /api/setup |
Device first-boot registration. Returns api_key + friendly_id. |
GET /api/display |
Polled every refresh_rate s. Returns the next image_url. |
POST /api/log |
Device log sink. |
GET /images/:f.bmp |
Serves a rendered screen (1-bit BMP). |
GET /preview |
PNG preview of the current dashboard — open it in a browser. |
GET /api/devices |
Operator view of devices that have checked in. |
GET /healthz |
Liveness/readiness probe. |
All via environment variables (see .env.example):
| Var | Default | Notes |
|---|---|---|
PORT |
8080 |
HTTP listen port. |
PUBLIC_BASE_URL |
(req) | Absolute URL the device can reach. Falls back to the Host header. |
REFRESH_RATE |
900 |
Device poll interval, seconds. |
DEVICE_NAME |
TRMNL |
Shown in the dashboard header. |
TZ |
UTC |
IANA timezone for the clock/date. |
LOG_LEVEL |
info |
pino log level. |
npm install
npm run dev # tsx watch on src/server.ts
# then open http://localhost:8080/previewSimulate a device:
curl -H "ID: AA:BB:CC:DD:EE:FF" localhost:8080/api/setup
curl -H "ID: AA:BB:CC:DD:EE:FF" -H "Battery-Voltage: 4.05" -H "RSSI: -62" \
localhost:8080/api/displaysrc/widgets.ts returns the dashboard content — it currently returns static
placeholders (weather / calendar / tasks). Replace it with real data sources;
src/screen.ts lays out whatever it returns. The screen is plain
satori-style elements (no JSX needed).
docker build -t trmnl-byos .
docker run -p 8080:8080 -e TZ=America/Sao_Paulo trmnl-byosCI (.github/workflows/build.yml) typechecks, builds, and publishes a
linux/amd64 image to ghcr.io/<owner>/trmnl on every push to main.
This app is deployed to the meduza Kubernetes cluster via Flux. The
manifests live in the spot GitOps repo
under apps/trmnl/.
- State is in-memory. Devices and rendered images don't survive a restart; devices simply re-register (by MAC) on their next poll. Add a real store if you need persistence.
- The 1-bit BMP encoder lives in
src/bmp.tsand thresholds on luminance.