New concepts/printer-usb-transport.md (the seam, usblp char device, reachability-only status, threat model). open-questions #14: confirm the on-site printer is USB and bake the usblp + udev write-access rule into the appliance image (provisioning, not app code; unverified on hardware). Updated rongta-printer.md (USB transport note), index.md, log.md. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
5.2 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-24 | settled |
Printer USB transport (kernel usblp, behind the ESC/POS layer)
The ESC/POS printer drivers (rongta-printer, cashino) can deliver their byte stream
over either a raw TCP socket (port 9100) or a local USB character device (/dev/usb/lp0),
selected per device by config.transport ("tcp-ip" | "usb"). The original architecture always
intended one ESC/POS adapter to cover "USB or network" (parking-system-architecture §BOM); the
first implementation shipped TCP-only, and this closes that gap.
The seam — render once, dispatch the transport
Every render*() function in packages/devices/src/drivers/printer-escpos.ts produces a
transport-independent ESC/POS Buffer. Only delivery differs. The transport is resolved once
per driver from config and every print/probe call site stays transport-blind:
transportFromConfig(config)→ a discriminatedTransport({ kind: "tcp", host, port }or{ kind: "usb", devicePath }). Anything other thantransport: "usb"is TCP, so existing host-only configs keep working unchanged (no migration).sendTo(t, payload, timeoutMs)/probeTo(t, timeoutMs)dispatch to the TCP pair (sendRaw/probe) or the USB pair (sendRawUsb/probeUsb).
Adding a transport = one more arm in the dispatcher; not a single rendered byte changes. This is why the CP852 map, the Code128/QR builders, roles/failover, and the receipt/ticket/voucher layouts are all untouched by USB support.
USB transport = the in-box usblp char device
A USB ESC/POS printer plugged into the appliance enumerates as a character device (e.g.
/dev/usb/lp0) via the kernel's in-box usblp driver. We just open it O_WRONLY and write
the same bytes:
- No native dependency. A plain
fswrite — no libusb, no CUPS, no native addon. This keeps the MIT/Apache/BSD-only dependency constraint and the offline-first, minimal-deps appliance posture (see technology-stack, offline-first). usblpis raw. Unlike the TCP path there is no FIN/half-close dance (the graceful-close fix was a TCP concern — an earlydestroy()could RST-truncate the stream; see rongta-printer). A single open + write delivers the job; we always close the handle.- Bounded by a timeout. A wedged USB printer can block the write (or the open) indefinitely; a
stuck print must surface as a failure, not hang the entry flow.
withTimeoutrejects aftertimeoutMs.
Status over USB — reachability only (honesty rule)
probeUsb is "does the char device exist and open writable" — the USB analogue of the TCP connect
probe. A present, openable /dev/usb/lp0 means usblp bound a powered, enumerated printer.
- The
cashinodriver is reachability-only on both transports (it never had a status page). - The
rongtadriver's richreadStatus()scrapes the board's HTTP/prn_stat.htm— a network feature. Over USB there is no such page, soreadStatus()degrades to the reachability floor (ready/offline only, never a guessed paper/cover state). A USB Rongta is effectively a Cashino for monitoring. This preserves the standing honesty rule from printer-status-monitoring: never report a paper/cover verdict the transport can't actually sense.
Threat model
The USB path is a local character device the booth operator (the threat model's adversary) cannot reach over the network — narrower attack surface than the unauthenticated TCP print socket on the VLAN. Printers are advisory output; nothing about the signed append-only-event-chain or barrier control is touched.
Provisioning dependency (NOT app code) — see open-questions #14
Driving a USB printer depends on the appliance image:
- the
usblpkernel module is loaded (it is in-box on Ubuntu 26.04; CUPS can claim the interface first — may needusblpto win, or CUPS masked for that device), and - a udev rule grants the server process write access to the node (e.g. a group on
/dev/usb/lp*), since the appliance server does not run as root.
This is a appliance-provisioning concern, recorded as open-questions #14 until the on-site printer is confirmed USB and the rule is baked into the image and verified on hardware.
Status
Built 2026-06-24 behind the existing render layer. sendRawUsb/probeUsb/transportFromConfig/
sendTo/probeTo in printer-escpos.ts; cashino + rongta resolve a Transport and dispatch.
The setup UI offers a Connection select (Network / USB) + a USB device path field (default
/dev/usb/lp0); host/port are not-required so a USB printer needs neither. Covered by
printer-escpos.test.ts (USB writes the exact rendered bytes; probe present/absent;
transportFromConfig TCP back-compat) and printer-cashino.test.ts (a USB-configured driver prints
to the node and reports ready/offline). The on-hardware confirmation + the udev/usblp provisioning
are pending (open-questions #14).
Related: rongta-printer, printer-status-monitoring, printer-roles-failover, appliance-provisioning, network-isolation, technology-stack.