Concrete, reusable learnings from a long self-hosted tomLadder/thermoprint debugging session on telep-mainframe (clone at /home/levander/thermoprint), driving a Marklife P15 BLE label printer to make back-to-back cable-flag labels. Covers a store-clobber bug that resets label size on connect, how the L11 protocol handles paper + the physical gap sensor, why there is no built-in 2-sided feature and the adopted manual workaround, and build/deploy facts. Builds on 2026-07-31-thermoprint-appliance-spec, 2026-07-31-thermoprint-appliance-plan and the now-resolved BLE blocker 2026-07-31-telep-mainframe-ax210-ble-scan-broken.
For Agents
Repo layout referenced below (all under /home/levander/thermoprint): packages/web (Vite editor UI), packages/core (@thermoprint/core — device profiles + L11 protocol + render pipeline), packages/cli (Noble BLE transport). packages/web resolves @thermoprint/core from source (packages/core/package.jsonmain = src/index.ts, no build step). Secrets (WiFi keys, tokens) are intentionally NOT in this note — reference config paths only.
1. Connect-flow clobber bug — “the layout changes when I connect the printer”
Gotcha: connecting the printer resets your chosen label size & paper type
On printer connect, two places overwrite the editor store’s label (size) and paperType with the device profile’s defaults:
Both copy labelConfig.defaultSize / labelConfig.defaultPaperType from the connected device profile into the editor store. Symptom: the user picks a label size/paper type, then the moment the printer connects the canvas jumps back to the profile default.
The device profile is the single source of truth for the default label — packages/core/src/device/profiles/p15.ts → labelConfig. To make a specific label “stick” through a connect, set that profile’s defaultSize / defaultPaperType (do not just fix it in the UI — the connect flow will re-clobber it).
Related upstream fix — does NOT fix this bug
Upstream PR #25 “fix/ignored-settings” (already in current HEAD) fixed a different but adjacent bug: print() was reading density / dither / threshold / paperType from the wrong store (printer-store instead of editor-store), so UI print-option changes were ignored at print time. PR #25 does not touch the connect-time clobber of label size / paper type described above. They are two separate defects that look similar (“my setting didn’t take”).
2. L11 protocol paper handling & the always-on physical gap sensor
After printBitmap, in gap mode it sends positionToGap(); in continuous mode it sends feedDots(100).
STATUS_CODES includes 0x01 out_of_paper.
The printer's physical die-cut/gap sensor is ALWAYS active — software paperType cannot disable it
The P15’s optical gap sensor runs in hardware regardless of the software paperType setting. If a die-cut gap falls partway through a single print bitmap, the head halts mid-print and the printer reports out-of-paper (0x01).
Symptom seen: “it only printed one side, the rest didn’t come out.” Setting software paperType: 'continuous' does NOT override the physical sensor — the print still halts at the gap.
Practical consequence: you cannot reliably print a bitmap that spans across a die-cut gap. Any layout whose printable area crosses a gap will stall.
3. Two-sided cable-flag labels — no built-in feature; use manual 2-print rotation
Conclusion
tomLadder/thermoprint has no built-in two-sided / fold / cable-flag feature. Attempting to auto-compose two mirrored halves into one bitmap that spans the fold fails, because the fold typically coincides with a die-cut gap — which halts the print (see §2).
Adopted workaround (the intended usage of the app):
Design one label WYSIWYG in the editor.
Print it — this is side 1.
Rotate the element 180° and print again — this is side 2.
Stick the two labels back-to-back around the cable.
The app natively supports per-element rotation and WYSIWYG single-label design — so this manual 2-print flow is exactly what the tooling is built for. Do not try to script a spanning composite.
4. Label geometry — Marklife P15, 35 × 12.5 mm
Label used: 35 mm long × 12.5 mm across-head (“filament” cable flag).
In the P15 profile gapSizes: widthMm = feed/length direction, heightMm = across-head width (the P15 print head is ~12 mm wide).
The editor rotates the canvas 90° before printing — this is documented, intended app behavior. Design accordingly.
Konva rotation pivots on the top-left origin
Konva elements rotate around their top-left origin (there is no offsetX/offsetY set). So a 180°-rotated box renders up-and-left of its (x, y) — it does not rotate in place. Account for this when positioning a rotated element for side 2, or it will land off-canvas / clipped.
5. Build / deploy notes
Editing core .ts only needs a web rebuild
packages/web resolves @thermoprint/core from source (core package.jsonmain = src/index.ts, no build step). So editing any core .ts (profiles, protocol) just needs a web rebuild:
cd /home/levander/thermoprint/packages/web && ~/.bun/bin/bun run build
The editor is served static from packages/web/dist.
Vite content-hashes bundle names → hard-refresh required
Vite hashes the output bundle filenames on every build, so after a rebuild users must hard-refresh / open incognito to pick up the new build — a normal reload can serve the cached old bundle.