qr-reader: gee-qr-reader driver — assign in wizard, resolve lane by serial
The QR reader is a push device and the setup wizard assigns random-UUID ids, so
'id = serial' can't be set via the UI. Add a dedicated gee-qr-reader driver
(reader category) with a single 'serial' config field; the admin assigns it
normally and enters the device's serial (its cjihao).
The QR endpoint now resolves the lane by matching lane_devices.config.serial to
the scan's cjihao (instead of row id == cjihao), so no DB hand-editing. An
unassigned serial resolves to no lane -> status:0, gracefully.
Verified via inject through the real /api/setup/assign: assign {serial:
H05M2AFA} -> .jsp scan with a matching permit QR -> status:1 (accept) + open;
re-scan -> permit exit; unknown card -> status:0; unassigned serial -> status:0.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { eq, laneDevices, type Db } from "@parking/db";
|
||||||
import type { DeviceReadEvent } from "../device-events.js";
|
import type { DeviceReadEvent } from "../device-events.js";
|
||||||
import type { ReadDispatcher } from "../read-dispatch.js";
|
import type { ReadDispatcher } from "../read-dispatch.js";
|
||||||
|
|
||||||
@@ -25,24 +26,39 @@ interface ReaderQuery {
|
|||||||
time?: string;
|
time?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function qrReaderRoutes(app: FastifyInstance, dispatcher: ReadDispatcher): Promise<void> {
|
export async function qrReaderRoutes(
|
||||||
|
app: FastifyInstance,
|
||||||
|
db: Db,
|
||||||
|
dispatcher: ReadDispatcher,
|
||||||
|
): 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 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
|
// 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.
|
// auth on its side. Public route, like the Dingtian input push.
|
||||||
const handler = async (req: { query: ReaderQuery }) => {
|
const handler = async (req: { query: ReaderQuery }) => {
|
||||||
const q = req.query;
|
const q = req.query;
|
||||||
const cardid = (q.cardid ?? "").trim();
|
const cardid = (q.cardid ?? "").trim();
|
||||||
const mjihao = q.mjihao != null ? Number(q.mjihao) : 0;
|
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.
|
// Map the reader's serial → its assigned lane_devices row id (the dispatcher
|
||||||
// The reader sends its own mjihao/cjihao; the admin records that as the device's
|
// resolves the lane from that row). If unassigned, deviceId stays the serial so
|
||||||
// config so we can resolve it. For now we key the read on the device serial
|
// the dispatcher simply finds no lane and rejects (status:0) — never crashes.
|
||||||
// (cjihao) as the lane_devices id — see wiki note; refine when assignment lands.
|
const deviceId = readerRowIdForSerial(serial) ?? serial;
|
||||||
const deviceId = (q.cjihao ?? "").trim() || String(mjihao);
|
|
||||||
|
|
||||||
let accepted = false;
|
let accepted = false;
|
||||||
if (cardid) {
|
if (cardid) {
|
||||||
const read: DeviceReadEvent = {
|
const read: DeviceReadEvent = {
|
||||||
driverId: "gee-qr-er80",
|
driverId: "gee-qr-reader",
|
||||||
deviceId,
|
deviceId,
|
||||||
value: cardid,
|
value: cardid,
|
||||||
kind: "qr",
|
kind: "qr",
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export async function buildServer(opts: BuildOptions = {}): Promise<FastifyInsta
|
|||||||
// GEE/Dingtian QR reader: it HTTP-GETs on each scan and beeps/acts on our JSON
|
// GEE/Dingtian QR reader: it HTTP-GETs on each scan and beeps/acts on our JSON
|
||||||
// verdict (host-in-the-loop, synchronous). Routes the read through the dispatcher
|
// verdict (host-in-the-loop, synchronous). Routes the read through the dispatcher
|
||||||
// and replies the SDK verdict. See wiki/entities/gee-qr-er80.md, qrcode-sdk.md.
|
// and replies the SDK verdict. See wiki/entities/gee-qr-er80.md, qrcode-sdk.md.
|
||||||
await qrReaderRoutes(app, readDispatcher);
|
await qrReaderRoutes(app, db, readDispatcher);
|
||||||
|
|
||||||
// Pay station (pay-on-foot): quote an open session against the active tariff +
|
// Pay station (pay-on-foot): quote an open session against the active tariff +
|
||||||
// take payment → signed `payment` event. See wiki/concepts/tariff.md.
|
// take payment → signed `payment` event. See wiki/concepts/tariff.md.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { registry } from "../registry.js";
|
|||||||
import { dingtianDriver } from "./access-dingtian.js";
|
import { dingtianDriver } from "./access-dingtian.js";
|
||||||
import { dahuaDriver, hikvisionDriver } from "./camera.js";
|
import { dahuaDriver, hikvisionDriver } from "./camera.js";
|
||||||
import { rongtaDriver } from "./printer-rongta.js";
|
import { rongtaDriver } from "./printer-rongta.js";
|
||||||
import { tcpipReaderDriver, wiegandReaderDriver } from "./reader.js";
|
import { geeQrReaderDriver, tcpipReaderDriver, wiegandReaderDriver } from "./reader.js";
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export function registerBuiltinDrivers(): void {
|
|||||||
registry.register(dingtianDriver);
|
registry.register(dingtianDriver);
|
||||||
registry.register(wiegandReaderDriver);
|
registry.register(wiegandReaderDriver);
|
||||||
registry.register(tcpipReaderDriver);
|
registry.register(tcpipReaderDriver);
|
||||||
|
registry.register(geeQrReaderDriver);
|
||||||
registry.register(hikvisionDriver);
|
registry.register(hikvisionDriver);
|
||||||
registry.register(dahuaDriver);
|
registry.register(dahuaDriver);
|
||||||
registry.register(rongtaDriver);
|
registry.register(rongtaDriver);
|
||||||
@@ -25,6 +26,7 @@ export {
|
|||||||
dingtianDriver,
|
dingtianDriver,
|
||||||
wiegandReaderDriver,
|
wiegandReaderDriver,
|
||||||
tcpipReaderDriver,
|
tcpipReaderDriver,
|
||||||
|
geeQrReaderDriver,
|
||||||
hikvisionDriver,
|
hikvisionDriver,
|
||||||
dahuaDriver,
|
dahuaDriver,
|
||||||
rongtaDriver,
|
rongtaDriver,
|
||||||
|
|||||||
@@ -58,3 +58,28 @@ export const tcpipReaderDriver: ReaderDriver = {
|
|||||||
configFields: [hostField, portField(9000)],
|
configFields: [hostField, portField(9000)],
|
||||||
create: (c) => new StubReader("tcpip-reader", c),
|
create: (c) => 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.<ext>) 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),
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export {
|
|||||||
dingtianDriver,
|
dingtianDriver,
|
||||||
wiegandReaderDriver,
|
wiegandReaderDriver,
|
||||||
tcpipReaderDriver,
|
tcpipReaderDriver,
|
||||||
|
geeQrReaderDriver,
|
||||||
hikvisionDriver,
|
hikvisionDriver,
|
||||||
dahuaDriver,
|
dahuaDriver,
|
||||||
rongtaDriver,
|
rongtaDriver,
|
||||||
|
|||||||
@@ -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` =
|
- 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.
|
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
|
## Open / next
|
||||||
|
|
||||||
- **Assign the reader** as `lane_devices.id = "H05M2AFA"`, category `reader`, on the same lane as an
|
- Re-test on hardware against the real app (now `.jsp`-aware + serial-resolved): scan → expect a
|
||||||
access device, so the endpoint resolves the lane. (The setup wizard doesn't yet capture a reader's
|
`status:1` 2-beep when the QR matches a permit/open session.
|
||||||
serial as its id — manual row or a wizard tweak; see [[first-run-setup]].)
|
- `output` is replied as `0` (Access). Confirm on hardware whether the reader needs `1`/`2` (WG26/34)
|
||||||
- Re-test against the real app endpoint (now `.jsp`-aware): scan → expect the GET to hit
|
to drive its access line, vs. `0`.
|
||||||
`/qa/mcardsea.jsp` and a `status:1` 2-beep when the credential matches a permit/open session.
|
|
||||||
|
|||||||
+13
@@ -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".
|
- 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).
|
Reader beeped on status:0 (invalid/1-beep); a matching permit/session → status:1 (2-beep accept).
|
||||||
- Updated [[gee-qr-er80]] (verified-on-hardware).
|
- 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).
|
||||||
|
|||||||
Reference in New Issue
Block a user