mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-02 23:18:40 +08:00
- 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>
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
export interface RadioSessionResponse {
|
|
session_id: string
|
|
flow_slug: string
|
|
current_state: string
|
|
variables: Record<string, any>
|
|
flags: Record<string, boolean>
|
|
expected_pilot_template: string | null
|
|
}
|
|
|
|
export interface RadioTransmitResponse {
|
|
session_id: string
|
|
next_state_id: string
|
|
active_flow: string
|
|
controller_say_template: string | null
|
|
controller_say_rendered: string | null
|
|
expected_pilot_template: string | null
|
|
variables: Record<string, any>
|
|
flags: Record<string, boolean>
|
|
trace: Array<{ type: string; message: string }>
|
|
fallback_used: boolean
|
|
fallback_reason: string | null
|
|
auto_advanced_states?: string[]
|
|
/** True when a telemetry tick actually fired a transition (vs. an idle poll). */
|
|
telemetry_fired?: boolean
|
|
/** True when the full chain is done — show the completion screen. */
|
|
session_complete?: boolean
|
|
/** Per-field readback diagnostic for the pilot state just evaluated. */
|
|
readback_report?: ReadbackFieldDetail[]
|
|
}
|
|
|
|
/**
|
|
* Sim-agnostic telemetry contract. The bridge layer (MSFS SimConnect today,
|
|
* X-Plane datarefs later) is normalised to exactly these keys before it reaches
|
|
* the backend, so the decision engine only ever compares plain scalars. Keep
|
|
* this in lock-step with `TelemetryParameter` in the Python backend (models.py).
|
|
*/
|
|
export interface NormalizedTelemetry {
|
|
altitude_ft?: number // feet MSL
|
|
ias_kts?: number // indicated airspeed, knots
|
|
gs_kts?: number // groundspeed, knots
|
|
vs_fpm?: number // vertical speed, feet/min (+climb, -descent)
|
|
heading_deg?: number // magnetic heading, 0..360
|
|
on_ground?: boolean // wheels on the ground
|
|
// 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 {
|
|
field: string
|
|
expected: string
|
|
matched: boolean
|
|
/** Which accepted spoken form matched ("two five right", "icao_phonetic"), or null. */
|
|
matched_via: string | null
|
|
/** All spoken forms that would have matched this field. */
|
|
accepted_forms: string[]
|
|
note?: string | null
|
|
}
|
|
|
|
export function useRadioBackend() {
|
|
const config = useRuntimeConfig()
|
|
|
|
function baseUrl(): string {
|
|
return (config.public.radioBackendUrl as string) || 'http://127.0.0.1:8000'
|
|
}
|
|
|
|
// Without a timeout, a hung Python-backend call never resolves — its
|
|
// `finally` (which clears transmitInFlightCount / re-arms the silence timer)
|
|
// never runs either, so the session reads as permanently stuck.
|
|
const TRANSMIT_TIMEOUT_MS = 30_000
|
|
const CREATE_SESSION_TIMEOUT_MS = 60_000 // taxi-route computation is slow
|
|
|
|
async function createSession(
|
|
flowSlug: string,
|
|
variables?: Record<string, any>,
|
|
noChain: boolean = false,
|
|
airportIcao?: string,
|
|
destinationIcao?: string,
|
|
aircraftPosition?: { lat: number; lon: number } | null,
|
|
): Promise<RadioSessionResponse> {
|
|
return await $fetch<RadioSessionResponse>(`${baseUrl()}/api/radio/session`, {
|
|
method: 'POST',
|
|
body: {
|
|
flow_slug: flowSlug,
|
|
variables: variables ?? null,
|
|
no_chain: noChain,
|
|
// Lets the backend compute the real OSM taxi route for taxi flows.
|
|
airport_icao: airportIcao ?? null,
|
|
destination_icao: destinationIcao ?? null,
|
|
// Bridge position: taxi route starts at the real parking spot.
|
|
aircraft_lat: aircraftPosition?.lat ?? null,
|
|
aircraft_lon: aircraftPosition?.lon ?? null,
|
|
},
|
|
signal: AbortSignal.timeout(CREATE_SESSION_TIMEOUT_MS),
|
|
})
|
|
}
|
|
|
|
async function transmit(sessionId: string, pilotUtterance: string): Promise<RadioTransmitResponse> {
|
|
return await $fetch<RadioTransmitResponse>(
|
|
`${baseUrl()}/api/radio/session/${sessionId}/transmissions`,
|
|
{
|
|
method: 'POST',
|
|
body: { pilot_utterance: pilotUtterance },
|
|
signal: AbortSignal.timeout(TRANSMIT_TIMEOUT_MS),
|
|
},
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Forward a normalised telemetry tick. The backend fires any telemetry-gated
|
|
* transition whose threshold is met and returns the same shape as transmit;
|
|
* idle ticks come back with `telemetry_fired: false` and the state unchanged.
|
|
*/
|
|
async function sendTelemetry(
|
|
sessionId: string,
|
|
telemetry: NormalizedTelemetry,
|
|
): Promise<RadioTransmitResponse> {
|
|
return await $fetch<RadioTransmitResponse>(
|
|
`${baseUrl()}/api/radio/session/${sessionId}/telemetry`,
|
|
{
|
|
method: 'POST',
|
|
body: { telemetry },
|
|
signal: AbortSignal.timeout(TRANSMIT_TIMEOUT_MS),
|
|
},
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Fire the silence timeout for the current pilot state. Call when the
|
|
* state's `auto_advance_timeout_ms` elapses without a pilot transmission;
|
|
* the backend advances through the state's silence auto-transition.
|
|
*/
|
|
async function timeout(sessionId: string): Promise<RadioTransmitResponse> {
|
|
return await $fetch<RadioTransmitResponse>(
|
|
`${baseUrl()}/api/radio/session/${sessionId}/timeout`,
|
|
{ method: 'POST', signal: AbortSignal.timeout(TRANSMIT_TIMEOUT_MS) },
|
|
)
|
|
}
|
|
|
|
async function deleteSession(sessionId: string): Promise<void> {
|
|
await $fetch(`${baseUrl()}/api/radio/session/${sessionId}`, { method: 'DELETE' })
|
|
}
|
|
|
|
async function fetchFlows(): Promise<any> {
|
|
return await $fetch(`${baseUrl()}/api/decision-flows/runtime`)
|
|
}
|
|
|
|
return { createSession, transmit, sendTelemetry, timeout, deleteSession, fetchFlows }
|
|
}
|