thermoprint 24/7 print appliance — design spec
Self-host tomLadder/thermoprint on telep-mainframe so a Marklife P15 BLE label printer is available 24/7: design labels in thermoprint’s own web editor (hosted on the tailnet + landing page), print through the mainframe from any device. Related: telep-mainframe, homelab.
Approved decisions
- Approach A — server renders & prints. The editor POSTs a rendered label to a print service on the mainframe; the mainframe owns all BLE/print logic. (Not B, the WebSocket-tunnelled BLE transport.)
- Reuse thermoprint’s existing web UI — do not build a new UI. Patch the editor’s print action + connect UI only.
- Native Bun + systemd deployment (not Docker) — Noble needs a raw HCI socket; native is simplest for BLE.
Why a bridge is required
A BLE printer accepts one connection at a time. If the mainframe holds the printer, a browser’s Web Bluetooth cannot also connect. Therefore the editor must send jobs to the mainframe rather than print directly. The mainframe uses connect-per-job (connect → print → disconnect), so the printer is never held and is always grabbable.
Architecture
Three components, all on telep-mainframe:
-
Print service (
@thermoprint-based Bun app, systemd, 127.0.0.1:PORT)- Reuses thermoprint’s proven server-side print path:
@thermoprint/corepipeline (grayscale → dither → pack → L11) + the CLI’s NobleBleTransport(packages/cli/src/transport/noble.ts) + image load (packages/cli/src/image/load.ts, sharp). - Endpoints:
POST /print— body: PNG bytes (multipart or raw) + print options (density 1–3, paper gap/continuous, dither mode, threshold, headWidth default 384). Renders via core pipeline →printer.print(image, opts)over Noble → Marklife P15.GET /status— service up, configured printer name/id, last-print result, whether printer was reachable.
- Config: printer name/id (Marklife P15) via env or a small config file. One job at a time (in-process mutex/queue). Per-job connect with a short retry (wakes a sleeping printer).
- Reuses thermoprint’s proven server-side print path:
-
Hosted editor (
@thermoprint/web, static Vite build)- Served on the tailnet via
tailscale serveand added to the tailscale landing page alongside knowledgebase/frigate. - Patched (contained edit only):
- Print action: instead of Web Bluetooth, capture the Konva stage as PNG (
stage.toCanvas()/toDataURL) andPOSTit + current print options to the print service. - Connect UI: replace device discovery/pairing with a fixed status “Server printer: Marklife P15” reflecting
GET /status.
- Print action: instead of Web Bluetooth, capture the Konva stage as PNG (
- Print-option controls in the existing UI map straight onto the
POST /printoptions.
- Served on the tailnet via
-
The link — editor →
POST /print→ print service → Noble → printer. Nothing else talks BLE.
Deployment
- Install Bun natively on the mainframe (
curl). Build@thermoprint/webto static assets. - Print service as
systemdunit,Restart=always,User=levander(needs BLE caps — grant Noble raw HCI access via setcap on the node/bun binary orAmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW). - Static editor served +
tailscale servemapping (HTTPS, tailnet-only), landing-page entry added. - App dir:
/home/levander/thermoprint/(clone + build + service + config), consistent with other homelab services.
Testing / verification
- Print service: unit-print a known PNG via
POST /print, assertprinter.printinvoked + Noble write succeeds;GET /statusreflects reachability. One runnable self-check that fails if the render→L11 path breaks. - End-to-end: from the hosted editor, design a label → print → physical label emerges on the P15.
- Serialize check: two rapid prints don’t collide on the single BLE link.
Caveats / known ceilings
BLOCKED 2026-07-31 — mainframe's AX210 has NO ANTENNA
The onboard Intel AX210 on telep-mainframe saw zero BLE devices (not even stray phones) because no antenna is plugged into the M.2 card’s u.FL connector (WiFi+BT share it). Not a firmware fault. This blocks the whole appliance. Fix = plug in the AX210 antenna (USB BT dongle →
hci1only as fallback). Full detail + what was tried: 2026-07-31-telep-mainframe-ax210-ble-scan-broken.
- Printer must be powered/awake in range. BLE label printers sleep; per-job connect wakes most Marklife units but not guaranteed.
# ponytail: connect-per-job, add keep-warm ping if wake latency hurts. - One job at a time (serialized).
- Editor patch tracks thermoprint’s
main; upstream changes to the print/connect components may need re-patching. Keep the patch minimal + documented.
Out of scope (for now)
- Multi-printer support, print queue persistence across restarts, auth on the print endpoint (tailnet-only is the boundary), the WebSocket-BLE approach (B).