From fcea992e1eb50958ec38216e73211621d48a52a0 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Mon, 6 Jul 2026 12:36:14 +0200 Subject: [PATCH] =?UTF-8?q?refactor(devices):=20rename=20driver=20"cashino?= =?UTF-8?q?"=20=E2=86=92=20"escpos"=20(generic=20ESC/POS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reachability-only clone driver carried its first unit's vendor name, which read as misleading in the setup UI once other clones (ICS/Xprinter XP-K200L, verified 2026-07-06: no /prn_stat.htm) used it. It was always the generic ESC/POS driver — now named so: - printer-cashino.ts → printer-generic.ts; GenericEscposPrinter; id "escpos", label "Generic ESC/POS 80mm printer (Cashino, ICS/Xprinter…)". - Migration 0023 rewrites stored devices.driver_id rows. - The registry keeps a PERMANENT cashino→escpos alias so restored pre-rename backups still resolve instead of "unknown driver". Prose mentions of the Cashino as physical hardware stay — it's a real, verified-fit printer; only the driver identity stopped being vendor-named. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- packages/db/drizzle/0023_driver_id_escpos.sql | 6 +++ packages/db/drizzle/meta/_journal.json | 7 ++++ packages/devices/src/drivers/index.ts | 6 +-- ...ashino.test.ts => printer-generic.test.ts} | 19 +++++----- ...{printer-cashino.ts => printer-generic.ts} | 37 +++++++++++-------- packages/devices/src/index.ts | 2 +- packages/devices/src/registry.ts | 12 +++++- 7 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 packages/db/drizzle/0023_driver_id_escpos.sql rename packages/devices/src/drivers/{printer-cashino.test.ts => printer-generic.test.ts} (72%) rename packages/devices/src/drivers/{printer-cashino.ts => printer-generic.ts} (75%) diff --git a/packages/db/drizzle/0023_driver_id_escpos.sql b/packages/db/drizzle/0023_driver_id_escpos.sql new file mode 100644 index 0000000..b738a3e --- /dev/null +++ b/packages/db/drizzle/0023_driver_id_escpos.sql @@ -0,0 +1,6 @@ +-- Driver rename (2026-07-06): "cashino" → "escpos". The driver was always the GENERIC +-- ESC/POS printer driver (reachability-only clones); it carried the first unit's vendor +-- name, which read as misleading in the setup UI once other clones (ICS/Xprinter +-- XP-K200L) used it. Rewrite stored device rows; the registry also keeps a permanent +-- cashino→escpos alias so restored pre-rename backups still resolve. +UPDATE `devices` SET `driver_id` = 'escpos' WHERE `driver_id` = 'cashino'; diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 9036507..1d2a170 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1781886500000, "tag": "0022_tariff_version_name", "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1781886600000, + "tag": "0023_driver_id_escpos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/devices/src/drivers/index.ts b/packages/devices/src/drivers/index.ts index 81af084..ba31cf1 100644 --- a/packages/devices/src/drivers/index.ts +++ b/packages/devices/src/drivers/index.ts @@ -5,7 +5,7 @@ import { registry } from "../registry.js"; import { dingtianDriver } from "./access-dingtian.js"; import { stubAccessDriver } from "./access-stub.js"; import { dahuaDriver, hikvisionDriver } from "./camera.js"; -import { cashinoDriver } from "./printer-cashino.js"; +import { escposDriver } from "./printer-generic.js"; import { rongtaDriver } from "./printer-rongta.js"; import { dingtianQrReaderDriver, tcpipReaderDriver, wiegandReaderDriver } from "./reader.js"; @@ -23,7 +23,7 @@ export function registerBuiltinDrivers(): void { registry.register(hikvisionDriver); registry.register(dahuaDriver); registry.register(rongtaDriver); - registry.register(cashinoDriver); + registry.register(escposDriver); } export { @@ -35,5 +35,5 @@ export { hikvisionDriver, dahuaDriver, rongtaDriver, - cashinoDriver, + escposDriver, }; diff --git a/packages/devices/src/drivers/printer-cashino.test.ts b/packages/devices/src/drivers/printer-generic.test.ts similarity index 72% rename from packages/devices/src/drivers/printer-cashino.test.ts rename to packages/devices/src/drivers/printer-generic.test.ts index 844347d..ac0ecc0 100644 --- a/packages/devices/src/drivers/printer-cashino.test.ts +++ b/packages/devices/src/drivers/printer-generic.test.ts @@ -2,19 +2,20 @@ 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 { escposDriver } from "./printer-generic.js"; import { renderTicket } from "./printer-escpos.js"; -// End-to-end transport routing through the real driver: a USB-configured Cashino must +// End-to-end transport routing through the real driver: a USB-configured generic +// ESC/POS printer (Cashino / ICS XP-K200L family) 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", () => { +describe("escposDriver (generic ESC/POS) — USB transport", () => { let dir: string; let devicePath: string; beforeEach(() => { - dir = mkdtempSync(join(tmpdir(), "cashino-usb-")); + dir = mkdtempSync(join(tmpdir(), "escpos-usb-")); devicePath = join(dir, "lp0"); // Stand in for an enumerated usblp node (the kernel creates it; we only open it). writeFileSync(devicePath, ""); @@ -24,7 +25,7 @@ describe("cashinoDriver — USB transport", () => { }); it("prints a ticket to the configured USB device path", async () => { - const printer = cashinoDriver.create({ transport: "usb", devicePath, timeoutMs: 1000 }); + const printer = escposDriver.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); @@ -32,10 +33,10 @@ describe("cashinoDriver — USB transport", () => { }); it("healthCheck reports ready when the node exists, offline when it doesn't", async () => { - const present = cashinoDriver.create({ transport: "usb", devicePath, timeoutMs: 1000 }); + const present = escposDriver.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({ + const absent = escposDriver.create({ transport: "usb", devicePath: join(dir, "absent-lp0"), timeoutMs: 1000, @@ -44,7 +45,7 @@ describe("cashinoDriver — USB transport", () => { }); it("advertises both transports", () => { - expect(cashinoDriver.transports).toContain("usb"); - expect(cashinoDriver.transports).toContain("tcp-ip"); + expect(escposDriver.transports).toContain("usb"); + expect(escposDriver.transports).toContain("tcp-ip"); }); }); diff --git a/packages/devices/src/drivers/printer-cashino.ts b/packages/devices/src/drivers/printer-generic.ts similarity index 75% rename from packages/devices/src/drivers/printer-cashino.ts rename to packages/devices/src/drivers/printer-generic.ts index 3c64b34..0ff1dc7 100644 --- a/packages/devices/src/drivers/printer-cashino.ts +++ b/packages/devices/src/drivers/printer-generic.ts @@ -23,25 +23,26 @@ import { type Transport, } from "./printer-escpos.js"; -// Cashino 80mm thermal printer driver (network OR USB). The Cashino is an ESC/POS -// clone: it PRINTS identically to the Rongta (same byte stream — see -// ./printer-escpos.ts), so tickets, reports and subscription cards render the same, -// over either transport. What it does NOT have is the Rongta board's decoded status -// web page (/prn_stat.htm). It cannot report paper-out / cover-open / cutter faults -// in a form we trust. +// GENERIC ESC/POS 80mm thermal printer driver (network OR USB) — any clone that +// PRINTS the shared ESC/POS byte stream (see ./printer-escpos.ts) but serves no +// Rongta-style decoded status page (/prn_stat.htm). Verified fits: Cashino (the +// first unit we drove — the driver carried its name until 2026-07-06), ICS/Xprinter +// XP-K200L. Tickets, reports and subscription cards render identically to the +// Rongta, over either transport; what these clones can NOT do is report paper-out / +// cover-open / cutter faults in a form we trust. // // TRANSPORT: a single `config.transport` ("tcp-ip" | "usb") picks the wire; the // driver resolves it ONCE into a Transport and every print/probe stays transport- // blind (see transportFromConfig/sendTo/probeTo). USB writes the same bytes to a // local usblp char device (/dev/usb/lp0); TCP writes to the raw print socket. This -// clone is the natural USB candidate — reachability-only, no status page to lose. +// clone family is the natural USB candidate — reachability-only, no page to lose. // // Therefore this driver deliberately does NOT implement MonitorableDevice // (no readStatus). The device monitor then falls back to the generic // `healthCheck()` — a plain TCP reachability PING of the print socket. So the // booth footer shows this printer as "ready" when it's reachable and "offline" // when it isn't, and never a wrong paper/cover verdict it cannot actually sense. -// (Reusing the Rongta driver made it scrape a status page the Cashino doesn't +// (Reusing the Rongta driver made it scrape a status page these clones don't // serve, producing the bogus "degraded" feedback this driver fixes.) // // No auth on the print socket — like the other field devices it lives on the @@ -49,8 +50,8 @@ import { // (entry-dispenser / booth-receipt + failoverRank); the server owns selection. // See wiki/concepts/printer-status-monitoring.md and printer-roles-failover.md. -class CashinoPrinter implements PrinterDevice { - readonly driverId = "cashino"; +class GenericEscposPrinter implements PrinterDevice { + readonly driverId = "escpos"; readonly #transport: Transport; readonly #timeout: number; @@ -69,7 +70,7 @@ class CashinoPrinter implements PrinterDevice { /** * Reachability only — a connect probe (TCP) or char-device open probe (USB) of - * the print path. The Cashino has no trustworthy status protocol, so this is the + * the print path. These clones have no trustworthy status protocol, so this is the * floor and the ceiling of what we report: reachable → ready, unreachable → * offline. Deliberately NO readStatus(): the monitor uses this for the * traffic-light, never a guessed paper/cover state. @@ -140,12 +141,16 @@ const rankField: ConfigField = { help: "Higher = tried first within the same role. The booth printer also backs up the entry dispenser.", }; -export const cashinoDriver: PrinterDriver = { - id: "cashino", +export const escposDriver: PrinterDriver = { + // Renamed from id "cashino" (the first clone we drove) on 2026-07-06 — the vendor + // name was misleading in the setup UI once other clones (ICS/Xprinter XP-K200L) + // used it. Stored configs with driverId "cashino" still resolve via the registry + // alias + are rewritten by migration 0023. + id: "escpos", category: "printer", - label: "Cashino 80mm thermal printer", + label: "Generic ESC/POS 80mm printer (Cashino, ICS/Xprinter…)", description: - "Cashino 80mm thermal printer (ESC/POS over raw TCP port 9100, OR local USB /dev/usb/lp0). Prints like the Rongta but has no status page — monitored by reachability only (no paper/cover/cutter reporting). No auth on the print socket — isolate the VLAN.", + "Generic ESC/POS 80mm thermal printer over raw TCP (port 9100) OR local USB /dev/usb/lp0 — Cashino, ICS/Xprinter XP-K200L, and similar clones. Prints like the Rongta but has no status page — monitored by reachability only (no paper/cover/cutter reporting). No auth on the print socket — isolate the VLAN.", transports: ["tcp-ip", "usb"], configFields: [ transportField, @@ -167,5 +172,5 @@ export const cashinoDriver: PrinterDriver = { default: 3000, }, ], - create: (c) => new CashinoPrinter(c), + create: (c) => new GenericEscposPrinter(c), }; diff --git a/packages/devices/src/index.ts b/packages/devices/src/index.ts index 4d30f14..130ddb2 100644 --- a/packages/devices/src/index.ts +++ b/packages/devices/src/index.ts @@ -18,7 +18,7 @@ export { hikvisionDriver, dahuaDriver, rongtaDriver, - cashinoDriver, + escposDriver, } from "./drivers/index.js"; export { isPrinter, type PrinterRole } from "./drivers/printer-rongta.js"; // Albanian human date/time for printed slips (receipts, tickets, shift Z-report), diff --git a/packages/devices/src/registry.ts b/packages/devices/src/registry.ts index acf03a6..e23126d 100644 --- a/packages/devices/src/registry.ts +++ b/packages/devices/src/registry.ts @@ -97,6 +97,14 @@ export function isDiscoverable( return typeof (driver as Partial).discover === "function"; } +/** Renamed driver ids: what a STORED config may still say → the current id. Kept + * tiny + permanent so old DB rows, exports, and backups resolve across renames + * (migration 0023 rewrites live rows, but a restored old backup may reintroduce + * the historical id). */ +const DRIVER_ID_ALIASES: Record = { + cashino: "escpos", // renamed 2026-07-06 — it was always the generic ESC/POS driver +}; + class DeviceRegistry { readonly #drivers = new Map(); @@ -114,12 +122,12 @@ class DeviceRegistry { } get(id: string): DeviceDriver | undefined { - return this.#drivers.get(id); + return this.#drivers.get(DRIVER_ID_ALIASES[id] ?? id); } /** Validate config against a driver's declared fields and build the adapter. */ create(id: string, config: DeviceConfig): Device { - const driver = this.#drivers.get(id); + const driver = this.get(id); if (!driver) throw new Error(`unknown driver: ${id}`); for (const field of driver.configFields) { if (field.required && config[field.key] === undefined) {