f6e35bbebf
An early wrong assumption named the QR/RFID access reader "GEE" / "GEE/Fondvision" / "GEE-QR-ER80" (and summarized a raw GEE PDF as its datasheet). There is no GEE device — it's the Dingtian DT-008 (dingtian-tech.com/en_us/qr_code_reader.html), the same vendor as the relay board, which is why it integrates the identical HTTP-GET-push way. Code: - Driver symbol geeQrReaderDriver → dingtianQrReaderDriver; label → "Dingtian DT-008 QR/RFID reader (HTTP push)"; comments/description rewritten to the real DT-008 facts (Wiegand 26/34, TCP/IP, USB, RS485 — not RS-232; QR/barcode + ID/IC/NFC — not DataMatrix/1D). - Persisted driverId "gee-qr-reader" → "dingtian-qr-reader" (the registry lookup key + the row created on assign in qr-reader.ts). - Migration 0015 rewrites existing devices.driver_id rows so configured readers keep resolving (applied to the dev DB — 2 rows; the booth applies it on boot). Behaviour is unchanged: naming + the persisted id only. Wiki + memory: - Renamed entities/gee-qr-er80.md → dingtian-dt008-reader.md and sources/gee-qr-er80.md → dingtian-dt008.md; rewrote both to the real DT-008 product-page specs while KEEPING all the verified-on-hardware protocol facts (cjihao serial, .jsp path, Connection: close). Fixed every cross-reference + "GEE" mention in 6 other pages. Memory gee-reader-serial-binding → dingtian-reader-serial-binding. The only surviving "GEE" mentions are deliberate naming-correction notes, the raw PDF filename, and the append-only log history. Full workspace build/lint/test green; dev DB readers verified resolving to the registered dingtian-qr-reader driver. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
129 lines
6.0 KiB
TypeScript
129 lines
6.0 KiB
TypeScript
import type { FastifyInstance } from "fastify";
|
||
import { eq, devices, type Db } from "@parking/db";
|
||
import type { DeviceReadEvent } from "../device-events.js";
|
||
import type { ReadDispatcher } from "../read-dispatch.js";
|
||
import type { CredentialCapture } from "../credential-capture.js";
|
||
|
||
// Dingtian DT-008 QR/RFID reader endpoint. The reader is configured (vendor tool) with
|
||
// our host as its "server"; on each scan it sends an HTTP GET and BEEPS/acts based on
|
||
// our JSON reply — host-in-the-loop and synchronous. Protocol from the QRCode SDK
|
||
// v1.6.5; see wiki/sources/qrcode-sdk.md and wiki/entities/dingtian-dt008-reader.md.
|
||
//
|
||
// reader → GET /qa/mcardsea.php?cardid=<QR>&mjihao=<devId>&cjihao=<devSN>&status=<2ch>&time=<utc>
|
||
// server → {"data":[{cardid,cjihao,mjihao,status,time,output}],"code":0,"message":""}
|
||
// reply status: 1 = valid (beep 2×) / 0 = invalid (beep 1×)
|
||
// reply output: 0 = Access, 1 = WG26, 2 = WG34 (line driven on a valid read)
|
||
// reply time: UTC — syncs the device clock
|
||
//
|
||
// The "server language" set on the device only selects this URL path; we accept the
|
||
// SDK default path. No auth on the device side (it can't); the reader sits on the
|
||
// device subnet (network-isolation) and the signed ledger is the real guarantee.
|
||
|
||
interface ReaderQuery {
|
||
cardid?: string;
|
||
mjihao?: string; // device id
|
||
cjihao?: string; // device serial
|
||
status?: string; // 2 chars: high valid/invalid, low 1=in/0=out
|
||
time?: string;
|
||
}
|
||
|
||
export async function qrReaderRoutes(
|
||
app: FastifyInstance,
|
||
db: Db,
|
||
dispatcher: ReadDispatcher,
|
||
capture: CredentialCapture,
|
||
): Promise<void> {
|
||
// Resolve the lane_devices row whose config.serial matches the reader's reported
|
||
// serial (cjihao). The row id is a normal UUID; the serial is config the admin
|
||
// enters when assigning the dingtian-qr-reader. Returns the row id, or null if no
|
||
// reader is assigned for that serial. (Small device set → scan in JS.)
|
||
const readerRowIdForSerial = (serial: string): string | null => {
|
||
if (!serial) return null;
|
||
const rows = db.select().from(devices).where(eq(devices.category, "reader")).all();
|
||
const match = rows.find((r) => r.enabled && (r.config as { serial?: string }).serial === serial);
|
||
return match?.id ?? null;
|
||
};
|
||
|
||
// No auth: the reader is a machine on the isolated device subnet and offers no
|
||
// auth on its side. Public route, like the Dingtian input push.
|
||
const handler = async (req: { query: ReaderQuery }, reply: import("fastify").FastifyReply) => {
|
||
const q = req.query;
|
||
// The reader sends `Connection: keep-alive` but only ACTS on our verdict (beep,
|
||
// drive output) once the socket CLOSES — every vendor demo replies
|
||
// `Connection: close` and shuts the socket. Without it the reader waits out a
|
||
// ~10 s keep-alive timeout before beeping. So force-close the connection.
|
||
// See wiki/sources/qrcode-sdk.md, entities/dingtian-dt008-reader.md.
|
||
reply.header("connection", "close");
|
||
const cardid = (q.cardid ?? "").trim();
|
||
const mjihao = q.mjihao != null ? Number(q.mjihao) : 0;
|
||
const serial = (q.cjihao ?? "").trim();
|
||
|
||
// Map the reader's serial → its assigned lane_devices row id (the dispatcher
|
||
// resolves the lane from that row). If unassigned, deviceId stays the serial so
|
||
// the dispatcher simply finds no lane and rejects (status:0) — never crashes.
|
||
const matchedRowId = readerRowIdForSerial(serial);
|
||
const deviceId = matchedRowId ?? serial;
|
||
|
||
let accepted = false;
|
||
if (cardid) {
|
||
// ENROLLMENT INTERCEPT: if THIS reader is armed for credential capture, grab the
|
||
// value for the subscription form and do NOT run the access flow (we must not
|
||
// open a barrier for a card being enrolled). Single-shot — capture auto-disarms.
|
||
// Reads from the OTHER reader are untouched and dispatch normally below.
|
||
if (capture.tryConsume(deviceId, cardid)) {
|
||
app.log.info(`CAPTURE serial=${serial || "?"} device=${matchedRowId ? matchedRowId.slice(0, 8) : "?"} value=${cardid}`);
|
||
accepted = true; // beep "ok" so the operator knows the card was read
|
||
} else {
|
||
const read: DeviceReadEvent = {
|
||
driverId: "dingtian-qr-reader",
|
||
deviceId,
|
||
value: cardid,
|
||
kind: "qr",
|
||
at: new Date().toISOString(),
|
||
};
|
||
try {
|
||
const outcome = await dispatcher.dispatch(read);
|
||
accepted = outcome.accepted;
|
||
// Per-read diagnostic: which reader (serial) sent it, which configured device
|
||
// it mapped to, and the verdict — so a barrier/serial mismatch is visible in
|
||
// the logs (e.g. an entry-side scan resolving to the exit relay).
|
||
app.log.info(
|
||
`READ serial=${serial || "?"} → device=${matchedRowId ? matchedRowId.slice(0, 8) : "UNASSIGNED"} ` +
|
||
`card=${cardid} verdict=${accepted ? "ACCEPT" : "REJECT"}${outcome.direction ? ` dir=${outcome.direction}` : ""}` +
|
||
`${accepted ? "" : ` reason="${outcome.reason ?? "?"}"`}`,
|
||
);
|
||
} catch (err) {
|
||
app.log.error(`QR dispatch failed for ${cardid}: ${(err as Error).message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Reply the SDK verdict. status 1 → beep 2× (valid) / 0 → beep 1× (invalid).
|
||
// output 0 = Access (drive the reader's access line on a valid read).
|
||
return {
|
||
data: [
|
||
{
|
||
cardid,
|
||
cjihao: q.cjihao ?? 0,
|
||
mjihao,
|
||
status: accepted ? 1 : 0,
|
||
time: String(Math.floor(Date.now() / 1000)),
|
||
output: 0,
|
||
},
|
||
],
|
||
code: 0,
|
||
message: "",
|
||
};
|
||
};
|
||
|
||
// The reader's "server language" setting (JSP/PHP/C#/ASP/CGI) selects the URL
|
||
// EXTENSION it GETs — verified on hardware: a JSP-configured unit posts
|
||
// /qa/mcardsea.jsp. Register every extension so the endpoint works whatever the
|
||
// device is set to; accept POST too in case a variant differs.
|
||
for (const ext of ["php", "jsp", "asp", "aspx", "cgi"]) {
|
||
const path = `/qa/mcardsea.${ext}`;
|
||
app.get<{ Querystring: ReaderQuery }>(path, handler);
|
||
app.post<{ Querystring: ReaderQuery }>(path, handler);
|
||
}
|
||
}
|