Files
parking_solution/wiki/entities/gee-qr-er80.md
julian 1efa77bf56 devices: pool-of-spaces model — drop lane, per-relay direction
A parking lot is one pool of spaces with a flexible set of entry/exit
points — no "lane". Direction is a property of each RELAY inside an access
controller; readers/cameras bind to a controller relay and inherit it.

Schema:
- drop `lane` from ledger_events, device_events, sessions
- rename lane_devices -> devices (no lane/direction columns)
- access config.relays=[{relay,direction,button?}]; reader/camera
  config.controllerId+relay binding
- fresh 0000_baseline migration (history reset; dev data was throwaway)

Signed ledger:
- remove `lane` from canonicalize(); bump signer keyId sw-hmac-v1 -> v2
  (v1 events won't verify under v2 — intentional, gated per-event by keyId)

Server:
- new device-resolve.ts (replaces lane-map.ts): relayForButton,
  relayForDevice, firstRelayByDirection, devicesByDirection
- entry-flow: button terminal -> its relay; exit/permit: reader's bound
  relay; dispatcher resolves the bound relay + inherited direction
- camera snapshots fire by direction site-wide, async, never block open
- DeviceConfig widened to nested JSON for relays[]

Web:
- wizard: no lane selector; add controllers (relay map + entry-button
  terminal) first, then bind readers/cameras/printers to a controller relay

Wiki: new entry-exit-points.md (replaces lane-direction); reworked
entry-exit-readers, parking-session, first-run-setup, device-registry,
append-only-event-chain, device-events; removed stale lane/LaneMap mentions.
2026-06-16 20:29:38 +02:00

124 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: entity
tags: [parking, hardware, readers, qr]
sources: [gee-qr-er80]
updated: 2026-06-16
status: open
---
# GEE-QR-ER80 (QR access reader)
The project's **QR-code reader** (GEE NFC LIMITED). A static optical scanner for **QR /
DataMatrix / 1D barcode**, optional ID/IC card. This is the **[[ticket-encoding|QR ticket]]
scanner** the design called for — read at the pay station and exit lane — and a path for **QR
[[permit]]** credentials. On hand: variant **`-Q-W`** (QR scanner; Wiegand/RS-232/RS-485).
(See [[gee-qr-er80|datasheet summary]] / `raw/`.)
## What it is (and isn't)
- **Optical, not RFID-prox.** Earlier we *assumed* "ER80-EM" = a 125 kHz EM4100 card reader — the
datasheet corrects that: it's a **QR/barcode scanner**. The `-EM` in the original label was a
mis-id; the real model is **GEE-QR-ER80**. Optional `D`/`C` variants add ID/IC card, but the unit
on hand is **QR-only** (`-Q`).
- **Multi-interface** (Wiegand 26/34, RS-232, RS-485, USB, TCP/IP); the `-W` variant exposes
**Wiegand + RS-232/RS-485**.
## How it integrates — HTTP-GET push, server replies the verdict (confirmed via SDK)
The protocol is settled by the **[[qrcode-sdk|QRCode SDK v1.6.5]]** (not serial as first guessed).
The reader is configured (Windows tool `QRCode_v1_6_5.exe`) with a **server IP/port** and a "server
language" (only picks the URL path, e.g. `/qa/mcardsea.php`). **On each scan it HTTP-GETs the host:**
```
GET /qa/mcardsea.php?cardid=<QR>&mjihao=<devId>&cjihao=<devSN>&status=<2 chars>&time=<utc>
```
`cardid` = scanned data; `status` low digit = **direction (1=in / 0=out)**. The host replies JSON
`{data:[{cardid,cjihao,mjihao,status,time,output}],code:0}` where reply **`status` 1=valid (beep
2×) / 0=invalid (beep 1×)**, **`output` 0=Access/1=WG26/2=WG34**, `time` syncs the clock.
This is **host-in-the-loop and SYNCHRONOUS**: the GET *is* the access query and **our reply is the
decision** — it drives the reader's beep + output. So unlike a fire-and-forget reader, the endpoint
must decide (valid/invalid, direction from `status`) and reply, then also emit a `DeviceReadEvent`
on the `read` bus for the entry/exit/permit flows ([[parking-session]], [[permit]]) to open the
barrier. ([[device-input-flow]] is the analogous push pattern; this one also returns a verdict.)
> **This explains the "no beep":** feedback comes from the server's JSON reply, not locally. A
> non-JSON / missing reply ⇒ no beep even though the scan worked. So "no beep" ≠ "didn't scan."
- Pushes over plain **HTTP** to our `10.0.10.x` host (on the device subnet); no serial wiring, no
Wiegand-decode hardware. Suits the host-in-the-loop model; autonomy is moot anyway
([[dingtian-relay]] has no onboard ACL).
- **Linux-supported**, 4–15 VDC, default IP `192.168.1.99` — fits the [[disk-os-hardening|appliance]].
## Resolved (2026-06-16)
- Protocol = HTTP GET poll + JSON verdict (above). The earlier "serial/Wiegand, find the baud"
open questions are **moot** — it's HTTP. Wiegand is the reader's *output line* on a valid read
(the reply `output` field), not the host transport.
## As-built (2026-06-16)
- **Endpoint** `GET/POST /qa/mcardsea.php` (`apps/server/src/routes/qr-reader.ts`, public — the
reader has no auth, sits on the device subnet). Parses `cardid/mjihao/cjihao/status/time`, runs
the scan through the **read dispatcher** (permit match → permit flow; else transient exit), and
replies the **SDK verdict**: `status` 1=valid(beep 2×)/0=invalid(beep 1×), `output` 0, `time`.
- The read flows were refactored to **return a `ReadOutcome` { accepted, direction, reason }** so the
endpoint's reply reflects the real accept/reject (the dispatcher decides AND opens the barrier via
the flows). A fire-and-forget reader ignores the outcome.
- **Reader identity:** the endpoint matches the device **serial (`cjihao`)** against each reader's
`config.serial` to find its `devices` row; the dispatcher then resolves the relay that row is
**bound** to (`config.controllerId` + `relay`) and opens it. See [[entry-exit-points]].
- Verified via inject: valid permit QR → `status:1` + open; re-scan → permit exit (still valid);
unknown QR → `status:0`; reader on a barrier-less lane → `status:0`.
## Verified on hardware (2026-06-16)
Captured a real scan (vendor-emulator logger on :3000). The reader **does scan, send, and beep** —
the earlier "no beep" was simply that no server was answering on :3000 with valid JSON. Real GET:
```
GET /qa/mcardsea.jsp?cardid=52020056&mjihao=1&cjihao=H05M2AFA&status=11&time=1781634494
from 10.0.10.7 (referer: http://www.fondvision.com — the OEM is Fondvision)
```
- **PATH carries the configured "server language" EXTENSION:** this unit is set to **JSP**, so it
GETs **`/qa/mcardsea.jsp`** — NOT `.php`. Our endpoint was registered at `.php` only → it would
have 404'd the real reader. **Fixed:** the route now registers `php/jsp/asp/aspx/cgi`.
- **`cjihao` = `H05M2AFA`** is the device **serial** — the value our endpoint matches against the
reader's `config.serial`. So assign the reader with **`config.serial = "H05M2AFA"`** and bind it
to a controller relay.
- **`mjihao` = 1** (device id). `cardid` = the scanned barcode (`52020056`). `status=11`.
- 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), enters the serial, and binds
it to a controller relay. The QR endpoint resolves the reader 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 + bind it here.
- Verified via inject: assign `gee-qr-reader` {serial:"H05M2AFA"} bound to an access relay →
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 relay, graceful).
- Note `tcpip-reader` is the WRONG model for this device (host-connects-out, a stub) — use
`gee-qr-reader`.
## Open / next
- 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`.
## ⚠️ Reply MUST set `Connection: close` (verified on hardware)
The reader sends `Connection: keep-alive` but **only acts on the verdict (beep/output) once the TCP
socket CLOSES**. Fastify's default keeps the connection alive → the reader waits out a **~10 s
keep-alive timeout before beeping**, even though the server replied in ~15 ms. Every vendor demo
replies **`Connection: close`** and shuts the socket. Fix: the endpoint sets
`reply.header("connection","close")`. Symptom if regressed: correct accept/reject but a ~10 s lag
before the beep. (The request arrives fast; the delay is entirely the reader waiting for close.)