Covers the roll/job colour rule that caused the "change it to Monochrome media" failures, the tri-state health result, and why network status is unavailable on this hardware.
5.3 KiB
Badge printing
The app renders the badge and sends it to the Brother QL-820NWB over TCP port 9100, speaking Brother's raster command language directly. No CUPS, no driver, no print dialog, and no Python.
Why not AirPrint
AirPrint from the kiosk browser means iPadOS renders the badge from print CSS and picks the page geometry from what the printer advertises. That costs exact control of the 60.96 mm print width, makes two-colour DK-22251 output impossible, and cannot suppress the print dialog — so a staff member has to tap "Print" for every visitor. Fine for a staffed desk, useless for unattended sign-in.
Doing it server-side also means the kiosk device stops mattering. An iPad, an old Android tablet, a browser on a NUC: they all just POST.
Layout
src/printing/
ql-raster.js command language: media, cut, expanded mode, bit packing
ql-status.js decoder for the 32-byte status frames
ql-transport.js TCP client for port 9100
ql-print.js printPng / printerHealth, plus the serial queue
src/printer.js badge layout and rendering (unchanged by this work)
Protocol details follow Brother's Raster Command Reference, QL-800/810W/820NWB v1.01. Where that manual and pklaus/brother_ql disagree, the manual wins; brother_ql targets older models and several of its defaults are wrong for this hardware (200-byte invalidate instead of 400, the print-quality bit set during two-colour jobs, the media-length valid bit asserted for continuous tape).
The roll setting is not cosmetic
sites.printer_label must match the roll physically in the machine:
| Roll | Setting |
|---|---|
| DK-22205, 62 mm white | 62 |
| DK-22251, 62 mm black/red | 62red |
This is enforced by the printer, not by us. A monochrome job sent to a
black/red roll is refused outright, and vice versa. It applies even when the
badge has no red on it — with 62red set, every job is built as two-colour and
the red plane is simply empty.
The printer's own error message is misleading here. It says "change it to Monochrome media", which points at a driver setting that doesn't exist in this setup. The fix is the roll dropdown in the admin console, or swapping the roll.
Status, and why there isn't much
Manual section 5.9: over a network connection the print data is sent as-is and nothing comes back. Status frames only arrive over USB. brother_ql documents the same limitation for its network backend.
So printerHealth returns a tri-state ready:
true— the printer answered and everything is finefalse— it answered and something is wrongnull— reachable, but it won't say
null is the normal answer here. Show it as amber/unknown in the admin console;
colouring it green claims something we don't know. Likewise printPng returns
confirmed: false when no status came back: the bytes were delivered, but
whether paper moved is unknown.
Consequence worth accepting: a jammed or empty printer cannot be detected. The practical signal is a visitor saying no badge appeared. That is tolerable because the evacuation record is written before printing is attempted. If real roll monitoring is ever needed, the lead to follow is SNMP — the Brother Status Monitor on Windows gets roll state over the network somehow, and it is not using port 9100.
Concurrency
The QL accepts one TCP connection at a time and has no job spooler worth the
name. Two visitors signing in together would otherwise produce a refused
connection, half a label, or both. Every job goes through a serial queue keyed
on printer host, in ql-print.js.
That queue is in-process. Running more than one app instance against one printer would need a lock in SQLite instead.
Geometry
| Print head | 720 pins, 60.96 mm at 300 dpi |
| 62 mm roll printable | 696 pins, 58.9 mm |
| Margins | 12 pins each side |
| Feed margin | 35 dots (3 mm), the documented minimum |
| Length | 150 to 11811 raster lines (12.7 mm to 1 m) |
Every raster line sent is the full 90 bytes regardless of the roll. Narrower media just means more pins miss the paper.
Diagnostics
Reachability and roll state:
docker compose exec visitor-signin node -e "
import('./src/printing/ql-print.js').then(async m =>
console.log(await m.printerHealth({ host: '10.142.177.169', label: '62red' })));
"
If a label never appears, work down this list:
Nothing at all, Cannot reach printer. Routing or firewall between the
container and the printer. Try nc -vz IP 9100 from the Ubuntu host; if that
works but the container doesn't, it is Docker networking.
Connects but silent. Normal. See above.
Media mismatch error. The roll setting and the physical roll disagree.
Labels come out blank. Roll in backwards, or a non-Brother roll with no end sensor. The printer reports no error for this.
Everything mirrored. The bit packing is wrong. The head clocks each raster
line out right-to-left; see packRow.
First label after a power cycle is misaligned. Feed one label from the front panel before the first sign-in of the day.
Known rough edge
Photos are not dithered. drawBadge draws them straight and the plane
conversion thresholds at luminance 180. A thermal head has no grey, so faces
come out blotchy. The fix is Floyd–Steinberg on the photo region before it goes
on the canvas, inside drawBadge.
Created by: Jess Rogerson (yelling commands at Claude.AI)