< all notes

Printing labels from a web app with TSPL2 over port 9100

ORRN is an ERP for aluminum extrusion plants, and every bundle that leaves the floor needs a label. Browsers are bad at this. A print dialog on a plant floor is a support call waiting to happen, and thermal printers don't speak the formats browsers send. So ORRN doesn't print. It hands the job to orrn-spool, a small Go service that runs on a machine inside the plant and owns the printers.

Talking to the printer

TSC printers accept TSPL2, a plain-text command language, on TCP port 9100. No driver. You open a socket and write lines like these:

SIZE 101.6 mm, 50.8 mm
GAP 3 mm, 0 mm
DIRECTION 1
CLS
TEXT 32,28,"4",0,1,1,"ORRN"
BARCODE 32,196,"128",120,0,0,2,4,"https://ansh.bhalala.org/projects/orrn"
PRINT 1,1

SIZE sets the label stock, here 4 by 2 inches. TEXT places text in a built-in font. BARCODE draws a Code 128 symbol. PRINT 1,1 prints one copy. For most labels, that's the whole protocol. Every project page on this site shows the TSPL2 for its own label, generated the same way.

One detail that bites: a double quote inside a string is written as \["], not \".

From JSON to TSPL2

Labels are templates: a JSON schema with variables. The spool fills them in and writes TSPL2. You can also describe a label in plain English, and Gemini drafts a layout that the spool then compiles to TSPL2. The model proposes. The compiler decides what reaches the printer.

The queue

Plants print in bursts, and printers run out of labels at bad times. Jobs go through a priority queue with automatic retries and configurable workers. A webhook fires when a job finishes or fails. Finished jobs are archived, encrypted with a passphrase. An HTMX web UI covers printers, templates, and test prints. It ships as a Docker image, with release binaries built in CI.

Built with an agent

I started the repo on February 13, 2026, with KiloCode running GLM 5. The README records one session: 65,375 tokens of context, 32% of the window, $0.61 spent. Fifteen commits later, by June, it had release CI and a container image on GHCR.

What made that cheap was the problem, not the model. I already knew the printer, the protocol, and the label sizes, because I'd been building label tracking for this plant since 2022. An agent is fast when the spec is narrow.

What I'd do again

  • Put a service between the web app and the printer. The web app sends intent, not bytes.
  • Keep the printer language as plain text, so you can read a failed job.
  • Treat model output as a draft that a compiler checks, never as bytes on the wire.

Related projects

More notes