Files
visitor-signin/docs/PRINTING.md
jessikitty 1a94c33304 Document the printing subsystem
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.
2026-09-07 14:51:04 +10:00

141 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 FloydSteinberg on the photo region before it goes
on the canvas, inside `drawBadge`.
---
Created by: Jess Rogerson (yelling commands at Claude.AI)