# 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 fine - `false` — it answered and something is wrong - `null` — 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: ```bash 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)