2a36830880
A credential read at an exit lane validates the session, then opens. Adds a 'read' channel to the device bus (DeviceReadEvent: ticket/plate/qr/card); entry stays button-driven so reads are exit/identity events. Flow: read -> fold the SIGNED ledger for that identity -> validate open + PAID + within gracePeriodExitMin -> signed vehicle_exit -> pulseOpen -> close the session cache. Unpaid / grace-expired / unknown -> signed anomaly, barrier stays closed (a deliberate business reject, not a fail-state; 'exit fails open' is about host/power loss). Validation reads the ledger (authoritative), not the cache. No payment events exist until the pay station is built, so every transient exit currently rejects -- the correct end-state, not yet passable. Verified against stubs: unpaid->anomaly+no-open; paid+grace->exit+open+closed; expired->anomaly; unknown->anomaly; verifyChain ok across entry->pay->exit. Flagged: lane_devices has no entry/exit direction model (exit door hardcoded to 1); needs a lane-direction/role model before multi-reader lanes.
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import type { PrinterStatus } from "@parking/devices";
|
|
|
|
// 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 (lane_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 at a lane: a ticket scanned at exit, a plate from LPR, a card
|
|
// at a reader. Drives identity-based flows (exit validation, and later permits /
|
|
// pay-station lookup). `kind` mirrors IdentitySource. See parking-session.md.
|
|
export interface DeviceReadEvent {
|
|
readonly driverId: string;
|
|
readonly deviceId: string; // lane_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
|
|
}
|
|
|
|
/** A printer's status as tracked by the live monitor (status + identity). */
|
|
export interface PrinterStatusEvent {
|
|
readonly deviceId: string; // lane_devices id
|
|
readonly lane: number;
|
|
readonly driverId: string;
|
|
readonly role?: string; // entry-dispenser | booth-receipt
|
|
readonly status: PrinterStatus;
|
|
}
|
|
|
|
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) at a lane. */
|
|
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);
|
|
}
|
|
}
|
|
|
|
/** Process-wide device event bus. */
|
|
export const deviceEvents = new DeviceEventBus();
|