Read an ADS-B vertical rate from one shared field reader

This commit is contained in:
Sameh Khamis committed 2026-11-02 00:09:18 -08:00
1 parent 780d8903ee
commit d479c1c6f6
8 files changed
+68 -9

No files matched your search

+3
View File
@@ -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).
+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 -3
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,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,
+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 {