diff --git a/wiki/concepts/printer-usb-transport.md b/wiki/concepts/printer-usb-transport.md new file mode 100644 index 0000000..beb9b5a --- /dev/null +++ b/wiki/concepts/printer-usb-transport.md @@ -0,0 +1,91 @@ +--- +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]]. diff --git a/wiki/decisions/open-questions.md b/wiki/decisions/open-questions.md index 119c33a..b62d87d 100644 --- a/wiki/decisions/open-questions.md +++ b/wiki/decisions/open-questions.md @@ -95,3 +95,15 @@ procurement. (See [[parking-system-architecture]] §10.) vs. serve-degraded — lean **serve-degraded + loud alarm** (fail-open on exit still governs; refusing to boot could strand a lane). Software-only, independent of the TPM/[[atecc608]] hardware. See [[append-only-event-chain]]. +14. **Printer USB transport — confirm the on-site printer + bake the provisioning.** _(Recorded + 2026-06-24; the transport code is built — see [[printer-usb-transport]].)_ The ESC/POS drivers + now drive **TCP (port 9100) OR local USB (`/dev/usb/lp0`)** behind one render layer, selectable + per device. **Open:** is the actual booth printer USB or network? (The site's verified units are + *networked* — Cashino `10.0.10.9`, Rongta `10.0.10.10` — so USB may be unused here; the original + BOM listed "Epson TM / Citizen (USB **or** network)", so a future site may need it.) If USB is + used, the **appliance image** must (a) load/keep the **`usblp`** kernel module bound to the + printer (CUPS can claim the interface first), and (b) ship a **udev rule** giving the non-root + server process write access to `/dev/usb/lp*`. Both are [[appliance-provisioning]] steps, **not + app code**, and are **unverified on hardware**. Close this once the printer transport per site is + fixed and (if USB) the udev/usblp rule is in the image and a real USB print is verified. Relates + to #1 (lane topology / image standardization). See [[printer-usb-transport]], [[rongta-printer]]. diff --git a/wiki/entities/rongta-printer.md b/wiki/entities/rongta-printer.md index 42b6665..1621c9b 100644 --- a/wiki/entities/rongta-printer.md +++ b/wiki/entities/rongta-printer.md @@ -34,6 +34,13 @@ many ESC/POS-compatible OEM clones that share its firmware). Driver `rongta` in We scrape that rather than hand-decode `DLE EOT` — this clone's DLE EOT reply bytes do **not** match the canonical ESC/POS bit layout (verified on hardware), so trusting the device's own decode avoids a false-healthy. Implemented as `readStatus()`; see [[printer-status-monitoring]]. +- **USB transport (added 2026-06-24).** The same driver can instead drive a printer over a local + USB `usblp` char device (`/dev/usb/lp0`) — `config.transport` (`tcp-ip` | `usb`) picks the wire + behind one render layer (the ESC/POS bytes are identical). The status web page is a **network** + feature, so a **USB Rongta degrades to reachability-only** monitoring (open-the-node probe, no + paper/cover verdict — the same honesty floor as the Cashino). Driving USB depends on the appliance + image (`usblp` bound + a udev write-access rule) — a provisioning step, open-questions #14. Full + rationale in [[printer-usb-transport]]. ## Deployment (this site) diff --git a/wiki/index.md b/wiki/index.md index c982951..16daaa1 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -44,7 +44,7 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. - [[zkteco-controller]] — ❌ rejected/historical; aux-input path was a contender, not pursued. - [[dingtian-relay]] — ✅ CHOSEN access controller; decoupled inputs solve the button blocker (driver verified on hardware); spare relays drive aux outputs (`setAux`). - [[hikvision-radar]] — vehicle-presence radar on a Dingtian input; the entry presence gate (per-input active-level caveat). -- [[rongta-printer]] — ✅ CHOSEN 80mm thermal printer; ESC/POS over raw TCP 9100; driver written, one unit reachable at 10.0.10.6. +- [[rongta-printer]] — ✅ CHOSEN 80mm thermal printer; ESC/POS over raw TCP 9100 (or local USB, see [[printer-usb-transport]]); driver written, one unit reachable at 10.0.10.6. - [[bom]] — reference bill of materials (barrier, loops, controller, readers, payment, host, network). ## Concepts — foundational forces @@ -66,6 +66,7 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. - [[barrier-not-a-door]] — never timed-close a barrier; safety lives in barrier firmware. - [[printer-roles-failover]] — ≥2 printers by role; entry ticket falls back outside→booth. - [[printer-status-monitoring]] — live poll of paper/cover/cutter/offline via the device's status page; SSE to the booth UI. +- [[printer-usb-transport]] — ESC/POS drivers drive TCP (9100) OR local USB (/dev/usb/lp0) behind one render layer; USB = usblp char device, reachability-only status; provisioning open (oq#14). - [[device-status-monitoring]] — unified live status across ALL device categories (healthCheck + printer readStatus) → the booth footer over /api/ws. - [[trust-boundary]] — the core fork: network vs. device; auditable vs. unforgeable. - [[fail-state-safety]] — entry fails closed, exit fails open; manual override; watchdog. diff --git a/wiki/log.md b/wiki/log.md index 491b691..d4112bd 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1588,3 +1588,21 @@ worker (one in-flight send/lamp, re-converges to the latest state → final stat in the UI works without a server restart. Workspace build+lint+test green (163 server tests). See [[dingtian-relay]] ("offline despite ping" + secret re-merge), [[button-light-indicator]] (serialized sends + hot-reload). + +## [2026-06-24] build | Printer USB transport behind the ESC/POS render layer +The ESC/POS printer drivers were **TCP-only** (every path went through `sendRaw`/`probe` to a raw +socket on port 9100); the original BOM intended one adapter to cover "USB **or** network". Added a +**USB transport** behind the existing render layer without touching a single `render*()` function: +a discriminated `Transport` (`transportFromConfig` → `{kind:"tcp",host,port}` | `{kind:"usb", +devicePath}`) and `sendTo`/`probeTo` dispatchers in `printer-escpos.ts`; USB writes 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). `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). Non-`usb` configs are unchanged +(host-only = TCP), so no migration. Setup UI gains a **Connection** select + **USB device** field; +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). Devices suite +green (29). **Flagged 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). See [[printer-usb-transport]], [[rongta-printer]].