--- name: bds-port description: Port a Mode S Comm-B BDS register decoder from pyModeS into FlightJar.Decoder.ModeS.CommB, wire it into the aircraft registry + snapshot + detail panel, and verify against pyModeS golden vectors. Use when the user asks to add a new BDS register (e.g. "add BDS 4,5 hazard data", "decode the hazard / windshear register", "show pilot-entered MET data"). The four heuristic registers 4,0 / 4,4 / 5,0 / 6,0 are already implemented; remaining candidates are BDS 4,5 (meteorological hazard — opt-in, noisy), and the format-ID registers BDS 1,0 / 1,7 / 2,0 / 3,0 (data link capability, GICB capability report, aircraft identification, ACAS active resolution). --- # Porting a pyModeS BDS register to FlightJar FlightJar's Comm-B decoder matches pyModeS 3.x byte-for-byte. When adding another register, keep the wire behaviour identical so we can cross-check golden vectors from pyModeS's own test corpus. Every deviation (even a "cleanup") is a source of silent drift later. ## Touchpoints A full port spans six files and one frontend + one docs update: 1. **`dotnet/src/FlightJar.Decoder/ModeS/CommB.cs`** — add `IsBdsXX(payload)` validator + `DecodeBdsXX(payload)` decoder + `BdsXXData` record. Extend the `Candidates` record and `Infer()` to include the new register. 2. **`dotnet/src/FlightJar.Decoder/ModeS/DecodedMessage.cs`** — add one field per value the register exposes. 3. **`dotnet/src/FlightJar.Decoder/ModeS/MessageDecoder.cs`** — add a branch in `InferCommB` that builds a `DecodedMessage` for the new register; extend `Merge()` to copy the new fields. 4. **`dotnet/src/FlightJar.Core/State/Aircraft.cs`** — add per-field state + one `BdsXXAt` timestamp. 5. **`dotnet/src/FlightJar.Core/State/AircraftRegistry.cs`** — add a `case "X,Y":` branch in `ApplyCommB` that writes the fields + stamps the timestamp. Extend `BuildCommBSnapshot` to gate the fields on freshness and feed the `SnapshotCommB` record. 6. **`dotnet/src/FlightJar.Core/State/RegistrySnapshot.cs`** — add the fields to `SnapshotCommB` (nullable, snake-case on the wire). 7. **`app/static/detail_panel.js`** — add metric tiles to the `.panel-met-grid` placeholder markup in `buildPopupContent` and a `set('.pop-met-xxx', …)` call per field in `renderCommBSection`. 8. **`dotnet/tests/FlightJar.Decoder.Tests/ModeS/CommBTests.cs`** — validator accept/reject tests + golden-vector decoder tests using hex captures from pyModeS's `tests/test_bds_commb.py`. 9. **`README.md`** — add the new fields to the "Enhanced Mode S air data" bullet and flag any register-specific caveats. 10. **`CLAUDE.md`** — update the decoder list + any behavioural nuances. ## Step 1 — Fetch the pyModeS reference pyModeS lives at `junzis/pyModeS` on GitHub (default branch `main`). BDS decoders live under `src/pyModeS/decoder/bds/bdsXX.py`, helpers under `_helpers.py`. Fetch into `/tmp/pymodes/` via the GitHub API (the raw CDN sometimes 404s; the contents API is reliable): ```bash # List available registers. curl -sL "https://api.github.com/repos/junzis/pyModeS/contents/src/pyModeS/decoder/bds?ref=main" \ | grep '"download_url"' # Fetch a specific register + the shared helpers. for f in bds45 _helpers _infer; do curl -sL "https://raw.githubusercontent.com/junzis/pyModeS/main/src/pyModeS/decoder/bds/$f.py" \ > /tmp/pymodes/$f.py done # And the golden-vector test corpus. curl -sL "https://raw.githubusercontent.com/junzis/pyModeS/main/tests/test_bds_commb.py" \ > /tmp/pymodes/test_bds_commb.py ``` ## Step 2 — Port the validator + decoder pyModeS operates on a 56-bit payload as a Python int with `(payload >> (55 - i)) & mask` indexing (MSB-first from payload bit 0). The C# port keeps **identical** bit indexing so the ported arithmetic reads one-to-one next to the Python. Add your new methods inside `public static class CommB` in `CommB.cs`. Use the existing helpers that are already in `CommB.cs`: - `WrongStatus(payload, statusBit, valueStart, valueWidth)` — mirrors pyModeS `_helpers.wrong_status` (status-bit / value-field consistency). - `Signed(value, width, sign)` — sign-magnitude to signed int (NOT two's complement; Mode S splits sign + magnitude bits). - `NormaliseAngle(deg)` — wrap into `[0, 360)`. Every range gate in `IsBdsXX` must match pyModeS's validator. If pyModeS rejects `> 600 kt` but you port it as `>= 600`, you will silently accept values pyModeS would reject. ## Step 3 — Wire into inference `CommB.Infer(payload)` returns a `Candidates` record with one bool per heuristic register. `MessageDecoder.InferCommB(msg)` returns a decoded message only when **exactly one** candidate validates. This single-match discipline is intentional: multi-match payloads are ambiguous and dropped rather than risk polluting aircraft state with fields decoded against the wrong register. Do not relax it without a replacement disambiguation strategy (e.g. pyModeS Phase 3 known-state scoring). BDS 4,5 (meteorological hazard) is opt-in in pyModeS because it false-positives on non-meteorological payloads. When porting, keep the validator strict; if ambiguity becomes a problem, add a `Candidates.Bds45` branch behind a config flag rather than unconditionally accepting it. ## Step 4 — Extend state + snapshot For every decoded field: 1. Add a nullable property to `Aircraft` (e.g. `public int? WindshearLevel { get; set; }`). 2. Add an identically-named property to `SnapshotCommB`. 3. In `ApplyCommB`, add a `case "X,Y":` that assigns from the `DecodedMessage`; **do not** touch fields from other registers (each register owns its own slice of state). 4. In `BuildCommBSnapshot`, compute a `bdsXXFresh` flag using `CommBMaxAge` (120 s) and use it to gate every field from the register. Include the register's `BdsXXAt` timestamp on the snapshot so the frontend can age values out independently. Naming convention on the wire: snake_case via the global serializer config in `FlightJar.Api.Configuration`. `MagneticHeadingDeg` becomes `magnetic_heading_deg` without any extra attributes. ## Step 5 — Extend the frontend panel The Enhanced Mode S panel is driven entirely by `a.comm_b` in the snapshot. In `detail_panel.js`: 1. Add placeholder tile markup inside `.panel-met-grid` in `buildPopupContent` — same shape as existing tiles (`