// Device-agnostic adapter interfaces. // // Business logic talks ONLY to these interfaces, never to a device SDK. Swapping // hardware means writing a new adapter that implements one of these — nothing // else changes. See wiki/concepts/device-adapter-pattern.md. // // SAFETY: a barrier is NOT a door. The relay interface expresses INTENT only // (`pulseOpen`); it never times or forces a close against a vehicle. Physical // safety (induction loops, anti-crush, auto-reverse) lives in the barrier // operator's own firmware. See wiki/concepts/barrier-not-a-door.md. /** The four device categories an admin configures per lane. */ export type DeviceCategory = "access" | "reader" | "camera" | "printer"; /** Lifecycle shared by every device adapter. */ export interface Device { /** Stable id of the driver that produced this instance (e.g. "zkteco"). */ readonly driverId: string; connect(): Promise; disconnect(): Promise; /** Liveness/health probe used by setup ("Test connection") and monitoring. */ healthCheck(): Promise; } export interface DeviceHealth { readonly status: "ready" | "offline" | "degraded"; readonly detail?: string; } // --- Access control (barrier relay) -------------------------------------- // ZKTeco, an ESP32 relay controller, UHPPOTE, etc. all implement this. export interface AccessControlDevice extends Device { /** Express intent to open. NEVER timed/forced closed against a vehicle. */ pulseOpen(doorId: number): Promise; getDoorStatus(doorId: number): Promise<"open" | "closed">; } // --- Inputs (buttons / dry contacts) ------------------------------------- // Optional capability for controllers that expose host-readable inputs SEPARATE // from their relays — e.g. the Dingtian board. This is what enables host-in-the- // loop entry: a button press is reported to the host, which decides (print a // ticket) before commanding the relay — instead of the input auto-firing the // relay. See wiki/decisions/access-controller-button-flow.md. export interface InputDevice { /** Read the current state of all inputs (true = active/pressed). */ readInputs(): Promise; /** * Subscribe to input edges. Returns an unsubscribe fn. Implementations may * back this with hardware push or polling — the consumer doesn't care. */ onInput(cb: (event: InputEvent) => void): () => void; } export interface InputEvent { /** 1-based input/channel index. */ readonly input: number; /** Edge: pressed = went active, released = went inactive. */ readonly edge: "pressed" | "released"; readonly at: string; // ISO-8601 } /** Type guard: does this device expose host-readable inputs? */ export function hasInputs(device: Device): device is Device & InputDevice { return ( typeof (device as Partial).readInputs === "function" && typeof (device as Partial).onInput === "function" ); } // --- Preconditions (device must be configured a certain way) ------------- // Optional capability: a device that depends on specific on-device configuration // to work correctly for parking can report it. Example: the Dingtian board must // have `input_link_relay` DISABLED, else a button press auto-fires the relay and // defeats host-in-the-loop entry (the same trap as the UHPPOTE, but fixable here). // The app does not own full device config (that's the vendor's web UI) — it only // checks the few preconditions our flow depends on, and optionally fixes them. // See wiki/decisions/access-controller-button-flow.md. export interface PreconditionDevice { checkPreconditions(): Promise; /** Apply automatic fixes for fixable issues; returns the re-checked result. */ fixPreconditions(): Promise; } export interface PreconditionResult { readonly ok: boolean; readonly issues: PreconditionIssue[]; } export interface PreconditionIssue { readonly key: string; readonly message: string; /** True if fixPreconditions() can correct this automatically. */ readonly fixable: boolean; } export function hasPreconditions( device: Device, ): device is Device & PreconditionDevice { return typeof (device as Partial).checkPreconditions === "function"; } // --- Readers (RF / optical; TCP-IP or Wiegand) --------------------------- export interface ReaderDevice extends Device { /** Emits when a credential is read (card number, plate, QR payload, …). */ onRead(cb: (read: ReaderEvent) => void): void; } export interface ReaderEvent { readonly value: string; readonly kind: "card" | "plate" | "qr" | "ticket"; readonly door: number; readonly at: string; // ISO-8601 } // --- Cameras (entry/exit snapshot) --------------------------------------- // Hikvision / Dahua implement this. Snapshot-on-event: the host asks for an // image at entry/exit; the image is stored and referenced from the signed event // as an independent record (anti-fraud). See wiki/concepts/append-only-event-chain. export interface CameraDevice extends Device { captureSnapshot(ctx: SnapshotContext): Promise; } export interface SnapshotContext { readonly lane: number; readonly direction: "entry" | "exit"; } export interface Snapshot { /** Storage reference for the captured image (file path / blob id). */ readonly imageRef: string; readonly contentType: string; readonly capturedAt: string; // ISO-8601 } // --- Printers (ticket dispenser / booth printer) ------------------------- export interface TicketData { readonly ticketId: string; readonly lane: number; readonly issuedAt: string; // ISO-8601 } export interface PrinterDevice extends Device { printTicket(data: TicketData): Promise; }