mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
feat(live-atc): forward aircraft position, un-throttle bridge polling
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<typeof setInterval> | 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('|')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user