diff --git a/apps/server/src/routes/qr-reader.ts b/apps/server/src/routes/qr-reader.ts index 6018f0f..71985a7 100644 --- a/apps/server/src/routes/qr-reader.ts +++ b/apps/server/src/routes/qr-reader.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { eq, laneDevices, type Db } from "@parking/db"; import type { DeviceReadEvent } from "../device-events.js"; import type { ReadDispatcher } from "../read-dispatch.js"; @@ -25,24 +26,39 @@ interface ReaderQuery { time?: string; } -export async function qrReaderRoutes(app: FastifyInstance, dispatcher: ReadDispatcher): Promise { +export async function qrReaderRoutes( + app: FastifyInstance, + db: Db, + dispatcher: ReadDispatcher, +): Promise { + // 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 gee-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(laneDevices).where(eq(laneDevices.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 }) => { const q = req.query; const cardid = (q.cardid ?? "").trim(); const mjihao = q.mjihao != null ? Number(q.mjihao) : 0; + const serial = (q.cjihao ?? "").trim(); - // The device id we map to a lane is the configured reader's lane_devices id. - // The reader sends its own mjihao/cjihao; the admin records that as the device's - // config so we can resolve it. For now we key the read on the device serial - // (cjihao) as the lane_devices id — see wiki note; refine when assignment lands. - const deviceId = (q.cjihao ?? "").trim() || String(mjihao); + // 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 deviceId = readerRowIdForSerial(serial) ?? serial; let accepted = false; if (cardid) { const read: DeviceReadEvent = { - driverId: "gee-qr-er80", + driverId: "gee-qr-reader", deviceId, value: cardid, kind: "qr", diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 4dc5120..c2778f3 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -117,7 +117,7 @@ export async function buildServer(opts: BuildOptions = {}): Promise new StubReader("tcpip-reader", c), }; + +// GEE/Fondvision QR access reader (e.g. GEE-QR-ER80). A PUSH device: on each scan +// it HTTP-GETs our backend (/qa/mcardsea.) carrying its serial (cjihao); the +// backend resolves the lane by matching that serial to this device's `serial` +// config, decides, and replies the verdict (drives the beep). No host-side +// connection — the adapter is a stub; the real integration is the HTTP endpoint +// (apps/server routes/qr-reader.ts). See wiki/entities/gee-qr-er80.md. +export const geeQrReaderDriver: ReaderDriver = { + id: "gee-qr-reader", + category: "reader", + label: "GEE/Fondvision QR reader (HTTP push)", + description: + "QR/barcode access reader that HTTP-pushes each scan to the backend. Set its server IP/port to this host in the vendor tool; enter its serial here so scans resolve to this lane.", + transports: ["tcp-ip"], + configFields: [ + { + key: "serial", + label: "Device serial (cjihao)", + type: "string", + required: true, + help: "The reader's serial as it reports in each scan (the `cjihao` field). Used to map scans to this lane.", + }, + ], + create: (c) => new StubReader("gee-qr-reader", c), +}; diff --git a/packages/devices/src/index.ts b/packages/devices/src/index.ts index 3089e07..7e07852 100644 --- a/packages/devices/src/index.ts +++ b/packages/devices/src/index.ts @@ -13,6 +13,7 @@ export { dingtianDriver, wiegandReaderDriver, tcpipReaderDriver, + geeQrReaderDriver, hikvisionDriver, dahuaDriver, rongtaDriver, diff --git a/wiki/entities/gee-qr-er80.md b/wiki/entities/gee-qr-er80.md index 8e56928..20fb5f3 100644 --- a/wiki/entities/gee-qr-er80.md +++ b/wiki/entities/gee-qr-er80.md @@ -90,10 +90,24 @@ from 10.0.10.7 (referer: http://www.fondvision.com — the OEM is Fondvision) - The reader **beeped on the vendor reply with `status:0`** — so it acts on the reply; `0` = invalid/1-beep as documented. A matching permit/session will return `status:1` → 2-beep accept. +## Assignment (as-built 2026-06-16) + +A dedicated **`gee-qr-reader`** driver ([[device-registry]], reader category) models the push reader: +its one config field is **`serial`** (the `cjihao` the device reports). The admin assigns it in the +[[first-run-setup|setup wizard]] like any device (normal UUID row id) and enters the serial. The QR +endpoint resolves the lane by **matching `config.serial` to the scan's `cjihao`** — not by row id — +so no DB hand-editing. Set the reader's server IP/port to this host in the **vendor tool**; assign + +enter its serial here. + +- Verified via inject: assign `gee-qr-reader` {serial:"H05M2AFA"} on a lane w/ an access device → + a `.jsp` scan with that serial + a matching permit QR → `status:1` (2-beep accept) + open; re-scan + → permit exit; unknown card → `status:0`; unassigned serial → `status:0` (no lane, graceful). +- Note `tcpip-reader` is the WRONG model for this device (host-connects-out, a stub) — use + `gee-qr-reader`. + ## Open / next -- **Assign the reader** as `lane_devices.id = "H05M2AFA"`, category `reader`, on the same lane as an - access device, so the endpoint resolves the lane. (The setup wizard doesn't yet capture a reader's - serial as its id — manual row or a wizard tweak; see [[first-run-setup]].) -- Re-test against the real app endpoint (now `.jsp`-aware): scan → expect the GET to hit - `/qa/mcardsea.jsp` and a `status:1` 2-beep when the credential matches a permit/open session. +- Re-test on hardware against the real app (now `.jsp`-aware + serial-resolved): scan → expect a + `status:1` 2-beep when the QR matches a permit/open session. +- `output` is replied as `0` (Access). Confirm on hardware whether the reader needs `1`/`2` (WG26/34) + to drive its access line, vs. `0`. diff --git a/wiki/log.md b/wiki/log.md index ed5c140..2baef3e 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -668,3 +668,16 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section). - Real serial **cjihao=H05M2AFA** = the lane key → assign reader as lane_devices.id="H05M2AFA". Reader beeped on status:0 (invalid/1-beep); a matching permit/session → status:1 (2-beep accept). - Updated [[gee-qr-er80]] (verified-on-hardware). + +## [2026-06-16] feature | gee-qr-reader driver — assign by serial, resolve lane by config +- The QR reader is a push device; setup wizard always assigns a random-UUID id, so "id = serial" + isn't possible via the UI. Clean fix instead: new **`gee-qr-reader`** driver (reader category) with + a single `serial` config field. Admin assigns it in the wizard (UUID id) + types the serial. +- QR endpoint now resolves the lane by **matching `lane_devices.config.serial` to the scan's + `cjihao`** (was: row id == cjihao). `qrReaderRoutes(app, db, dispatcher)`. Unassigned serial → + no lane → status:0 (graceful). +- `tcpip-reader` flagged as the WRONG model for this device (host-connects-out stub). +- VERIFIED via inject through the real /api/setup/assign: assign {serial:"H05M2AFA"} → .jsp scan + + matching permit → status:1 + open; re-scan → exit; unknown card → status:0; unassigned serial → + status:0. Full build 5/5. +- Updated [[gee-qr-er80]] (assignment as-built).