f6e35bbebf
An early wrong assumption named the QR/RFID access reader "GEE" / "GEE/Fondvision" / "GEE-QR-ER80" (and summarized a raw GEE PDF as its datasheet). There is no GEE device — it's the Dingtian DT-008 (dingtian-tech.com/en_us/qr_code_reader.html), the same vendor as the relay board, which is why it integrates the identical HTTP-GET-push way. Code: - Driver symbol geeQrReaderDriver → dingtianQrReaderDriver; label → "Dingtian DT-008 QR/RFID reader (HTTP push)"; comments/description rewritten to the real DT-008 facts (Wiegand 26/34, TCP/IP, USB, RS485 — not RS-232; QR/barcode + ID/IC/NFC — not DataMatrix/1D). - Persisted driverId "gee-qr-reader" → "dingtian-qr-reader" (the registry lookup key + the row created on assign in qr-reader.ts). - Migration 0015 rewrites existing devices.driver_id rows so configured readers keep resolving (applied to the dev DB — 2 rows; the booth applies it on boot). Behaviour is unchanged: naming + the persisted id only. Wiki + memory: - Renamed entities/gee-qr-er80.md → dingtian-dt008-reader.md and sources/gee-qr-er80.md → dingtian-dt008.md; rewrote both to the real DT-008 product-page specs while KEEPING all the verified-on-hardware protocol facts (cjihao serial, .jsp path, Connection: close). Fixed every cross-reference + "GEE" mention in 6 other pages. Memory gee-reader-serial-binding → dingtian-reader-serial-binding. The only surviving "GEE" mentions are deliberate naming-correction notes, the raw PDF filename, and the append-only log history. Full workspace build/lint/test green; dev DB readers verified resolving to the registered dingtian-qr-reader driver. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
197 lines
8.7 KiB
TypeScript
197 lines
8.7 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import type { PrinterStatus } from "@parking/devices";
|
|
import type { LedgerEventRow } from "@parking/db";
|
|
|
|
// Internal event bus for device-originated events (button presses, etc.).
|
|
// Hardware drivers / inbound device pushes emit here; business logic (entry
|
|
// flow, event-log) subscribes — keeping the HTTP/transport layer thin and the
|
|
// app device-agnostic. See wiki/entities/fastify.md.
|
|
|
|
export interface DeviceInputEvent {
|
|
readonly driverId: string; // e.g. "dingtian"
|
|
readonly deviceId: string; // which configured device (devices id)
|
|
readonly input: number; // 1-based input/channel
|
|
readonly edge: "on" | "off"; // active / inactive
|
|
readonly at: string; // ISO-8601 (server receive time)
|
|
readonly source: "push" | "poll";
|
|
}
|
|
|
|
// A credential read: a ticket scanned at exit, a plate from LPR, a card at a reader.
|
|
// Drives identity-based flows (exit validation, subscriptions, pay-station lookup). `kind`
|
|
// mirrors IdentitySource. See parking-session.md.
|
|
export interface DeviceReadEvent {
|
|
readonly driverId: string;
|
|
readonly deviceId: string; // devices id of the reader/scanner/camera
|
|
readonly value: string; // the ticket id / plate / card number
|
|
readonly kind: "ticket" | "plate" | "qr" | "card";
|
|
readonly at: string; // ISO-8601
|
|
}
|
|
|
|
/**
|
|
* The decision a read produced. Returned by the read flows so a SYNCHRONOUS reader
|
|
* (e.g. the QR reader, whose HTTP reply drives its beep + output) can answer the
|
|
* device. A fire-and-forget reader simply ignores it. See wiki/entities/dingtian-dt008-reader.md.
|
|
*/
|
|
export interface ReadOutcome {
|
|
/** Was the vehicle admitted/exited (barrier opened)? Drives the reader's beep. */
|
|
readonly accepted: boolean;
|
|
/** Which way it went, when known (subscription/exit infer this). */
|
|
readonly direction?: "entry" | "exit";
|
|
/** Human-readable reason (for logs / the reader UI), esp. on reject. */
|
|
readonly reason?: string;
|
|
}
|
|
|
|
/** A printer's status as tracked by the live monitor (status + identity). */
|
|
export interface PrinterStatusEvent {
|
|
readonly deviceId: string; // devices id
|
|
readonly driverId: string;
|
|
readonly role?: string; // entry-dispenser | booth-receipt
|
|
readonly status: PrinterStatus;
|
|
}
|
|
|
|
/**
|
|
* The unified live status of ANY configured device — what the booth footer shows.
|
|
* Every enabled device is polled: printers via their rich `readStatus()`
|
|
* (paper/cover/cutter), all other categories via the generic `healthCheck()`
|
|
* reachability probe. `state` is the common traffic-light; `detail` carries the
|
|
* human summary (e.g. "paper out", or an unreachable error). See device-monitor.ts
|
|
* and wiki/concepts/device-status-monitoring.md.
|
|
*/
|
|
export interface DeviceStatusEvent {
|
|
readonly deviceId: string; // devices id
|
|
readonly driverId: string;
|
|
readonly category: "access" | "reader" | "camera" | "printer" | "vision";
|
|
/**
|
|
* The device's ROLE descriptor for the footer label — NOT the vendor. A
|
|
* direction-style token the client localises and pairs with the category, so the
|
|
* chip reads e.g. "Lexuesi hyrje" / "Kamera dalje" / "Printer kabina":
|
|
* - reader/camera: "entry" | "exit" | "both" (inherited from its bound relay)
|
|
* - access: "entry" | "exit" | "both" | "mixed" (from its relays[])
|
|
* - printer: "lane" (entry-dispenser) | "booth" (booth-receipt)
|
|
* - undetermined: null (chip shows the category alone)
|
|
*/
|
|
readonly roleKind: "entry" | "exit" | "both" | "mixed" | "lane" | "booth" | null;
|
|
readonly state: "ready" | "degraded" | "offline";
|
|
readonly detail?: string;
|
|
readonly checkedAt: string; // ISO-8601
|
|
}
|
|
|
|
/** Lane occupancy from a camera's vehicle detection — a per-direction "busy/free"
|
|
* the booth shows as barrier lights. ADVISORY ONLY: a detection is a hint, never a
|
|
* gate (it never blocks a ticket or opens a barrier). "busy" is set by a vehicle
|
|
* `active` event; it auto-clears to "free" after a timeout (this camera class sends
|
|
* no leave/`inactive` signal — see wiki/entities/lpr-camera.md). */
|
|
export interface LaneStatusEvent {
|
|
readonly entry: boolean; // true = busy (a vehicle is at the entry vicinity)
|
|
readonly exit: boolean; // true = busy (a vehicle is at the exit vicinity)
|
|
}
|
|
|
|
/** A plate was RECOGNIZED for a session AFTER its entry/exit event already shipped. Plate
|
|
* recognition is async/advisory (a vision round-trip off the snapshot), so it lands a
|
|
* moment after the signed event — too late for the event's own WS push to carry it. This
|
|
* notifies the booth so it can fill in the plate badge on the already-rendered feed row /
|
|
* active session in place, no refresh. Advisory; never touches the signed ledger. See
|
|
* snapshot.ts (recognizePlate) + event-enrich.ts. */
|
|
export interface PlateRecognizedEvent {
|
|
readonly identity: string; // the session identity the plate is tied to
|
|
readonly plate: string; // normalized plate text (trimmed, upper)
|
|
readonly direction: "entry" | "exit";
|
|
}
|
|
|
|
/** Per-lane RADAR presence — a vehicle-presence INPUT (loop/radar) is shorted at the
|
|
* entry/exit barrier, i.e. "something is in the lane vicinity" BEFORE the camera has
|
|
* confirmed a vehicle. Same signal that makes the physical button lamp (relay 3) blink:
|
|
* radar-present + camera-not-busy. Drives the booth's barrier light blink. Advisory only —
|
|
* it gates nothing. See wiki/concepts/button-light-indicator.md. */
|
|
export interface LanePresenceEvent {
|
|
readonly entry: boolean; // true = a presence input on an entry barrier is active
|
|
readonly exit: boolean; // true = a presence input on an exit barrier is active
|
|
}
|
|
|
|
class DeviceEventBus extends EventEmitter {
|
|
emitInput(event: DeviceInputEvent): void {
|
|
this.emit("input", event);
|
|
}
|
|
onInput(cb: (event: DeviceInputEvent) => void): () => void {
|
|
this.on("input", cb);
|
|
return () => this.off("input", cb);
|
|
}
|
|
|
|
/** A credential read (ticket scan, plate, card). */
|
|
emitRead(event: DeviceReadEvent): void {
|
|
this.emit("read", event);
|
|
}
|
|
onRead(cb: (event: DeviceReadEvent) => void): () => void {
|
|
this.on("read", cb);
|
|
return () => this.off("read", cb);
|
|
}
|
|
|
|
/** Emitted by the printer monitor whenever a printer's status CHANGES. */
|
|
emitPrinterStatus(event: PrinterStatusEvent): void {
|
|
this.emit("printer-status", event);
|
|
}
|
|
onPrinterStatus(cb: (event: PrinterStatusEvent) => void): () => void {
|
|
this.on("printer-status", cb);
|
|
return () => this.off("printer-status", cb);
|
|
}
|
|
|
|
/** Emitted by the device monitor whenever ANY device's unified status CHANGES
|
|
* (all categories — relays, readers, cameras, printers). Drives the booth
|
|
* device-status footer over the WS. */
|
|
emitDeviceStatus(event: DeviceStatusEvent): void {
|
|
this.emit("device-status", event);
|
|
}
|
|
onDeviceStatus(cb: (event: DeviceStatusEvent) => void): () => void {
|
|
this.on("device-status", cb);
|
|
return () => this.off("device-status", cb);
|
|
}
|
|
|
|
/**
|
|
* Emitted AFTER a signed business event is appended to the ledger (entry, exit,
|
|
* payment, void, …). The payload is the persisted row — business facts only, no
|
|
* secrets — so it is safe to fan out to authenticated booth clients over the WS.
|
|
* This is a read-side notification ONLY: it never feeds back into append/sign/
|
|
* chain logic. See event-log.ts (emitted from EventLog.append) and routes/ws.ts.
|
|
*/
|
|
emitLedger(event: LedgerEventRow): void {
|
|
this.emit("ledger", event);
|
|
}
|
|
onLedger(cb: (event: LedgerEventRow) => void): () => void {
|
|
this.on("ledger", cb);
|
|
return () => this.off("ledger", cb);
|
|
}
|
|
|
|
/** Emitted whenever a lane's busy/free state CHANGES (from camera vehicle
|
|
* detection). Drives the booth's barrier lights. Advisory only. */
|
|
emitLaneStatus(event: LaneStatusEvent): void {
|
|
this.emit("lane-status", event);
|
|
}
|
|
onLaneStatus(cb: (event: LaneStatusEvent) => void): () => void {
|
|
this.on("lane-status", cb);
|
|
return () => this.off("lane-status", cb);
|
|
}
|
|
|
|
/** Emitted whenever a lane's RADAR presence CHANGES (a presence input shorted/cleared
|
|
* at an entry/exit barrier). Drives the booth barrier light's blink. Advisory only. */
|
|
emitLanePresence(event: LanePresenceEvent): void {
|
|
this.emit("lane-presence", event);
|
|
}
|
|
onLanePresence(cb: (event: LanePresenceEvent) => void): () => void {
|
|
this.on("lane-presence", cb);
|
|
return () => this.off("lane-presence", cb);
|
|
}
|
|
|
|
/** Emitted when an async plate recognition completes for a session (after its event
|
|
* already shipped). Lets the booth backfill the plate badge in place. Advisory only. */
|
|
emitPlateRecognized(event: PlateRecognizedEvent): void {
|
|
this.emit("plate-recognized", event);
|
|
}
|
|
onPlateRecognized(cb: (event: PlateRecognizedEvent) => void): () => void {
|
|
this.on("plate-recognized", cb);
|
|
return () => this.off("plate-recognized", cb);
|
|
}
|
|
}
|
|
|
|
/** Process-wide device event bus. */
|
|
export const deviceEvents = new DeviceEventBus();
|