diff --git a/docs/PRINTING.md b/docs/PRINTING.md new file mode 100644 index 0000000..5ea67e6 --- /dev/null +++ b/docs/PRINTING.md @@ -0,0 +1,140 @@ +# 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)