From c8a07b980f4612016c7ab9c6b3ea366d981b568a Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Fri, 10 Jul 2026 09:09:17 +0200 Subject: [PATCH] refactor(live-atc): extract bridge-telemetry normalization to shared/utils, add tests Co-Authored-By: Claude Sonnet 5 --- app/pages/live-atc.vue | 35 +------------------ shared/utils/bridgeTelemetry.ts | 38 +++++++++++++++++++++ tests/shared/bridgeTelemetry.test.ts | 51 ++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 shared/utils/bridgeTelemetry.ts create mode 100644 tests/shared/bridgeTelemetry.test.ts diff --git a/app/pages/live-atc.vue b/app/pages/live-atc.vue index dbf3ef0..afa946d 100644 --- a/app/pages/live-atc.vue +++ b/app/pages/live-atc.vue @@ -1524,6 +1524,7 @@ import type { PizzicatoLite } from '../../shared/utils/pizzicatoLite' import { createNoiseGenerators, getReadabilityProfile } from '../../shared/utils/radioEffects' import { createAtisAudioLoop, type AtisAudioLoop } from '../../shared/utils/atisAudioLoop' import { normalizeManualFreq } from '../../shared/utils/frequency' +import { normalizeSimFreq, normalizeBridgeTelemetry, telemetrySignature } from '../../shared/utils/bridgeTelemetry' import type { CandidateTraceElimination, CandidateTraceEntry, @@ -5300,43 +5301,9 @@ let bridgePoller: ReturnType | null = null // the sim's standby radio. let lastSyncedSimActive: string | null = null -function normalizeSimFreq(value: unknown): string | null { - const num = typeof value === 'number' ? value : Number(value) - if (!Number.isFinite(num) || num < 118 || num >= 137) return null - return num.toFixed(3) -} - -// 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. -function normalizeBridgeTelemetry(raw: any): import('../composables/useRadioBackend').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 - } - return { - altitude_ft: num(raw.PLANE_ALTITUDE), - ias_kts: num(raw.AIRSPEED_INDICATED), - gs_kts: num(raw.GROUND_VELOCITY), - 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, - } -} - // 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. let lastSentTelemetrySig: string | null = null -function telemetrySignature(t: import('../composables/useRadioBackend').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', - ].join('|') -} async function forwardTelemetryToBackend(rawTelemetry: any) { if (!backendSessionId.value) return diff --git a/shared/utils/bridgeTelemetry.ts b/shared/utils/bridgeTelemetry.ts new file mode 100644 index 0000000..5bffc20 --- /dev/null +++ b/shared/utils/bridgeTelemetry.ts @@ -0,0 +1,38 @@ +import type { NormalizedTelemetry } from '../../app/composables/useRadioBackend' + +export function normalizeSimFreq(value: unknown): string | null { + const num = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(num) || num < 118 || num >= 137) return null + return num.toFixed(3) +} + +// 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. +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 + } + return { + altitude_ft: num(raw.PLANE_ALTITUDE), + ias_kts: num(raw.AIRSPEED_INDICATED), + gs_kts: num(raw.GROUND_VELOCITY), + 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, + } +} + +// 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. +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', + ].join('|') +} diff --git a/tests/shared/bridgeTelemetry.test.ts b/tests/shared/bridgeTelemetry.test.ts new file mode 100644 index 0000000..81005b0 --- /dev/null +++ b/tests/shared/bridgeTelemetry.test.ts @@ -0,0 +1,51 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + normalizeSimFreq, + normalizeBridgeTelemetry, + telemetrySignature, +} from '../../shared/utils/bridgeTelemetry.ts' + +test('normalizeSimFreq formats a valid COM value to 3 decimals', () => { + assert.equal(normalizeSimFreq(119.9), '119.900') +}) + +test('normalizeSimFreq rejects out-of-band values', () => { + assert.equal(normalizeSimFreq(50), null) + assert.equal(normalizeSimFreq('not-a-number'), null) +}) + +test('normalizeBridgeTelemetry maps SimConnect field names', () => { + const result = normalizeBridgeTelemetry({ + PLANE_ALTITUDE: 3500, + AIRSPEED_INDICATED: 140, + GROUND_VELOCITY: 138, + VERTICAL_SPEED: 800, + PLANE_HEADING_DEGREES_TRUE: 270, + SIM_ON_GROUND: false, + }) + assert.deepEqual(result, { + altitude_ft: 3500, + ias_kts: 140, + gs_kts: 138, + vs_fpm: 800, + heading_deg: 270, + on_ground: false, + }) +}) + +test('normalizeBridgeTelemetry returns null for non-object input', () => { + assert.equal(normalizeBridgeTelemetry(null), null) +}) + +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 }) + assert.equal(a, b) +}) + +test('telemetrySignature changes on a meaningful altitude change', () => { + const a = telemetrySignature({ altitude_ft: 3500, on_ground: false }) + const b = telemetrySignature({ altitude_ft: 4200, on_ground: false }) + assert.notEqual(a, b) +})