1b55e2034d
The Dingtian board's inputs are independent of its relays (configurable), so a button on an input can report to the host WITHOUT auto-firing a relay — solving the access-controller-button-flow blocker the UHPPOTE/ZKTeco couldn't. packages/devices: - access-dingtian.ts: `dingtian` access driver implementing AccessControlDevice (relay pulse/latch via UDP string protocol :60001), InputDevice (read inputs + poll-based press/release events, active-LOW), and the new PreconditionDevice. - PreconditionDevice capability on the interface: a device can report config it requires for parking and optionally fix it. Dingtian checks input_link_relay via the HTTP config API and can disable it. - httpPort config field — the web/config API port is separate from UDP control (this unit uses 8080, not the default 80). - Register dingtian; export driver objects from the package. Verified on real hardware (DT-R004 @ 10.0.10.172): status read, relay pulse, input events; disabled input_link_relay via the driver, then confirmed pressing inputs fires NO relay (0000) — host-in-the-loop entry works. Config-write gotcha recorded: config_set.cgi requires "command":"setconfig" injected after "status" (GET omits it) or the POST silently no-ops. apps/server/scripts/dingtian-test.mjs: status / watch / pulse hardware test. wiki: dingtian-relay verified; button-flow marked RESOLVED; index + log.
145 lines
5.6 KiB
TypeScript
145 lines
5.6 KiB
TypeScript
// 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<void>;
|
|
disconnect(): Promise<void>;
|
|
/** Liveness/health probe used by setup ("Test connection") and monitoring. */
|
|
healthCheck(): Promise<DeviceHealth>;
|
|
}
|
|
|
|
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<void>;
|
|
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<boolean[]>;
|
|
/**
|
|
* 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<InputDevice>).readInputs === "function" &&
|
|
typeof (device as Partial<InputDevice>).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<PreconditionResult>;
|
|
/** Apply automatic fixes for fixable issues; returns the re-checked result. */
|
|
fixPreconditions(): Promise<PreconditionResult>;
|
|
}
|
|
|
|
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<PreconditionDevice>).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<Snapshot>;
|
|
}
|
|
|
|
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<void>;
|
|
}
|