mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(squawk): carry emergencySource + observation time into analyst records
Review on #856 (kvnloo): the analyst projection flattened a 7700 squawk and the readsb ADS-B emergency field into the same emergency/emergencyKind pair. - mapAnalystRecord (flights, military) adds emergencySource ('squawk' | 'ads-b' | null, from aircraftEmergency().source), emergencyBroadcast (new pure emergencyBroadcastPhrase: "broadcasting squawk 7700 (general emergency)" vs "ADS-B reports general emergency") and observedAtMs (the record's lastContactEpochMs: last transponder message, epoch ms). - emergencySource is a queryable analyst text field; the voice payload passes it and emergencyBroadcast through, and observedAtMs only for aircraft with a status. Tool description asks the model to narrate with the phrase. - Source-to-analyst fixture test: OpenSky 7700 vs adsb.lol emergency=general on squawk 1200 differ in provenance and timestamp; filterable by source. - Voice tool-payload pins re-derived for the additive description change. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
6c643c8113
commit
7d6f31e54d
10 files changed
+227
-4
No files matched your search
+4
-1
@@ -103,7 +103,10 @@
|
||||
Minimum-fuel and lifeguard statuses are reported but not tinted. The status
|
||||
follows the latest poll instead of sticking, so a cleared code stops
|
||||
highlighting on the next refresh. The analyst and voice query engine can
|
||||
filter by `squawk`, `emergency` and `emergencyKind`. No new source or
|
||||
filter by `squawk`, `emergency`, `emergencyKind` and `emergencySource`
|
||||
(whether the squawk or the ADS-B field named the status), and each result
|
||||
says when the transponder was last heard, so answers read "broadcasting
|
||||
squawk 7700" or "ADS-B reports general emergency". No new source or
|
||||
request: OpenSky already carries the squawk and adsb.lol carries both.
|
||||
|
||||
- Street Level: Google Street View is the second provider. On the Google 3D
|
||||
|
||||
@@ -831,7 +831,12 @@ the same tint when a billboard is first created and when a tracked contact is
|
||||
released. While a broadcasting aircraft is tracked, the Cockpit aircraft-meta
|
||||
line reads `SQUAWK 7700 · GENERAL EMERGENCY` when the code names the status,
|
||||
else `ADS-B EMERGENCY · …` or `ADS-B PRIORITY · …`, in red. Analyst records and
|
||||
the voice query fields expose `squawk`, `emergency` (flag) and `emergencyKind`.
|
||||
the voice query fields expose `squawk`, `emergency` (flag), `emergencyKind` and
|
||||
`emergencySource` (`squawk` when the Mode A code names the status, `ads-b` for
|
||||
the readsb field), plus `emergencyBroadcast` (`broadcasting squawk 7700 (general
|
||||
emergency)` vs `ADS-B reports general emergency`) and `observedAtMs`, the epoch
|
||||
ms of the last transponder message. Voice results carry `observedAtMs` only for
|
||||
aircraft with a status.
|
||||
`scripts/qa-emergency-squawk.mjs` proves the tint, HUD line and clearing.
|
||||
|
||||
## Browser live-source observations
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"src/data/infrastructure.js",
|
||||
"src/data/aircraftEmergency.js",
|
||||
"src/data/aircraftEmergency.test.mjs",
|
||||
"src/data/aircraftEmergencyAnalyst.test.mjs",
|
||||
"scripts/qa-emergency-squawk.mjs",
|
||||
"src/data/infrastructure.test.mjs",
|
||||
"src/data/localGeojsonCore.js",
|
||||
|
||||
@@ -63,6 +63,22 @@ export function aircraftEmergency({ squawk, emergency } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* How a summary may phrase the status without flattening it into an incident:
|
||||
* quote the code when the Mode A squawk itself names the status, otherwise
|
||||
* attribute it to the ADS-B emergency field.
|
||||
* @param {{kind?: string, squawk?: string|null, source?: string}|null|undefined} status
|
||||
* @returns {string|null} e.g. 'broadcasting squawk 7700 (general emergency)'
|
||||
* or 'ADS-B reports general emergency'; null when there is no status.
|
||||
*/
|
||||
export function emergencyBroadcastPhrase(status) {
|
||||
if (!status || !Object.hasOwn(KINDS, status.kind)) return null;
|
||||
const label = KINDS[status.kind].label.toLowerCase();
|
||||
return status.source === 'squawk' && status.squawk
|
||||
? `broadcasting squawk ${status.squawk} (${label})`
|
||||
: `ADS-B reports ${label}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the aircraft is broadcasting an emergency (not a priority status).
|
||||
* @param {{severity?: string}|null|undefined} status
|
||||
|
||||
@@ -3,6 +3,7 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
aircraftEmergency,
|
||||
emergencyBroadcastPhrase,
|
||||
isEmergency,
|
||||
normalizeSquawk,
|
||||
} from './aircraftEmergency.js';
|
||||
@@ -62,3 +63,22 @@ test('priority statuses are reported but are not emergencies', () => {
|
||||
assert.equal(isEmergency(aircraftEmergency({ emergency: 'minfuel' })), false);
|
||||
assert.equal(isEmergency(null), false);
|
||||
});
|
||||
|
||||
test('the broadcast phrase quotes a naming code, else attributes ADS-B', () => {
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(aircraftEmergency({ squawk: '7700' })),
|
||||
'broadcasting squawk 7700 (general emergency)',
|
||||
);
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(
|
||||
aircraftEmergency({ squawk: '1200', emergency: 'general' }),
|
||||
),
|
||||
'ADS-B reports general emergency',
|
||||
);
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(aircraftEmergency({ emergency: 'lifeguard' })),
|
||||
'ADS-B reports lifeguard (medical priority)',
|
||||
);
|
||||
assert.equal(emergencyBroadcastPhrase(null), null);
|
||||
assert.equal(emergencyBroadcastPhrase({ kind: 'reserved' }), null);
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
// src/data/aircraftEmergencyAnalyst.test.mjs
|
||||
// Source-to-analyst fixture: an OpenSky state vector squawking 7700 and an
|
||||
// adsb.lol (readsb) aircraft whose ADS-B `emergency` field says `general`
|
||||
// on an ordinary code must reach the analyst with the same kind but distinct
|
||||
// provenance, so a summary never flattens one into the other.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openSkySnapshot, readsbSnapshot } from '../sources/live/index.js';
|
||||
import { FlightRecords } from '../layers/flights/records.js';
|
||||
import { MilitaryFlightRecords } from '../layers/military/records.js';
|
||||
import { mapAnalystRecord as mapFlight } from './flights.js';
|
||||
import { mapAnalystRecord as mapMilitary } from './militaryFlights.js';
|
||||
import { createAnalystEngine } from './analystEngine.js';
|
||||
|
||||
const NOW_MS = 1800000000000;
|
||||
|
||||
// OpenSky /states/all: [icao24, callsign, origin_country, time_position,
|
||||
// last_contact, lon, lat, baro_alt, on_ground, velocity, true_track,
|
||||
// vertical_rate, sensors, geo_alt, squawk, spi, position_source, category].
|
||||
const OPENSKY = {
|
||||
time: NOW_MS / 1000,
|
||||
states: [
|
||||
[
|
||||
'a1b2c3',
|
||||
'SWA696 ',
|
||||
'United States',
|
||||
NOW_MS / 1000 - 6,
|
||||
NOW_MS / 1000 - 3,
|
||||
-97.67,
|
||||
30.19,
|
||||
3000,
|
||||
false,
|
||||
150,
|
||||
180,
|
||||
-5,
|
||||
null,
|
||||
3040,
|
||||
'7700',
|
||||
false,
|
||||
0,
|
||||
4,
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
// adsb.lol /v2/mil: readsb aircraft with `seen`/`seen_pos` ages in seconds.
|
||||
const ADSB_LOL = {
|
||||
ac: [
|
||||
{
|
||||
hex: 'ae01ce',
|
||||
flight: 'RCH123 ',
|
||||
t: 'C17',
|
||||
lat: 31.1,
|
||||
lon: -97.4,
|
||||
alt_baro: 18000,
|
||||
gs: 300,
|
||||
track: 90,
|
||||
squawk: '1200',
|
||||
emergency: 'general',
|
||||
seen: 1.5,
|
||||
seen_pos: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function civilAnalystRecord() {
|
||||
const [row] = openSkySnapshot(OPENSKY, { now: NOW_MS }).records;
|
||||
const store = new FlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (altitude) => altitude,
|
||||
approxDistanceKm: () => 0,
|
||||
});
|
||||
const { meta } = store.receive(row, {
|
||||
viewerLatDeg: null,
|
||||
viewerLonDeg: null,
|
||||
trackedId: null,
|
||||
floorWarmPoints: [],
|
||||
});
|
||||
return mapFlight(row.id, meta);
|
||||
}
|
||||
|
||||
function militaryAnalystRecord() {
|
||||
const [row] = readsbSnapshot(ADSB_LOL, {
|
||||
observedAtMs: NOW_MS,
|
||||
now: NOW_MS,
|
||||
}).records;
|
||||
const store = new MilitaryFlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (altitude) => altitude,
|
||||
});
|
||||
const { meta } = store.receive(row, {
|
||||
observedAtMs: NOW_MS,
|
||||
floorWarmPoints: [],
|
||||
modelOwnsVisual: false,
|
||||
});
|
||||
return mapMilitary(row.id, meta);
|
||||
}
|
||||
|
||||
test('OpenSky 7700 and adsb.lol emergency=general differ only in provenance', () => {
|
||||
const civil = civilAnalystRecord();
|
||||
const military = militaryAnalystRecord();
|
||||
|
||||
assert.equal(civil.emergency, true);
|
||||
assert.equal(civil.emergencyKind, 'general');
|
||||
assert.equal(civil.squawk, '7700');
|
||||
assert.equal(civil.emergencySource, 'squawk');
|
||||
assert.equal(
|
||||
civil.emergencyBroadcast,
|
||||
'broadcasting squawk 7700 (general emergency)',
|
||||
);
|
||||
assert.equal(civil.observedAtMs, NOW_MS - 3000, 'OpenSky last_contact');
|
||||
|
||||
assert.equal(military.emergency, true);
|
||||
assert.equal(military.emergencyKind, 'general');
|
||||
assert.equal(military.squawk, '1200', 'the code is not an emergency code');
|
||||
assert.equal(military.emergencySource, 'ads-b');
|
||||
assert.equal(military.emergencyBroadcast, 'ADS-B reports general emergency');
|
||||
assert.equal(military.observedAtMs, NOW_MS - 1500, 'snapshot time - seen');
|
||||
});
|
||||
|
||||
test('the analyst can filter emergencies by source', async () => {
|
||||
const records = {
|
||||
flights: [civilAnalystRecord()],
|
||||
military: [militaryAnalystRecord()],
|
||||
};
|
||||
const engine = createAnalystEngine({
|
||||
getRecords: (key) => records[key] || [],
|
||||
resolveRegionRing: async () => null,
|
||||
getViewContext: () => ({ lat: 30.5, lon: -97.5, viewRadiusKm: 500 }),
|
||||
});
|
||||
const query = (value) =>
|
||||
engine.query({
|
||||
layers: ['flights', 'military'],
|
||||
scope: { kind: 'anywhere' },
|
||||
filters: [
|
||||
{ field: 'emergency', op: 'eq', value: true },
|
||||
{ field: 'emergencySource', op: 'eq', value },
|
||||
],
|
||||
});
|
||||
const bySquawk = await query('squawk');
|
||||
assert.deepEqual(
|
||||
bySquawk.items.map((item) => [item.icao24, item.emergencyBroadcast]),
|
||||
[['a1b2c3', 'broadcasting squawk 7700 (general emergency)']],
|
||||
);
|
||||
const byAdsb = await query('ads-b');
|
||||
assert.deepEqual(
|
||||
byAdsb.items.map((item) => [item.icao24, item.emergencyBroadcast]),
|
||||
[['ae01ce', 'ADS-B reports general emergency']],
|
||||
);
|
||||
});
|
||||
@@ -74,6 +74,9 @@ test('flights analyst record: full record maps every contract field', () => {
|
||||
squawk: null,
|
||||
emergency: false,
|
||||
emergencyKind: null,
|
||||
emergencySource: null,
|
||||
emergencyBroadcast: null,
|
||||
observedAtMs: null,
|
||||
routeOrigin: 'AUS',
|
||||
routeDestination: 'LAX',
|
||||
});
|
||||
|
||||
@@ -70,6 +70,9 @@ test('military analyst record: full record maps every contract field', () => {
|
||||
squawk: null,
|
||||
emergency: false,
|
||||
emergencyKind: null,
|
||||
emergencySource: null,
|
||||
emergencyBroadcast: null,
|
||||
observedAtMs: null,
|
||||
routeOrigin: null,
|
||||
routeDestination: null,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
FOCUS_EVIDENCE_DEV,
|
||||
FLEET_DR_INTERVAL_MS,
|
||||
} from './policy.js';
|
||||
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
|
||||
|
||||
export function createQueries({
|
||||
flightState,
|
||||
@@ -223,7 +224,8 @@ export function createQueries({
|
||||
* heading: number|null, verticalRateMps: number|null, onGround: boolean,
|
||||
* military: boolean, aircraftClass: string|null, originCountry: string|null,
|
||||
* operator: string|null, squawk: string|null, emergency: boolean,
|
||||
* emergencyKind: string|null, routeOrigin: string|null,
|
||||
* emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null,
|
||||
* emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: string|null,
|
||||
* routeDestination: string|null}}
|
||||
*/
|
||||
|
||||
@@ -264,6 +266,14 @@ export function createQueries({
|
||||
// without the flag.
|
||||
emergency: info?.emergency?.severity === 'emergency',
|
||||
emergencyKind: text(info?.emergency?.kind),
|
||||
// Provenance, so a summary can say "broadcasting squawk 7700" rather
|
||||
// than "ADS-B reports general emergency" (or flatten both): 'squawk'
|
||||
// when the Mode A code names the status, 'ads-b' for the readsb field.
|
||||
emergencySource: text(info?.emergency?.source),
|
||||
emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency),
|
||||
// Epoch ms of the last transponder message — when this squawk/status
|
||||
// was heard (OpenSky time_contact; readsb snapshot time minus `seen`).
|
||||
observedAtMs: num(info?.lastContactEpochMs),
|
||||
routeOrigin: routeOk ? text(info?.route?.origin?.code) : null,
|
||||
routeDestination: routeOk ? text(info?.route?.destination?.code) : null,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CONTACT_MATCH_TIER,
|
||||
} from '../../data/contactMatch.js';
|
||||
import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js';
|
||||
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
|
||||
|
||||
export function createQueries({
|
||||
flightState,
|
||||
@@ -160,7 +161,8 @@ export function createQueries({
|
||||
* heading: number|null, verticalRateMps: number|null, onGround: boolean,
|
||||
* military: boolean, aircraftClass: string|null, originCountry: null,
|
||||
* operator: string|null, squawk: string|null, emergency: boolean,
|
||||
* emergencyKind: string|null, routeOrigin: null, routeDestination: null}}
|
||||
* emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null,
|
||||
* emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: null, routeDestination: null}}
|
||||
*/
|
||||
|
||||
function mapAnalystRecord(icao24, info) {
|
||||
@@ -197,6 +199,14 @@ export function createQueries({
|
||||
// without the flag.
|
||||
emergency: info?.emergency?.severity === 'emergency',
|
||||
emergencyKind: text(info?.emergency?.kind),
|
||||
// Provenance, so a summary can say "broadcasting squawk 7700" rather
|
||||
// than "ADS-B reports general emergency" (or flatten both): 'squawk'
|
||||
// when the Mode A code names the status, 'ads-b' for the readsb field.
|
||||
emergencySource: text(info?.emergency?.source),
|
||||
emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency),
|
||||
// Epoch ms of the last transponder message — when this squawk/status
|
||||
// was heard (OpenSky time_contact; readsb snapshot time minus `seen`).
|
||||
observedAtMs: num(info?.lastContactEpochMs),
|
||||
routeOrigin: null,
|
||||
routeDestination: null,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user