diff --git a/docs/SDS.md b/docs/SDS.md new file mode 100644 index 0000000..af27f54 --- /dev/null +++ b/docs/SDS.md @@ -0,0 +1,426 @@ +# Software Design Specification (SDS) + +**Project:** LifeLane — Smart Traffic Signal for Emergency Vehicles +**Document version:** 1.0 +**Status:** MVP / proof of concept + +--- + +## 1. Introduction + +### 1.1 Purpose + +This Software Design Specification describes *how* LifeLane is built: its +architecture, module decomposition, key algorithms, data structures, +interfaces, and the design decisions behind them. It is the design counterpart +to the [Software Requirements Specification](SRS.md) (which defines *what* the +system does) and expands on [docs/ARCHITECTURE.md](ARCHITECTURE.md) with a +requirement-oriented, component-level view. + +### 1.2 Scope + +The design covers three deployable packages — `shared` (engine + simulator), +`server` (REST + real-time transport + persistence), and `client` (React + +Leaflet dashboard) — and the two client runtimes that let the same engine run +either on a backend or entirely in the browser. + +### 1.3 Design goals + +1. **One engine, two homes.** The controller logic must run byte-for-byte + identically in Node and the browser. +2. **Local, fast preemption.** Detection decisions are made per intersection, + off the central path. +3. **Graceful degradation.** No single sensing channel is a single point of + failure. +4. **Zero-dependency deployability.** No geocoding service, no API keys required + to run. +5. **Interchangeable runtimes.** The UI is agnostic to whether a real backend is + present. + +--- + +## 2. Architectural overview + +### 2.1 Layered model + +The design mirrors the three physical layers of a real deployment: + +| Physical layer | LifeLane realization | +| --- | --- | +| Onboard vehicle unit (GPS + siren + radio) | `shared/src/DeviceSimulator.js` | +| Roadside/intersection controller (decision logic) | `shared/src/TrafficEngine.js` | +| Central management + transport + dashboard | `server/` + `client/` | + +### 2.2 Package layout + +``` +shared/ Isomorphic traffic engine + device simulator (runs in Node and browser) +server/ Express REST API + Socket.IO live state, SQLite (WAL) event log +client/ React + Leaflet dashboard with two interchangeable runtimes: + - socketRuntime: real backend over REST + WebSocket + - localRuntime: the same shared engine ticking in-browser +``` + +### 2.3 Runtime selection + +`client/src/runtime/createRuntime.js` probes `GET /api/health` on load. If it +answers, the client builds a `socketRuntime`; otherwise it falls back to +`localRuntime`. Both expose the identical interface: + +``` +{ network, state, events, mode, dispatch, recall, setAutoDispatch, + setRegion, canChangeRegion } +``` + +so every downstream component is oblivious to which is active. This is what +makes the static GitHub Pages build fully interactive with no server. + +### 2.4 Component diagram + +``` + ┌─────────────────────────────────────────┐ + │ client/ │ + │ App → useTrafficSystem → runtime │ + │ MapView · ControlPanel · StatsPanel │ + │ EventLog · Legend · Header │ + └──────────────┬───────────────┬───────────┘ + socketRuntime │ │ localRuntime + (REST + Socket.IO) │ │ (in-browser tick) + ▼ ▼ + ┌───────────────────────┐ ┌───────────────────────┐ + │ server/ │ │ shared/ (in browser)│ + │ server.js routes.js │ │ createTrafficSystem │ + │ db.js (SQLite/WAL) │ └───────────────────────┘ + └───────────┬───────────┘ + ▼ + ┌───────────────────────────────────────────┐ + │ shared/ │ + │ createTrafficSystem(regionId): │ + │ regions.js → network.js → TrafficEngine │ + │ ← DeviceSimulator → EventBus → geo.js │ + └───────────────────────────────────────────┘ +``` + +--- + +## 3. Module design (`shared/`) + +The `shared` package is the heart of the system and is deliberately free of any +Node- or browser-specific API so it runs unmodified in both environments. + +### 3.1 `regions.js` — region catalog + +Holds the `REGIONS` catalog: for each region, a real-world lat/lng anchor, a +grid step size, a city name, and per-axis street names. `listRegions()` enumerates +them for the UI. Design intent: the grid *shape* (4×3) is fixed in code, but its +*geography and labels* are data, so adding a city is a one-entry change with no +logic touched (satisfies FR-2, FR-13). No live geocoding is used — every region +is a small hand-anchored preset baked into the repo, keeping the system +dependency- and key-free. + +### 3.2 `network.js` — topology and routing + +`buildNetwork(regionId)` resolves a region through `regions.js` and generates the +intersection graph: 12 nodes with column/row coordinates, real lat/lng derived +from the region anchor + step, and neighbor links. `findRoute()` does BFS over +the neighbor graph to compute a vehicle's path (satisfies FR-1, FR-6). +`listIntersectionIds()` / `listRegions()` are the lookup helpers the server +validates dispatches against. + +### 3.3 `geo.js` — geodesy + +Haversine distance and point interpolation. Because distances are computed in +real meters, the detection radii in `constants.js` are correct regardless of a +region's lat/lng step size — the preemption logic never needs to know which city +it is running in. + +### 3.4 `constants.js` — single source of tunables + +All detection, timing, and rate parameters live here (satisfies NFR-12): + +| Constant | Value | Role | +| --- | --- | --- | +| `DETECTION_RADIUS_M` | 350 m | GPS preemption trigger range | +| `ACOUSTIC_RANGE_M` | 250 m | Siren preemption trigger range | +| `EARLY_WARNING_RADIUS_M` | 750 m | "vehicle inbound" UI cue range | +| `PASS_THRESHOLD_M` | 35 m | Distance considered "through" the intersection | +| `CLEARANCE_DELAY_MS` | 2500 ms | All-red hold before resuming normal cycle | +| `GPS_CONFIDENCE_TRIGGER` | 0.6 | Min GPS confidence to preempt | +| `SIREN_CONFIDENCE_TRIGGER` | 0.75 | Min siren confidence to preempt | +| `NS_GREEN_MS` / `EW_GREEN_MS` | 12000 ms | Normal green durations | +| `ALL_RED_MS` | 1500 ms | Normal all-red transition | +| `DEFAULT_VEHICLE_SPEED_MPS` | 14 m/s | ≈ 50 km/h cruising speed | +| `TICK_MS` | 400 ms | Engine tick / broadcast cadence | +| `AUTO_DISPATCH_MIN/MAX_MS` | 18000 / 40000 ms | Auto-dispatch interval bounds | +| `MAX_EVENT_LOG` | 300 | In-memory event log cap | + +### 3.5 `TrafficEngine.js` — the controller + +The state machine and telemetry-fusion logic — layer 2. Each intersection is in +one of four states: + +``` + NORMAL CYCLING ──(GPS or siren trigger)──▶ PREEMPTED (green corridor) + ▲ │ + │ vehicle passes through + │ (releaseIntersection) + │ ▼ + NORMAL CYCLING ◀──(clearanceMs counts down)── ALL-RED CLEARANCE +``` + +Key methods: + +- **`ingestTelemetry(frame)`** — the fusion decision (satisfies FR-11). Computes + distance via `geo.js` and applies the OR-trigger: + + ``` + gpsTrigger = distance <= DETECTION_RADIUS_M (350) AND gpsConfidence >= 0.6 + sirenTrigger = distance <= ACOUSTIC_RANGE_M (250) AND sirenConfidence >= 0.75 + preempt = gpsTrigger OR sirenTrigger + ``` + + GPS has longer range but a lower confidence bar; siren is shorter-range but + demands higher confidence because false-positive sirens (other vehicles, + recordings) are a real concern. On a trigger, the approach axis is forced green, + `preempted` is set for the dashboard, and a `preemption` event is logged with + source + distance (FR-12). +- **Early-warning flagging** — intersections within `EARLY_WARNING_RADIUS_M` are + marked `preparing` (FR-13), producing the rolling green-wave cue. +- **Conflict handling** — a second vehicle approaching a still-preempted + intersection from a different axis logs a `conflict` event and holds priority + for the vehicle already in the intersection rather than flapping (FR-14). +- **`releaseIntersection` / clearance** — once a vehicle is within + `PASS_THRESHOLD_M`, the intersection holds all-red for `CLEARANCE_DELAY_MS` + before resuming normal cycling (FR-15), mirroring the real safety rule that + cross-traffic isn't released the instant the vehicle clears. +- **`getSnapshot()`** — returns only fields that change per tick (phase timers, + preemption flags, vehicle positions), *not* static topology. This keeps + steady-state broadcast volume proportional to live activity (NFR-2). +- **`getEvents(limit)`** — returns the capped in-memory event log. + +### 3.6 `DeviceSimulator.js` — the simulated fleet (layer 1) + +Plays the role of real onboard hardware: it moves each dispatched vehicle along +its route, fabricates GPS and siren confidence with realistic noise/dropout, and +calls `engine.ingestTelemetry()` every tick. Public operations: `dispatch()` +(FR-6), `recall()` (FR-7), and `setAutoDispatch()` (FR-8), the last driving +periodic randomized dispatches within the auto-dispatch interval bounds. + +### 3.7 `EventBus.js` and `index.js` + +`EventBus.js` is a minimal pub/sub used to surface `log` events out of the +engine. `index.js` exposes `createTrafficSystem(regionId)`, the factory that +wires region → network → engine → simulator → bus together; both the Node server +and the browser `localRuntime` call this same factory. + +--- + +## 4. Module design (`server/`) + +### 4.1 `server.js` — composition and transport + +Wires Express + Socket.IO around a `createTrafficSystem(REGION_ID)` instance, +runs the `TICK_MS` tick loop, and on each tick broadcasts a trimmed state +snapshot plus re-broadcasts every engine `log` event. Security middleware +installed here: `helmet` with a CSP scoped to allow OSM tile loading and +same-origin XHR/WebSocket only (NFR-7), `compression`, and a CORS allowlist +derived from `CORS_ORIGIN` applied to both Express and the Socket.IO handshake +(NFR-8). A global rate limiter (300 req/min) covers all of `/api` (NFR-9). + +### 4.2 `routes.js` — REST surface + +Defines the read endpoints (`/health`, `/regions`, `/network`, `/state`, +`/events`, `/history`) and write endpoints (`/dispatch`, `/recall/:id`, +`/auto-dispatch`, `/telemetry`). Design details: + +- A stricter `writeLimiter` (60 req/min) plus `requireApiKey` guard the four + write endpoints (NFR-9, NFR-10). +- `/dispatch` validates `startId`/`endId` against `listIntersectionIds()` and + returns 400 for unknown ids or an uncomputable route (FR-6). +- `/telemetry` is the ingestion contract real hardware would POST; it requires a + `vehicleId` and numeric `lat`/`lng` and rejects malformed frames with 400 + (FR-10). The simulator just happens to be the only caller in the demo. +- `/events` and `/history` clamp their `limit` query (≤ 300 and ≤ 500 + respectively) to bound response size. + +### 4.3 `auth.js` — optional API-key gate + +`requireApiKey` checks an `x-api-key` header against `TRAFFIC_API_KEY`. When the +variable is unset it is a deliberate no-op so the demo needs no configuration; a +real deployment sets it to require the header on every write (NFR-10). + +### 4.4 `db.js` — durable event log + +Persists the event log to SQLite via `node:sqlite` in WAL mode, so `GET /history` +survives restarts and reads are not blocked by concurrent writes (FR-22, NFR-5). +The database path is `DATA_DIR`. + +--- + +## 5. Module design (`client/`) + +### 5.1 Runtimes + +- **`createRuntime.js`** — probes `/api/health` and constructs the appropriate + runtime (FR-21). It normalizes both into one interface so components never + branch on transport. +- **`socketRuntime.js`** — REST for commands, Socket.IO for live `network` / + `state` / `events`. It maps Socket.IO `connect`/`disconnect` into a + `reconnecting` mode so a dropped connection is visible rather than stale + (FR-20, NFR-6). It does not implement `setRegion` (a backend's region is fixed). +- **`localRuntime.js`** — runs the same `createTrafficSystem()` on a `setInterval` + tick inside the tab; state lives only in the tab. It implements `setRegion()`, + which tears down and rebuilds the engine and re-emits `network`/`state`/`events` + so the dashboard redraws against the new city (FR-4). + +### 5.2 Hook and components + +- **`useTrafficSystem.js`** — the React hook bridging the active runtime into + component state. +- **`MapView.jsx`** — renders roads as Leaflet polylines and intersections as + `divIcon` markers colored by state; vehicles are 🚑 `divIcon` markers hoisted + to a single shared icon instance (rather than allocated per marker per render) + for efficiency. A `RecenterOnChange` helper calls `useMap().setView()` whenever + the computed center changes, because `MapContainer` only honors `center`/`zoom` + on first mount — without it, switching regions would strand the viewport over + the old city (FR-16, NFR-15). +- **`ControlPanel.jsx`** — dispatch/recall/auto-dispatch controls, and a region + `