Files
parking_solution/packages/devices/src/drivers/printer-cashino.test.ts
T
julian 7366ad19cb feat(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. 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
2026-06-24 20:32:10 +02:00

51 lines
2.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { cashinoDriver } from "./printer-cashino.js";
import { renderTicket } from "./printer-escpos.js";
// End-to-end transport routing through the real driver: a USB-configured Cashino must
// resolve to the char-device transport and write the SAME ESC/POS bytes the TCP path
// would. (The TCP path is exercised by the routing/escpos suites and on hardware.)
describe("cashinoDriver — USB transport", () => {
let dir: string;
let devicePath: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "cashino-usb-"));
devicePath = join(dir, "lp0");
// Stand in for an enumerated usblp node (the kernel creates it; we only open it).
writeFileSync(devicePath, "");
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
it("prints a ticket to the configured USB device path", async () => {
const printer = cashinoDriver.create({ transport: "usb", devicePath, timeoutMs: 1000 });
const data = { ticketId: "12345678901", issuedAt: "2026-06-21T10:00:00.000Z" };
await printer.printTicket(data);
const written = readFileSync(devicePath);
expect(written.equals(renderTicket(data))).toBe(true);
});
it("healthCheck reports ready when the node exists, offline when it doesn't", async () => {
const present = cashinoDriver.create({ transport: "usb", devicePath, timeoutMs: 1000 });
expect((await present.healthCheck()).status).toBe("ready");
// An absent device node (printer unplugged / not enumerated) → offline.
const absent = cashinoDriver.create({
transport: "usb",
devicePath: join(dir, "absent-lp0"),
timeoutMs: 1000,
});
expect((await absent.healthCheck()).status).toBe("offline");
});
it("advertises both transports", () => {
expect(cashinoDriver.transports).toContain("usb");
expect(cashinoDriver.transports).toContain("tcp-ip");
});
});