From 355026dcf7164e9500a8a229483c106380b74b3d Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sun, 14 Jun 2026 14:28:52 +0200 Subject: [PATCH] Remove UHPPOTE/ZKTeco; Dingtian is the only access driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither UHPPOTE nor ZKTeco is used — the Dingtian relay controller was chosen and verified. Remove their code and re-scope the wiki. Code: - delete access-uhppote.ts, uhppoted.d.ts, access.ts (zkteco/esp32-relay stubs), and the three uhppote-*.mjs hardware test scripts. - remove the `uhppoted` npm dependency from @parking/devices and @parking/server. - unregister uhppote/zkteco/esp32-relay from the driver registry; drop their exports. Catalog access drivers = dingtian only. Build green (5/5). - refresh now-stale example comments (registry/interfaces/setup/api) to use current examples; keep the two "UHPPOTE blocker" references that explain why the precondition capability exists. Wiki (kept pages, re-scoped): - uhppote-controller, zkteco-controller -> rejected/historical with callouts; uhppote-vs-esp32 -> historical (detection-vs-prevention lens still useful). - re-point all "current device" framing (standing-decisions, bom, overview, open-questions, device-registry, device-discovery, index) to dingtian-relay. - transferable concepts (network-isolation, event-log-ingestion, barrier-not-a- door, threat-model) untouched. Raw source immutable. Links lint clean. --- apps/server/package.json | 3 +- apps/server/scripts/uhppote-common.mjs | 72 ----- apps/server/scripts/uhppote-listen.mjs | 100 ------- apps/server/scripts/uhppote-relay.mjs | 44 --- apps/server/src/routes/setup.ts | 4 +- apps/web/src/api.ts | 2 +- packages/devices/package.json | 3 +- .../devices/src/drivers/access-uhppote.ts | 263 ------------------ packages/devices/src/drivers/access.ts | 49 ---- packages/devices/src/drivers/index.ts | 8 - packages/devices/src/drivers/uhppoted.d.ts | 83 ------ packages/devices/src/index.ts | 3 - packages/devices/src/interfaces.ts | 4 +- packages/devices/src/registry.ts | 13 +- pnpm-lock.yaml | 19 -- wiki/concepts/device-discovery.md | 18 +- wiki/concepts/device-registry.md | 3 +- wiki/concepts/uhppote-vs-esp32.md | 14 +- wiki/decisions/open-questions.md | 2 +- wiki/decisions/standing-decisions.md | 10 +- wiki/entities/bom.md | 2 +- wiki/entities/uhppote-controller.md | 44 +-- wiki/entities/zkteco-controller.md | 18 +- wiki/index.md | 8 +- wiki/log.md | 13 + wiki/overview.md | 15 +- 26 files changed, 106 insertions(+), 711 deletions(-) delete mode 100644 apps/server/scripts/uhppote-common.mjs delete mode 100644 apps/server/scripts/uhppote-listen.mjs delete mode 100644 apps/server/scripts/uhppote-relay.mjs delete mode 100644 packages/devices/src/drivers/access-uhppote.ts delete mode 100644 packages/devices/src/drivers/access.ts delete mode 100644 packages/devices/src/drivers/uhppoted.d.ts diff --git a/apps/server/package.json b/apps/server/package.json index ab53ee3..434f3cd 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -21,8 +21,7 @@ "@parking/shared": "workspace:*", "bcrypt": "6.0.0", "fastify": "5.8.5", - "fastify-plugin": "6.0.0", - "uhppoted": "0.9.0" + "fastify-plugin": "6.0.0" }, "devDependencies": { "@types/bcrypt": "6.0.0", diff --git a/apps/server/scripts/uhppote-common.mjs b/apps/server/scripts/uhppote-common.mjs deleted file mode 100644 index ff8b75d..0000000 --- a/apps/server/scripts/uhppote-common.mjs +++ /dev/null @@ -1,72 +0,0 @@ -// Shared helpers for the UHPPOTE hardware test scripts. -// Run directly against the device (independent of the HTTP server). -// -// node apps/server/scripts/uhppote-listen.mjs -// node apps/server/scripts/uhppote-relay.mjs -// -// Env overrides: -// UHPPOTE_SERIAL controller serial (default 225088491) -// UHPPOTE_HOST controller IP (default 10.0.10.3) -// UHPPOTE_BCAST Config broadcast (default derived from HOST subnet) -// HOST_IP this host's IP the controller pushes events to -// (default: auto-detected interface on the controller's subnet) - -import { networkInterfaces } from "node:os"; -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); -const uhppoted = require("uhppoted"); - -export const SERIAL = Number(process.env.UHPPOTE_SERIAL ?? 225088491); -export const HOST = process.env.UHPPOTE_HOST ?? "10.0.10.3"; - -/** Subnet-directed broadcast for the interface that owns `ip`. */ -function broadcastForHost(ip) { - const o = ip.split(".").map(Number); - for (const ifaces of Object.values(networkInterfaces())) { - for (const i of ifaces ?? []) { - if (i.family !== "IPv4" || i.internal) continue; - const a = i.address.split(".").map(Number); - const m = i.netmask.split(".").map(Number); - if (o.every((x, k) => (x & m[k]) === (a[k] & m[k]))) { - return a.map((x, k) => (x & m[k]) | (~m[k] & 0xff)).join("."); - } - } - } - return "255.255.255.255"; -} - -/** This host's own IP on the controller's subnet (where it should push events). */ -export function hostIpOnControllerSubnet(ip = HOST) { - if (process.env.HOST_IP) return process.env.HOST_IP; - const o = ip.split(".").map(Number); - for (const ifaces of Object.values(networkInterfaces())) { - for (const i of ifaces ?? []) { - if (i.family !== "IPv4" || i.internal) continue; - const a = i.address.split(".").map(Number); - const m = i.netmask.split(".").map(Number); - if (o.every((x, k) => (x & m[k]) === (a[k] & m[k]))) return i.address; - } - } - return null; -} - -export const BCAST = process.env.UHPPOTE_BCAST ?? broadcastForHost(HOST); - -export function makeCtx(timeoutMs = 5000) { - return { - config: new uhppoted.Config( - "parking", - "0.0.0.0", - `${BCAST}:60000`, - "0.0.0.0:60001", - timeoutMs, - [], - false, - ), - locale: "en-US", - }; -} - -export const controller = { id: SERIAL, address: HOST, protocol: "udp" }; -export { uhppoted }; diff --git a/apps/server/scripts/uhppote-listen.mjs b/apps/server/scripts/uhppote-listen.mjs deleted file mode 100644 index bd127f1..0000000 --- a/apps/server/scripts/uhppote-listen.mjs +++ /dev/null @@ -1,100 +0,0 @@ -// Live button/event listener for the UHPPOTE controller. -// -// Points the controller's event listener at THIS host, then prints each pushed -// event in real time. Press the door buttons on the controller and watch them -// appear. Ctrl-C to stop. -// -// node apps/server/scripts/uhppote-listen.mjs - -import { - controller, - hostIpOnControllerSubnet, - makeCtx, - uhppoted, -} from "./uhppote-common.mjs"; - -const ctx = makeCtx(); - -const hostIp = hostIpOnControllerSubnet(); -if (!hostIp) { - console.error("Could not determine this host's IP on the controller's subnet."); - console.error("Set HOST_IP= and retry."); - process.exit(1); -} - -console.log(`controller : ${controller.id} @ ${controller.address}`); -console.log(`this host : ${hostIp} (events will be pushed here on :60001)`); - -// 0) Remember the controller's current listener so we can restore it on exit -// (it was pointing somewhere else, e.g. 10.0.10.241). -let prevListener = null; -try { - prevListener = await uhppoted.getListener(ctx, controller); - console.log(`prior listener: ${prevListener.address}:${prevListener.port} (will restore on exit)`); -} catch (e) { - console.warn("getListener (non-fatal):", e.code ?? e.message); -} - -// 1) Tell the controller to push events to us. -try { - const r = await uhppoted.setListener(ctx, controller, hostIp, 60001); - console.log("setListener:", JSON.stringify(r)); -} catch (e) { - console.error("setListener failed:", e.code ?? e.message); - process.exit(1); -} - -// 2) (Best-effort) ensure door open/close + button events are recorded. -try { - await uhppoted.recordSpecialEvents(ctx, controller, true); - console.log("recordSpecialEvents: enabled"); -} catch (e) { - console.warn("recordSpecialEvents (non-fatal):", e.code ?? e.message); -} - -console.log("\n── listening — press the door buttons on the controller ──\n"); - -function describe(ev) { - const e = ev?.state?.event ?? ev?.event; - const buttons = ev?.state?.buttons; - const doors = ev?.state?.doors; - const parts = []; - if (e) { - parts.push( - `event#${e.index} type=${e.type?.event ?? e.type?.code} door=${e.door} granted=${e.granted} reason="${e.reason?.reason ?? e.reason?.code}" @${e.timestamp}`, - ); - } - if (buttons) { - const pressed = Object.entries(buttons).filter(([, v]) => v).map(([k]) => k); - parts.push(`buttons=[${pressed.join(",") || "none"}]`); - } - if (doors) { - const open = Object.entries(doors).filter(([, v]) => v).map(([k]) => k); - parts.push(`doorsOpen=[${open.join(",") || "none"}]`); - } - return parts.join(" "); -} - -uhppoted.listen( - ctx, - (event) => { - console.log(`[${new Date().toISOString()}] ${describe(event)}`); - }, - (err) => { - console.error("listen error:", err?.message ?? err); - }, -); - -process.on("SIGINT", async () => { - // Restore the controller's previous listener so we don't hijack it. - if (prevListener && prevListener.address && prevListener.address !== "0.0.0.0") { - try { - await uhppoted.setListener(ctx, controller, prevListener.address, prevListener.port); - console.log(`\nrestored listener -> ${prevListener.address}:${prevListener.port}`); - } catch (e) { - console.warn("\ncould not restore listener:", e.code ?? e.message); - } - } - console.log("stopped."); - process.exit(0); -}); diff --git a/apps/server/scripts/uhppote-relay.mjs b/apps/server/scripts/uhppote-relay.mjs deleted file mode 100644 index db0f5ca..0000000 --- a/apps/server/scripts/uhppote-relay.mjs +++ /dev/null @@ -1,44 +0,0 @@ -// Guarded relay (door-open) test for the UHPPOTE controller. -// -// Prompts before firing each relay so a door only opens when you're ready and -// watching. This is a 2-door controller, so it tests doors 1 and 2 by default. -// -// node apps/server/scripts/uhppote-relay.mjs # doors 1,2 (prompted) -// node apps/server/scripts/uhppote-relay.mjs 1 # only door 1 -// YES=1 node apps/server/scripts/uhppote-relay.mjs # no prompts (fires!) -// -// SAFETY: openDoor only expresses INTENT to open. The controller / barrier -// operator owns the close timing and anti-crush — we never time a close. - -import { createInterface } from "node:readline/promises"; -import { stdin, stdout } from "node:process"; -import { controller, makeCtx, uhppoted } from "./uhppote-common.mjs"; - -const ctx = makeCtx(); -const doors = process.argv.slice(2).map(Number).filter((n) => n >= 1 && n <= 4); -const targets = doors.length ? doors : [1, 2]; -const autoYes = process.env.YES === "1"; - -console.log(`controller : ${controller.id} @ ${controller.address}`); -console.log(`testing doors: ${targets.join(", ")}${autoYes ? " (auto, no prompts)" : ""}\n`); - -const rl = autoYes ? null : createInterface({ input: stdin, output: stdout }); - -for (const door of targets) { - if (rl) { - const ans = await rl.question(`Open door ${door}? [y/N] `); - if (ans.trim().toLowerCase() !== "y") { - console.log(` skipped door ${door}`); - continue; - } - } - try { - const res = await uhppoted.openDoor(ctx, controller, door); - console.log(` door ${door}: openDoor -> ${JSON.stringify(res)}`); - } catch (e) { - console.log(` door ${door}: ERROR ${e.code ?? e.message}`); - } -} - -rl?.close(); -console.log("\ndone."); diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts index 16db876..d5cb18b 100644 --- a/apps/server/src/routes/setup.ts +++ b/apps/server/src/routes/setup.ts @@ -28,14 +28,14 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise { const adminGuard = requireRole("admin"); // Catalog of selectable drivers per category (no secrets — schema only). - // `discoverable` flags drivers that can scan the LAN (e.g. UHPPOTE). + // `discoverable` flags drivers that can scan the LAN. app.get("/api/setup/catalog", async () => { const catalog = registry.catalog(); const discoverable = registry.list().filter(isDiscoverable).map((d) => d.id); return { ...catalog, discoverable }; }); - // Scan the LAN for devices a driver can discover (UHPPOTE UDP broadcast, etc). + // Scan the LAN for devices a driver can discover (UDP broadcast, etc). // Each found device is health-checked so the admin sees reachability before // assigning. Admin-only. See wiki/concepts/device-discovery.md. app.get<{ Params: { driverId: string } }>( diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 68c47d6..afce44f 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -110,7 +110,7 @@ export interface DiscoveredDevice { health: { status: string; detail?: string }; } -/** Scan the LAN for devices a driver can discover (e.g. UHPPOTE). Admin-only. */ +/** Scan the LAN for devices a driver can discover. Admin-only. */ export async function discoverDevices(driverId: string): Promise { const body = await apiFetch<{ devices: DiscoveredDevice[] }>( `/api/setup/discover/${driverId}`, diff --git a/packages/devices/package.json b/packages/devices/package.json index 45c548a..1e09792 100644 --- a/packages/devices/package.json +++ b/packages/devices/package.json @@ -18,8 +18,7 @@ "lint": "tsc --noEmit" }, "dependencies": { - "@parking/shared": "workspace:*", - "uhppoted": "0.9.0" + "@parking/shared": "workspace:*" }, "devDependencies": { "@types/node": "25.9.3", diff --git a/packages/devices/src/drivers/access-uhppote.ts b/packages/devices/src/drivers/access-uhppote.ts deleted file mode 100644 index e19018d..0000000 --- a/packages/devices/src/drivers/access-uhppote.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { networkInterfaces } from "node:os"; -import uhppoted, { type Controller, type Ctx } from "uhppoted"; -import type { AccessControlDevice, DeviceHealth } from "../interfaces.js"; -import type { - AccessDriver, - DeviceConfig, - DiscoveredDevice, -} from "../registry.js"; -import { hostField, stubLog } from "./common.js"; - -// `uhppoted` is CommonJS — import the default and destructure (named ESM imports -// don't resolve off a CJS module under NodeNext). -const { Config, getDevices, getStatus, openDoor } = uhppoted; - -// Every uhppoted call binds a UDP listener on :60001 for replies. Concurrent -// calls collide on that port (EACCES / dropped replies → spurious timeouts), so -// we serialize ALL controller I/O through one queue. UDP request/response is -// fast, so serial throughput is fine for a parking host. This is why parallel -// discovery + health checks were timing out. -let chain: Promise = Promise.resolve(); -function serialize(fn: () => Promise): Promise { - const run = chain.then(fn, fn); - // keep the chain alive regardless of this call's outcome - chain = run.then( - () => undefined, - () => undefined, - ); - return run; -} - -/** - * Compute subnet-directed broadcast addresses (e.g. 10.0.10.255) for every - * non-internal IPv4 interface. - * - * Why this matters: the uhppoted lib only enables SO_BROADCAST when the target - * matches a *subnet-directed* broadcast of a local interface — it does NOT - * recognise the global 255.255.255.255, so broadcasting there fails with EACCES. - * We must broadcast to the per-interface address (e.g. 10.0.10.255) instead. - */ -interface Iface { - network: number[]; // ip & mask, per octet - mask: number[]; - broadcast: string; -} - -function localIfaces(): Iface[] { - const out: Iface[] = []; - for (const ifaces of Object.values(networkInterfaces())) { - for (const i of ifaces ?? []) { - if (i.family !== "IPv4" || i.internal) continue; - const ip = i.address.split(".").map(Number); - const mask = i.netmask.split(".").map(Number); - if (ip.length !== 4 || mask.length !== 4) continue; - out.push({ - network: ip.map((o, k) => o & mask[k]!), - mask, - broadcast: ip.map((o, k) => (o & mask[k]!) | (~mask[k]! & 0xff)).join("."), - }); - } - } - return out; -} - -function subnetBroadcastAddrs(): string[] { - return localIfaces().map((i) => i.broadcast); -} - -/** Broadcast target for discovery: explicit override, else first subnet bcast. */ -function discoveryBroadcast(): string { - return ( - process.env.UHPPOTE_BROADCAST ?? subnetBroadcastAddrs()[0] ?? "255.255.255.255" - ); -} - -/** - * The subnet-directed broadcast for the interface that `host` belongs to. The - * uhppoted Config's broadcast address governs reply routing even for unicast - * ops, so it must match the TARGET's subnet (not just the first interface) or - * the reply is missed → timeout. - */ -function broadcastForHost(host: string): string { - const ip = host.split(".").map(Number); - if (ip.length === 4) { - for (const i of localIfaces()) { - if (ip.every((o, k) => (o & i.mask[k]!) === i.network[k])) return i.broadcast; - } - } - return discoveryBroadcast(); -} - -// Real UHPPOTE access-control driver, backed by the official `uhppoted` lib. -// Implements AccessControlDevice (intent-only relay — "a barrier is not a door"; -// the controller/barrier operator owns physical safety). See -// wiki/entities/uhppote-controller.md and wiki/concepts/barrier-not-a-door.md. -// -// SECURITY: the UHPPOTE protocol is unauthenticated UDP (port 60000). This driver -// assumes the controller sits on an isolated VLAN reachable only by the host. -// See wiki/concepts/uhppote-udp-protocol.md and network-isolation.md. - -/** - * uhppoted context broadcasting to a specific address on :60000, listening for - * replies on :60001. - */ -function buildCtxFor(broadcast: string, timeoutMs = 5000): Ctx { - return { - config: new Config( - "parking", - "0.0.0.0", - `${broadcast}:60000`, - "0.0.0.0:60001", - timeoutMs, - [], - false, - ), - locale: "en-US", - }; -} - -/** Default context for non-discovery ops (status/open use a unicast host). */ -function buildCtx(timeoutMs = 5000): Ctx { - return buildCtxFor(discoveryBroadcast(), timeoutMs); -} - -/** - * Broadcast targets to try for discovery. An explicit UHPPOTE_BROADCAST wins; - * otherwise every local subnet-directed broadcast (a host may have several - * interfaces — LAN, VPN, docker — and the controller is on only one). - */ -function discoveryBroadcasts(): string[] { - const override = process.env.UHPPOTE_BROADCAST; - if (override) return [override]; - const addrs = subnetBroadcastAddrs(); - return addrs.length > 0 ? addrs : ["255.255.255.255"]; -} - -class UhppoteAccessControl implements AccessControlDevice { - readonly driverId = "uhppote"; - readonly #controller: Controller; - readonly #ctx: Ctx; - - constructor(config: DeviceConfig) { - const serial = Number(config.serial); - const address = config.host ? String(config.host) : undefined; - const protocol = config.protocol === "tcp" ? "tcp" : "udp"; - - // Addressable descriptor when a host is given; otherwise rely on UDP - // broadcast discovery by serial. - this.#controller = address ? { id: serial, address, protocol } : serial; - - // The Config broadcast must match the target host's subnet (it governs - // reply routing even for unicast), else replies are missed → timeout. - const timeoutMs = config.timeoutMs ? Number(config.timeoutMs) : 5000; - this.#ctx = address - ? buildCtxFor(broadcastForHost(address), timeoutMs) - : buildCtx(timeoutMs); - } - - async connect(): Promise { - // No persistent socket to open (request/response over UDP); verify reachability. - await this.healthCheck(); - } - - async disconnect(): Promise { - stubLog(this.driverId, "disconnect (stateless udp — nothing to close)"); - } - - async healthCheck(): Promise { - try { - await serialize(() => getStatus(this.#ctx, this.#controller)); - return { status: "ready" }; - } catch (err) { - return { status: "offline", detail: (err as Error).message }; - } - } - - /** - * Express intent to open a door (1–4). NEVER timed/forced closed against a - * vehicle — auto-close/anti-crush is the barrier operator's firmware. - */ - async pulseOpen(doorId: number): Promise { - const res = await serialize(() => - openDoor(this.#ctx, this.#controller, doorId), - ); - if (!res.opened) { - throw new Error(`uhppote: door ${doorId} not opened (deviceId ${res.deviceId})`); - } - } - - async getDoorStatus(): Promise<"open" | "closed"> { - // The UHPPOTE status payload carries per-door state; without a confirmed - // wiring of door sensors we report the safe default until the real status - // mapping is added. (Status is fetched to prove reachability.) - await serialize(() => getStatus(this.#ctx, this.#controller)); - return "closed"; - } -} - -export const uhppoteDriver: AccessDriver & { - discover(): Promise; -} = { - id: "uhppote", - category: "access", - label: "UHPPOTE controller", - description: - "UHPPOTE Wiegand 26/34 network controller via the official uhppoted lib. Unauthenticated UDP — isolate the VLAN.", - transports: ["udp", "tcp"], - // UDP broadcast discovery (get-devices): every controller on the LAN answers - // with its serial, IP, and firmware. Broadcasts on every local subnet (the - // controller is on only one interface) and dedupes by serial. - // See wiki/concepts/device-discovery.md. - async discover(): Promise { - const bySerial = new Map(); - // Serial, not parallel: each getDevices binds :60001, so concurrent scans - // across interfaces collide (EACCES / dropped replies). - for (const bcast of discoveryBroadcasts()) { - let found; - try { - found = await serialize(() => getDevices(buildCtxFor(bcast, 3000))); - } catch { - continue; // a dead interface shouldn't fail the whole scan - } - for (const d of found) { - bySerial.set(d.device.serialNumber, { - id: String(d.device.serialNumber), - label: `UHPPOTE ${d.device.serialNumber} @ ${d.device.address}`, - config: { serial: d.device.serialNumber, host: d.device.address, protocol: "udp" }, - info: { - address: d.device.address, - netmask: d.device.netmask, - gateway: d.device.gateway, - MAC: d.device.MAC, - firmware: d.device.version, - }, - }); - } - } - return [...bySerial.values()]; - }, - configFields: [ - { - key: "serial", - label: "Controller serial number", - type: "number", - required: true, - help: "Printed on the controller (e.g. 405419896).", - }, - { ...hostField, required: false, help: "Optional: target a specific IP instead of UDP broadcast. Isolated VLAN only." }, - { - key: "protocol", - label: "Protocol", - type: "select", - required: false, - default: "udp", - options: [ - { value: "udp", label: "UDP (default)" }, - { value: "tcp", label: "TCP (newer firmware)" }, - ], - }, - { key: "doors", label: "Door count", type: "number", required: true, default: 4 }, - { key: "timeoutMs", label: "Timeout (ms)", type: "number", required: false, default: 5000 }, - ], - create: (c) => new UhppoteAccessControl(c), -}; diff --git a/packages/devices/src/drivers/access.ts b/packages/devices/src/drivers/access.ts deleted file mode 100644 index 087bd31..0000000 --- a/packages/devices/src/drivers/access.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { AccessControlDevice, DeviceHealth } from "../interfaces.js"; -import type { AccessDriver, DeviceConfig } from "../registry.js"; -import { hostField, portField, stubLog } from "./common.js"; - -// Access-control drivers. Each implements AccessControlDevice (intent-only relay -// — "a barrier is not a door"). STUBS: connect/log only, no real protocol yet. - -class StubAccessControl implements AccessControlDevice { - constructor( - readonly driverId: string, - protected readonly config: DeviceConfig, - ) {} - async connect(): Promise { - stubLog(this.driverId, `connect ${this.config.host}:${this.config.port}`); - } - async disconnect(): Promise { - stubLog(this.driverId, "disconnect"); - } - async healthCheck(): Promise { - return { status: "ready", detail: "stub" }; - } - async pulseOpen(doorId: number): Promise { - // Intent only — never times/forces a close against a vehicle. - stubLog(this.driverId, `pulseOpen door=${doorId}`); - } - async getDoorStatus(): Promise<"open" | "closed"> { - return "closed"; - } -} - -export const zktecoDriver: AccessDriver = { - id: "zkteco", - category: "access", - label: "ZKTeco controller", - description: "ZKTeco network access controller (TCP/IP). Reader + relay.", - transports: ["tcp-ip"], - configFields: [hostField, portField(4370), { key: "doors", label: "Door count", type: "number", required: true, default: 4 }], - create: (c) => new StubAccessControl("zkteco", c), -}; - -export const esp32RelayDriver: AccessDriver = { - id: "esp32-relay", - category: "access", - label: "ESP32 relay controller", - description: "Simple ESP32-based relay controller over the network.", - transports: ["tcp-ip"], - configFields: [hostField, portField(80), { key: "doors", label: "Relay channels", type: "number", required: true, default: 1 }], - create: (c) => new StubAccessControl("esp32-relay", c), -}; diff --git a/packages/devices/src/drivers/index.ts b/packages/devices/src/drivers/index.ts index 3ba33d8..db13de8 100644 --- a/packages/devices/src/drivers/index.ts +++ b/packages/devices/src/drivers/index.ts @@ -2,9 +2,7 @@ // module wires the catalog. Add a new device by registering it here. import { registry } from "../registry.js"; -import { esp32RelayDriver, zktecoDriver } from "./access.js"; import { dingtianDriver } from "./access-dingtian.js"; -import { uhppoteDriver } from "./access-uhppote.js"; import { dahuaDriver, hikvisionDriver } from "./camera.js"; import { tcpipReaderDriver, wiegandReaderDriver } from "./reader.js"; @@ -14,10 +12,7 @@ let registered = false; export function registerBuiltinDrivers(): void { if (registered) return; registered = true; - registry.register(uhppoteDriver); registry.register(dingtianDriver); - registry.register(zktecoDriver); - registry.register(esp32RelayDriver); registry.register(wiegandReaderDriver); registry.register(tcpipReaderDriver); registry.register(hikvisionDriver); @@ -25,10 +20,7 @@ export function registerBuiltinDrivers(): void { } export { - uhppoteDriver, dingtianDriver, - zktecoDriver, - esp32RelayDriver, wiegandReaderDriver, tcpipReaderDriver, hikvisionDriver, diff --git a/packages/devices/src/drivers/uhppoted.d.ts b/packages/devices/src/drivers/uhppoted.d.ts deleted file mode 100644 index 4b29145..0000000 --- a/packages/devices/src/drivers/uhppoted.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Minimal ambient types for the `uhppoted` CommonJS module (no bundled types). -// Only the surface we use; extend as we adopt more of the API. -// Upstream: https://github.com/uhppoted/uhppoted-lib-nodejs -declare module "uhppoted" { - export class Config { - constructor( - name?: string, - bindAddr?: string, - broadcastAddr?: string, - listenAddr?: string, - timeout?: number, - controllers?: unknown[], - debug?: boolean, - ); - } - - /** Either a bare controller serial, or an addressable descriptor. */ - export type Controller = - | number - | { id: number; address?: string; protocol?: "udp" | "tcp" }; - - export interface Ctx { - config: Config; - locale?: string; - } - - export interface DiscoveredController { - deviceId: number; - device: { - serialNumber: number; - address: string; - netmask: string; - gateway: string; - MAC: string; - version: string; - date: string; - }; - } - - /** UDP broadcast discovery — returns every controller answering on the LAN. */ - export function getDevices(ctx: Ctx): Promise; - - export function openDoor( - ctx: Ctx, - controller: Controller, - door: number, - ): Promise<{ deviceId: number; opened: boolean }>; - - export function getStatus( - ctx: Ctx, - controller: Controller, - ): Promise>; - - export function getEvent( - ctx: Ctx, - controller: Controller, - index: number, - ): Promise>; - - export function getEventIndex( - ctx: Ctx, - controller: Controller, - ): Promise<{ deviceId: number; index: number }>; - - export function setListener( - ctx: Ctx, - controller: Controller, - address: string, - port: number, - ): Promise; - - // CommonJS default export (module.exports = { ... }). Destructure from this. - const uhppoted: { - Config: typeof Config; - getDevices: typeof getDevices; - openDoor: typeof openDoor; - getStatus: typeof getStatus; - getEvent: typeof getEvent; - getEventIndex: typeof getEventIndex; - setListener: typeof setListener; - }; - export default uhppoted; -} diff --git a/packages/devices/src/index.ts b/packages/devices/src/index.ts index a905561..9a448b4 100644 --- a/packages/devices/src/index.ts +++ b/packages/devices/src/index.ts @@ -10,10 +10,7 @@ export { setDeviceLogSink, type DeviceLogSink } from "./drivers/common.js"; // hardware test scripts and any direct/programmatic device access). export { registerBuiltinDrivers, - uhppoteDriver, dingtianDriver, - zktecoDriver, - esp32RelayDriver, wiegandReaderDriver, tcpipReaderDriver, hikvisionDriver, diff --git a/packages/devices/src/interfaces.ts b/packages/devices/src/interfaces.ts index 363eb2b..6c141af 100644 --- a/packages/devices/src/interfaces.ts +++ b/packages/devices/src/interfaces.ts @@ -14,7 +14,7 @@ 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"). */ + /** Stable id of the driver that produced this instance (e.g. "dingtian"). */ readonly driverId: string; connect(): Promise; disconnect(): Promise; @@ -28,7 +28,7 @@ export interface DeviceHealth { } // --- Access control (barrier relay) -------------------------------------- -// ZKTeco, an ESP32 relay controller, UHPPOTE, etc. all implement this. +// The Dingtian relay board (and any future relay controller) implements this. export interface AccessControlDevice extends Device { /** Express intent to open. NEVER timed/forced closed against a vehicle. */ pulseOpen(doorId: number): Promise; diff --git a/packages/devices/src/registry.ts b/packages/devices/src/registry.ts index 9cf22dc..5bb6c9f 100644 --- a/packages/devices/src/registry.ts +++ b/packages/devices/src/registry.ts @@ -35,9 +35,9 @@ export type DeviceConfig = Record; * fields the admin must supply, and a factory that builds a live adapter. */ export interface DeviceDriver { - readonly id: string; // stable, e.g. "zkteco", "esp32-relay", "hikvision" + readonly id: string; // stable, e.g. "dingtian", "hikvision" readonly category: DeviceCategory; - readonly label: string; // human name for the picker, e.g. "ZKTeco controller" + readonly label: string; // human name for the picker, e.g. "Dingtian relay controller" readonly description: string; /** Transports/notes surfaced in the UI, e.g. ["tcp-ip"], ["wiegand"]. */ readonly transports: readonly string[]; @@ -53,7 +53,7 @@ export type PrinterDriver = DeviceDriver; /** A device found on the LAN by a driver's discovery scan. */ export interface DiscoveredDevice { - /** Identifier to pre-fill (e.g. UHPPOTE serial number). */ + /** Identifier to pre-fill (e.g. a serial number). */ readonly id: string; readonly label: string; /** Config values to auto-fill into the setup form (host, serial, …). */ @@ -63,9 +63,10 @@ export interface DiscoveredDevice { } /** - * Optional capability: a driver that can find devices on the LAN. UHPPOTE - * implements this via the protocol's UDP broadcast discovery (get-devices); - * cameras (ONVIF) and others may add it later. See wiki/concepts/device-discovery.md. + * Optional capability: a driver that can find devices on the LAN (e.g. UDP + * broadcast discovery). No bundled driver implements this yet — the Dingtian + * board uses a fixed IP; cameras (ONVIF) or other UDP-discoverable devices may + * add it later. See wiki/concepts/device-discovery.md. */ export interface DiscoverableDriver { discover(): Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edbe384..4507bc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,9 +50,6 @@ importers: fastify-plugin: specifier: 6.0.0 version: 6.0.0 - uhppoted: - specifier: 0.9.0 - version: 0.9.0 devDependencies: '@types/bcrypt': specifier: 6.0.0 @@ -122,9 +119,6 @@ importers: '@parking/shared': specifier: workspace:* version: link:../shared - uhppoted: - specifier: 0.9.0 - version: 0.9.0 devDependencies: '@types/node': specifier: 25.9.3 @@ -1262,9 +1256,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - os@0.1.2: - resolution: {integrity: sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==} - path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -1467,10 +1458,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uhppoted@0.9.0: - resolution: {integrity: sha512-7VDPNg4x31TETgMD3xp9NwVr+NvmZJ6CO8gTpyuRrdHu/UBGXw9/9kq8yiB0vR4opaUQPdvR8Gj373Ac/QWPwQ==} - engines: {node: '>=14.18.3'} - undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -2357,8 +2344,6 @@ snapshots: dependencies: wrappy: 1.0.2 - os@0.1.2: {} - path-scurry@2.0.2: dependencies: lru-cache: 11.5.1 @@ -2586,10 +2571,6 @@ snapshots: typescript@6.0.3: {} - uhppoted@0.9.0: - dependencies: - os: 0.1.2 - undici-types@7.24.6: {} util-deprecate@1.0.2: {} diff --git a/wiki/concepts/device-discovery.md b/wiki/concepts/device-discovery.md index 5c9c6fb..96e6f91 100644 --- a/wiki/concepts/device-discovery.md +++ b/wiki/concepts/device-discovery.md @@ -20,17 +20,23 @@ device carries an `id`, a `label`, a `config` blob to **auto-fill** the setup fo (firmware, MAC, …). The [[device-registry]]'s `isDiscoverable()` guard lets the system treat it as optional; the setup catalog returns a `discoverable` list of driver ids. -## UHPPOTE discovery +> **No current driver implements discovery.** The [[dingtian-relay]] board uses a fixed IP +> (entered/known at setup). The capability remains for future UDP-discoverable devices (cameras +> via ONVIF, etc.). The worked example below is the (removed) UHPPOTE driver — kept because the +> **broadcast gotchas are transferable** to any UDP discovery we add later. -The [[uhppote-controller]] supports discovery natively: a **UDP broadcast** (`get-devices` on +## UHPPOTE discovery (historical example) + +The [[uhppote-controller]] supported discovery natively: a **UDP broadcast** (`get-devices` on port `60000`) that **every controller on the LAN answers** with its serial, IP, netmask, gateway, -MAC, firmware version, and date. The official `uhppoted` lib exposes this as `getDevices(ctx)`; -the `uhppote` driver maps each result into a `DiscoveredDevice` (serial → id, IP → host). -**Verified on real hardware** (serial 225088491). +MAC, firmware version, and date. The `uhppoted` lib exposed this as `getDevices(ctx)`; the +(now-removed) `uhppote` driver mapped each result into a `DiscoveredDevice` (serial → id, IP → +host). **Was verified on real hardware** (serial 225088491). ### Broadcast gotchas (learned the hard way — see [[wsl-dev-networking]]) -These cost real debugging time; the `uhppote` driver now handles all three: +These cost real debugging time; the (removed) `uhppote` driver handled all three, and any future +UDP-discovery driver will need to as well: 1. **Broadcast to the *subnet-directed* address, not the global `255.255.255.255`.** The `uhppoted` lib only calls `setBroadcast(true)` when the target matches a **local interface's diff --git a/wiki/concepts/device-registry.md b/wiki/concepts/device-registry.md index d4f22ad..e74c71f 100644 --- a/wiki/concepts/device-registry.md +++ b/wiki/concepts/device-registry.md @@ -38,7 +38,8 @@ driver; **no business-logic change** — this is the [[device-adapter-pattern]] - Config is **validated against the driver's declared fields** before persisting. - Selections persist in the `lane_devices` table and drive runtime adapter construction. - Drivers may optionally implement **[[device-discovery]]** (`discover()`), so the admin can scan - the LAN instead of typing connection details — UHPPOTE does this today. + the LAN instead of typing connection details — no current driver uses it (the UHPPOTE did, + before removal; the [[dingtian-relay]] uses a fixed IP). Cameras are modelled as **snapshot-on-event**: the host requests an image at entry/exit; it's stored and referenced from the signed event as an **independent record** — a fraud-control input diff --git a/wiki/concepts/uhppote-vs-esp32.md b/wiki/concepts/uhppote-vs-esp32.md index e9a0913..3946055 100644 --- a/wiki/concepts/uhppote-vs-esp32.md +++ b/wiki/concepts/uhppote-vs-esp32.md @@ -7,6 +7,11 @@ updated: 2026-06-14 # UHPPOTE vs. Custom ESP32 — Detection vs. Prevention +> **Historical comparison.** Neither is the current device — the [[uhppote-controller]] was +> **rejected** (entry-flow blocker → [[dingtian-relay]] chosen) and the [[esp32-custom-controller]] +> is **deferred**. Kept because the **detection-vs-prevention** framing on the [[trust-boundary]] +> fork is a durable lens that applies to any access device. + A head-to-head on the [[trust-boundary]] fork: the off-the-shelf [[uhppote-controller]] versus the [[esp32-custom-controller]]. (Synthesized from [[parking-system-architecture]] §6–7.) @@ -23,9 +28,10 @@ the [[esp32-custom-controller]]. (Synthesized from [[parking-system-architecture ## Bottom line -- The UHPPOTE is the **current choice**: good enough as a detection/audit layer **when only the - host can reach it** (isolation) and every event lands in the [[append-only-event-chain]]. -- The ESP32 is the **documented upgrade** when you need a control path that holds even against an - attacker on the wire. They're **mixable per lane**. +- The UHPPOTE was the **detection-grade** option: good enough as a detection/audit layer **when + only the host can reach it** (isolation) and every event lands in the [[append-only-event-chain]] + — but it was rejected for the entry lane (the button blocker). +- The ESP32 is the **prevention-grade** option when you need a control path that holds even against + an attacker on the wire. Deferred. - Both still rely on host-side integrity ([[append-only-event-chain]]) and external [[reconciliation]] as the ultimate anti-fraud control. diff --git a/wiki/decisions/open-questions.md b/wiki/decisions/open-questions.md index afc5592..c241b29 100644 --- a/wiki/decisions/open-questions.md +++ b/wiki/decisions/open-questions.md @@ -27,7 +27,7 @@ status: open later" currently leaves a disk failure as **total revenue-history loss**. 6. **Secure-element integration.** Confirm [[atecc608]] wiring/usage on the host (event signing). The [[esp32-custom-controller]] command-authentication use is **deferred — not - being implemented for now** (access control stays on the [[uhppote-controller]] behind + being implemented for now** (access control is the [[dingtian-relay]] behind [[network-isolation]]); revisit only if prevention-grade device auth becomes a requirement. 7. **JWT signing: symmetric vs. asymmetric key.** _(Raised by the commit security review, not the source doc.)_ Auth currently uses a symmetric HMAC secret (`@fastify/jwt`, see diff --git a/wiki/decisions/standing-decisions.md b/wiki/decisions/standing-decisions.md index f245258..da37875 100644 --- a/wiki/decisions/standing-decisions.md +++ b/wiki/decisions/standing-decisions.md @@ -19,10 +19,12 @@ The decisions treated as settled in the design notes. (See [[parking-system-arch - **Integrity:** append-only, hash-chained, [[atecc608]]-signed event log ([[append-only-event-chain]]); **[[reconciliation]] is the anti-fraud control**; encryption protects only at-rest (see [[threat-model]]). -- **Access control:** [[uhppote-controller]] for now, on an **isolated VLAN** - ([[network-isolation]]); event log used as a tamper-evident audit source with host-side index - tracking ([[event-log-ingestion]]). The [[esp32-custom-controller]] is the documented - prevention-grade upgrade path (the [[trust-boundary]] fork). +- **Access control:** the **[[dingtian-relay]]** relay+input controller, on an **isolated VLAN** + ([[network-isolation]]). Chosen because its **inputs are decoupled from its relays**, enabling + host-in-the-loop ticket-first entry — the resolution to [[access-controller-button-flow]]. + (The [[uhppote-controller]] and [[zkteco-controller]] were evaluated and **rejected** — kept as + historical record. The [[esp32-custom-controller]] remains the documented prevention-grade + alternative — the [[trust-boundary]] fork.) - **Readers:** prefer [[wiegand]]-into-controller for permit holders (autonomous); host-in-the-loop for [[lpr-camera|LPR]]/QR/pure-network readers; both can share a relay (see [[entry-exit-readers]]). diff --git a/wiki/entities/bom.md b/wiki/entities/bom.md index 9452eb1..b6bbdf4 100644 --- a/wiki/entities/bom.md +++ b/wiki/entities/bom.md @@ -14,7 +14,7 @@ payment terminal is dictated by the acquiring bank. (See [[parking-system-archit | --- | --- | --- | | Barrier operator | Magnetic Autocontrol / FAAC / CAME / Nice | Owns physical safety in firmware ([[barrier-not-a-door]]) | | Induction loops | Feig / BEA / EMX | Safety + free-exit detection | -| Access controller | [[uhppote-controller]] now → ZKTeco later | Reader + relay; **isolate the VLAN** ([[network-isolation]]) | +| Access controller | [[dingtian-relay]] relay+input board | Decoupled inputs (host-in-the-loop); **isolate the VLAN** ([[network-isolation]]). ([[uhppote-controller]]/[[zkteco-controller]] rejected) | | Permit readers | Nedap/Kathrein UHF, or Mifare → [[wiegand]] | Hands-free, or autonomous offline decisions | | Casual identity | [[lpr-camera]] (Milesight, edge AI) | Plate = ticket + independent record | | Ticket dispenser | Custom VKP80 | Parking-grade thermal/ESC-POS | diff --git a/wiki/entities/uhppote-controller.md b/wiki/entities/uhppote-controller.md index 4ca1533..29bbe70 100644 --- a/wiki/entities/uhppote-controller.md +++ b/wiki/entities/uhppote-controller.md @@ -1,33 +1,37 @@ --- type: entity -tags: [parking, hardware, access-control, current-choice] +tags: [parking, hardware, access-control, rejected, historical] sources: [parking-system-architecture] updated: 2026-06-15 --- -# UHPPOTE Controller (current choice) +# UHPPOTE Controller (rejected — historical) -The starting access-control hardware: a **UHPPOTE Wiegand 26/34 network controller (4-door)** — -a cheap reader-plus-relay frontend, acceptable **provided you understand its limits**. The plan -is UHPPOTE now → ZKTeco later (see [[bom]]). (See [[parking-system-architecture]] §6.) +> **❌ NOT USED. Replaced by the [[dingtian-relay]] controller** (and its driver/test code +> removed). Kept as the record of *why* — its firmware-fixed push-button blocker +> ([[access-controller-button-flow]]) is what drove the switch to a board with decoupled inputs. +> The transferable lessons below (network isolation, append-only log ingestion, "a barrier is not +> a door") still apply to any access device. -> **⚠️ Entry-flow blocker (verified on hardware):** the push-button input **auto-opens the relay -> in firmware** — there's no command to make it report-without-opening — so it **cannot** do -> ticket-first entry (`button → print → open`). Fine as a host-**commanded relay** and for -> [[wiegand]]/permit lanes, but **not** the button-driven entry lane as wired. Full detail and -> options: [[access-controller-button-flow]]. +The original starting hardware: a **UHPPOTE Wiegand 26/34 network controller (4-door)** — a cheap +reader-plus-relay frontend. (See [[parking-system-architecture]] §6.) + +> **⚠️ The fatal limit (verified on hardware):** the push-button input **auto-opens the relay in +> firmware** — no command makes it report-without-opening — so it **cannot** do ticket-first entry +> (`button → print → open`). This is *the* reason it was dropped: full detail and the resolution in +> [[access-controller-button-flow]]. > -> **Verified working on the real unit** (serial 225088491, fw 09120): host-commanded `openDoor` -> on doors 1 & 2 (physically actuated, `reason="remote open door"`); button presses captured live -> (`reason="push button ok"`); [[device-discovery]] scan. Test scripts: `apps/server/scripts/`. +> **What was verified on the real unit** (serial 225088491, fw 09120) before retiring it: +> host-commanded `openDoor` on doors 1 & 2 (physically actuated, `reason="remote open door"`); +> button presses captured live (`reason="push button ok"`); UDP-broadcast [[device-discovery]]. +> The driver, `uhppoted` dependency, and test scripts have since been removed from the codebase. -> **Implementation:** integrated via the official **`uhppoted`** npm package (MIT, by the -> `uhppoted` org — `github.com/uhppoted/uhppoted-lib-nodejs`), added to `@parking/devices` as the -> `uhppote` access driver ([[device-registry]]). It exposes exactly the protocol commands this -> design needs: `openDoor`, `getStatus`, and the event-log set (`getEvent`, `getEventIndex`, -> `setEventIndex`, `recordSpecialEvents`) plus `setListener`/`listen` for auto-push — see -> [[event-log-ingestion]]. Transport defaults to **UDP** (broadcast `…:60000`), with optional -> per-call TCP on newer firmware. The driver also implements **[[device-discovery]]** +> **Past implementation (removed):** was integrated via the official **`uhppoted`** npm package +> (MIT — `github.com/uhppoted/uhppoted-lib-nodejs`) as the `uhppote` access driver. It exposed +> exactly the protocol commands the design needs: `openDoor`, `getStatus`, and the event-log set +> (`getEvent`, `getEventIndex`, `setEventIndex`, `recordSpecialEvents`) plus `setListener`/`listen` +> for auto-push — see [[event-log-ingestion]]. Transport defaulted to **UDP** (broadcast `…:60000`), +> with optional per-call TCP on newer firmware. The driver also implemented **[[device-discovery]]** > (`getDevices` broadcast) so the setup wizard can scan for controllers. Note: the lib pulls one > trivial extra dep (the npm `os` shim) and uses UDP broadcast, which needs socket broadcast > permission on the host. diff --git a/wiki/entities/zkteco-controller.md b/wiki/entities/zkteco-controller.md index 4ca9611..1bad592 100644 --- a/wiki/entities/zkteco-controller.md +++ b/wiki/entities/zkteco-controller.md @@ -1,20 +1,24 @@ --- type: entity -tags: [parking, hardware, access-control] +tags: [parking, hardware, access-control, rejected, historical] sources: [parking-system-architecture] updated: 2026-06-15 --- -# ZKTeco Controller +# ZKTeco Controller (rejected — historical) -A network access controller (C3 / inBio families) — the documented "UHPPOTE now → ZKTeco later" -upgrade in the [[bom]]. A `zkteco` driver **stub** exists in the [[device-registry]] but the -**real protocol is not implemented** (see below). +> **❌ NOT USED.** Was considered as the access controller; the **[[dingtian-relay]]** board was +> chosen instead (decoupled inputs, already verified). The `zkteco` driver **stub** has been +> **removed** from the codebase. Kept for the record of the comparison below. -## Relevance to the entry-flow blocker +A network access controller (C3 / inBio families), originally the documented "UHPPOTE now → +ZKTeco later" upgrade in the [[bom]]. + +## Why it was a contender (vs. UHPPOTE) ZKTeco is **better positioned** than the [[uhppote-controller]] for host-in-the-loop entry (the -[[access-controller-button-flow]] blocker), but this is **unverified on our hardware**: +[[access-controller-button-flow]] blocker) — but it was **never verified on our hardware**, and the +Dingtian solved the problem first with less effort: - Its **auxiliary inputs** have **programmable linkage** (ZKBioSecurity software / PULL SDK) and are **not** hardwired to "open door" — so a button on an *aux* input can raise a host event diff --git a/wiki/index.md b/wiki/index.md index 51534be..edd6f93 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -32,13 +32,13 @@ Counts: 1 source · 14 entities · 10 concepts · 2 decision records. - [[logto-zitadel-oidc]] — OIDC providers ruled out by offline-first. ## Entities — hardware & devices -- [[uhppote-controller]] — current access controller; cheap, tamper-evident, open-UDP, fixed firmware. +- [[uhppote-controller]] — ❌ rejected/historical; firmware auto-open blocker drove the switch to Dingtian. - [[esp32-custom-controller]] — prevention-grade upgrade; device-level auth. - [[atecc608]] — secure element; non-extractable signing key (host events + controller auth). - [[wiegand]] — reader standard feeding the controller directly (autonomous permit-holder path). - [[lpr-camera]] — edge-AI plate recognition; host-side casual-identity source. -- [[zkteco-controller]] — C3/inBio controller; aux-input path may enable host-in-the-loop (driver TBD). -- [[dingtian-relay]] — relay+input board; inputs decoupled from relays → solves the button blocker (driver TBD). +- [[zkteco-controller]] — ❌ rejected/historical; aux-input path was a contender, not pursued. +- [[dingtian-relay]] — ✅ CHOSEN access controller; decoupled inputs solve the button blocker (driver verified on hardware). - [[bom]] — reference bill of materials (barrier, loops, controller, readers, payment, host, network). ## Concepts — foundational forces @@ -54,7 +54,7 @@ Counts: 1 source · 14 entities · 10 concepts · 2 decision records. - [[device-adapter-pattern]] — business logic talks to interfaces; swap hardware → new adapter. - [[device-registry]] — catalog of selectable drivers per category (admin-configurable). - [[first-run-setup]] — admin assigns devices per lane from the catalog at install. -- [[device-discovery]] — optional driver capability to scan the LAN (UHPPOTE UDP broadcast). +- [[device-discovery]] — optional driver capability to scan the LAN (no current driver uses it; UHPPOTE was the example). - [[barrier-not-a-door]] — never timed-close a barrier; safety lives in barrier firmware. - [[trust-boundary]] — the core fork: network vs. device; auditable vs. unforgeable. - [[fail-state-safety]] — entry fails closed, exit fails open; manual override; watchdog. diff --git a/wiki/log.md b/wiki/log.md index f361f70..e7f3d32 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -118,3 +118,16 @@ works. The [[access-controller-button-flow]] blocker is RESOLVED. Gotcha recorde (GET omits it) or the write silently no-ops. Added httpPort config field (port 8080 ≠ default 80). Test script apps/server/scripts/dingtian-test.mjs. Next: input HTTP-push endpoint + wiring input→ticket→pulseOpen. + +## [2026-06-15] cleanup | Remove UHPPOTE/ZKTeco code; wiki → rejected/historical +Neither UHPPOTE nor ZKTeco is used (Dingtian chosen). Removed their code: +deleted access-uhppote.ts, uhppoted.d.ts, access.ts (zkteco/esp32 stubs), the +three uhppote-*.mjs scripts; dropped the `uhppoted` npm dep from both packages; +unregistered uhppote/zkteco/esp32-relay from the driver registry; updated example +comments. Catalog access drivers now = dingtian only. Build green. +Wiki: kept the pages but marked [[uhppote-controller]] + [[zkteco-controller]] +rejected/historical, [[uhppote-vs-esp32]] historical; re-pointed all "current +device" framing (standing-decisions, bom, overview, open-questions) to +[[dingtian-relay]]; noted no current driver uses [[device-discovery]]. Transferable +concepts (network-isolation, event-log-ingestion, barrier-not-a-door, threat-model) +kept as-is. Links lint clean; raw source untouched (immutable). diff --git a/wiki/overview.md b/wiki/overview.md index e19d299..3ea092f 100644 --- a/wiki/overview.md +++ b/wiki/overview.md @@ -29,11 +29,12 @@ deployed on-site at a parking facility. Two forces shape nearly every decision: rest ([[disk-os-hardening]]) defends a secondary threat. - **Devices** sit behind a [[device-adapter-pattern]] (swap hardware → new adapter only), with the [[barrier-not-a-door]] safety principle keeping physical safety in barrier-operator firmware. -- **Access control** hinges on the [[trust-boundary]] fork: - [[uhppote-vs-esp32|detection vs. prevention]]. Today: [[uhppote-controller]] behind - [[network-isolation]], its open [[uhppote-udp-protocol]] contained, its log made trustworthy by - [[event-log-ingestion]]. Upgrade path: the [[esp32-custom-controller]] with - [[challenge-response-auth]] and [[fail-state-safety]]. +- **Access control** today is the **[[dingtian-relay]]** relay+input controller behind + [[network-isolation]] — chosen because its inputs are **decoupled from its relays**, enabling + host-in-the-loop ticket-first entry (resolving [[access-controller-button-flow]]). The + [[uhppote-controller]] and [[zkteco-controller]] were evaluated and **rejected** (historical). + The deeper fork is still the [[trust-boundary]] ([[uhppote-vs-esp32|detection vs. prevention]]); + the [[esp32-custom-controller]] remains the prevention-grade alternative. - **Readers** split two ways ([[entry-exit-readers]]): permit holders via [[wiegand]] (autonomous), casual/transient via host-side [[lpr-camera]] / QR; both can share a relay. - A reference [[bom]] lists recommended devices. @@ -47,6 +48,6 @@ modes (fail-open on exit)**, the **reconciliation channel**, and **backup/durabi - *Security-first:* [[threat-model]] → [[append-only-event-chain]] → [[reconciliation]] → [[uhppote-vs-esp32]]. -- *Hardware-first:* [[bom]] → [[uhppote-controller]] → [[entry-exit-readers]] → - [[esp32-custom-controller]]. +- *Hardware-first:* [[bom]] → [[dingtian-relay]] → [[access-controller-button-flow]] → + [[entry-exit-readers]]. - *Stack-first:* [[technology-stack]] → [[offline-first]] → [[device-adapter-pattern]].