refactor(devices): rename driver "cashino" → "escpos" (generic ESC/POS)

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
This commit is contained in:
2026-07-06 12:36:14 +02:00
parent 81bc2e357c
commit fcea992e1e
7 changed files with 58 additions and 31 deletions
@@ -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';
+7
View File
@@ -162,6 +162,13 @@
"when": 1781886500000, "when": 1781886500000,
"tag": "0022_tariff_version_name", "tag": "0022_tariff_version_name",
"breakpoints": true "breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1781886600000,
"tag": "0023_driver_id_escpos",
"breakpoints": true
} }
] ]
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import { registry } from "../registry.js";
import { dingtianDriver } from "./access-dingtian.js"; import { dingtianDriver } from "./access-dingtian.js";
import { stubAccessDriver } from "./access-stub.js"; import { stubAccessDriver } from "./access-stub.js";
import { dahuaDriver, hikvisionDriver } from "./camera.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 { rongtaDriver } from "./printer-rongta.js";
import { dingtianQrReaderDriver, tcpipReaderDriver, wiegandReaderDriver } from "./reader.js"; import { dingtianQrReaderDriver, tcpipReaderDriver, wiegandReaderDriver } from "./reader.js";
@@ -23,7 +23,7 @@ export function registerBuiltinDrivers(): void {
registry.register(hikvisionDriver); registry.register(hikvisionDriver);
registry.register(dahuaDriver); registry.register(dahuaDriver);
registry.register(rongtaDriver); registry.register(rongtaDriver);
registry.register(cashinoDriver); registry.register(escposDriver);
} }
export { export {
@@ -35,5 +35,5 @@ export {
hikvisionDriver, hikvisionDriver,
dahuaDriver, dahuaDriver,
rongtaDriver, rongtaDriver,
cashinoDriver, escposDriver,
}; };
@@ -2,19 +2,20 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { cashinoDriver } from "./printer-cashino.js"; import { escposDriver } from "./printer-generic.js";
import { renderTicket } from "./printer-escpos.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 // 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.) // 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 dir: string;
let devicePath: string; let devicePath: string;
beforeEach(() => { beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "cashino-usb-")); dir = mkdtempSync(join(tmpdir(), "escpos-usb-"));
devicePath = join(dir, "lp0"); devicePath = join(dir, "lp0");
// Stand in for an enumerated usblp node (the kernel creates it; we only open it). // Stand in for an enumerated usblp node (the kernel creates it; we only open it).
writeFileSync(devicePath, ""); writeFileSync(devicePath, "");
@@ -24,7 +25,7 @@ describe("cashinoDriver — USB transport", () => {
}); });
it("prints a ticket to the configured USB device path", async () => { 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" }; const data = { ticketId: "12345678901", issuedAt: "2026-06-21T10:00:00.000Z" };
await printer.printTicket(data); await printer.printTicket(data);
const written = readFileSync(devicePath); 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 () => { 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"); expect((await present.healthCheck()).status).toBe("ready");
// An absent device node (printer unplugged / not enumerated) → offline. // An absent device node (printer unplugged / not enumerated) → offline.
const absent = cashinoDriver.create({ const absent = escposDriver.create({
transport: "usb", transport: "usb",
devicePath: join(dir, "absent-lp0"), devicePath: join(dir, "absent-lp0"),
timeoutMs: 1000, timeoutMs: 1000,
@@ -44,7 +45,7 @@ describe("cashinoDriver — USB transport", () => {
}); });
it("advertises both transports", () => { it("advertises both transports", () => {
expect(cashinoDriver.transports).toContain("usb"); expect(escposDriver.transports).toContain("usb");
expect(cashinoDriver.transports).toContain("tcp-ip"); expect(escposDriver.transports).toContain("tcp-ip");
}); });
}); });
@@ -23,25 +23,26 @@ import {
type Transport, type Transport,
} from "./printer-escpos.js"; } from "./printer-escpos.js";
// Cashino 80mm thermal printer driver (network OR USB). The Cashino is an ESC/POS // GENERIC ESC/POS 80mm thermal printer driver (network OR USB) — any clone that
// clone: it PRINTS identically to the Rongta (same byte stream — see // PRINTS the shared ESC/POS byte stream (see ./printer-escpos.ts) but serves no
// ./printer-escpos.ts), so tickets, reports and subscription cards render the same, // Rongta-style decoded status page (/prn_stat.htm). Verified fits: Cashino (the
// over either transport. What it does NOT have is the Rongta board's decoded status // first unit we drove — the driver carried its name until 2026-07-06), ICS/Xprinter
// web page (/prn_stat.htm). It cannot report paper-out / cover-open / cutter faults // XP-K200L. Tickets, reports and subscription cards render identically to the
// in a form we trust. // 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 // 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- // 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 // 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 // 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 // Therefore this driver deliberately does NOT implement MonitorableDevice
// (no readStatus). The device monitor then falls back to the generic // (no readStatus). The device monitor then falls back to the generic
// `healthCheck()` — a plain TCP reachability PING of the print socket. So the // `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" // 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. // 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.) // serve, producing the bogus "degraded" feedback this driver fixes.)
// //
// No auth on the print socket — like the other field devices it lives on the // 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. // (entry-dispenser / booth-receipt + failoverRank); the server owns selection.
// See wiki/concepts/printer-status-monitoring.md and printer-roles-failover.md. // See wiki/concepts/printer-status-monitoring.md and printer-roles-failover.md.
class CashinoPrinter implements PrinterDevice { class GenericEscposPrinter implements PrinterDevice {
readonly driverId = "cashino"; readonly driverId = "escpos";
readonly #transport: Transport; readonly #transport: Transport;
readonly #timeout: number; readonly #timeout: number;
@@ -69,7 +70,7 @@ class CashinoPrinter implements PrinterDevice {
/** /**
* Reachability only — a connect probe (TCP) or char-device open probe (USB) of * 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 → * floor and the ceiling of what we report: reachable → ready, unreachable →
* offline. Deliberately NO readStatus(): the monitor uses this for the * offline. Deliberately NO readStatus(): the monitor uses this for the
* traffic-light, never a guessed paper/cover state. * 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.", help: "Higher = tried first within the same role. The booth printer also backs up the entry dispenser.",
}; };
export const cashinoDriver: PrinterDriver = { export const escposDriver: PrinterDriver = {
id: "cashino", // 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", category: "printer",
label: "Cashino 80mm thermal printer", label: "Generic ESC/POS 80mm printer (Cashino, ICS/Xprinter…)",
description: 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"], transports: ["tcp-ip", "usb"],
configFields: [ configFields: [
transportField, transportField,
@@ -167,5 +172,5 @@ export const cashinoDriver: PrinterDriver = {
default: 3000, default: 3000,
}, },
], ],
create: (c) => new CashinoPrinter(c), create: (c) => new GenericEscposPrinter(c),
}; };
+1 -1
View File
@@ -18,7 +18,7 @@ export {
hikvisionDriver, hikvisionDriver,
dahuaDriver, dahuaDriver,
rongtaDriver, rongtaDriver,
cashinoDriver, escposDriver,
} from "./drivers/index.js"; } from "./drivers/index.js";
export { isPrinter, type PrinterRole } from "./drivers/printer-rongta.js"; export { isPrinter, type PrinterRole } from "./drivers/printer-rongta.js";
// Albanian human date/time for printed slips (receipts, tickets, shift Z-report), // Albanian human date/time for printed slips (receipts, tickets, shift Z-report),
+10 -2
View File
@@ -97,6 +97,14 @@ export function isDiscoverable(
return typeof (driver as Partial<DiscoverableDriver>).discover === "function"; return typeof (driver as Partial<DiscoverableDriver>).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<string, string> = {
cashino: "escpos", // renamed 2026-07-06 — it was always the generic ESC/POS driver
};
class DeviceRegistry { class DeviceRegistry {
readonly #drivers = new Map<string, DeviceDriver>(); readonly #drivers = new Map<string, DeviceDriver>();
@@ -114,12 +122,12 @@ class DeviceRegistry {
} }
get(id: string): DeviceDriver | undefined { 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. */ /** Validate config against a driver's declared fields and build the adapter. */
create(id: string, config: DeviceConfig): Device { 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}`); if (!driver) throw new Error(`unknown driver: ${id}`);
for (const field of driver.configFields) { for (const field of driver.configFields) {
if (field.required && config[field.key] === undefined) { if (field.required && config[field.key] === undefined) {