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
108 lines
7.4 KiB
Markdown
108 lines
7.4 KiB
Markdown
---
|
|
type: concept
|
|
tags: [parking, domain, business, devices, entry-flow]
|
|
sources: []
|
|
updated: 2026-06-19
|
|
status: open
|
|
---
|
|
|
|
# Ticket Encoding & Scanning
|
|
|
|
How a transient [[parking-session]]'s **ticket id** is printed, carried by the customer, and read
|
|
back at the pay station and exit. This is the **physical backbone of the transient flow** — the
|
|
thing that links entry → pay → exit when there's no plate.
|
|
|
|
## The ticket id is the session key
|
|
|
|
At entry the system mints a `vehicle_entry` event with a **ticket id** (`identity`) and prints a
|
|
ticket the customer keeps. That same id is read back later to find the session. Properties the id
|
|
must have:
|
|
|
|
- **Opaque + unguessable** — a random id (not a sequential count an attacker could iterate to claim
|
|
someone else's cheaper session). Sequential **physical** stock numbering is a separate
|
|
reconciliation aid ([[reconciliation]] pre-numbered stock), not the scan key.
|
|
- **All-numeric** (as-built 2026-06-17) — so ANY legacy 1D barcode scanner reads it and an operator
|
|
can hand-key it. Random (not sequential), so "all-numeric" does not weaken the unguessable
|
|
property. Format (**shortened 13→11 on 2026-06-19**): **11 digits = 10 cryptographically-random
|
|
digits + 1 Luhn check digit**. **Length is driven by guess-resistance, not volume** — with 10^10
|
|
valid ids and the Luhn digit rejecting 9/10 malformed guesses, a blind attempt at a currently-OPEN
|
|
ticket lands at ~1-in-10^7 even with thousands parked (the [[threat-model|booth-operator]] threat),
|
|
which is the property that matters; raw count (a billion) was never the constraint. Collisions stay
|
|
negligible at lot scale. `newTicketId()` in `apps/server/src/entry-flow.ts`; validate with
|
|
`validateTicketCode()` — now **length-agnostic** (`\d{10,14}` + Luhn) so legacy 13-digit tickets in
|
|
circulation keep validating. Gate MANUAL entry only — a scanned/looked-up id already in the ledger
|
|
is authoritative regardless of format.
|
|
> **Why 11, not the requested 9 (2026-06-19):** 9 digits (10^8 space) against ~1000 live tickets
|
|
> gives ~1-in-10^5 per blind guess — an operator scripting guesses could find a valid open ticket
|
|
> in minutes. 11 keeps a strong anti-forgery margin AND was the actual fix for a printer overflow
|
|
> (below). The shorter id is *why* the width-3 barcode now fits the 72mm Cashino line.
|
|
- **Format is a property of minting, not the schema** — `identity` / `sessions.id` are free-form
|
|
`text`, so changing the id format is a code change with **no migration**. Legacy `T-<uuid>` ids
|
|
(pre-2026-06-17) remain valid keys and coexist with numeric ones.
|
|
- **Single logical session** — scanning it at the pay station finds the open session; after payment
|
|
it's the proof-of-paid the exit checks.
|
|
|
|
## Encoding: Code128 numeric barcode — printed by the booth dispenser
|
|
|
|
- The [[rongta-printer]] prints the ticket id as a **1D Code128 barcode** (the all-numeric code),
|
|
with the **same code in large human-readable digits below it**, then the entry time. Code128 over
|
|
QR for the primary symbology because the booth's reader hardware is unknown and a legacy 1D laser
|
|
scanner is the lowest common denominator — and the printed digits mean total reader failure still
|
|
leaves a hand-keyable code. A **QR for phone/imager scanning may be added later behind an admin
|
|
toggle** (deferred — see Open).
|
|
|
|
> **As-built (2026-06-17).** `renderTicket()` in [[rongta-printer]]
|
|
> (`packages/devices/src/drivers/printer-rongta.ts`) emits the Code128 via ESC/POS `GS k` (code set
|
|
> B) — **rendered by the printer firmware**, so there is no image-rendering step and no new
|
|
> dependency (keeps the MIT/Apache/BSD constraint). Resilience rationale: the booth's reader is
|
|
> uncertain, so the id is carried in two independently-readable forms (1D barcode / printed digits).
|
|
> The "operator scans with a phone" path reuses the
|
|
> existing dispatch flow ([[entry-exit-readers]]) and is tracked separately (not yet built).
|
|
|
|
> **Barcode GEOMETRY must fit the paper width — root cause of a real garbage-print incident
|
|
> (2026-06-19).** The Cashino [[rongta-printer|KP-300H]] entry dispenser printed tickets as **raster
|
|
> garbage** (solid black bars / banding) while the [[rongta-printer|Rongta]] printed the *identical*
|
|
> byte stream fine. Not data corruption: the **Code128 symbol overflowed the print line.** Math: a
|
|
> Code128-B symbol is `(11·chars + 35) · moduleWidth` dots. At 203 dpi the KP-300H prints **72mm =
|
|
> 512 usable dots** (the Rongta runs 80mm = 576). The old **13-digit** id at `GS w 3` = ~534 dots
|
|
> **overran 72mm**, and the firmware rendered the overflow as pixels; the Rongta's 80mm had just
|
|
> enough room — which is why only the Cashino failed. The **11-digit** id at width 3 = **~468 dots**,
|
|
> fits both widths, and scanned the full value at the exit reader (verified on hardware). Lessons:
|
|
> (1) keep `(11·len+35)·moduleWidth` under the **narrowest** deployed printer's usable dots; (2) a
|
|
> too-NARROW module (`GS w 2`) is also bad — it scanned but returned **truncated** values (partial
|
|
> reads logged as `exit.refused.noSession` anomalies). Width 3 + 11 digits is the verified sweet
|
|
> spot. `code128()` in `packages/devices/src/drivers/printer-escpos.ts`.
|
|
- **Scan points** (both host-side reads — [[entry-exit-readers]]):
|
|
- **Pay station** — customer scans the ticket → host finds the session → shows fee → takes
|
|
payment ([[tariff]], pay-on-foot) → appends `payment`.
|
|
- **Exit lane** — customer scans the (now paid) ticket → host validates paid + within
|
|
`gracePeriodExit` → `vehicle_exit` → `pulseOpen`.
|
|
- The **scanner is a device behind an adapter** ([[device-adapter-pattern]]): a new `ReaderDevice`
|
|
kind (QR/barcode imager) — likely the same `IdentitySource = "ticket"` / `"qr"` path. Keeps the
|
|
app device-agnostic; hardware model is procurement ([[bom]], [[open-questions]]).
|
|
- **On hand:** the **[[dingtian-dt008-reader|Dingtian DT-008]]** QR/RFID reader (Wiegand/TCP-IP/USB/RS485,
|
|
Linux-supported) — the concrete scanner for this path. A serial `ReaderDevice` adapter feeds the
|
|
`read` bus; via HTTP-GET push (see [[dingtian-dt008-reader]]).
|
|
|
|
## Ticketless alternative (plate as the ticket)
|
|
|
|
Where the [[opencv-anpr-service|vision service]]/LPR captures the plate, the **plate can be the
|
|
session key** instead of a printed ticket — drive in, plate read, drive to pay station and enter
|
|
plate (or it's looked up), pay, exit by plate. No paper. The two can coexist per lane
|
|
([[entry-exit-readers]] "both share a relay"); a printed QR ticket is the fallback when a plate
|
|
isn't captured or is low-confidence (recognition is advisory — [[opencv-anpr-service]]).
|
|
|
|
## Open
|
|
|
|
- Primary symbology **decided**: Code128 set B over the all-numeric id (as-built above). Still open:
|
|
what *else* prints (site name, tariff summary, help number).
|
|
- **Optional QR (deferred)** — an admin toggle to ALSO print a QR for phone/imager users. The
|
|
`code128()`/`qrCode()` ESC/POS helpers were prototyped 2026-06-16; QR was dropped 2026-06-17 in
|
|
favor of "1D barcode + hand-keyable numeric code" because the booth's reader hardware is unknown.
|
|
Revisit when mobile scanning is wanted.
|
|
- **Phone-scan fallback** (operator scans a ticket with a phone when a reader is down) — designed
|
|
but not built: an authenticated route feeding the same dispatcher + a minimal mobile scan UI.
|
|
- Scanner hardware (imager model; same unit at pay station and exit?).
|
|
- Lost/damaged ticket → the lost-ticket path ([[parking-session]], [[tariff]] admin-arbitrary
|
|
amount).
|