From db1155c8c9b0379c04d025c5e286db1fe9fc44cd Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Sat, 18 Jul 2026 20:48:09 +0200 Subject: [PATCH] feat(live-atc): forward aircraft position, un-throttle bridge polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NormalizedTelemetry carries lat/lon (0/0 no-GPS guard, position in the change-detection signature) so the backend can derive distance triggers - bridge polling moves from setInterval to a Worker tick: with the sim in the foreground the /live-atc tab is backgrounded and Chrome throttles plain intervals to ~1/min — telemetry-driven ATC lagged behind the silence fallback Co-Authored-By: Claude Fable 5 --- app/composables/useRadioBackend.ts | 6 ++++-- app/composables/useSimBridgeSync.ts | 11 ++++++++--- shared/utils/bridgeTelemetry.ts | 18 ++++++++++++++---- tests/shared/bridgeTelemetry.test.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/app/composables/useRadioBackend.ts b/app/composables/useRadioBackend.ts index e286933..80d0f38 100644 --- a/app/composables/useRadioBackend.ts +++ b/app/composables/useRadioBackend.ts @@ -41,8 +41,10 @@ export interface NormalizedTelemetry { vs_fpm?: number // vertical speed, feet/min (+climb, -descent) heading_deg?: number // magnetic heading, 0..360 on_ground?: boolean // wheels on the ground - distance_to_dest_nm?: number // great-circle nm to destination airport - distance_to_dep_nm?: number // great-circle nm from departure airport + // Raw position in degrees. The backend derives distance_to_dep_nm / + // distance_to_dest_nm from these via the session's airport ICAO codes. + lat?: number + lon?: number } export interface ReadbackFieldDetail { diff --git a/app/composables/useSimBridgeSync.ts b/app/composables/useSimBridgeSync.ts index 6213d9b..1e430d9 100644 --- a/app/composables/useSimBridgeSync.ts +++ b/app/composables/useSimBridgeSync.ts @@ -1,6 +1,7 @@ import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue' import { useRoute } from 'vue-router' import { normalizeSimFreq, normalizeBridgeTelemetry, telemetrySignature } from '../../shared/utils/bridgeTelemetry' +import { createIntervalWorker } from '../../shared/utils/intervalWorker' import { pmLog } from '../../shared/utils/pmLog' import type { SimControlCommandResult } from '../../shared/utils/simControl' import type { useFrequencyPresets } from '~/composables/useFrequencyPresets' @@ -60,7 +61,11 @@ export function useSimBridgeSync( // Telemetry older than this means the bridge stopped posting. const BRIDGE_TELEMETRY_STALE_MS = 12_000 const BRIDGE_POLL_INTERVAL_MS = 3_000 - let bridgePoller: ReturnType | null = null + // Worker-based ticker: the intended setup is the sim (or WebSim tab) in the + // foreground and /live-atc in the background, where a plain setInterval gets + // throttled to ~1/min — telemetry-driven ATC (airborne handoff, level checks) + // would lag by up to a minute. A Worker tick isn't throttled. + let bridgePoller: { stop: () => void } | null = null // Last sim active frequency we pushed into the radio — only re-tune active when // the sim value actually changes, so manual/flow tuning isn't constantly // overridden. Standby has no such anchor: while connected it strictly mirrors @@ -184,12 +189,12 @@ export function useSimBridgeSync( stopBridgeSync() if (!bridgeToken.value) return void pollBridgeTelemetry() - bridgePoller = setInterval(pollBridgeTelemetry, BRIDGE_POLL_INTERVAL_MS) + bridgePoller = createIntervalWorker(BRIDGE_POLL_INTERVAL_MS, () => void pollBridgeTelemetry()) } function stopBridgeSync() { if (bridgePoller) { - clearInterval(bridgePoller) + bridgePoller.stop() bridgePoller = null } } diff --git a/shared/utils/bridgeTelemetry.ts b/shared/utils/bridgeTelemetry.ts index 5bffc20..6a1320e 100644 --- a/shared/utils/bridgeTelemetry.ts +++ b/shared/utils/bridgeTelemetry.ts @@ -7,16 +7,22 @@ export function normalizeSimFreq(value: unknown): string | null { } // Map the raw bridge telemetry (SimConnect-style field names) to the -// sim-agnostic contract the backend understands. distance_to_*_nm are omitted -// until pm.vue has a reliable airport-coordinate source to compute them from; -// heading is currently true (bridge only reports true) — magnetic correction is -// needed before any localizer/heading trigger can rely on it. +// sim-agnostic contract the backend understands. The raw lat/lon position is +// forwarded so the backend can derive distance_to_dep_nm / distance_to_dest_nm +// (it has the airport coordinates via the session's ICAO codes); heading is +// currently true (bridge only reports true) — magnetic correction is needed +// before any localizer/heading trigger can rely on it. export function normalizeBridgeTelemetry(raw: any): NormalizedTelemetry | null { if (!raw || typeof raw !== 'object') return null const num = (v: unknown): number | undefined => { const n = typeof v === 'number' ? v : Number(v) return Number.isFinite(n) ? n : undefined } + const lat = num(raw.PLANE_LATITUDE) + const lon = num(raw.PLANE_LONGITUDE) + // 0/0 is the bridge's "no GPS data" default — don't forward it as a position. + const hasPosition = + lat !== undefined && lon !== undefined && (Math.abs(lat) > 0.1 || Math.abs(lon) > 0.1) return { altitude_ft: num(raw.PLANE_ALTITUDE), ias_kts: num(raw.AIRSPEED_INDICATED), @@ -24,15 +30,19 @@ export function normalizeBridgeTelemetry(raw: any): NormalizedTelemetry | null { vs_fpm: num(raw.VERTICAL_SPEED), heading_deg: num(raw.PLANE_HEADING_DEGREES_TRUE), on_ground: typeof raw.SIM_ON_GROUND === 'boolean' ? raw.SIM_ON_GROUND : undefined, + ...(hasPosition ? { lat, lon } : {}), } } // Only forward telemetry that meaningfully changed, so idle cruise doesn't POST // an identical tick every poll. Rounded so tiny jitter doesn't count as change. +// Position rounds at ~0.01° (≈0.6 nm) so a moving aircraft keeps ticking the +// backend's distance triggers without spamming while parked. export function telemetrySignature(t: NormalizedTelemetry): string { const r = (v: number | undefined, step: number) => (v === undefined ? '_' : Math.round(v / step)) return [ r(t.altitude_ft, 100), r(t.ias_kts, 5), r(t.gs_kts, 5), r(t.vs_fpm, 100), r(t.heading_deg, 5), t.on_ground ? 'G' : 'A', + r(t.lat, 0.01), r(t.lon, 0.01), ].join('|') } diff --git a/tests/shared/bridgeTelemetry.test.ts b/tests/shared/bridgeTelemetry.test.ts index 81005b0..e7d2dc0 100644 --- a/tests/shared/bridgeTelemetry.test.ts +++ b/tests/shared/bridgeTelemetry.test.ts @@ -38,6 +38,26 @@ test('normalizeBridgeTelemetry returns null for non-object input', () => { assert.equal(normalizeBridgeTelemetry(null), null) }) +test('normalizeBridgeTelemetry forwards the position', () => { + const result = normalizeBridgeTelemetry({ + PLANE_ALTITUDE: 3500, + PLANE_LATITUDE: 50.02671, + PLANE_LONGITUDE: 8.55835, + }) + assert.equal(result?.lat, 50.02671) + assert.equal(result?.lon, 8.55835) +}) + +test('normalizeBridgeTelemetry drops the 0/0 no-GPS default position', () => { + const result = normalizeBridgeTelemetry({ + PLANE_ALTITUDE: 3500, + PLANE_LATITUDE: 0, + PLANE_LONGITUDE: 0, + }) + assert.equal(result?.lat, undefined) + assert.equal(result?.lon, undefined) +}) + test('telemetrySignature is stable under sub-threshold jitter', () => { const a = telemetrySignature({ altitude_ft: 3500, ias_kts: 140, on_ground: false }) const b = telemetrySignature({ altitude_ft: 3540, ias_kts: 141, on_ground: false }) @@ -49,3 +69,9 @@ test('telemetrySignature changes on a meaningful altitude change', () => { const b = telemetrySignature({ altitude_ft: 4200, on_ground: false }) assert.notEqual(a, b) }) + +test('telemetrySignature changes as the aircraft moves (~0.6 nm)', () => { + const a = telemetrySignature({ altitude_ft: 3500, lat: 50.0, lon: 8.0 }) + const b = telemetrySignature({ altitude_ft: 3500, lat: 50.02, lon: 8.0 }) + assert.notEqual(a, b) +})