import type { DeviceHealth, PrinterDevice, PrintReport, ReceiptData, SubscriptionCardData, TicketData, WindowChargeNoticeData, } from "../interfaces.js"; import type { ConfigField, DeviceConfig, PrinterDriver } from "../registry.js"; import { hostField, portField, stubLog } from "./common.js"; import { devicePathField, probeTo, renderReceipt, renderReport, renderSubscriptionCard, renderTicket, renderWindowChargeNotice, sendTo, transportField, transportFromConfig, 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. // // 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. // // 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 // serve, producing the bogus "degraded" feedback this driver fixes.) // // No auth on the print socket — like the other field devices it lives on the // isolated device VLAN. Roles + failover work exactly as for the Rongta // (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"; readonly #transport: Transport; readonly #timeout: number; constructor(config: DeviceConfig) { this.#transport = transportFromConfig(config); this.#timeout = config.timeoutMs ? Number(config.timeoutMs) : 3000; } async connect(): Promise { await this.healthCheck(); } async disconnect(): Promise { stubLog(this.driverId, "disconnect"); } /** * 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 * 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. */ async healthCheck(): Promise { try { await probeTo(this.#transport, this.#timeout); return { status: "ready" }; } catch (err) { return { status: "offline", detail: (err as Error).message }; } } async printTicket(data: TicketData): Promise { await sendTo(this.#transport, renderTicket(data), this.#timeout); stubLog(this.driverId, `printed ticket ${data.ticketId}`); } async printReport(report: PrintReport): Promise { await sendTo(this.#transport, renderReport(report), this.#timeout); stubLog( this.driverId, `printed report "${report.title}" (${report.lines.length} lines)`, ); } async printSubscriptionCard(data: SubscriptionCardData): Promise { await sendTo(this.#transport, renderSubscriptionCard(data), this.#timeout); stubLog(this.driverId, `printed subscription card ${data.code}`); } async printReceipt(data: ReceiptData): Promise { await sendTo(this.#transport, renderReceipt(data), this.#timeout); stubLog( this.driverId, `printed ${data.voucher ? "voucher" : "receipt"} ${data.ticketId}`, ); } async printWindowChargeNotice(data: WindowChargeNoticeData): Promise { await sendTo(this.#transport, renderWindowChargeNotice(data), this.#timeout); stubLog(this.driverId, `printed out-of-window slip ${data.occurrenceId}`); } } const roleField: ConfigField = { key: "role", label: "Role", type: "select", required: true, default: "entry-dispenser", options: [ { value: "entry-dispenser", label: "Entry dispenser (outside / at the lane)", }, { value: "booth-receipt", label: "Booth printer (receipts + backup)" }, ], help: "Entry tickets print on the entry dispenser, falling back to the booth printer if it is offline.", }; const rankField: ConfigField = { key: "failoverRank", label: "Failover rank", type: "number", required: false, default: 0, help: "Higher = tried first within the same role. The booth printer also backs up the entry dispenser.", }; export const cashinoDriver: PrinterDriver = { id: "cashino", category: "printer", label: "Cashino 80mm thermal printer", 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.", transports: ["tcp-ip", "usb"], configFields: [ transportField, devicePathField, // host/port are TCP-only; not required because a USB printer needs neither. { ...hostField, required: false, help: `${hostField.help} Leave blank for a USB printer.` }, { ...portField(9100), required: false, help: "Raw print socket (ESC/POS over JetDirect/RAW, default 9100). TCP only.", }, roleField, rankField, { key: "timeoutMs", label: "Timeout (ms)", type: "number", required: false, default: 3000, }, ], create: (c) => new CashinoPrinter(c), };