392d44d842
The reader HTTP-GETs on each scan and beeps/acts on our JSON reply (host-in-the-
loop, synchronous). New route GET/POST /qa/mcardsea.php parses the SDK query,
runs the scan through the read dispatcher (permit match -> permit flow; else
transient exit), and replies the SDK verdict: status 1=valid (beep 2x) /
0=invalid (beep 1x), output, time-sync.
Refactored the read flows to return a ReadOutcome {accepted, direction, reason}
so the reply reflects the real accept/reject decision (ReadDispatcher.dispatch,
ExitFlow.handleAt, PermitFlow.run). Fire-and-forget readers ignore it.
Reader's lane is keyed off its serial (cjihao) as lane_devices.id for now;
endpoint is public (reader has no auth, on the device subnet).
Verified via inject: valid permit QR -> status:1 + open; re-scan -> permit exit;
unknown QR -> status:0; barrier-less lane -> status:0.
43 lines
1.6 KiB
TypeScript
43 lines
1.6 KiB
TypeScript
import type { Db } from "@parking/db";
|
|
import type { FastifyBaseLogger } from "fastify";
|
|
import type { DeviceReadEvent, ReadOutcome } from "./device-events.js";
|
|
import type { ExitFlow } from "./exit-flow.js";
|
|
import type { PermitFlow } from "./permit-flow.js";
|
|
import { readerLaneWithAccess } from "./lane-map.js";
|
|
|
|
// Routes a credential read (ticket scan / plate / card) to the right flow. A read
|
|
// can mean a permit entry/exit OR a transient exit, so we dispatch by WHAT the
|
|
// credential is (decision 2026-06-15):
|
|
// - matches a permit (card/QR/bound plate) → PERMIT flow (direction inferred from
|
|
// the car's open-session state),
|
|
// - else → transient EXIT flow (open ticket session → exit, else reject+log).
|
|
// Lane is resolved once here; both flows act on a known access-equipped lane.
|
|
|
|
export class ReadDispatcher {
|
|
readonly #db: Db;
|
|
readonly #exit: ExitFlow;
|
|
readonly #permit: PermitFlow;
|
|
readonly #logger: FastifyBaseLogger;
|
|
|
|
constructor(db: Db, exit: ExitFlow, permit: PermitFlow, logger: FastifyBaseLogger) {
|
|
this.#db = db;
|
|
this.#exit = exit;
|
|
this.#permit = permit;
|
|
this.#logger = logger;
|
|
}
|
|
|
|
async dispatch(e: DeviceReadEvent): Promise<ReadOutcome> {
|
|
const lane = await readerLaneWithAccess(this.#db, e.deviceId);
|
|
if (lane == null) {
|
|
return { accepted: false, reason: "reader not on an access-equipped lane" };
|
|
}
|
|
|
|
const permit = this.#permit.match(e);
|
|
if (permit) {
|
|
return this.#permit.run(lane, e, permit);
|
|
}
|
|
// Not a permit → transient ticket exit (the exit flow rejects+logs if unknown).
|
|
return this.#exit.handleAt(lane, e);
|
|
}
|
|
}
|