Add multi-decoder split and pit timing support

This commit is contained in:
larssand
2026-08-25 19:45:40 +02:00
parent 6f02e955a5
commit 8ea8b3a84e
20 changed files with 876 additions and 147 deletions

View File

@@ -893,6 +893,96 @@ sequenceDiagram
A->>A: rerender timing/overlay
```
### 8.3.1 Multi-decoder topology and ownership
The decoder runtime supports four explicit roles. Roles are semantic, not inferred from `loop_id`; the WebSocket connection used to receive a message determines how it is processed.
| Runtime role | Frontend URL setting | Managed AMMC config | Default port | Canonical mutation |
|---|---|---|---:|---|
| `main` | `settings.wsUrl` | `decoderHost` / `wsPort` | `9000` | validates and appends `result.passings`; increments laps |
| `split` | `settings.splitWsUrl` | `splitDecoderHost` / `splitWsPort` | `9001` | appends `result.splits`; updates split fields only |
| `pit_entry` | `settings.pitEntryWsUrl` | `pitEntryDecoderHost` / `pitEntryWsPort` | `9002` | opens pit state and appends a pit entry event |
| `pit_exit` | `settings.pitExitWsUrl` | `pitExitDecoderHost` / `pitExitWsPort` | `9003` | closes pit state and appends the measured stop |
Physical and network chain:
```text
physical loop -> physical decoder IP -> one AMMC child process on backend host
-> unique AMMC WebSocket port -> browser WebSocket client with fixed role
-> processDecoderMessageHelper(message, deps, role)
```
Example complete installation:
```text
Main decoder 192.168.1.10 -> backend AMMC :9000 -> role main
Split decoder 192.168.1.11 -> backend AMMC :9001 -> role split
Pit entry decoder 192.168.1.12 -> backend AMMC :9002 -> role pit_entry
Pit exit decoder 192.168.1.13 -> backend AMMC :9003 -> role pit_exit
Browser host 192.168.1.100 uses ws://192.168.1.100:9000 through :9003
```
Invariants:
- only `main` may change `entry.laps`, `entry.lastTimestamp`, or append to `result.passings`
- split timing is measured from the latest main timestamp, or mass-start timestamp before the first completed lap
- a duplicate pit-entry signal while `entry.pitActive` is true is ignored
- pit exit without a prior pit entry is ignored
- an optional channel can disconnect without stopping main lap counting
- main, split, pit entry, and pit exit must use distinct AMMC WebSocket ports
- every physical decoder normally has a distinct IP; reusing one feed for different roles would process the same crossing with conflicting semantics
### 8.3.2 Multi-decoder frontend lifecycle
`src/app.js` owns role-indexed `wsClients` and `reconnectTimers` maps. `connectDecoderHelper()` first disconnects all clients, always connects `main`, then connects enabled optional roles. `connectDecoderRole()` binds the role into every message callback. Auto-reconnect checks the current enable flags before reconnecting optional channels.
Decoder runtime state is persisted with separate status fields:
```js
decoder: {
connected, lastMessageAt, lastError,
splitConnected, splitLastMessageAt, splitLastError,
pitEntryConnected, pitEntryLastMessageAt, pitEntryLastError,
pitExitConnected, pitExitLastMessageAt, pitExitLastError
}
```
Race data is stored under `resultsBySession[sessionId]`:
```js
{
passings: MainPassing[],
splits: SplitPassing[],
pitEvents: PitEvent[],
pitStops: PitStop[],
competitors: {
[competitorKey]: {
lastSplitMs, previousSplitMs, lastSplitDeltaMs, bestSplitMs,
lastSplitTimestamp, lastSplitLap,
pitActive, pitEnteredAt, pitStops,
lastPitStopMs, totalPitMs, bestPitStopMs
}
}
}
```
The full result object is included in `/api/state` persistence. Only main passings are posted to `/api/passings` and the SQLite passings table. This preserves the existing lap ledger and prevents split/pit records from being interpreted as laps. If split or pit history later needs SQL reporting, add dedicated tables and endpoints rather than inserting those records into `passings`.
### 8.3.3 Managed AMMC process lifecycle
`server.js` owns four process states in `ammcState.channels`: `main`, `split`, `pitEntry`, and `pitExit`. `startAmmcProcesses()` normalizes and validates the complete configuration before spawning one child process per enabled role. All roles use the same resolved AMMC executable and working directory but receive different `-w <port> <decoderHost>` arguments.
The AMMC status API remains backward compatible: top-level status fields represent `main`; nested `split`, `pitEntry`, and `pitExit` objects expose optional process status. Combined `lastOutput` records include a `role` field. `stopAmmcProcesses()` stops all roles during an API stop request or server shutdown.
Configuration validation rejects:
- ports outside `1..65535`
- duplicate ports among enabled channels
- an enabled role without its decoder host when AMMC is started
- missing AMMC executable
Changing backend AMMC process code requires `npm restart` or `npm.cmd restart`. Editing browser modules alone is handled by app-version polling, but a server process cannot hot-reload its own implementation.
## 8.4 Public overlay flow
```mermaid
@@ -1309,9 +1399,11 @@ Start and finish lifecycle announcements are initiated from `timing_views.js`, w
- `normalizeAmmcConfig(...)`
- validates/stabilizes AMMC config
- `buildAmmcStatus()`
- current AMMC process status object
- `startAmmcProcess()` / `stopAmmcProcess()`
- process lifecycle
- backward-compatible main status plus nested split/pit channel statuses
- `startAmmcProcesses()` / `stopAmmcProcesses()`
- lifecycle for every enabled decoder role
- `startAmmcProcess(role)` / `stopAmmcProcess(role)`
- lifecycle for one AMMC child process
- PDF export helpers below `/api/export/pdf`
#### Critical backend routes