mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
refactor(live-atc): extract bridge-telemetry normalization to shared/utils, add tests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof setInterval> | 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
|
||||
|
||||
38
shared/utils/bridgeTelemetry.ts
Normal file
38
shared/utils/bridgeTelemetry.ts
Normal file
@@ -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('|')
|
||||
}
|
||||
51
tests/shared/bridgeTelemetry.test.ts
Normal file
51
tests/shared/bridgeTelemetry.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user