mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-15 03:25:40 +08:00
feat(atc): add Live ATC v2 phase machine foundation
- shared/atc/types.ts: All type definitions (Phase, Interaction, EngineState, Transmission, etc.)
- shared/atc/phases/: 9 phase modules (clearance → ground → tower → departure → enroute → approach → landing → taxiIn + emergency)
- shared/atc/phases/index.ts: Phase registry with getPhase(), getPhaseOrder(), getAllPhases()
- shared/atc/templateRenderer.ts: {var} placeholder renderer
- shared/atc/telemetryWatcher.ts: Telemetry condition evaluator for auto-advance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
67
shared/atc/phases/approach.ts
Normal file
67
shared/atc/phases/approach.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const approachPhase: Phase = {
|
||||
id: 'approach',
|
||||
name: 'Approach Control',
|
||||
frequency: '119.100',
|
||||
unit: 'APP',
|
||||
nextPhase: 'landing',
|
||||
interactions: [
|
||||
{
|
||||
id: 'contact_approach',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot makes initial contact with approach control',
|
||||
pilotExample: '{callsign}, descending flight level {flight_level}, information {atis_code}',
|
||||
atcResponse: '{callsign}, radar contact. Expect {approach_type} approach runway {arrival_runway}.',
|
||||
},
|
||||
{
|
||||
id: 'descend_instruction',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to descend to an assigned altitude',
|
||||
atcResponse: '{callsign}, descend altitude {assigned_alt} feet, QNH {qnh}.',
|
||||
readback: {
|
||||
required: ['assigned_alt', 'qnh'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, descend altitude {assigned_alt} feet, QNH {qnh}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cleared_approach',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC clears pilot for the instrument approach',
|
||||
atcResponse: '{callsign}, cleared {approach_type} approach runway {arrival_runway}.',
|
||||
readback: {
|
||||
required: ['approach_type', 'arrival_runway'],
|
||||
atcConfirm: 'Correct. Contact tower on {tower_freq}.',
|
||||
atcCorrect: 'Negative, cleared {approach_type} approach runway {arrival_runway}.',
|
||||
},
|
||||
handoff: { toPhase: 'landing', say: 'Contact tower on {tower_freq}.' },
|
||||
},
|
||||
{
|
||||
id: 'request_alternate_approach',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests a different approach type than assigned',
|
||||
pilotExample: '{callsign}, request {requested_approach} approach runway {arrival_runway}',
|
||||
atcResponse: '{callsign}, approved, expect {requested_approach} approach runway {arrival_runway}.',
|
||||
updates: { approach_type: '{requested_approach}' },
|
||||
},
|
||||
{
|
||||
id: 'go_around',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot executes a go-around and reports to approach',
|
||||
pilotExample: '{callsign}, going around',
|
||||
atcResponse: '{callsign}, roger, climb altitude {initial_alt} feet, fly runway heading, contact approach on {approach_freq}.',
|
||||
},
|
||||
{
|
||||
id: 'speed_instruction',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to reduce or maintain a specific speed',
|
||||
atcResponse: '{callsign}, reduce speed {assigned_speed} knots.',
|
||||
readback: {
|
||||
required: ['assigned_speed'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, reduce speed {assigned_speed} knots.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
32
shared/atc/phases/clearance.ts
Normal file
32
shared/atc/phases/clearance.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const clearancePhase: Phase = {
|
||||
id: 'clearance',
|
||||
name: 'Clearance Delivery',
|
||||
frequency: '121.900',
|
||||
unit: 'DEL',
|
||||
nextPhase: 'ground',
|
||||
interactions: [
|
||||
{
|
||||
id: 'request_clearance',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests IFR clearance to destination',
|
||||
pilotExample: '{callsign}, request IFR clearance to {dest}, information {atis_code}',
|
||||
atcResponse: '{callsign}, cleared to {dest} via {sid} departure, runway {runway}, climb initially {initial_alt} feet, expect flight level {flight_level}, squawk {squawk}',
|
||||
readback: {
|
||||
required: ['dest', 'sid', 'runway', 'squawk', 'initial_alt'],
|
||||
atcConfirm: 'Readback correct. Contact ground on {ground_freq}.',
|
||||
atcCorrect: 'Negative, I say again: cleared to {dest} via {sid}, runway {runway}, climb initially {initial_alt} feet, squawk {squawk}.',
|
||||
},
|
||||
updates: { clearance_received: 'true' },
|
||||
handoff: { toPhase: 'ground', say: 'Contact ground on {ground_freq}.' },
|
||||
},
|
||||
{
|
||||
id: 'request_information',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests ATIS information or QNH',
|
||||
pilotExample: '{callsign}, request ATIS information',
|
||||
atcResponse: 'Information {atis_code} is current, QNH {qnh}.',
|
||||
},
|
||||
],
|
||||
}
|
||||
61
shared/atc/phases/departure.ts
Normal file
61
shared/atc/phases/departure.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const departurePhase: Phase = {
|
||||
id: 'departure',
|
||||
name: 'Departure Control',
|
||||
frequency: '125.100',
|
||||
unit: 'DEP',
|
||||
nextPhase: 'enroute',
|
||||
autoAdvance: {
|
||||
parameter: 'altitude_ft',
|
||||
operator: '>=',
|
||||
value: 18000,
|
||||
},
|
||||
interactions: [
|
||||
{
|
||||
id: 'contact_departure',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot makes initial contact with departure after handoff from tower',
|
||||
pilotExample: '{callsign}, passing {altitude} feet, climbing {initial_alt}',
|
||||
atcResponse: '{callsign}, radar contact. Climb flight level {flight_level}.',
|
||||
readback: {
|
||||
required: ['flight_level'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, climb flight level {flight_level}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'climb_instruction',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to climb to a new flight level',
|
||||
atcResponse: '{callsign}, climb flight level {assigned_fl}.',
|
||||
readback: {
|
||||
required: ['assigned_fl'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, climb flight level {assigned_fl}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'turn_instruction',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to turn to a specific heading',
|
||||
atcResponse: '{callsign}, turn {turn_direction} heading {assigned_heading}.',
|
||||
readback: {
|
||||
required: ['turn_direction', 'assigned_heading'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, turn {turn_direction} heading {assigned_heading}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'direct_to',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC clears pilot to proceed direct to a waypoint',
|
||||
atcResponse: '{callsign}, proceed direct {waypoint}.',
|
||||
readback: {
|
||||
required: ['waypoint'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, proceed direct {waypoint}.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
65
shared/atc/phases/emergency.ts
Normal file
65
shared/atc/phases/emergency.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
/**
|
||||
* Emergency phase -- can be activated from ANY other phase.
|
||||
*
|
||||
* When the engine detects a MAYDAY or PAN PAN call it should:
|
||||
* 1. Save `flags.previousPhase` with the current phase id
|
||||
* 2. Set `flags.emergencyActive = true`
|
||||
* 3. Transition to this phase
|
||||
*
|
||||
* When the emergency is cancelled the engine should:
|
||||
* 1. Set `flags.emergencyActive = false`
|
||||
* 2. Restore `currentPhase` to `flags.previousPhase`
|
||||
* 3. Clear `flags.previousPhase`
|
||||
*/
|
||||
export const emergencyPhase: Phase = {
|
||||
id: 'emergency',
|
||||
name: 'Emergency',
|
||||
frequency: '121.500',
|
||||
unit: 'EMG',
|
||||
nextPhase: null,
|
||||
interactions: [
|
||||
{
|
||||
id: 'declare_mayday',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot declares a MAYDAY emergency (distress)',
|
||||
pilotExample: 'MAYDAY MAYDAY MAYDAY, {callsign}, {emergency_nature}, {pob} persons on board, fuel remaining {fuel_remaining} minutes',
|
||||
atcResponse: '{callsign}, MAYDAY acknowledged. Roger {emergency_nature}. All stations, stop transmitting, MAYDAY in progress. {callsign}, say intentions.',
|
||||
updates: { emergency_type: 'mayday', emergency_active: 'true' },
|
||||
},
|
||||
{
|
||||
id: 'declare_panpan',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot declares a PAN PAN (urgency)',
|
||||
pilotExample: 'PAN PAN PAN PAN PAN PAN, {callsign}, {emergency_nature}',
|
||||
atcResponse: '{callsign}, PAN PAN acknowledged. Roger {emergency_nature}. Say intentions.',
|
||||
updates: { emergency_type: 'panpan', emergency_active: 'true' },
|
||||
},
|
||||
{
|
||||
id: 'cancel_emergency',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot cancels the emergency, situation resolved',
|
||||
pilotExample: '{callsign}, cancel {emergency_type}, situation resolved',
|
||||
atcResponse: '{callsign}, roger, {emergency_type} cancelled. Resume normal operations. Contact {resume_freq}.',
|
||||
updates: { emergency_active: 'false' },
|
||||
},
|
||||
{
|
||||
id: 'emergency_landing',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC provides vectors for an emergency landing',
|
||||
atcResponse: '{callsign}, turn {turn_direction} heading {assigned_heading}, vectors for {arrival_runway}. Cleared {approach_type} approach runway {arrival_runway}, emergency services standing by.',
|
||||
readback: {
|
||||
required: ['assigned_heading', 'arrival_runway'],
|
||||
atcConfirm: 'Correct, emergency services standing by.',
|
||||
atcCorrect: 'Negative, turn {turn_direction} heading {assigned_heading}, vectors for {arrival_runway}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'souls_on_board',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC requests souls on board and fuel remaining for emergency coordination',
|
||||
atcResponse: '{callsign}, say souls on board and fuel remaining.',
|
||||
},
|
||||
],
|
||||
}
|
||||
54
shared/atc/phases/enroute.ts
Normal file
54
shared/atc/phases/enroute.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const enroutePhase: Phase = {
|
||||
id: 'enroute',
|
||||
name: 'Center / En-Route Control',
|
||||
frequency: '132.600',
|
||||
unit: 'CTR',
|
||||
nextPhase: 'approach',
|
||||
interactions: [
|
||||
{
|
||||
id: 'contact_center',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot makes initial contact with center after handoff from departure',
|
||||
pilotExample: '{callsign}, flight level {flight_level}',
|
||||
atcResponse: '{callsign}, radar contact. Maintain flight level {flight_level}.',
|
||||
},
|
||||
{
|
||||
id: 'maintain_level',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to maintain current flight level',
|
||||
atcResponse: '{callsign}, maintain flight level {flight_level}.',
|
||||
readback: {
|
||||
required: ['flight_level'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, maintain flight level {flight_level}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'request_level_change',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests a different flight level due to weather or ride comfort',
|
||||
pilotExample: '{callsign}, request flight level {requested_fl}',
|
||||
atcResponse: '{callsign}, climb flight level {requested_fl}.',
|
||||
readback: {
|
||||
required: ['requested_fl'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, climb flight level {requested_fl}.',
|
||||
},
|
||||
alternatives: [
|
||||
{
|
||||
intent: 'Request denied due to traffic',
|
||||
atcResponse: '{callsign}, unable flight level {requested_fl} due traffic. Maintain flight level {flight_level}.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'position_report',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot reports current position over a waypoint or fix',
|
||||
pilotExample: '{callsign}, position {waypoint}, flight level {flight_level}, estimating {next_waypoint} at {est_time}',
|
||||
atcResponse: '{callsign}, roger, report {next_waypoint}.',
|
||||
},
|
||||
],
|
||||
}
|
||||
59
shared/atc/phases/ground.ts
Normal file
59
shared/atc/phases/ground.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const groundPhase: Phase = {
|
||||
id: 'ground',
|
||||
name: 'Ground Control',
|
||||
frequency: '121.700',
|
||||
unit: 'GND',
|
||||
nextPhase: 'tower',
|
||||
interactions: [
|
||||
{
|
||||
id: 'request_pushback',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests pushback and start-up clearance',
|
||||
pilotExample: '{callsign}, stand {stand}, request pushback and start-up',
|
||||
atcResponse: '{callsign}, pushback approved, face {push_direction}.',
|
||||
readback: {
|
||||
required: ['push_direction'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, pushback approved, face {push_direction}.',
|
||||
},
|
||||
updates: { pushback_approved: 'true' },
|
||||
},
|
||||
{
|
||||
id: 'request_taxi',
|
||||
type: 'pilot_initiates',
|
||||
when: 'vars.pushback_approved',
|
||||
pilotIntent: 'Pilot requests taxi clearance to the runway',
|
||||
pilotExample: '{callsign}, request taxi',
|
||||
atcResponse: '{callsign}, taxi to holding point {runway} via {taxi_route}.',
|
||||
readback: {
|
||||
required: ['runway', 'taxi_route'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, taxi to holding point {runway} via {taxi_route}.',
|
||||
},
|
||||
updates: { taxi_clearance_received: 'true' },
|
||||
},
|
||||
{
|
||||
id: 'report_holding_short',
|
||||
type: 'pilot_initiates',
|
||||
when: 'vars.taxi_clearance_received',
|
||||
pilotIntent: 'Pilot reports holding short of the runway',
|
||||
pilotExample: '{callsign}, holding short runway {runway}',
|
||||
atcResponse: '{callsign}, contact tower on {tower_freq}.',
|
||||
handoff: { toPhase: 'tower', say: 'Contact tower on {tower_freq}.' },
|
||||
},
|
||||
{
|
||||
id: 'request_cross_runway',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot requests permission to cross an active runway',
|
||||
pilotExample: '{callsign}, request cross runway {cross_runway}',
|
||||
atcResponse: '{callsign}, cross runway {cross_runway}, report vacated.',
|
||||
readback: {
|
||||
required: ['cross_runway'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, cross runway {cross_runway}, report vacated.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
48
shared/atc/phases/index.ts
Normal file
48
shared/atc/phases/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Phase } from '../types'
|
||||
import { clearancePhase } from './clearance'
|
||||
import { groundPhase } from './ground'
|
||||
import { towerPhase } from './tower'
|
||||
import { departurePhase } from './departure'
|
||||
import { enroutePhase } from './enroute'
|
||||
import { approachPhase } from './approach'
|
||||
import { landingPhase } from './landing'
|
||||
import { taxiInPhase } from './taxiIn'
|
||||
import { emergencyPhase } from './emergency'
|
||||
|
||||
const phases: Phase[] = [
|
||||
clearancePhase,
|
||||
groundPhase,
|
||||
towerPhase,
|
||||
departurePhase,
|
||||
enroutePhase,
|
||||
approachPhase,
|
||||
landingPhase,
|
||||
taxiInPhase,
|
||||
emergencyPhase,
|
||||
]
|
||||
|
||||
const phaseMap = new Map<string, Phase>(phases.map(p => [p.id, p]))
|
||||
|
||||
export function getPhase(id: string): Phase | undefined {
|
||||
return phaseMap.get(id)
|
||||
}
|
||||
|
||||
export function getPhaseOrder(): string[] {
|
||||
return phases.filter(p => p.id !== 'emergency').map(p => p.id)
|
||||
}
|
||||
|
||||
export function getAllPhases(): Phase[] {
|
||||
return [...phases]
|
||||
}
|
||||
|
||||
export {
|
||||
clearancePhase,
|
||||
groundPhase,
|
||||
towerPhase,
|
||||
departurePhase,
|
||||
enroutePhase,
|
||||
approachPhase,
|
||||
landingPhase,
|
||||
taxiInPhase,
|
||||
emergencyPhase,
|
||||
}
|
||||
45
shared/atc/phases/landing.ts
Normal file
45
shared/atc/phases/landing.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const landingPhase: Phase = {
|
||||
id: 'landing',
|
||||
name: 'Tower (Landing)',
|
||||
frequency: '118.500',
|
||||
unit: 'TWR',
|
||||
nextPhase: 'taxiIn',
|
||||
autoAdvance: {
|
||||
parameter: 'on_ground',
|
||||
operator: '==',
|
||||
value: 1,
|
||||
},
|
||||
interactions: [
|
||||
{
|
||||
id: 'report_established',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot reports established on the final approach',
|
||||
pilotExample: '{callsign}, established {approach_type} runway {arrival_runway}',
|
||||
atcResponse: '{callsign}, roger, continue approach runway {arrival_runway}.',
|
||||
},
|
||||
{
|
||||
id: 'cleared_land',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC clears pilot to land',
|
||||
atcResponse: '{callsign}, wind {wind}, runway {arrival_runway}, cleared to land.',
|
||||
readback: {
|
||||
required: ['arrival_runway'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, runway {arrival_runway}, cleared to land.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'go_around_instruction',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to go around due to traffic or runway obstruction',
|
||||
atcResponse: '{callsign}, go around, I say again go around. Climb altitude {go_around_alt} feet, fly runway heading.',
|
||||
readback: {
|
||||
required: ['go_around_alt'],
|
||||
atcConfirm: 'Correct, contact approach on {approach_freq}.',
|
||||
atcCorrect: 'Negative, go around, climb altitude {go_around_alt} feet, fly runway heading.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
42
shared/atc/phases/taxiIn.ts
Normal file
42
shared/atc/phases/taxiIn.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const taxiInPhase: Phase = {
|
||||
id: 'taxiIn',
|
||||
name: 'Ground Control (Arrival)',
|
||||
frequency: '121.700',
|
||||
unit: 'GND',
|
||||
nextPhase: null,
|
||||
interactions: [
|
||||
{
|
||||
id: 'contact_ground_arrival',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot contacts ground after vacating the runway',
|
||||
pilotExample: '{callsign}, runway {arrival_runway} vacated, request taxi to stand',
|
||||
atcResponse: '{callsign}, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
|
||||
readback: {
|
||||
required: ['arrival_stand', 'arrival_taxi_route'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'taxi_to_gate',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC provides taxi instructions to the assigned stand',
|
||||
atcResponse: '{callsign}, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
|
||||
readback: {
|
||||
required: ['arrival_stand', 'arrival_taxi_route'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'report_at_gate',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot reports on stand with engines shut down',
|
||||
pilotExample: '{callsign}, on stand {arrival_stand}, engines shut down',
|
||||
atcResponse: '{callsign}, roger, welcome to {dest}. Frequency change approved.',
|
||||
updates: { session_complete: 'true' },
|
||||
},
|
||||
],
|
||||
}
|
||||
59
shared/atc/phases/tower.ts
Normal file
59
shared/atc/phases/tower.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Phase } from '../types'
|
||||
|
||||
export const towerPhase: Phase = {
|
||||
id: 'tower',
|
||||
name: 'Tower',
|
||||
frequency: '118.500',
|
||||
unit: 'TWR',
|
||||
nextPhase: 'departure',
|
||||
autoAdvance: {
|
||||
parameter: 'altitude_ft',
|
||||
operator: '>=',
|
||||
value: 1000,
|
||||
},
|
||||
interactions: [
|
||||
{
|
||||
id: 'report_ready_departure',
|
||||
type: 'pilot_initiates',
|
||||
pilotIntent: 'Pilot reports ready for departure',
|
||||
pilotExample: '{callsign}, ready for departure runway {runway}',
|
||||
atcResponse: '{callsign}, hold position, standby.',
|
||||
},
|
||||
{
|
||||
id: 'lineup_and_wait',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC instructs pilot to line up on the runway and wait',
|
||||
atcResponse: '{callsign}, runway {runway}, line up and wait.',
|
||||
readback: {
|
||||
required: ['runway'],
|
||||
atcConfirm: 'Correct.',
|
||||
atcCorrect: 'Negative, runway {runway}, line up and wait.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cleared_takeoff',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC clears pilot for takeoff',
|
||||
atcResponse: '{callsign}, wind {wind}, runway {runway}, cleared for takeoff.',
|
||||
readback: {
|
||||
required: ['runway'],
|
||||
atcConfirm: 'Correct, contact departure on {departure_freq} when airborne.',
|
||||
atcCorrect: 'Negative, runway {runway}, cleared for takeoff.',
|
||||
},
|
||||
updates: { takeoff_clearance: 'true' },
|
||||
handoff: { toPhase: 'departure', say: 'Contact departure on {departure_freq}.' },
|
||||
},
|
||||
{
|
||||
id: 'cancel_takeoff',
|
||||
type: 'atc_initiates',
|
||||
pilotIntent: 'ATC cancels takeoff clearance',
|
||||
atcResponse: '{callsign}, cancel takeoff, I say again cancel takeoff. Hold position.',
|
||||
readback: {
|
||||
required: [],
|
||||
atcConfirm: 'Correct, hold position.',
|
||||
atcCorrect: 'Negative, cancel takeoff, hold position.',
|
||||
},
|
||||
updates: { takeoff_clearance: 'false' },
|
||||
},
|
||||
],
|
||||
}
|
||||
61
shared/atc/telemetryWatcher.ts
Normal file
61
shared/atc/telemetryWatcher.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Phase, TelemetryCondition, TelemetryState } from './types'
|
||||
|
||||
export interface TelemetryEvent {
|
||||
type: 'phase_advance' | 'atc_interrupt'
|
||||
toPhase?: string
|
||||
interactionId?: string
|
||||
trigger: {
|
||||
parameter: string
|
||||
condition: string
|
||||
value: number | boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a single telemetry condition against current telemetry state.
|
||||
* Returns true if the condition is met, false otherwise.
|
||||
*/
|
||||
export function evaluateCondition(
|
||||
telemetry: TelemetryState,
|
||||
condition: TelemetryCondition
|
||||
): boolean {
|
||||
const actual = telemetry[condition.parameter]
|
||||
if (actual === undefined) return false
|
||||
|
||||
const val = condition.value
|
||||
switch (condition.operator) {
|
||||
case '>': return (actual as number) > val
|
||||
case '>=': return (actual as number) >= val
|
||||
case '<': return (actual as number) < val
|
||||
case '<=': return (actual as number) <= val
|
||||
case '==': return actual == val // loose equality for boolean/number comparison (on_ground == 1)
|
||||
case '!=': return actual != val
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates telemetry against the current phase's autoAdvance condition.
|
||||
* If the condition is met and nextPhase is set, returns a phase_advance event.
|
||||
* Otherwise returns null.
|
||||
*/
|
||||
export function evaluateTelemetry(
|
||||
telemetry: TelemetryState,
|
||||
currentPhase: Phase
|
||||
): TelemetryEvent | null {
|
||||
if (currentPhase.autoAdvance && currentPhase.nextPhase) {
|
||||
if (evaluateCondition(telemetry, currentPhase.autoAdvance)) {
|
||||
return {
|
||||
type: 'phase_advance',
|
||||
toPhase: currentPhase.nextPhase,
|
||||
trigger: {
|
||||
parameter: currentPhase.autoAdvance.parameter,
|
||||
condition: `${currentPhase.autoAdvance.parameter} ${currentPhase.autoAdvance.operator} ${currentPhase.autoAdvance.value}`,
|
||||
value: telemetry[currentPhase.autoAdvance.parameter] as number,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
11
shared/atc/templateRenderer.ts
Normal file
11
shared/atc/templateRenderer.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Simple template renderer that replaces {variable} placeholders with values.
|
||||
* Renders a template string like "{callsign}, cleared to {dest} via {sid}"
|
||||
* by replacing {key} placeholders with values from the vars object.
|
||||
* Unreplaced placeholders are left as-is (useful for debugging).
|
||||
*/
|
||||
export function renderTemplate(template: string, vars: Record<string, string>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return vars[key] ?? match
|
||||
})
|
||||
}
|
||||
191
shared/atc/types.ts
Normal file
191
shared/atc/types.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
// Phase definition
|
||||
export interface Phase {
|
||||
id: string
|
||||
name: string
|
||||
frequency: string
|
||||
unit: string
|
||||
interactions: Interaction[]
|
||||
autoAdvance?: TelemetryCondition
|
||||
nextPhase: string | null
|
||||
}
|
||||
|
||||
export interface Interaction {
|
||||
id: string
|
||||
type: 'pilot_initiates' | 'atc_initiates' | 'readback_check'
|
||||
when?: string // Condition expression
|
||||
pilotIntent: string // For LLM prompt
|
||||
pilotExample?: string // Template with {vars}
|
||||
atcResponse: string // Template with {vars}
|
||||
readback?: ReadbackSpec
|
||||
updates?: Record<string, any>
|
||||
handoff?: { toPhase: string; say?: string }
|
||||
alternatives?: AlternativeResponse[]
|
||||
}
|
||||
|
||||
export interface ReadbackSpec {
|
||||
required: string[]
|
||||
atcConfirm: string
|
||||
atcCorrect: string
|
||||
}
|
||||
|
||||
export interface AlternativeResponse {
|
||||
intent: string
|
||||
atcResponse: string
|
||||
updates?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface TelemetryCondition {
|
||||
parameter: string
|
||||
operator: '>' | '>=' | '<' | '<=' | '==' | '!='
|
||||
value: number
|
||||
holdMs?: number
|
||||
}
|
||||
|
||||
// Flight variables
|
||||
export interface FlightVars {
|
||||
callsign: string
|
||||
aircraft: string
|
||||
dep: string
|
||||
dest: string
|
||||
stand: string
|
||||
runway: string
|
||||
sid: string
|
||||
squawk: string
|
||||
atis_code: string
|
||||
initial_alt: string
|
||||
flight_level: string
|
||||
qnh: string
|
||||
taxi_route: string
|
||||
ground_freq: string
|
||||
tower_freq: string
|
||||
departure_freq: string
|
||||
approach_freq: string
|
||||
center_freq: string
|
||||
atis_freq: string
|
||||
wind: string
|
||||
arrival_runway: string
|
||||
arrival_stand: string
|
||||
arrival_taxi_route: string
|
||||
star: string
|
||||
approach_type: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
// Telemetry from SimBridge
|
||||
export interface TelemetryState {
|
||||
altitude_ft: number
|
||||
speed_kts: number
|
||||
groundspeed_kts: number
|
||||
vertical_speed_fpm: number
|
||||
heading_deg: number
|
||||
latitude_deg: number
|
||||
longitude_deg: number
|
||||
on_ground: boolean
|
||||
[key: string]: number | boolean
|
||||
}
|
||||
|
||||
// Engine state
|
||||
export interface EngineState {
|
||||
currentPhase: string
|
||||
currentInteraction: string | null
|
||||
waitingFor: 'pilot' | 'readback' | 'none'
|
||||
vars: FlightVars
|
||||
flags: {
|
||||
inAir: boolean
|
||||
emergencyActive: boolean
|
||||
previousPhase: string | null
|
||||
}
|
||||
telemetry: TelemetryState
|
||||
sessionId: string
|
||||
transmissions: Transmission[]
|
||||
}
|
||||
|
||||
// Transmission with debug info
|
||||
export interface Transmission {
|
||||
id: string
|
||||
timestamp: Date
|
||||
speaker: 'pilot' | 'atc' | 'system'
|
||||
message: string
|
||||
normalized?: string
|
||||
phase: string
|
||||
frequency: string
|
||||
debug: TransmissionDebug
|
||||
}
|
||||
|
||||
export interface TransmissionDebug {
|
||||
sttRaw?: string
|
||||
llmRequest?: {
|
||||
currentPhase: string
|
||||
currentInteraction: string | null
|
||||
pilotSaid: string
|
||||
candidates: { id: string; intent: string }[]
|
||||
contextSent: Record<string, any>
|
||||
}
|
||||
llmResponse?: {
|
||||
chosenInteraction: string
|
||||
confidence: 'high' | 'medium' | 'low'
|
||||
reason: string
|
||||
tokensUsed: number
|
||||
durationMs: number
|
||||
model: string
|
||||
}
|
||||
engineAction?: {
|
||||
templateUsed: string
|
||||
variablesUpdated: Record<string, any>
|
||||
handoff?: { from: string; to: string }
|
||||
phaseChanged?: { from: string; to: string }
|
||||
}
|
||||
telemetryTrigger?: {
|
||||
parameter: string
|
||||
condition: string
|
||||
value: number
|
||||
}
|
||||
readbackResult?: {
|
||||
complete: boolean
|
||||
missing?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// Flight plan input
|
||||
export interface FlightPlan {
|
||||
callsign: string
|
||||
aircraft?: string
|
||||
dep: string
|
||||
arr: string
|
||||
route?: string
|
||||
altitude?: string
|
||||
squawk?: string
|
||||
id?: number
|
||||
assignedsquawk?: string
|
||||
}
|
||||
|
||||
// LLM Router types
|
||||
export interface RouteRequest {
|
||||
pilotSaid: string
|
||||
phase: string
|
||||
interaction: string | null
|
||||
waitingFor: 'pilot' | 'readback' | 'none'
|
||||
candidates: RouteCandidate[]
|
||||
vars: Record<string, any>
|
||||
recentTransmissions: string[]
|
||||
}
|
||||
|
||||
export interface RouteCandidate {
|
||||
id: string
|
||||
intent: string
|
||||
example?: string
|
||||
}
|
||||
|
||||
export interface RouteResponse {
|
||||
chosen: string
|
||||
reason: string
|
||||
pilotIntent: string
|
||||
confidence: 'high' | 'medium' | 'low'
|
||||
tokensUsed: number
|
||||
durationMs: number
|
||||
model: string
|
||||
readbackResult?: {
|
||||
complete: boolean
|
||||
missing?: string[]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user