35e593ab63
Two independent wiki updates bundled (all docs):
1. dingtian-dt008-reader.md: phantom optical decodes on the park-buzi EXIT
reader (empty pre-opening site, low-sun afternoons). Chain of evidence:
READ log lines carry the reader's own serial (H05MA5B0) → physical device,
not a network source; snapshot shows nobody present; code shapes are the
giveaway (6-digit numerics = checksum-less Interleaved 2-of-5, lone "C" =
Code39/Codabar artifact) → 1D engine decoding sun-made stripe patterns
(striped arm, fence shadows, glare). No fraud exposure (11-digit Luhn ids
can't match); noise only. Fix on the entity page: vendor-tool symbology cut
to QR+Code128 + min decode length, BOTH readers; config lives ON the device
→ re-apply after any factory reset/swap. Deliberately NOT filtering
impossible codes server-side — probe recording is the anomaly path's job.
2. Backfilled two shipped-but-undocumented features (six code files already
linked the first page as if it existed):
- concepts/entry-presence-bypass.md — admin drops a FAULTY presence signal
(granular radar/camera by decision, not a master switch); every flip is a
signed config_change; persists till off; tickets stamped presenceBypassed;
radar-bypass cooldown tradeoff; "the admin is not the adversary, but
trusted never means invisible".
- concepts/setup-relay-test.md — admin-only commissioning pulse, signed
barrier_open_command BEFORE the fire so a test open never reads as the
out-of-band-open fraud signal; saved controllers/declared relays only;
radarAlert lamps excluded; pulseOpen only.
Cross-linked from operator-issued-entry.md, cataloged in index.md, logged.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
158 lines
9.9 KiB
Markdown
158 lines
9.9 KiB
Markdown
---
|
||
type: entity
|
||
tags: [parking, hardware, readers, qr, dingtian]
|
||
sources: [dingtian-dt008]
|
||
updated: 2026-07-04
|
||
status: open
|
||
---
|
||
|
||
# Dingtian DT-008 (QR/RFID access reader)
|
||
|
||
The project's **QR / barcode / RFID reader** — Dingtian **DT-008**, the **same vendor as the
|
||
[[dingtian-relay|relay board]]** (which is why it integrates the same HTTP-push way). A static
|
||
optical + card scanner for **QR / barcode (simple)** and **ID/IC/NFC cards**. This is the
|
||
**[[ticket-encoding|QR ticket]]** scanner (read at the pay station / exit lane) and a path for **QR
|
||
or RFID [[subscription]]** credentials. Product page: dingtian-tech.com/en_us/qr_code_reader.html.
|
||
(See [[dingtian-dt008|spec summary]] / `raw/`.)
|
||
|
||
> **Naming correction (2026-06-28).** Through most of this project this reader was wrongly called
|
||
> **"GEE" / "GEE/Fondvision" / "GEE-QR-ER80"** — a bad early assumption. There is no GEE device; it
|
||
> is the **Dingtian DT-008**. The driver id was renamed `gee-qr-reader` → `dingtian-qr-reader`
|
||
> (migration 0015 rewrites existing rows). All *protocol/behaviour* facts below were observed on the
|
||
> real hardware and remain correct — only the device identity was wrong.
|
||
|
||
## What it is
|
||
|
||
- **QR/barcode + card.** Reads **QR / simple barcode** AND **ID/IC/NFC cards** (~0.2 s, 0–10 cm).
|
||
(An earlier note guessed "ER80-EM = 125 kHz EM4100 prox" — that was part of the same mis-id.)
|
||
- **Interfaces: Wiegand 26/34, TCP/IP, USB, RS485.** (NOT RS-232 — that was a fictional-datasheet
|
||
claim.) For our integration it talks **HTTP over TCP/IP**; Wiegand is the reader's *output line*
|
||
on a valid read, not the host transport.
|
||
- **Power** 9–16 V DC, 800 mA; **86×86×42 mm**; −30…70 °C. Fits the [[disk-os-hardening|appliance]].
|
||
|
||
## 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]], [[subscription]]) 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).
|
||
- Default IP `192.168.1.99` (set its server IP/port to this host in the vendor tool).
|
||
|
||
## 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 **`dingtian-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 `dingtian-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
|
||
`dingtian-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`.
|
||
|
||
## ⚠️ Phantom optical decodes from sunlight patterns (park-buzi, 2026-07-04)
|
||
|
||
With the site EMPTY (pre-opening, verified live + by snapshot), the **exit reader
|
||
(`cjihao=H05MA5B0`) pushed spontaneous scans** at random afternoon times (observed 16:38–18:14,
|
||
low-western-sun hours): `cardid` values like `997492`, `389861`, `024358`, `192793` — and once a
|
||
lone **`C`**. Server logs (`READ serial=` lines) confirm the pushes carry the reader's own serial
|
||
and resolve to its assigned row, so this is **the physical reader decoding, not a network source**.
|
||
|
||
**Diagnosis:** the scan engine ships with many 1D symbologies enabled, some with weak/no checksums.
|
||
Six-digit all-numeric strings are the signature of **Interleaved 2-of-5** (even-length, digits-only,
|
||
no checksum — any high-contrast stripe pattern of the right proportions "decodes"); a lone `C` is a
|
||
**Code39/Codabar** artifact (Codabar start/stop chars are A–D). Low sun creates exactly such
|
||
patterns at a gate: the striped barrier arm, fence/railing shadows sweeping as the sun moves, glare
|
||
bands. RFID noise would instead give repeating UID-shaped values.
|
||
|
||
**Impact: noise, not risk.** Every phantom was REFUSED fail-closed (`exit.refused.noSession`, a
|
||
signed anomaly — #52–58 in the feed); a phantom can never match a ticket ([[ticket-encoding]] ids
|
||
are 11-digit + Luhn, so a 6-digit read has nothing to match). Do NOT filter "impossible" codes
|
||
server-side — recording every probe of an exit reader is what the anomaly path is for; fix at the
|
||
source instead:
|
||
|
||
**Fix (vendor tool, per reader — config lives ON THE DEVICE, not in our DB):** disable every
|
||
symbology except **QR + Code128** (all our credentials); if offered, set **minimum decode length
|
||
≥ 10** and require checksums. Apply to BOTH readers. ⚠️ A factory reset or a swapped unit silently
|
||
re-enables the phantom symbologies — re-apply after any reset/replacement. Physical fallback if any
|
||
noise survives: hood/visor the window, tilt it down, avoid facing the striped arm.
|
||
|
||
## ⚠️ 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.)
|