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:
@@ -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,
|
||||
};
|
||||
|
||||
+10
-9
@@ -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");
|
||||
});
|
||||
});
|
||||
+21
-16
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -97,6 +97,14 @@ export function isDiscoverable(
|
||||
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 {
|
||||
readonly #drivers = new Map<string, DeviceDriver>();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user