81bc2e357c
Field bug (ICS XP-K200L over USB): text printed, barcode + cut missing; same bytes over TCP fine. sendRawUsb did ONE write() on an O_NONBLOCK usblp fd and never checked bytesWritten — the kernel accepts only what fits the printer's ~8 KB USB buffer and returns a short write, so the tail of any job bigger than one buffer (the barcode mid-payload, the cut at the end) was silently discarded. The regular-file test stand-in can't short-write, which is why tests never caught it. writeAllUsb now pushes 4 KB chunks until every byte is accepted, continues after partial writes, retries EAGAIN/zero-byte with a short pause, and fails at the deadline with an (N/M bytes) diagnostic. Driven by fake-handle tests (short writes, EAGAIN interleave, wedged-printer timeout, non-EAGAIN passthrough). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
115 lines
6.9 KiB
Markdown
115 lines
6.9 KiB
Markdown
---
|
|
type: concept
|
|
tags: [parking, device, printer, transport, usb, escpos, provisioning]
|
|
sources: []
|
|
updated: 2026-07-06
|
|
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).
|
|
|
|
## Field bug — the NONBLOCK partial-write truncation (found + fixed 2026-07-06)
|
|
|
|
First on-hardware USB test (ICS XP-K200L, an ESC/POS clone): over TCP it printed + cut fine; over
|
|
USB it printed the ticket's TEXT but **no barcode and no cut**. Root cause was in OUR transport,
|
|
not the printer: `sendRawUsb` opened the node with `O_NONBLOCK` and issued ONE `write()` for the
|
|
whole job. On a non-blocking usblp fd the kernel accepts only what fits the printer's USB buffer
|
|
(~8 KB) and returns a **short write**; the old code never checked `bytesWritten`, closed the
|
|
handle, and silently dropped the tail — which is exactly where the barcode (mid-payload) and the
|
|
CUT (last bytes) live. Small jobs fit one buffer, hence "text prints fine". The regular-file test
|
|
stand-in can't short-write, so tests never caught it.
|
|
|
|
Fix: `writeAllUsb` — chunked loop (4 KB, safely under the usblp buffer) that continues after
|
|
partial writes, retries `EAGAIN`/zero-byte writes with a short pause, and fails at the caller's
|
|
deadline with a `(N/M bytes accepted)` diagnostic. Driven by fake-handle tests (short writes,
|
|
EAGAIN interleave, wedged-printer timeout, non-EAGAIN passthrough) since a real file can't
|
|
reproduce the char device's behaviour.
|
|
|
|
> Driver-choice note for this clone: the ICS XP-K200L does NOT serve the Rongta `/prn_stat.htm`
|
|
> status page (checked on hardware at 10.0.10.11 — print socket 9100 open, status page absent),
|
|
> so on NETWORK the honest driver is **cashino** (reachability-only monitoring); under `rongta`
|
|
> the monitor would mark a perfectly working printer offline/degraded. Over USB the two drivers
|
|
> behave identically (reachability floor), so either works post-fix. See [[rongta-printer]].
|
|
|
|
Related: [[rongta-printer]], [[printer-status-monitoring]], [[printer-roles-failover]],
|
|
[[appliance-provisioning]], [[network-isolation]], [[technology-stack]].
|