Remove UHPPOTE/ZKTeco; Dingtian is the only access driver
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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
@@ -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=<your-ip-on-the-controller-LAN> 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);
|
||||
});
|
||||
@@ -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.");
|
||||
@@ -28,14 +28,14 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
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 } }>(
|
||||
|
||||
+1
-1
@@ -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<DiscoveredDevice[]> {
|
||||
const body = await apiFetch<{ devices: DiscoveredDevice[] }>(
|
||||
`/api/setup/discover/${driverId}`,
|
||||
|
||||
Reference in New Issue
Block a user