|
| 1 | +# EFP2 Demo — Bout Lifecycle |
| 2 | + |
| 3 | +Demonstrates a **Server** (competition software) and a **Master** (scoring machine) communicating over real UDP packets on loopback, using the full EFP2 protocol stack built in this repository. |
| 4 | + |
| 5 | +## Run it |
| 6 | + |
| 7 | +``` |
| 8 | +npm run demo |
| 9 | +``` |
| 10 | + |
| 11 | +This bundles `demo/run.js` with esbuild and executes it. No external dependencies, no separate processes — both actors run in the same Node.js process on two UDP sockets. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Network topology |
| 16 | + |
| 17 | +``` |
| 18 | +Server 127.0.0.1:50100 Competition software — manages bouts, accepts results |
| 19 | +Master 127.0.0.1:50101 Scoring machine on piste RED |
| 20 | +``` |
| 21 | + |
| 22 | +This mirrors the real EFP2 topology defined in the spec: |
| 23 | + |
| 24 | +| Node | IP | Port | |
| 25 | +|------|----|------| |
| 26 | +| Server | 172.20.0.1 | 50100 (from Masters) | |
| 27 | +| Master (piste x) | 172.20.x.1 | 50101 (from Additional devices) | |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Bout fixture |
| 32 | + |
| 33 | +The demo uses real competition data from the protocol examples: |
| 34 | + |
| 35 | +| Field | Value | |
| 36 | +|-------|-------| |
| 37 | +| Piste | `RED` | |
| 38 | +| Event ID | `24` | |
| 39 | +| Competition | `EIM` (Épée Individual Men) | |
| 40 | +| Phase | `T32` (Table of 32) | |
| 41 | +| Bout ID | `32` | |
| 42 | +| Right fencer | IVANOV Sidor — `RUS` — ID `33` | |
| 43 | +| Left fencer | LIMON Jua — `FRA` — ID `531` | |
| 44 | +| Final score | **5 – 3** (right wins) | |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Lifecycle walkthrough |
| 49 | + |
| 50 | +### 1. Handshake |
| 51 | + |
| 52 | +The Master comes online and announces itself with `HELLO`. The Server receives it and responds by assigning a bout with `DISP`. |
| 53 | + |
| 54 | +``` |
| 55 | +MASTER → HELLO pisteCode=RED |
| 56 | + |EFP2|HELLO|RED| |
| 57 | +
|
| 58 | +SERVER → DISP RED — IVANOV Sidor (RUS) vs LIMON Jua (FRA) |
| 59 | + |EFP2|DISP|RED|24|EIM|T32|1|32|14:45|3:00|33|IVANOV Sidor|RUS|||531|LIMON Jua|FRA||| |
| 60 | +``` |
| 61 | + |
| 62 | +### 2. Not active |
| 63 | + |
| 64 | +On receiving `DISP` the Master transitions to **Not active** and sends an `INFO` confirming that state, with score 0–0. |
| 65 | + |
| 66 | +``` |
| 67 | +MASTER → INFO state=Not active score=0–0 |
| 68 | + |EFP2|INFO|RED|24|EIM|T32|1|1|32|14:45|3:00|A|0|N|0|0|0|0|0|0| |
| 69 | + 33|IVANOV Sidor|RUS|||0|0|0|0|0|0|0| |
| 70 | + 531|LIMON Jua|FRA|||0|0|0|0|0|0|0| |
| 71 | +``` |
| 72 | + |
| 73 | +The `INFO` state field encodes the Master's current state. State codes used in this demo: |
| 74 | + |
| 75 | +| Code | State | |
| 76 | +|------|-------| |
| 77 | +| `A` | Not active — bout assigned, waiting for referee to start | |
| 78 | +| `F` | Fencing — bout in progress | |
| 79 | +| `E` | Ending — referee deactivated, awaiting `ACK` or `NAK` | |
| 80 | + |
| 81 | +### 3. Fencing |
| 82 | + |
| 83 | +The referee starts the bout. The Master sends an `INFO` for each touch, advancing the score. The lamp fields (`1`/`0`) indicate which fencer triggered the scoring. |
| 84 | + |
| 85 | +``` |
| 86 | +MASTER → INFO state=Fencing score=1–0 (right scores) |
| 87 | +MASTER → INFO state=Fencing score=1–1 (left scores) |
| 88 | +MASTER → INFO state=Fencing score=2–1 |
| 89 | +MASTER → INFO state=Fencing score=3–1 |
| 90 | +MASTER → INFO state=Fencing score=3–2 |
| 91 | +MASTER → INFO state=Fencing score=4–2 |
| 92 | +MASTER → INFO state=Fencing score=4–3 |
| 93 | +MASTER → INFO state=Fencing score=5–3 (right wins — final touch) |
| 94 | +``` |
| 95 | + |
| 96 | +Each wire message carries the full bout context. For example, the final touch: |
| 97 | + |
| 98 | +``` |
| 99 | +|EFP2|INFO|RED|24|EIM|T32|1|1|32|14:45|3:00|F|0|N|0|0|0|0|0|0| |
| 100 | +33|IVANOV Sidor|RUS|||5|0|0|0|0|1|0| |
| 101 | +531|LIMON Jua|FRA|||3|0|0|0|0|0|0| |
| 102 | +``` |
| 103 | + |
| 104 | +### 4. Ending |
| 105 | + |
| 106 | +The referee deactivates the bout. The Master enters the **Ending** state and broadcasts `INFO` once per second for up to 4 seconds, waiting for an `ACK` or `NAK` from the Server. |
| 107 | + |
| 108 | +``` |
| 109 | +MASTER → INFO state=Ending score=5–3 (1/4) |
| 110 | + |EFP2|INFO|RED|24|EIM|T32|1|1|32|14:45|3:00|E|...|5|...|3|...| |
| 111 | +``` |
| 112 | + |
| 113 | +### 5. ACK — result accepted |
| 114 | + |
| 115 | +The Server receives the `Ending` INFO and sends `ACK` to accept the result. The Master clears its ending timer and transitions back to **Not active**. |
| 116 | + |
| 117 | +``` |
| 118 | +SERVER → ACK |
| 119 | + |EFP2|ACK| |
| 120 | +
|
| 121 | +MASTER → INFO state=Not active score=5–3 (bout complete) |
| 122 | +``` |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## Timeout path (NAK / no response) |
| 127 | + |
| 128 | +If the Server sends `NAK`, or if no `ACK`/`NAK` arrives within 4 seconds (4 Ending broadcasts), the Master gives up and would transition to **Waiting** state. The demo logs this path: |
| 129 | + |
| 130 | +``` |
| 131 | +MASTER → INFO state=Ending score=5–3 (4/4) |
| 132 | + timeout — no ACK after 4 s — would enter Waiting state |
| 133 | +``` |
| 134 | + |
| 135 | +To trigger it, comment out the `ACK` send in `demo/run.js` inside the `'E'` state handler. |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +## How it uses the protocol stack |
| 140 | + |
| 141 | +Every message in the demo flows through the same code used in production: |
| 142 | + |
| 143 | +| Direction | Function | File | |
| 144 | +|-----------|----------|------| |
| 145 | +| Build outgoing wire bytes | `compose(commandObject)` | `src/protocol/cyrano.js` | |
| 146 | +| Parse incoming wire bytes | `process(rawMessage)` | `src/protocol/cyrano.js` | |
| 147 | +| Tokenise raw string | `tokenize(raw)` | `src/protocol/cylex.js` | |
| 148 | +| Per-command serialise | `build(obj)` | `src/commands/*.js` | |
| 149 | +| Per-command deserialise | `parse(tokens)` | `src/commands/*.js` | |
| 150 | + |
| 151 | +The demo does not reach into command files directly — it only calls `compose()` and `process()` at the protocol layer, the same API any real integration would use. |
0 commit comments