From c7377362a5cfb8422e1ff5fa7cad403a445b661b Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:18:30 +0530 Subject: [PATCH 1/3] fix(local-adsb): show a decoder feed's GNSS-sourced vertical rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dump1090-fa and readsb file an aircraft's vertical rate under geom_rate instead of baro_rate when its source is GNSS. The decoder-feed adapter read only baro_rate, so those aircraft showed "V/S —" on the Local ADS-B card. It now falls back to geom_rate; a sent baro_rate still wins. --- docs/CURRENT-STATE.md | 4 +++- src/sources/adsbRecords.js | 4 +++- src/sources/adsbRecords.test.mjs | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c55031a6a..1d2825fcd 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -6134,7 +6134,9 @@ Receivers publish one record shape from `src/sources/adsbRecords.js` `lastMessageAt`, `messageCount`, `rssiDbfs`, `band`, `source`). `band` is `1090` or `978`; `source` is `webusb` (browser SDR) or `feed` (decoder feed). `normalizeDump1090Aircraft(json, nowMs, { band })` maps a dump1090/readsb/ -skyaware978 `aircraft.json` document into the same records. Voice +skyaware978 `aircraft.json` document into the same records; a row without +`baro_rate` reads its vertical rate from `geom_rate`, where a GNSS-sourced rate +is sent. Voice `set_layer_visibility` accepts `local-adsb` ("local ADS-B", "my receiver", "my antenna"). diff --git a/src/sources/adsbRecords.js b/src/sources/adsbRecords.js index 814da60b2..465321902 100644 --- a/src/sources/adsbRecords.js +++ b/src/sources/adsbRecords.js @@ -247,7 +247,9 @@ export function normalizeDump1090Aircraft(json, nowMs, { band = '1090' } = {}) { altitudeFt: altitude, groundSpeedKt: finiteOrNull(entry.gs), trackDeg: normalizeTrack(entry.track), - verticalRateFpm: finiteOrNull(entry.baro_rate), + // A GNSS-sourced rate is sent as geom_rate instead of baro_rate. + verticalRateFpm: + finiteOrNull(entry.baro_rate) ?? finiteOrNull(entry.geom_rate), lastPositionAt: hasPosition ? receivedAt - Math.max(0, seenPosS) * 1000 : null, diff --git a/src/sources/adsbRecords.test.mjs b/src/sources/adsbRecords.test.mjs index 12070d47b..aa14133b6 100644 --- a/src/sources/adsbRecords.test.mjs +++ b/src/sources/adsbRecords.test.mjs @@ -169,6 +169,20 @@ test('a feed document names its band on every record', () => { assert.ok(uat.every((record) => record.source === 'feed')); }); +test('a decoder feed reads a GNSS-sourced vertical rate when no baro rate is sent', () => { + // dump1090-fa and readsb file a velocity message's rate under geom_rate + // instead of baro_rate when its source bit says GNSS. + const rate = (fields) => + normalizeDump1090Aircraft( + { aircraft: [{ hex: 'abc123', seen: 1, ...fields }] }, + 10_000, + )[0].verticalRateFpm; + assert.equal(rate({ geom_rate: -832 }), -832); + assert.equal(rate({ baro_rate: -768, geom_rate: -832 }), -768); + assert.equal(rate({ baro_rate: 0, geom_rate: -832 }), 0); + assert.equal(rate({}), null); +}); + function heard(overrides = {}) { return { icao: 'abc123', From 58e9f5ab2e5f5a7a1fd0c7174722e3391298b4aa Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:24:57 +0530 Subject: [PATCH 2/3] docs(changelog): add #1448 fragment --- changelog.d/1448-local-adsb-geom-rate.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1448-local-adsb-geom-rate.md diff --git a/changelog.d/1448-local-adsb-geom-rate.md b/changelog.d/1448-local-adsb-geom-rate.md new file mode 100644 index 000000000..276e58f3e --- /dev/null +++ b/changelog.d/1448-local-adsb-geom-rate.md @@ -0,0 +1,3 @@ +- Local ADS-B decoder feeds now carry a GNSS-sourced vertical rate + (`geom_rate`) when no barometric rate is sent, so the card shows V/S + instead of a dash (Aditya, @Rohilalala, #1448). From d479c1c6f66d6407436fd40e531e0386287036de Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 22:58:52 -0800 Subject: [PATCH 3/3] Read an ADS-B vertical rate from one shared field reader --- changelog.d/1448-local-adsb-geom-rate.md | 3 +++ scripts/format-scope.json | 3 ++- scripts/package-boundaries.json | 7 ++++- src/data/adsbLolFallback.js | 5 ++-- src/sources/adsbFields.js | 16 ++++++++++++ src/sources/adsbFields.test.mjs | 33 ++++++++++++++++++++++++ src/sources/adsbRecords.js | 6 ++--- src/sources/live/aircraft.js | 4 +-- 8 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/sources/adsbFields.js create mode 100644 src/sources/adsbFields.test.mjs diff --git a/changelog.d/1448-local-adsb-geom-rate.md b/changelog.d/1448-local-adsb-geom-rate.md index 276e58f3e..a4a0c54f8 100644 --- a/changelog.d/1448-local-adsb-geom-rate.md +++ b/changelog.d/1448-local-adsb-geom-rate.md @@ -1,3 +1,6 @@ - Local ADS-B decoder feeds now carry a GNSS-sourced vertical rate (`geom_rate`) when no barometric rate is sent, so the card shows V/S instead of a dash (Aditya, @Rohilalala, #1448). +- Local ADS-B, military and the adsb.lol flight fallback read the vertical + rate through one shared readsb field reader, so all three prefer the + barometric rate and fall back to the GNSS rate the same way (#1448). diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 0cd9edca5..11134114f 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1183,5 +1183,6 @@ "src/firstRunLauncherView.test.mjs", "src/firstRunCopy.js", "src/data/headingDegrees.test.mjs", - "src/data/distanceFormat.test.mjs" + "src/data/distanceFormat.test.mjs", + "src/sources/adsbFields.test.mjs" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 96057df6c..303107647 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -179,6 +179,7 @@ "runtime": "node", "exports": ["./server/providers/live"], "modules": [ + "src/sources/adsbFields.js", "server/providers/live.js", "server/providers/common/http.js", "server/providers/common/query.js", @@ -198,7 +199,7 @@ }, "adsb-lol-source": { "exports": ["./sources/adsb-lol"], - "modules": ["src/data/adsbLolFallback.js"], + "modules": ["src/data/adsbLolFallback.js", "src/sources/adsbFields.js"], "external": [] }, "place-providers": { @@ -1247,6 +1248,7 @@ "live-observations": { "exports": ["./sources/live"], "modules": [ + "src/sources/adsbFields.js", "src/i18n/marker.js", "src/data/aircraftEmergency.js", "src/sources/live/aircraft.js", @@ -2203,6 +2205,7 @@ "./standalone/catalog" ], "modules": [ + "src/sources/adsbFields.js", "src/data/headingDegrees.js", "src/data/distanceFormat.js", "src/ui/placeNames.js", @@ -3179,6 +3182,7 @@ "application-layer-construction": { "exports": ["./application/layers"], "modules": [ + "src/sources/adsbFields.js", "src/data/headingDegrees.js", "src/data/distanceFormat.js", "src/cyberSonar.js", @@ -4721,6 +4725,7 @@ "tools-services": { "exports": ["./tools/services"], "modules": [ + "src/sources/adsbFields.js", "src/tools/services.js", "src/layers/bikeshare/registry.js", "src/layers/bikeshare/source.js", diff --git a/src/data/adsbLolFallback.js b/src/data/adsbLolFallback.js index 40dd2bbc0..61d07387f 100644 --- a/src/data/adsbLolFallback.js +++ b/src/data/adsbLolFallback.js @@ -1,3 +1,5 @@ +import { readAdsbVerticalRateFpm } from '../sources/adsbFields.js'; + const KNOT_TO_MPS = 0.514444; const FOOT_TO_M = 0.3048; const FPM_TO_MPS = 0.00508; @@ -61,8 +63,7 @@ export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) { const barometricFeet = onGround ? null : finiteNumber(aircraft?.alt_baro); const geometricFeet = finiteNumber(aircraft?.alt_geom); const groundSpeedKnots = finiteNumber(aircraft?.gs); - const verticalRateFpm = - finiteNumber(aircraft?.baro_rate) ?? finiteNumber(aircraft?.geom_rate); + const verticalRateFpm = readAdsbVerticalRateFpm(aircraft); const track = finiteNumber(aircraft?.track); return [ diff --git a/src/sources/adsbFields.js b/src/sources/adsbFields.js new file mode 100644 index 000000000..4d4822e2a --- /dev/null +++ b/src/sources/adsbFields.js @@ -0,0 +1,16 @@ +/** A readsb rate field: a finite JSON number, else null. */ +function rateField(value) { + return typeof value === 'number' && Number.isFinite(value) ? value : null; +} + +/** + * Vertical rate in ft/min from a readsb-style aircraft row (readsb, tar1090, + * adsb.lol): the barometric `baro_rate`, else the GNSS-sourced `geom_rate` + * readsb sends instead when it has no barometric rate. A barometric 0 is kept. + * Only a finite number counts; null when neither field has one. + * @param {object|null|undefined} row + * @returns {number|null} + */ +export function readAdsbVerticalRateFpm(row) { + return rateField(row?.baro_rate) ?? rateField(row?.geom_rate); +} diff --git a/src/sources/adsbFields.test.mjs b/src/sources/adsbFields.test.mjs new file mode 100644 index 000000000..68fc52f79 --- /dev/null +++ b/src/sources/adsbFields.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { readAdsbVerticalRateFpm } from './adsbFields.js'; + +test('the barometric rate wins, a barometric 0 included', () => { + assert.equal( + readAdsbVerticalRateFpm({ baro_rate: -640, geom_rate: -832 }), + -640, + ); + assert.equal(readAdsbVerticalRateFpm({ baro_rate: 0, geom_rate: 600 }), 0); +}); + +test('a GNSS rate stands in when no barometric rate is usable', () => { + for (const baro_rate of [ + undefined, + null, + Number.NaN, + Infinity, + '300', + false, + ]) { + assert.equal(readAdsbVerticalRateFpm({ baro_rate, geom_rate: -832 }), -832); + } +}); + +test('no usable rate reads as null', () => { + assert.equal(readAdsbVerticalRateFpm({}), null); + assert.equal(readAdsbVerticalRateFpm(null), null); + assert.equal( + readAdsbVerticalRateFpm({ baro_rate: '300', geom_rate: true }), + null, + ); +}); diff --git a/src/sources/adsbRecords.js b/src/sources/adsbRecords.js index 465321902..afa952503 100644 --- a/src/sources/adsbRecords.js +++ b/src/sources/adsbRecords.js @@ -30,6 +30,8 @@ * This module is portable: no Cesium, DOM, network or storage access. */ +import { readAdsbVerticalRateFpm } from './adsbFields.js'; + /** A marker is dropped once its newest position is this old. */ export const LOCAL_ADSB_POSITION_STALE_MS = 60_000; /** An aircraft is forgotten once no message has arrived for this long. */ @@ -247,9 +249,7 @@ export function normalizeDump1090Aircraft(json, nowMs, { band = '1090' } = {}) { altitudeFt: altitude, groundSpeedKt: finiteOrNull(entry.gs), trackDeg: normalizeTrack(entry.track), - // A GNSS-sourced rate is sent as geom_rate instead of baro_rate. - verticalRateFpm: - finiteOrNull(entry.baro_rate) ?? finiteOrNull(entry.geom_rate), + verticalRateFpm: readAdsbVerticalRateFpm(entry), lastPositionAt: hasPosition ? receivedAt - Math.max(0, seenPosS) * 1000 : null, diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 9da4f7a5f..19472a92a 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -6,6 +6,7 @@ import { epoch, finite, } from './contract.js'; +import { readAdsbVerticalRateFpm } from '../adsbFields.js'; const openSkyId = (row) => Array.isArray(row) ? cleanText(row[0]).toLowerCase() : ''; @@ -48,8 +49,7 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) { const baroFt = finite(row.alt_baro), geoFt = finite(row.alt_geom); const speedKts = finite(row.gs), - // readsb sends a GNSS-sourced rate as geom_rate instead of baro_rate. - rateFtMin = finite(row.baro_rate) ?? finite(row.geom_rate); + rateFtMin = readAdsbVerticalRateFpm(row); const seen = finite(row.seen), seenPos = finite(row.seen_pos); return {