Device-agnostic driver registry + first-run setup
Make the device-adapter pattern selectable so the admin chooses hardware at install — per lane, from a catalog of supported drivers. Adding a device = registering one more driver; no business-logic change. packages/devices: - interfaces.ts: AccessControlDevice / ReaderDevice / CameraDevice / PrinterDevice (adds CameraDevice for entry/exit snapshot-on-event; access relay stays intent-only per "a barrier is not a door"). - registry.ts: driver catalog with per-driver config fields + factory, config validation, and a catalog payload for the setup UI. - drivers/: stub adapters — access (zkteco, esp32-relay), reader (wiegand, tcp-ip), camera (hikvision, dahua). Real vendor protocols TBD. packages/db: - lane_devices + setup_state tables (migration 0001); re-export query helpers. apps/server: - routes/setup.ts: GET /api/setup/catalog (public schema), and admin-only /assign, /state, /complete with registry validation before persisting. - extract auth.ts (requireJwtSecret, requireRole, JWT type aug). apps/web: - SetupWizard scaffold + api client: pick a driver per category for a lane, render its config fields. wiki: device-registry + first-run-setup concept pages; cross-link from device-adapter-pattern; index + log updated. Verified: full turbo build (5/5); catalog lists all drivers; admin assign persists; missing-config and no-token requests are rejected.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import type { DeviceHealth, ReaderDevice, ReaderEvent } from "../interfaces.js";
|
||||
import type { DeviceConfig, ReaderDriver } from "../registry.js";
|
||||
import { hostField, portField, stubLog } from "./common.js";
|
||||
|
||||
// Reader drivers (RF / optical). Two integration paths: Wiegand reads reach the
|
||||
// access controller directly (autonomous); TCP-IP readers are seen host-side.
|
||||
// See wiki/concepts/entry-exit-readers.md. STUBS only.
|
||||
|
||||
class StubReader implements ReaderDevice {
|
||||
#cb: ((r: ReaderEvent) => void) | null = null;
|
||||
constructor(
|
||||
readonly driverId: string,
|
||||
protected readonly config: DeviceConfig,
|
||||
) {}
|
||||
async connect(): Promise<void> {
|
||||
stubLog(this.driverId, "connect");
|
||||
}
|
||||
async disconnect(): Promise<void> {
|
||||
stubLog(this.driverId, "disconnect");
|
||||
}
|
||||
async healthCheck(): Promise<DeviceHealth> {
|
||||
return { status: "ready", detail: "stub" };
|
||||
}
|
||||
onRead(cb: (r: ReaderEvent) => void): void {
|
||||
this.#cb = cb;
|
||||
stubLog(this.driverId, "onRead handler registered");
|
||||
}
|
||||
/** Test hook for stubs — real drivers emit from hardware events. */
|
||||
protected emit(r: ReaderEvent): void {
|
||||
this.#cb?.(r);
|
||||
}
|
||||
}
|
||||
|
||||
export const wiegandReaderDriver: ReaderDriver = {
|
||||
id: "wiegand-reader",
|
||||
category: "reader",
|
||||
label: "Wiegand reader (into controller)",
|
||||
description:
|
||||
"RF/optical reader wired Wiegand 26/34 into the access controller's reader port. Autonomous offline decisions.",
|
||||
transports: ["wiegand"],
|
||||
configFields: [
|
||||
{ key: "door", label: "Controller reader port / door", type: "number", required: true, default: 1 },
|
||||
{ key: "format", label: "Wiegand format", type: "select", required: true, default: "26", options: [
|
||||
{ value: "26", label: "Wiegand 26" },
|
||||
{ value: "34", label: "Wiegand 34" },
|
||||
] },
|
||||
],
|
||||
create: (c) => new StubReader("wiegand-reader", c),
|
||||
};
|
||||
|
||||
export const tcpipReaderDriver: ReaderDriver = {
|
||||
id: "tcpip-reader",
|
||||
category: "reader",
|
||||
label: "TCP/IP reader (host-side)",
|
||||
description:
|
||||
"Network RF/optical reader seen only by the host; host decides and commands the relay.",
|
||||
transports: ["tcp-ip"],
|
||||
configFields: [hostField, portField(9000)],
|
||||
create: (c) => new StubReader("tcpip-reader", c),
|
||||
};
|
||||
Reference in New Issue
Block a user