diff --git a/changelog.d/1436-military-geom-vertical-rate.md b/changelog.d/1436-military-geom-vertical-rate.md new file mode 100644 index 000000000..ddeb5b4aa --- /dev/null +++ b/changelog.d/1436-military-geom-vertical-rate.md @@ -0,0 +1,3 @@ +- Military aircraft whose transponders report a GNSS vertical rate now carry it + in analyst and tracked-aircraft data instead of null (Aditya, @Rohilalala, + #1436). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6dbed2900..030138906 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1138,7 +1138,8 @@ The tracked-aircraft descriptor both flight layers hand to the cockpit carries `verticalRate` and military records as `verticalRateMps`; the query seam maps both to one name, so `resolveTrackedAircraftInfo` returns a single shape for either layer. The rate is reported, never derived: a blank poll keeps the last transmitted value and an -absent rate stays null rather than reading as level flight. +absent rate stays null rather than reading as level flight. A military readsb row +without `baro_rate` reports its GNSS rate as `geom_rate`, which is used instead. ## Transponder emergency highlighting diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 1af1f6b47..9da4f7a5f 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -48,7 +48,8 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) { const baroFt = finite(row.alt_baro), geoFt = finite(row.alt_geom); const speedKts = finite(row.gs), - rateFtMin = finite(row.baro_rate); + // readsb sends a GNSS-sourced rate as geom_rate instead of baro_rate. + rateFtMin = finite(row.baro_rate) ?? finite(row.geom_rate); const seen = finite(row.seen), seenPos = finite(row.seen_pos); return { diff --git a/src/sources/live/contract.test.mjs b/src/sources/live/contract.test.mjs index b8b1787da..79626f6b3 100644 --- a/src/sources/live/contract.test.mjs +++ b/src/sources/live/contract.test.mjs @@ -145,6 +145,20 @@ test('readsb units and position/contact ages are normalized once against the sou ); }); +test('readsb vertical rate falls back to the GNSS rate when no baro rate is sent', () => { + // readsb reports the rate under geom_rate when the aircraft's source is + // GNSS; 26 of 157 positioned rows in a live /v2/mil fetch had only that. + const row = { hex: 'ae638d', lat: 20.3, lon: -158.5, geom_rate: -2432 }; + assert.equal( + normalizeReadsbAircraft(row, now).verticalRateMps, + -2432 * 0.00508, + ); + assert.equal( + normalizeReadsbAircraft({ ...row, baro_rate: -2400 }, now).verticalRateMps, + -2400 * 0.00508, + ); +}); + test('vessel identity, opaque reference and heading/course remain distinct, unknowns stay null', () => { const row = normalizeVesselObservation( {