import type { DeviceHealth, ReaderDevice, ReaderEvent } from "../interfaces.js"; import type { DeviceConfig, ReaderDriver } from "../registry.js"; import { hostField, portField, stubLog } from "./common.js"; import { icmpPing } from "./icmp.js"; // Reader drivers (RF / optical). Two integration paths: Wiegand reads reach the // access controller directly (autonomous); TCP-IP readers are seen host-side. // See wiki/concepts/entry-exit-readers.md. STUBS only. class StubReader implements ReaderDevice { #cb: ((r: ReaderEvent) => void) | null = null; constructor( readonly driverId: string, protected readonly config: DeviceConfig, ) {} async connect(): Promise { stubLog(this.driverId, "connect"); } async disconnect(): Promise { stubLog(this.driverId, "disconnect"); } /** * Liveness. These readers PUSH (scan → GET our backend) and expose no TCP port, so * there's nothing to connect-probe. If the admin gave the reader's IP we ICMP-ping * it (powered + on-network); a reply → ready, no reply → offline. With NO IP we * report `degraded` ("set IP to monitor") rather than a false `ready` — a push * device that's silent is indistinguishable from a dead one, so claiming `ready` * unconditionally (the old behaviour) hid offline readers behind a green dot. */ async healthCheck(): Promise { const host = this.config.host ? String(this.config.host) : ""; if (!host) { return { status: "degraded", detail: "push device — set IP to monitor" }; } const alive = await icmpPing(host); return alive ? { status: "ready", detail: `ping ${host}` } : { status: "offline", detail: `no ping reply from ${host}` }; } onRead(cb: (r: ReaderEvent) => void): void { this.#cb = cb; stubLog(this.driverId, "onRead handler registered"); } /** Test hook for stubs — real drivers emit from hardware events. */ protected emit(r: ReaderEvent): void { this.#cb?.(r); } } export const wiegandReaderDriver: ReaderDriver = { id: "wiegand-reader", category: "reader", label: "Wiegand reader (into controller)", description: "RF/optical reader wired Wiegand 26/34 into the access controller's reader port. Autonomous offline decisions.", transports: ["wiegand"], configFields: [ { key: "door", label: "Controller reader port / door", type: "number", required: true, default: 1 }, { key: "format", label: "Wiegand format", type: "select", required: true, default: "26", options: [ { value: "26", label: "Wiegand 26" }, { value: "34", label: "Wiegand 34" }, ] }, ], create: (c) => new StubReader("wiegand-reader", c), }; export const tcpipReaderDriver: ReaderDriver = { id: "tcpip-reader", category: "reader", label: "TCP/IP reader (host-side)", description: "Network RF/optical reader seen only by the host; host decides and commands the relay.", transports: ["tcp-ip"], configFields: [hostField, portField(9000)], create: (c) => new StubReader("tcpip-reader", c), }; // Dingtian DT-008 QR/RFID access reader (same vendor as the relay board). Scans QR/barcode // + ID/IC/NFC cards. A PUSH device: on each scan it HTTP-GETs our backend (/qa/mcardsea.) // carrying its serial (cjihao); the backend resolves the lane by matching that serial to this // device's `serial` config, decides, and replies the verdict (drives the beep). No host-side // connection — the adapter is a stub; the real integration is the HTTP endpoint (apps/server // routes/qr-reader.ts). Interfaces: Wiegand 26/34, TCP/IP, USB, RS485. See // wiki/entities/dingtian-dt008-reader.md. export const dingtianQrReaderDriver: ReaderDriver = { id: "dingtian-qr-reader", category: "reader", label: "Dingtian DT-008 QR/RFID reader (HTTP push)", description: "Dingtian DT-008 QR/barcode/RFID access reader that HTTP-pushes each scan to the backend. Set its server IP/port to this host in the vendor tool; enter its serial here so scans resolve to this lane.", transports: ["tcp-ip"], configFields: [ { key: "serial", label: "Device serial (cjihao)", type: "string", required: true, help: "The reader's serial as it reports in each scan (the `cjihao` field). Used to map scans to this lane.", }, { // OPTIONAL: the reader pushes by serial (operation needs no IP), but giving its // IP lets the status monitor ICMP-ping it for a real online/offline dot instead // of an always-green stub. Leave blank to skip monitoring (shows "set IP"). ...hostField, required: false, help: "Optional: the reader's IP, used ONLY to monitor it (ping). Scans still resolve by serial. Leave blank to skip liveness monitoring.", }, ], create: (c) => new StubReader("dingtian-qr-reader", c), };