mrfyda/trmnl-joan-bridge
GitHub: mrfyda/trmnl-joan-bridge
Stars: 0 | Forks: 0
# trmnl-joan-bridge
A standalone Go server that drives a **Joan 6** e-ink display from a
[TRMNL](https://github.com/usetrmnl/byos_hanami) (BYOS) server — with
**no Visionect cloud (VSS) dependency**.
The Joan 6 is a 13" 1024×758 4-bit grayscale e-ink panel with a capacitive
touchscreen. Out of the box it only talks to Visionect's hosted software. This
shim reimplements the device-side wire protocol so the panel can be pointed at a
server you control and show anything you can render to an image — a Home
Assistant dashboard, a TRMNL plugin, a clock, whatever.
┌─────────┐ PV3 / TCP:11112 ┌───────────────────┐ HTTP /api/display ┌──────────┐
│ Joan 6 │ ◀─────────────────▶ │ trmnl-joan-bridge │ ◀───────────────────▶ │ TRMNL │
│ e-ink │ image frames │ (this repo) │ image_url + rate │ (BYOS) │
└─────────┘ └───────────────────┘ └──────────┘
## How it works
1. **Polls TRMNL** on the standard TRMNL device protocol: `GET /api/display`
with `ID` (the device MAC) and `Access-Token` headers. TRMNL replies with
an `image_url` and a `refresh_rate`.
2. **Fetches and encodes** the image into a Visionect **PV3** frame: resize to
1024×758, convert to 4-bit grayscale, pack 2 px/byte, split into 80
LZ4-compressed blocks, and wrap with the device descriptor and headers.
3. **Serves the panel** over raw TCP on port 11112. Joan opens a connection and
sends a status "hello" roughly every 3 minutes; the shim replies with a
session ACK and, when the image is new, the frame. When only part of the
screen changed it sends a **partial update** — just the changed rectangle, so
the panel does a fast, flicker-free local refresh instead of a full repaint; a
whole-screen change (or the first push after a (re)connect) sends a full frame.
See [`docs/partial-updates.md`](docs/partial-updates.md).
4. **Reports device health.** The status hello also carries battery voltage and
WiFi RSSI; the shim parses them and forwards them to TRMNL as the standard
`Battery-Voltage` and `RSSI` headers, so battery and signal appear in the
TRMNL device dashboard. See [`docs/status-hello.md`](docs/status-hello.md).
The PV3 wire format, block layout, and session handshake were reverse-engineered
from captured device traffic; see `docs/` for the protocol notes.
## Quick start
The image is published multi-arch (arm64 + amd64) to the GitHub Container
Registry by CI on every push to `main`.
### Docker
docker run -d --name trmnl-joan-bridge \
-p 11112:11112 \
-e TRMNL_SERVER="http://your-trmnl-host:2300" \
-e DEVICE_ID="AA:BB:CC:DD:EE:FF" \
-e ACCESS_TOKEN="your-trmnl-device-token" \
ghcr.io/mrfyda/trmnl-joan-bridge:latest
### Portainer / Docker Compose
services:
trmnl-joan-bridge:
image: ghcr.io/mrfyda/trmnl-joan-bridge:latest
restart: unless-stopped
ports:
- "11112:11112"
environment:
TRMNL_SERVER: http://your-trmnl-host:2300
DEVICE_ID: AA:BB:CC:DD:EE:FF # Joan MAC, UPPERCASE
ACCESS_TOKEN: your-trmnl-device-token
Then point the panel at the shim with the **Joan Configurator** app: set the
server address to `your-shim-host:11112`.
## Configuration
All configuration is via environment variables (or the equivalent flags).
| Variable | Required | Default | Description |
| ------------------ | -------- | --------- | ------------------------------------------------------------------ |
| `TRMNL_SERVER` | yes | — | TRMNL base URL, e.g. `http://192.168.1.10:2300` |
| `DEVICE_ID` | yes | — | Joan MAC address, **uppercase**, e.g. `AA:BB:CC:DD:EE:FF` |
| `ACCESS_TOKEN` | yes | — | TRMNL device access token |
| `REFRESH_INTERVAL` | no | `60s` | Fallback re-fetch interval when TRMNL omits `refresh_rate` |
| `LISTEN_ADDR` | no | `:11112` | TCP address the panel connects to |
## Building from source
# Local binary (requires Go 1.22+)
go build -o bin/trmnl-joan-bridge .
# Or via the Makefile, using a container toolchain
make build # native dev binary → bin/trmnl-joan-bridge-local
make build-arm # static linux/arm64 binary → bin/trmnl-joan-bridge
# Docker image
docker build -t trmnl-joan-bridge .
## Hardware notes
- **Panel:** Joan 6 — 1024×758, 4-bit grayscale e-ink, capacitive touch.
- **Transport:** Visionect PV3 over TCP port 11112.
- **Rotation:** the encoder applies a fixed 180° rotation to match this panel's
scan orientation.
## Repository layout
main.go TCP server, TRMNL polling, frame store, heartbeat loop
pv3/ PV3 wire protocol: framing + decode, full & partial frame encode, session ACK
docs/ reverse-engineered protocol notes
Dockerfile multi-stage build → scratch runtime image
.github/ CI: gofmt/vet/test + multi-arch build & push to ghcr.io
## Acknowledgements
Built by reverse-engineering the Visionect device protocol purely for
interoperability, to keep a perfectly good display out of a landfill.
标签:EVTX分析