Files
parking_solution/wiki/concepts/entry-exit-points.md
T
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

111 lines
5.3 KiB
Markdown
Raw 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: concept
tags: [parking, architecture, devices, setup]
sources: []
updated: 2026-06-16
---
# Entry / Exit Points (pool-of-spaces model)
A parking lot is **one pool of spaces** with a flexible set of **entry points** and **exit
points** — any number of each, in any combination (1 in + 1 out, 1 in + 2 out, 2 in + 1 out, …).
There is **no "lane"** concept anywhere in the system (dropped 2026-06-16 — see below).
## Direction lives on the relay, not the controller
An access controller (e.g. a [[dingtian-relay]] board) has **several relays** — each relay opens
one barrier. Direction is a property of **each relay**, declared in the controller's config:
```jsonc
// access `devices` row — one Dingtian board
config: {
host: "192.168.1.100",
relays: [
{ relay: 1, direction: "entry", button: 1 }, // entry barrier; entry button on input 1
{ relay: 2, direction: "exit" } // exit barrier; opened by a reader, no button
]
}
```
- `direction`: `entry` | `exit` | `both` (`both` = one barrier/relay serving in and out).
- `button`: the **input terminal** the transient **entry button** is wired to. Only entry/both
relays have one. Absent = no button at that barrier (subscriber/reader-driven only).
The four real layouts all fall out of this:
| Layout | Controllers | Relays |
| --- | --- | --- |
| 1 barrier, both directions | 1 | `{relay:1, both, button:1}` |
| 2 barriers, 1 board | 1 | `{relay:1, entry, button:1}`, `{relay:2, exit}` |
| 2 barriers far apart | 2 | board A `{relay:1, entry}`, board B `{relay:1, exit}` |
| 1 entry + 2 exit | 3 | A entry; B, C each exit |
## Readers / cameras BIND to a relay
A reader or camera points at the barrier it physically sits at, via its config:
```jsonc
config: { ...readerConfig, controllerId: "<access devices.id>", relay: 2 }
```
Its **direction is inherited** from that relay. So an exit read opens **exactly that relay** —
no ambiguity even with multiple exit barriers ("the relay at that reader", decided 2026-06-16).
Binding is optional: an unbound device falls back to a `config.direction` + the first relay
site-wide of that direction (keeps the single-barrier case trivial). LPR is a snapshot sink —
an ANPR service ([[opencv-anpr-service]]) POSTs the plate as a `plate` read to the reader
endpoint, flowing through the same dispatcher.
## Resolution (one module: `apps/server/src/device-resolve.ts`)
- **Button press** → `relayForButton(controllerId, terminal)` → the entry relay whose `button`
matches → entry flow → `pulseOpen(relay)`.
- **Reader/permit/LPR read** → `relayForDevice(reader)` → the bound relay → `pulseOpen(relay)`;
direction inherited.
- **Snapshots** → `devicesByDirection("camera", dir)` → every camera serving that direction.
A directional barrier that contradicts the car's open-session state (an exit barrier scanned by a
car not inside, or an entry barrier by a car already in) is a wrong-barrier / [[anti-passback]]
refusal. A `both` relay defers to session state.
## The flows
| Flow | Trigger | Opens |
| --- | --- | --- |
| Transient entry | entry **button** press | the entry relay (button-mapped) → ticket prints |
| Transient exit | voucher scan at exit reader | the exit relay (reader-bound), if paid+grace |
| Subscriber entry | QR/RFID/plate at entry reader | the entry relay (reader-bound), if permit valid |
| Subscriber exit | QR/RFID/plate at exit reader | the exit relay (reader-bound), if permit valid |
Every open also fires a [[camera snapshot|append-only-event-chain]] (async, never blocks the open).
## Why no lane
"Lane" was a leftover from a rows-of-gates mental model. It added nothing here:
- **Occupancy** is a site-wide fold over the ledger (entries − exits); it never grouped by lane.
- **Device grouping** is now done by the reader→relay binding, far more precisely than a lane key.
- **Anti-fraud** doesn't use it — the signed chain, the "open must match a signed event" check,
and [[reconciliation]] all work on *what happened*, not *which gate*. The relay's direction
already catches an exit firing an entry barrier, better than a lane number would.
Dropping it removed `lane` from `ledger_events`, `device_events`, `sessions`, and the device
table (renamed `lane_devices` → `devices`). Because `lane` was part of the **signed canonical
form**, this is a versioned change: the canonical array no longer includes lane, and the signer
keyId bumped `sw-hmac-v1` → `sw-hmac-v2`. v1 events won't verify under v2 — intentional, gated by
each event's stored `keyId` (done pre-deployment, on throwaway data, so zero real cost). See
[[append-only-event-chain]].
## Camera snapshots (evidence, not a gate)
Captured **after** the barrier opens, **never awaited** — a camera failure can't delay or block an
open (the signed ledger is the decision). Stored as a **BLOB in the `snapshots` table** (single
backed-up DB, nothing scattered on disk), in its own table so hot telemetry scans don't drag image
bytes and images prune independently. Linked to the signed `vehicle_entry/exit` by `identity`.
Served read-only via `GET /api/snapshots/:id`. **Retention is unresolved** — see [[open-questions]].
## Related
[[entry-exit-readers]] · [[device-events]] · [[parking-session]] · [[anti-passback]] ·
[[append-only-event-chain]] · [[barrier-not-a-door]] · [[opencv-anpr-service]] ·
[[dingtian-relay]] · [[first-run-setup]]