Files
parking_solution/apps/server/src/device-events.ts
T
julian 2a36830880 server: exit flow (pay-on-foot validation)
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.
2026-06-15 18:57:14 +02:00

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();