The ESC/POS printer drivers were TCP-only — every path went through
sendRaw/probe to a raw socket on port 9100. Add a USB transport behind
the existing render layer without touching a single render*() function.
- printer-escpos.ts: sendRawUsb/probeUsb write the same ESC/POS bytes to a
kernel usblp char device (/dev/usb/lp0) via a plain fs write — no
libusb/CUPS/native dep (keeps MIT-only + minimal-deps appliance). A
discriminated Transport + transportFromConfig/sendTo/probeTo dispatch the
wire; anything not transport:"usb" is TCP, so existing host-only configs
need no migration. Shared transportField/devicePathField config fields.
- cashino + rongta resolve a Transport once; both are reachability-only over
USB, and the Rongta's HTTP status page degrades to the open-the-node probe
over USB (no guessed paper/cover — the standing honesty rule). host/port
made not-required so a USB printer needs neither.
- Tests: printer-escpos.test.ts (USB writes the exact rendered bytes; probe
present/absent; transportFromConfig TCP back-compat) + printer-cashino.test.ts
(USB-configured driver prints to the node, ready/offline).
USB itself is unverified on hardware (the on-site printers are networked);
the appliance-side usblp + udev provisioning is tracked as open-questions #14.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Pins the device-layer bugs we kept hand-verifying, as pure byte-stream assertions
(no sockets, no hardware):
- printer-escpos.test.ts (12): CP852 codepage select; the ë→0x89 / Ë→0xD3 mapping
and the em-dash/⚠ ASCII fallbacks (never a stray 0x3f "?"); and the Code128 MODULE
WIDTH contract — a short ticket id at width 3, but the ~20-char out-of-window
occurrence id at width 2 so it fits the 80mm head (width 3 overflows ~576 dots and
the firmware silently aborts the barcode). Plus the QR-and-Code128 dual encoding and
the Albanian stamp() format.
- printer-routing.test.ts (6): the failover order (booth printer is a fallback for
entry tickets; a receipt never prints on the outside dispenser), rank-then-id
tiebreak, and printWithFailover walking the order + NoPrinterAvailableError.
Wires Vitest into @parking/devices. devices 18/18 green.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V