--- type: concept tags: [parking, device, printer, transport, usb, escpos, provisioning] sources: [] updated: 2026-06-24 status: settled --- # Printer USB transport (kernel usblp, behind the ESC/POS layer) The ESC/POS printer drivers ([[rongta-printer|rongta]], `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 discriminated `Transport` (`{ kind: "tcp", host, port }` or `{ kind: "usb", devicePath }`). Anything other than `transport: "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 `fs` write — 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]]). - **`usblp` is raw.** Unlike the TCP path there is **no FIN/half-close dance** (the graceful-close fix was a *TCP* concern — an early `destroy()` 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. `withTimeout` rejects after `timeoutMs`. ## 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 `cashino` driver is reachability-only on **both** transports (it never had a status page). - The `rongta` driver's rich `readStatus()` scrapes the board's **HTTP** `/prn_stat.htm` — a **network feature**. Over USB there is no such page, so `readStatus()` **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|ledger]] or barrier control is touched. ## Provisioning dependency (NOT app code) — see open-questions #14 Driving a USB printer depends on the appliance image: 1. the **`usblp`** kernel module is loaded (it is in-box on Ubuntu 26.04; CUPS can claim the interface first — may need `usblp` to win, or CUPS masked for that device), and 2. 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]].