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:

  1. Print service (@thermoprint-based Bun app, systemd, 127.0.0.1:PORT)

    • Reuses thermoprint’s proven server-side print path: @thermoprint/core pipeline (grayscale → dither → pack → L11) + the CLI’s Noble BleTransport (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).
  2. Hosted editor (@thermoprint/web, static Vite build)

    • Served on the tailnet via tailscale serve and 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) and POST it + 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-option controls in the existing UI map straight onto the POST /print options.
  3. The link — editor → POST /print → print service → Noble → printer. Nothing else talks BLE.

Deployment

  • Install Bun natively on the mainframe (curl). Build @thermoprint/web to static assets.
  • Print service as systemd unit, Restart=always, User=levander (needs BLE caps — grant Noble raw HCI access via setcap on the node/bun binary or AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW).
  • Static editor served + tailscale serve mapping (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, assert printer.print invoked + Noble write succeeds; GET /status reflects 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 → hci1 only 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).