Files
OpenSquawk/app/composables/useRadioBackend.ts
leubeem b7b86b2a17 feat(pm): forward normalized sim telemetry to the backend decision engine
Send live bridge telemetry to the backend so it can drive proactive,
aircraft-state-aware ATC. Purely additive — bridge-less sessions are unchanged.

- NormalizedTelemetry contract + sendTelemetry() + telemetry_fired
  (useRadioBackend.ts), sim-agnostic and matched to the backend contract
- extract applyBackendDecision() so telemetry- and pilot-driven ATC responses
  land through one shared path
- normalizeBridgeTelemetry() + change-detection + forwardTelemetryToBackend()
  wired into the existing bridge poll

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:23:53 +02:00

124 lines
4.5 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
distance_to_dest_nm?: number // great-circle nm to destination airport
distance_to_dep_nm?: number // great-circle nm from departure airport
}
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'
}
async function createSession(
flowSlug: string,
variables?: Record<string, any>,
noChain: boolean = false,
airportIcao?: string,
destinationIcao?: string,
): 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,
},
})
}
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 },
},
)
}
/**
* 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 },
},
)
}
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, deleteSession, fetchFlows }
}