Merge pull request #1448 from Rohilalala/fix/e-local-adsb-geom-rate

fix(local-adsb): show a decoder feed's GNSS-sourced vertical rate
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-02 00:18:11 -08:00
commit 151ce3dee7
10 files changed
+88 -8

No files matched your search

+6
View File
@@ -0,0 +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).
+3 -1
View File
@@ -6322,7 +6322,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").
+2 -1
View File
@@ -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"
]
+6 -1
View File
@@ -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",
+3 -2
View File
@@ -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 [
+16
View File
@@ -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);
}
+33
View File
@@ -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,
);
});
+3 -1
View File
@@ -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,7 +249,7 @@ export function normalizeDump1090Aircraft(json, nowMs, { band = '1090' } = {}) {
altitudeFt: altitude,
groundSpeedKt: finiteOrNull(entry.gs),
trackDeg: normalizeTrack(entry.track),
verticalRateFpm: finiteOrNull(entry.baro_rate),
verticalRateFpm: readAdsbVerticalRateFpm(entry),
lastPositionAt: hasPosition
? receivedAt - Math.max(0, seenPosS) * 1000
: null,
+14
View File
@@ -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',
+2 -2
View File
@@ -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 {