Appearance
E-Ink Displays
SafeCall serves wifi e-ink displays (door signs, room status panels) that poll an unauthenticated endpoint and draw a layout designed by an admin in the web UI. The device-side protocol is documented in the E-Ink Display Spec.
Lifecycle
- Auto-registration — a display is configured with the server URL and starts polling
GET /eink/display?voltage=<v>&mac=<mac>. Unknown MACs are inserted intoeink_displayswithstate = 'pending'and receive a neutral "Pending verification" screen. Nothing about the system is revealed to unapproved devices. - Approval — an admin reviews the device under Devices → E-Ink Displays (MAC, IP, battery voltage, poll count, last seen) and approves or blocks it.
- Design — the admin designs the layout in the visual editor (canvas preview, all 8 drawing primitives, 12 fonts, 4 colors, plus sensor value elements bound to sensor beacons, filterable by location).
- Serving — on each poll the server renders the designer layout, substitutes live sensor readings, and responds with the device JSON encoded as Windows-1250.
Change detection (version bumping)
The firmware only redraws when the layout version changes (1–99, wraps to 1). On every poll the server:
- Renders the layout (pending screen, or designer layout + current sensor values).
- Hashes
{rotation, validity, objects}. - If the hash differs from
rendered_hashstored on the row, bumpsversion = version % 99 + 1and persists both.
Admin edits and sensor reading changes therefore propagate automatically on the next poll. Sensor values are rounded to the per-element decimals setting, which keeps tiny fluctuations from causing constant redraws.
HTTP endpoints
| Endpoint | Auth | Purpose |
|---|---|---|
GET|POST /eink/display (also with trailing slash) | none | Device poll: tracking + layout JSON (Windows-1250) |
GET /eink/displays | admin | Listing with location names |
POST /eink/display/:id | admin | Update name/location/size/rotation/validity/layout |
POST /eink/display/:id/state | admin | pending / approved / blocked |
DELETE /eink/display/:id | admin | Remove (device re-registers as pending if it keeps polling) |
GET /eink/sensors | admin | Sensor beacons + latest readings for the designer picker |
Abuse protection: MACs must be valid 12-hex-digit values, and auto-registration stops once 100 rows are pending (the neutral screen is still served, nothing is persisted).
Data model
eink_displays (migration 037_create_eink_displays.ts):
- Identity & state:
mac(unique),name,state,location_id - Display settings:
width,height(default 800x480),rotation(0–3),validity(poll seconds) - Layout:
layout(designer JSON),version(1–99),rendered_hash - Tracking:
last_voltage,last_ip,poll_count,first_seen,last_seen
The designer layout (stored in layout) uses numeric fields and adds a virtual sensor element (sensor_mac, metric, label, unit, decimals, position, font, color). At serve time eink_layout_render() materializes sensor elements into text objects using the latest sensor_data reading per metric (1 humidity, 2 temperature, 3 battery).
Key files
| File | Purpose |
|---|---|
lib/src/eink.ts | Shared module: designer/device schemas, eink_layout_render(), font/color tables, Windows-1250 encoder, pending screen. Used by server and web so the editor preview matches the device pixel-for-pixel |
server/src/eink.ts | Device poll handler, tracking, version bumping, admin endpoints |
web/src/admin/pages/eink-displays.ts | Listing page (approve/block/edit/delete) |
web/src/admin/pages/eink-designer.ts | Canvas layout editor |
server/tests/eink.test.ts | Unit tests (encoder, render) |
server/tests/api/eink.test.ts | End-to-end device + admin API tests |
web/tests/e2e/eink-displays.spec.ts | Playwright UI tests |
Encoding
Bun has no Windows-1250 encoder, so eink_encode_windows1250() maps the 0x80–0xFF range by lookup table (full Czech diacritics support) and replaces unmappable characters with ?. JSON structural characters are ASCII, so the encoded bytes remain valid JSON.