mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-08 10:41:49 +08:00
Enable flow-aware decision routing
This commit is contained in:
@@ -1,585 +0,0 @@
|
||||
const atcDecisionTree = {
|
||||
"schema_version": "1.0",
|
||||
"name": "icao_atc_decision_tree",
|
||||
"description": "Machine-readable ICAO/FAA-style ATC flow for IFR, suited for LLM+Whisper+TTS loop.",
|
||||
"start_state": "CD_CHECK_ATIS",
|
||||
"end_states": ["FLOW_COMPLETE"],
|
||||
"variables": {
|
||||
"callsign": "DLH39A",
|
||||
"acf_type": "A320",
|
||||
"dep": "EDDF",
|
||||
"dest": "EDDM",
|
||||
"stand": "A12",
|
||||
"runway": "25R",
|
||||
"sid": "ANEKI 7S",
|
||||
"transition": "ANEKI",
|
||||
"squawk": "1234",
|
||||
"initial_altitude_ft": 5000,
|
||||
"climb_altitude_ft": 7000,
|
||||
"cruise_flight_level": "FL360",
|
||||
"star": "RNAV X",
|
||||
"approach_type": "ILS Z",
|
||||
"taxi_route": "V A",
|
||||
"missed_approach": "as published",
|
||||
"delivery_freq": "121.900",
|
||||
"ground_freq": "121.700",
|
||||
"tower_freq": "118.700",
|
||||
"departure_freq": "125.350",
|
||||
"approach_freq": "120.800",
|
||||
"handoff_freq": "121.800",
|
||||
"atis_freq": "118.025",
|
||||
"atis_code": "K",
|
||||
"gate": "B24",
|
||||
"trans_level": "FL070",
|
||||
"qnh_hpa": 1015,
|
||||
"push_delay_min": 5,
|
||||
"surface_wind": "220/05",
|
||||
"speed_restriction": "210 knots",
|
||||
"emergency_heading": "180",
|
||||
"remarks": "standard",
|
||||
"time_now": "ISO8601"
|
||||
},
|
||||
"flags": {
|
||||
"in_air": false,
|
||||
"emergency_active": false,
|
||||
"current_unit": "DEL",
|
||||
"stack": []
|
||||
},
|
||||
"policies": {
|
||||
"timeouts": {
|
||||
"pilot_readback_timeout_s": 8,
|
||||
"controller_ack_timeout_s": 6,
|
||||
"no_reply_retry_after_s": 5,
|
||||
"no_reply_max_retries": 2,
|
||||
"lost_comms_detect_after_s": 90
|
||||
},
|
||||
"no_reply_sequence": [
|
||||
{"after_s": 5, "controller_say_tpl": "{callsign}, confirm last transmission."},
|
||||
{"after_s": 10, "controller_say_tpl": "{callsign}, do you read?"},
|
||||
{"after_s": 20, "controller_say_tpl": "{callsign}, if you read, ident."}
|
||||
],
|
||||
"interrupts_allowed_when": {
|
||||
"MAYDAY": "flags.in_air === true",
|
||||
"PANPAN": "flags.in_air === true",
|
||||
"TCAS_RA": "flags.in_air === true",
|
||||
"GO_AROUND": "true",
|
||||
"LOST_COMMS": "true",
|
||||
"UNABLE": "true",
|
||||
"STANDBY": "true"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"on_state_enter": true,
|
||||
"on_state_exit": true,
|
||||
"on_timeout": true,
|
||||
"on_interrupt": true,
|
||||
"on_handoff": true,
|
||||
"on_readback_check": true,
|
||||
"on_pilot_speech": true,
|
||||
"router_note": "Given pilot utterance → detect intent → if valid in active flow, branch; else jump to flow start with matching intent."
|
||||
},
|
||||
"roles": ["pilot", "atc", "system"],
|
||||
"phases": ["Preflight","Clearance","PushStart","TaxiOut","Departure","Climb","Enroute","Descent","Approach","Landing","TaxiIn","Postflight","Interrupt","LostComms","Missed"],
|
||||
"states": {
|
||||
"PREFLIGHT_START": {
|
||||
"role": "pilot",
|
||||
"phase": "Preflight",
|
||||
"prompt_out": "Initial contact with Clearance Delivery.",
|
||||
"next": [{"to": "CD_CHECK_ATIS"}]
|
||||
},
|
||||
"CD_CHECK_ATIS": {
|
||||
"role": "pilot",
|
||||
"phase": "Clearance",
|
||||
"utterance_tpl": "{callsign} information {atis_code}, IFR to {dest}, stand {stand}, request clearance.",
|
||||
"next": [{"to": "CD_ISSUE_CLR"}],
|
||||
"on_timeout": [{"after_s": 5, "action": "remind", "say_tpl": "{callsign}, say request."}]
|
||||
},
|
||||
"CD_ISSUE_CLR": {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"say_tpl": "{callsign}, cleared to {dest} via {sid} departure, runway {runway}, climb {initial_altitude_ft} feet, squawk {squawk}.",
|
||||
"readback_required": ["dest","sid","runway","initial_altitude_ft","squawk"],
|
||||
"next": [
|
||||
{"to": "CD_VERIFY_READBACK"},
|
||||
{"to": "CD_AMEND_CLR", "when": "pilot_requests_amendment === true"}
|
||||
]
|
||||
},
|
||||
"CD_VERIFY_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "Clearance",
|
||||
"utterance_tpl": "{callsign} cleared {dest} via {sid}, runway {runway}, climb {initial_altitude_ft}, squawk {squawk}.",
|
||||
"next": [{"to": "CD_READBACK_CHECK"}]
|
||||
},
|
||||
"CD_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["dest","sid","runway","initial_altitude_ft","squawk"],
|
||||
"ok_next": [{"to": "CD_CLR_COMPLETE"}],
|
||||
"bad_next": [{"to": "CD_READBACK_CORRECT"}],
|
||||
"on_timeout": [{"after_s": 6, "action": "say", "say_tpl": "{callsign}, read back clearance."}]
|
||||
},
|
||||
"CD_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"say_tpl": "{callsign}, negative; expect {sid}, runway {runway}, climb {initial_altitude_ft}, squawk {squawk}.",
|
||||
"next": [{"to": "CD_VERIFY_READBACK"}]
|
||||
},
|
||||
"CD_AMEND_CLR": {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"say_tpl": "{callsign}, amended clearance: {sid} {transition} departure, runway {runway}.",
|
||||
"next": [{"to": "CD_VERIFY_READBACK"}]
|
||||
},
|
||||
"CD_CLR_COMPLETE": {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"say_tpl": "{callsign}, readback correct. Start-up at own discretion. Contact Ground {ground_freq} when ready for push and start.",
|
||||
"actions": [{"set": "flags.current_unit", "to": "DEL"}],
|
||||
"handoff": {"to": "GROUND","freq": "{ground_freq}"},
|
||||
"next": [{"to": "GRD_READY_FOR_PUSH"}]
|
||||
},
|
||||
|
||||
"GRD_READY_FOR_PUSH": {
|
||||
"role": "pilot",
|
||||
"phase": "PushStart",
|
||||
"utterance_tpl": "{callsign} stand {stand}, ready for push and start.",
|
||||
"next": [
|
||||
{"to": "GRD_PUSH_APPROVE", "when": "push_available === true"},
|
||||
{"to": "GRD_PUSH_WAIT", "when": "push_available === false"}
|
||||
]
|
||||
},
|
||||
"GRD_PUSH_WAIT": {
|
||||
"role": "atc",
|
||||
"phase": "PushStart",
|
||||
"say_tpl": "{callsign}, push and start approved in {push_delay_min} minutes. Expect taxi via {taxi_route}.",
|
||||
"timer_next": [{"after_s": 60, "to": "GRD_PUSH_APPROVE"}],
|
||||
"next": [{"to": "GRD_PUSH_APPROVE"}]
|
||||
},
|
||||
"GRD_PUSH_APPROVE": {
|
||||
"role": "atc",
|
||||
"phase": "PushStart",
|
||||
"say_tpl": "{callsign}, push and start approved, facing {runway}. QNH {qnh_hpa}.",
|
||||
"next": [{"to": "GRD_TAXI_REQUEST"}]
|
||||
},
|
||||
|
||||
"GRD_TAXI_REQUEST": {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiOut",
|
||||
"utterance_tpl": "{callsign}, request taxi.",
|
||||
"next": [{"to": "GRD_TAXI_INSTR"}]
|
||||
},
|
||||
"GRD_TAXI_INSTR": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"say_tpl": "{callsign}, taxi to runway {runway} via {taxi_route}, hold short runway {runway}.",
|
||||
"readback_required": ["runway","taxi_route","hold_short"],
|
||||
"next": [{"to": "GRD_TAXI_READBACK"}]
|
||||
},
|
||||
"GRD_TAXI_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiOut",
|
||||
"utterance_tpl": "{callsign} taxi to {runway} via {taxi_route}, holding short {runway}.",
|
||||
"next": [{"to": "GRD_TAXI_READBACK_CHECK"}]
|
||||
},
|
||||
"GRD_TAXI_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["runway","taxi_route","hold_short"],
|
||||
"ok_next": [{"to": "TWR_CONTACT"}],
|
||||
"bad_next": [{"to": "GRD_TAXI_READBACK_CORRECT"}]
|
||||
},
|
||||
"GRD_TAXI_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"say_tpl": "{callsign}, negative; taxi to runway {runway} via {taxi_route}, hold short runway {runway}.",
|
||||
"next": [{"to": "GRD_TAXI_READBACK"}]
|
||||
},
|
||||
|
||||
"TWR_CONTACT": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"say_tpl": "{callsign}, contact Tower {tower_freq} when number one.",
|
||||
"handoff": {"to": "TOWER","freq": "{tower_freq}"},
|
||||
"next": [{"to": "TWR_LINEUP_REQ"}]
|
||||
},
|
||||
"TWR_LINEUP_REQ": {
|
||||
"role": "pilot",
|
||||
"phase": "Departure",
|
||||
"utterance_tpl": "{callsign} holding short {runway}, ready for departure.",
|
||||
"next": [
|
||||
{"to": "TWR_LINEUP", "when": "runway_occupied === true"},
|
||||
{"to": "TWR_TAKEOFF_CLR", "when": "runway_occupied === false"}
|
||||
]
|
||||
},
|
||||
"TWR_LINEUP": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"say_tpl": "{callsign}, line up and wait runway {runway}.",
|
||||
"next": [{"to": "TWR_TAKEOFF_CLR"}]
|
||||
},
|
||||
"TWR_TAKEOFF_CLR": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"say_tpl": "{callsign}, wind {surface_wind}, runway {runway} cleared for take-off.",
|
||||
"readback_required": ["runway","cleared_takeoff"],
|
||||
"actions": [{"set": "flags.in_air", "to": true}],
|
||||
"next": [{"to": "TWR_TAKEOFF_READBACK"}]
|
||||
},
|
||||
"TWR_TAKEOFF_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "Departure",
|
||||
"utterance_tpl": "{callsign} cleared for take-off {runway}.",
|
||||
"next": [{"to": "TWR_TAKEOFF_READBACK_CHECK"}]
|
||||
},
|
||||
"TWR_TAKEOFF_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["runway","cleared_takeoff"],
|
||||
"ok_next": [{"to": "DEP_CONTACT"}],
|
||||
"bad_next": [{"to": "TWR_TAKEOFF_READBACK_CORRECT"}]
|
||||
},
|
||||
"TWR_TAKEOFF_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"say_tpl": "{callsign}, negative; runway {runway}, cleared for take-off.",
|
||||
"next": [{"to": "TWR_TAKEOFF_READBACK"}]
|
||||
},
|
||||
|
||||
"DEP_CONTACT": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"say_tpl": "{callsign}, contact Departure {departure_freq}.",
|
||||
"handoff": {"to": "DEPARTURE","freq": "{departure_freq}"},
|
||||
"actions": [{"set": "flags.current_unit", "to": "DEP"}],
|
||||
"next": [{"to": "DEP_IDENT"}]
|
||||
},
|
||||
"DEP_IDENT": {
|
||||
"role": "pilot",
|
||||
"phase": "Climb",
|
||||
"utterance_tpl": "{callsign} passing {initial_altitude_ft}, on SID {sid}.",
|
||||
"next": [{"to": "DEP_CLIMB_INSTR"}]
|
||||
},
|
||||
"DEP_CLIMB_INSTR": {
|
||||
"role": "atc",
|
||||
"phase": "Climb",
|
||||
"say_tpl": "{callsign}, climb {climb_altitude_ft} feet, proceed direct {transition} if able.",
|
||||
"next": [
|
||||
{"to": "DEP_CLIMB_READBACK", "when": "pilot_able === true"},
|
||||
{"to": "DEP_UNABLE_DIR", "when": "pilot_able === false"}
|
||||
]
|
||||
},
|
||||
"DEP_UNABLE_DIR": {
|
||||
"role": "pilot",
|
||||
"phase": "Climb",
|
||||
"utterance_tpl": "{callsign} unable direct {transition}.",
|
||||
"next": [{"to": "DEP_ALT_RTE"}]
|
||||
},
|
||||
"DEP_ALT_RTE": {
|
||||
"role": "atc",
|
||||
"phase": "Climb",
|
||||
"say_tpl": "{callsign}, continue SID, report passing {climb_altitude_ft}.",
|
||||
"next": [{"to": "ENR_HANDOFF"}]
|
||||
},
|
||||
"DEP_CLIMB_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "Climb",
|
||||
"utterance_tpl": "{callsign} climb {climb_altitude_ft}, direct {transition}.",
|
||||
"next": [{"to": "ENR_HANDOFF"}]
|
||||
},
|
||||
|
||||
"ENR_HANDOFF": {
|
||||
"role": "atc",
|
||||
"phase": "Enroute",
|
||||
"say_tpl": "{callsign}, contact Center {handoff_freq}.",
|
||||
"handoff": {"to": "CENTER","freq": "{handoff_freq}"},
|
||||
"actions": [{"set": "flags.current_unit", "to": "CTR"}],
|
||||
"next": [{"to": "ENR_CRUISE"}]
|
||||
},
|
||||
"ENR_CRUISE": {
|
||||
"role": "pilot",
|
||||
"phase": "Enroute",
|
||||
"auto": "monitor",
|
||||
"next": [{"to": "DES_INITIATE"}]
|
||||
},
|
||||
|
||||
"DES_INITIATE": {
|
||||
"role": "atc",
|
||||
"phase": "Descent",
|
||||
"say_tpl": "{callsign}, descend via {star} {transition}, QNH {qnh_hpa}.",
|
||||
"next": [{"to": "DES_READBACK"}]
|
||||
},
|
||||
"DES_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "Descent",
|
||||
"utterance_tpl": "{callsign} descend via {star} {transition}, QNH {qnh_hpa}.",
|
||||
"next": [{"to": "APP_HANDOFF"}]
|
||||
},
|
||||
|
||||
"APP_HANDOFF": {
|
||||
"role": "atc",
|
||||
"phase": "Descent",
|
||||
"say_tpl": "{callsign}, contact Approach {approach_freq}.",
|
||||
"handoff": {"to": "APPROACH","freq": "{approach_freq}"},
|
||||
"actions": [{"set": "flags.current_unit", "to": "APP"}],
|
||||
"next": [{"to": "APP_VECTORING"}]
|
||||
},
|
||||
"APP_VECTORING": {
|
||||
"role": "atc",
|
||||
"phase": "Approach",
|
||||
"say_tpl": "{callsign}, turn left heading 220, descend to {initial_altitude_ft} feet, reduce speed {speed_restriction}.",
|
||||
"next": [{"to": "APP_CLEARED_APP"}]
|
||||
},
|
||||
"APP_CLEARED_APP": {
|
||||
"role": "atc",
|
||||
"phase": "Approach",
|
||||
"say_tpl": "{callsign}, cleared {approach_type} approach runway {runway}, report established.",
|
||||
"next": [{"to": "APP_ESTABLISHED"}]
|
||||
},
|
||||
"APP_ESTABLISHED": {
|
||||
"role": "pilot",
|
||||
"phase": "Approach",
|
||||
"utterance_tpl": "{callsign} established localizer {runway}.",
|
||||
"next": [{"to": "TWR_LAND_CONTACT"}]
|
||||
},
|
||||
|
||||
"TWR_LAND_CONTACT": {
|
||||
"role": "atc",
|
||||
"phase": "Approach",
|
||||
"say_tpl": "{callsign}, contact Tower {tower_freq}.",
|
||||
"handoff": {"to": "TOWER","freq": "{tower_freq}"},
|
||||
"actions": [{"set": "flags.current_unit", "to": "TWR"}],
|
||||
"next": [{"to": "TWR_LAND_CLEARABLE"}]
|
||||
},
|
||||
"TWR_LAND_CLEARABLE": {
|
||||
"role": "atc",
|
||||
"phase": "Landing",
|
||||
"condition": "runway_available === true",
|
||||
"say_tpl": "{callsign}, wind {surface_wind}, runway {runway} cleared to land.",
|
||||
"else_say_tpl": "{callsign}, continue approach, expect late landing clearance.",
|
||||
"next": [
|
||||
{"to": "TWR_LAND_READBACK", "when": "runway_available === true"},
|
||||
{"to": "TWR_CONTINUE_APPROACH", "when": "runway_available === false"}
|
||||
]
|
||||
},
|
||||
"TWR_CONTINUE_APPROACH": {
|
||||
"role": "atc",
|
||||
"phase": "Landing",
|
||||
"say_tpl": "{callsign}, continue approach.",
|
||||
"next": [{"to": "TWR_LAND_CLEARABLE"}]
|
||||
},
|
||||
"TWR_LAND_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "Landing",
|
||||
"utterance_tpl": "{callsign} cleared to land {runway}.",
|
||||
"next": [{"to": "TWR_VACATE"}]
|
||||
},
|
||||
"TWR_VACATE": {
|
||||
"role": "atc",
|
||||
"phase": "Landing",
|
||||
"say_tpl": "{callsign}, vacate via {taxi_route}, contact Ground {ground_freq}.",
|
||||
"actions": [{"set": "flags.in_air", "to": false}],
|
||||
"handoff": {"to": "GROUND","freq": "{ground_freq}"},
|
||||
"next": [{"to": "GRD_TAXI_IN_REQ"}]
|
||||
},
|
||||
|
||||
"GRD_TAXI_IN_REQ": {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiIn",
|
||||
"utterance_tpl": "{callsign} runway vacated, request taxi to stand.",
|
||||
"next": [{"to": "GRD_TAXI_INSTR_IN"}]
|
||||
},
|
||||
"GRD_TAXI_INSTR_IN": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiIn",
|
||||
"say_tpl": "{callsign}, taxi to stand {gate} via {taxi_route}.",
|
||||
"readback_required": ["gate","taxi_route"],
|
||||
"next": [{"to": "GRD_TAXI_IN_READBACK"}]
|
||||
},
|
||||
"GRD_TAXI_IN_READBACK": {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiIn",
|
||||
"utterance_tpl": "{callsign} taxi to stand {gate} via {taxi_route}.",
|
||||
"next": [{"to": "GRD_TAXI_IN_READBACK_CHECK"}]
|
||||
},
|
||||
"GRD_TAXI_IN_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiIn",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["gate","taxi_route"],
|
||||
"ok_next": [{"to": "FLOW_COMPLETE"}],
|
||||
"bad_next": [{"to": "GRD_TAXI_IN_READBACK_CORRECT"}]
|
||||
},
|
||||
"GRD_TAXI_IN_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiIn",
|
||||
"say_tpl": "{callsign}, negative; taxi to stand {gate} via {taxi_route}.",
|
||||
"next": [{"to": "GRD_TAXI_IN_READBACK"}]
|
||||
},
|
||||
"FLOW_COMPLETE": {
|
||||
"role": "system",
|
||||
"phase": "Postflight",
|
||||
"auto": "end",
|
||||
"next": []
|
||||
},
|
||||
|
||||
/* ===== Interrupts (conditioned) ===== */
|
||||
|
||||
"INT_MAYDAY": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"guard": "flags.in_air === true",
|
||||
"utterance_tpl": "MAYDAY MAYDAY MAYDAY, {callsign}, {problem}, intentions {intent}.",
|
||||
"priority": "highest",
|
||||
"actions": [{"set": "flags.emergency_active", "to": true}],
|
||||
"next": [{"to": "ATC_MAYDAY_VECTOR"}]
|
||||
},
|
||||
"ATC_MAYDAY_VECTOR": {
|
||||
"role": "atc",
|
||||
"phase": "Interrupt",
|
||||
"say_tpl": "{callsign}, roger MAYDAY, fly heading {emergency_heading}, climb/descend {initial_altitude_ft}, cleared direct {dest} when able, QNH {qnh_hpa}.",
|
||||
"next": [{"to": "ATC_MAYDAY_COORD"}]
|
||||
},
|
||||
"ATC_MAYDAY_COORD": {
|
||||
"role": "system",
|
||||
"phase": "Interrupt",
|
||||
"actions": ["alert_emergency_services","notify_adjacent_units"],
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
|
||||
"INT_PANPAN": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"guard": "flags.in_air === true",
|
||||
"utterance_tpl": "PAN PAN PAN, {callsign}, {problem}, request priority.",
|
||||
"priority": "high",
|
||||
"actions": [{"set": "flags.emergency_active", "to": true}],
|
||||
"next": [{"to": "ATC_PAN_ACK"}]
|
||||
},
|
||||
"ATC_PAN_ACK": {
|
||||
"role": "atc",
|
||||
"phase": "Interrupt",
|
||||
"say_tpl": "{callsign}, PAN acknowledged, priority granted, expect vectors direct {dest} or nearest suitable.",
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
|
||||
"INT_TCAS_RA": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"guard": "flags.in_air === true",
|
||||
"utterance_tpl": "{callsign} TCAS RA, deviating.",
|
||||
"actions": ["suspend_clearances"],
|
||||
"next": [{"to": "ATC_TCAS_ACK"}]
|
||||
},
|
||||
"ATC_TCAS_ACK": {
|
||||
"role": "atc",
|
||||
"phase": "Interrupt",
|
||||
"say_tpl": "{callsign}, roger TCAS RA, report clear of conflict.",
|
||||
"next": [{"to": "ATC_TCAS_RESUME"}]
|
||||
},
|
||||
"ATC_TCAS_RESUME": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"utterance_tpl": "{callsign} clear of conflict, returning to clearance.",
|
||||
"actions": [{"set": "flags.emergency_active", "to": false}],
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
|
||||
"INT_GOA": {
|
||||
"role": "pilot",
|
||||
"phase": "Missed",
|
||||
"utterance_tpl": "{callsign} going around, {missed_approach}.",
|
||||
"actions": [{"set": "flags.in_air", "to": true}],
|
||||
"next": [{"to": "ATC_GOA_INSTR"}]
|
||||
},
|
||||
"ATC_GOA_INSTR": {
|
||||
"role": "atc",
|
||||
"phase": "Missed",
|
||||
"say_tpl": "{callsign}, roger go-around, fly published missed approach, climb {initial_altitude_ft}, contact Approach {approach_freq}.",
|
||||
"handoff": {"to": "APPROACH","freq": "{approach_freq}"},
|
||||
"next": [{"to": "APP_VECTORING"}]
|
||||
},
|
||||
|
||||
"INT_NORDO": {
|
||||
"role": "system",
|
||||
"phase": "LostComms",
|
||||
"trigger": "no_reply > policies.timeouts.lost_comms_detect_after_s",
|
||||
"actions": ["lost_comms_procedure"],
|
||||
"next": [{"to": "ATC_NORDO_ACTION"}]
|
||||
},
|
||||
"ATC_NORDO_ACTION": {
|
||||
"role": "atc",
|
||||
"phase": "LostComms",
|
||||
"say_tpl": "(Transmitted blind) {callsign}, if you read, squawk IDENT and continue per last clearance. Expect vectors.",
|
||||
"next": [{"to": "SYSTEM_NORDO_COORD"}]
|
||||
},
|
||||
"SYSTEM_NORDO_COORD": {
|
||||
"role": "system",
|
||||
"phase": "LostComms",
|
||||
"actions": ["notify_adjacent_units","monitor_light_gun","publish_ATIS_note"],
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
|
||||
"INT_UNABLE": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"utterance_tpl": "{callsign} unable {instruction}.",
|
||||
"next": [{"to": "ATC_ALT_PROPOSAL"}]
|
||||
},
|
||||
"ATC_ALT_PROPOSAL": {
|
||||
"role": "atc",
|
||||
"phase": "Interrupt",
|
||||
"say_tpl": "{callsign}, alternative: {alt_instruction}.",
|
||||
"next": [
|
||||
{"to": "PILOT_ACCEPT_ALT"},
|
||||
{"to": "PILOT_REJECT_ALT"}
|
||||
]
|
||||
},
|
||||
"PILOT_ACCEPT_ALT": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"utterance_tpl": "{callsign} wilco.",
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
"PILOT_REJECT_ALT": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"utterance_tpl": "{callsign} negative, request {intent}.",
|
||||
"next": [{"to": "ATC_ALT_PROPOSAL"}]
|
||||
},
|
||||
|
||||
"INT_STANDBY": {
|
||||
"role": "pilot",
|
||||
"phase": "Interrupt",
|
||||
"utterance_tpl": "{callsign} standby.",
|
||||
"actions": ["pause_exchange"],
|
||||
"next": [{"to": "RESUME_PRIOR_FLOW"}]
|
||||
},
|
||||
|
||||
/* ===== Generic glue & router ===== */
|
||||
|
||||
"RESUME_PRIOR_FLOW": {
|
||||
"role": "system",
|
||||
"phase": "Interrupt",
|
||||
"auto": "pop_stack_or_route_by_intent",
|
||||
"actions": [
|
||||
{"if": "flags.emergency_active === true && flags.in_air === true", "set": "flags.emergency_active", "to": false}
|
||||
],
|
||||
"next": []
|
||||
},
|
||||
|
||||
"GEN_NO_REPLY": {
|
||||
"role": "system",
|
||||
"phase": "Interrupt",
|
||||
"trigger": "no_reply",
|
||||
"policy_ref": "policies.no_reply_sequence",
|
||||
"escalate_to": "INT_NORDO",
|
||||
"next": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default atcDecisionTree;
|
||||
@@ -33,6 +33,35 @@ export interface DecisionNodeAutoTrigger {
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
export type DecisionNodeTriggerType = 'auto_time' | 'auto_variable' | 'regex' | 'none'
|
||||
|
||||
export interface DecisionNodeTrigger {
|
||||
id: string
|
||||
type: DecisionNodeTriggerType
|
||||
order?: number
|
||||
delaySeconds?: number
|
||||
variable?: string
|
||||
operator?: DecisionComparisonOperator
|
||||
value?: number | string | boolean
|
||||
pattern?: string
|
||||
patternFlags?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type DecisionNodeConditionType = 'variable_value' | 'regex' | 'regex_not'
|
||||
|
||||
export interface DecisionNodeCondition {
|
||||
id: string
|
||||
type: DecisionNodeConditionType
|
||||
order?: number
|
||||
variable?: string
|
||||
operator?: DecisionComparisonOperator
|
||||
value?: number | string | boolean
|
||||
pattern?: string
|
||||
patternFlags?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface DecisionTransitionMetadata {
|
||||
color?: string
|
||||
icon?: string
|
||||
@@ -114,6 +143,8 @@ export interface DecisionNodeModel {
|
||||
trigger?: string
|
||||
frequency?: string
|
||||
frequencyName?: string
|
||||
triggers?: DecisionNodeTrigger[]
|
||||
conditions?: DecisionNodeCondition[]
|
||||
transitions: DecisionNodeTransition[]
|
||||
layout?: DecisionNodeLayout
|
||||
metadata?: DecisionNodeMetadata
|
||||
@@ -198,10 +229,13 @@ export interface RuntimeDecisionState {
|
||||
frequency?: string
|
||||
frequencyName?: string
|
||||
auto_transitions?: RuntimeDecisionAutoTransition[]
|
||||
triggers?: DecisionNodeTrigger[]
|
||||
conditions?: DecisionNodeCondition[]
|
||||
metadata?: DecisionNodeMetadata
|
||||
}
|
||||
|
||||
export interface RuntimeDecisionTree {
|
||||
slug: string
|
||||
schema_version: string
|
||||
name: string
|
||||
description?: string
|
||||
@@ -216,6 +250,12 @@ export interface RuntimeDecisionTree {
|
||||
states: Record<string, RuntimeDecisionState>
|
||||
}
|
||||
|
||||
export interface RuntimeDecisionSystem {
|
||||
main: string
|
||||
order: string[]
|
||||
flows: Record<string, RuntimeDecisionTree>
|
||||
}
|
||||
|
||||
export interface DecisionFlowSummary {
|
||||
id: string
|
||||
slug: string
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export interface LLMDecisionInput {
|
||||
state_id: string
|
||||
state: any
|
||||
candidates: Array<{ id: string; state: any }>
|
||||
candidates: Array<{ id: string; state: any; flow?: string }>
|
||||
variables: Record<string, any>
|
||||
flags: Record<string, any>
|
||||
pilot_utterance: string
|
||||
flow_slug?: string
|
||||
}
|
||||
|
||||
export interface LLMDecision {
|
||||
@@ -14,4 +15,6 @@ export interface LLMDecision {
|
||||
controller_say_tpl?: string
|
||||
off_schema?: boolean
|
||||
radio_check?: boolean
|
||||
activate_flow?: string
|
||||
resume_previous?: boolean
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// communicationsEngine composable
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { ref, computed, readonly, reactive } from 'vue'
|
||||
import type {
|
||||
RuntimeDecisionTree,
|
||||
RuntimeDecisionSystem,
|
||||
RuntimeDecisionState,
|
||||
RuntimeDecisionAutoTransition,
|
||||
DecisionNodeAutoTrigger,
|
||||
@@ -93,6 +94,19 @@ export interface EngineLog {
|
||||
state: string
|
||||
radioCheck?: boolean
|
||||
offSchema?: boolean
|
||||
flow?: string
|
||||
}
|
||||
|
||||
interface FlowSnapshot {
|
||||
tree: RuntimeDecisionTree
|
||||
variables: Record<string, any>
|
||||
flags: EngineFlags
|
||||
telemetry: TelemetryState
|
||||
currentStateId: string
|
||||
communicationLog: EngineLog[]
|
||||
autoHistory: Map<string, Set<string>>
|
||||
flightContext: FlightContext
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
type TelemetryState = {
|
||||
@@ -111,6 +125,33 @@ export function normalizeATCText(text: string, context: Record<string, any>): st
|
||||
return normalizeRadioPhrase(rendered)
|
||||
}
|
||||
|
||||
function createDefaultFlightContext(): FlightContext {
|
||||
return {
|
||||
callsign: '',
|
||||
aircraft: 'A320',
|
||||
dep: 'EDDF',
|
||||
dest: 'EDDM',
|
||||
stand: 'A12',
|
||||
runway: '25R',
|
||||
squawk: '1234',
|
||||
atis_code: 'K',
|
||||
sid: 'ANEKI7S',
|
||||
transition: 'ANEKI',
|
||||
flight_level: 'FL360',
|
||||
atis_freq: '118.025',
|
||||
ground_freq: '121.700',
|
||||
tower_freq: '118.700',
|
||||
departure_freq: '125.350',
|
||||
approach_freq: '120.800',
|
||||
handoff_freq: '121.800',
|
||||
qnh_hpa: 1015,
|
||||
taxi_route: 'A, V',
|
||||
remarks: 'standard',
|
||||
time_now: undefined,
|
||||
phase: 'clearance',
|
||||
}
|
||||
}
|
||||
|
||||
function renderTpl(tpl: string, ctx: Record<string, any>): string {
|
||||
return tpl.replace(/\{([\w.]+)\}/g, (_m, key) => {
|
||||
const parts = key.split('.')
|
||||
@@ -121,9 +162,15 @@ function renderTpl(tpl: string, ctx: Record<string, any>): string {
|
||||
}
|
||||
|
||||
export default function useCommunicationsEngine() {
|
||||
const runtimeSystem = ref<RuntimeDecisionSystem | null>(null)
|
||||
const flowOrder = ref<string[]>([])
|
||||
const activeFlowSlug = ref<string>('')
|
||||
|
||||
const tree = ref<RuntimeDecisionTree | null>(null)
|
||||
const ready = ref(false)
|
||||
|
||||
const flowSnapshots = reactive<Record<string, FlowSnapshot>>({})
|
||||
|
||||
const states = computed<Record<string, RuntimeDecisionState>>(() => tree.value?.states ?? {})
|
||||
|
||||
const variables = ref<Record<string, any>>({})
|
||||
@@ -148,31 +195,8 @@ export default function useCommunicationsEngine() {
|
||||
heading_deg: 0,
|
||||
})
|
||||
|
||||
const autoExecutionHistory = new Map<string, Set<string>>()
|
||||
|
||||
// Flight context used for pm_alt.vue integration
|
||||
const flightContext = ref<FlightContext>({
|
||||
callsign: '',
|
||||
aircraft: 'A320',
|
||||
dep: 'EDDF',
|
||||
dest: 'EDDM',
|
||||
stand: 'A12',
|
||||
runway: '25R',
|
||||
squawk: '1234',
|
||||
atis_code: 'K',
|
||||
sid: 'ANEKI7S',
|
||||
transition: 'ANEKI',
|
||||
flight_level: 'FL360',
|
||||
atis_freq: '118.025',
|
||||
ground_freq: '121.700',
|
||||
tower_freq: '118.700',
|
||||
departure_freq: '125.350',
|
||||
approach_freq: '120.800',
|
||||
handoff_freq: '121.800',
|
||||
qnh_hpa: 1015,
|
||||
taxi_route: 'A, V',
|
||||
phase: 'clearance'
|
||||
})
|
||||
const flightContext = ref<FlightContext>(createDefaultFlightContext())
|
||||
|
||||
const currentState = computed<RuntimeDecisionState & { id: string } | null>(() => {
|
||||
const stateMap = states.value
|
||||
@@ -183,6 +207,140 @@ export default function useCommunicationsEngine() {
|
||||
return base ? { ...base, id } : null
|
||||
})
|
||||
|
||||
function ensureSnapshot(slug: string): FlowSnapshot {
|
||||
const snapshot = flowSnapshots[slug]
|
||||
if (!snapshot) {
|
||||
throw new Error(`Flow snapshot not loaded: ${slug}`)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function getActiveSnapshot(): FlowSnapshot | null {
|
||||
if (!activeFlowSlug.value) return null
|
||||
return flowSnapshots[activeFlowSlug.value] || null
|
||||
}
|
||||
|
||||
function assignActiveVariables(next: Record<string, any>) {
|
||||
variables.value = next
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].variables = next
|
||||
}
|
||||
}
|
||||
|
||||
function assignActiveFlags(next: EngineFlags) {
|
||||
flags.value = next
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].flags = next
|
||||
}
|
||||
}
|
||||
|
||||
function assignActiveTelemetry(next: TelemetryState) {
|
||||
telemetry.value = next
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].telemetry = next
|
||||
}
|
||||
}
|
||||
|
||||
function assignCommunicationLog(next: EngineLog[]) {
|
||||
communicationLog.value = next
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].communicationLog = next
|
||||
}
|
||||
}
|
||||
|
||||
function assignFlightContext(next: FlightContext) {
|
||||
flightContext.value = next
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].flightContext = next
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveStateId(stateId: string) {
|
||||
currentStateId.value = stateId
|
||||
if (activeFlowSlug.value && flowSnapshots[activeFlowSlug.value]) {
|
||||
flowSnapshots[activeFlowSlug.value].currentStateId = stateId
|
||||
}
|
||||
}
|
||||
|
||||
function createSnapshotFromTree(treeData: RuntimeDecisionTree): FlowSnapshot {
|
||||
const variables = { ...treeData.variables }
|
||||
const baseFlags = (treeData.flags && typeof treeData.flags === 'object') ? { ...treeData.flags } : {}
|
||||
const stack = Array.isArray((baseFlags as any).stack) ? [...(baseFlags as any).stack] : []
|
||||
const flags: EngineFlags = {
|
||||
in_air: Boolean((baseFlags as any).in_air),
|
||||
emergency_active: Boolean((baseFlags as any).emergency_active),
|
||||
current_unit: typeof (baseFlags as any).current_unit === 'string'
|
||||
? (baseFlags as any).current_unit
|
||||
: 'DEL',
|
||||
stack,
|
||||
off_schema_count: Number((baseFlags as any).off_schema_count) || 0,
|
||||
radio_checks_done: Number((baseFlags as any).radio_checks_done) || 0,
|
||||
...(baseFlags as EngineFlags),
|
||||
}
|
||||
if (!Array.isArray(flags.stack)) {
|
||||
flags.stack = []
|
||||
}
|
||||
|
||||
const telemetry: TelemetryState = {
|
||||
altitude_ft: Number((baseFlags as any).altitude_ft) || 0,
|
||||
speed_kts: Number((baseFlags as any).speed_kts) || 0,
|
||||
groundspeed_kts: Number((baseFlags as any).groundspeed_kts) || 0,
|
||||
vertical_speed_fpm: Number((baseFlags as any).vertical_speed_fpm) || 0,
|
||||
latitude_deg: Number((baseFlags as any).latitude_deg) || 0,
|
||||
longitude_deg: Number((baseFlags as any).longitude_deg) || 0,
|
||||
heading_deg: Number((baseFlags as any).heading_deg) || 0,
|
||||
}
|
||||
|
||||
const log: EngineLog[] = []
|
||||
const snapshotContext = createDefaultFlightContext()
|
||||
snapshotContext.phase = 'clearance'
|
||||
|
||||
const autoHistory = new Map<string, Set<string>>()
|
||||
if (treeData.start_state) {
|
||||
autoHistory.set(treeData.start_state, new Set())
|
||||
}
|
||||
|
||||
return {
|
||||
tree: treeData,
|
||||
variables,
|
||||
flags,
|
||||
telemetry,
|
||||
currentStateId: treeData.start_state,
|
||||
communicationLog: log,
|
||||
autoHistory,
|
||||
flightContext: snapshotContext,
|
||||
ready: true,
|
||||
}
|
||||
}
|
||||
|
||||
function persistActiveSnapshot() {
|
||||
if (!activeFlowSlug.value) return
|
||||
const snapshot = flowSnapshots[activeFlowSlug.value]
|
||||
if (!snapshot) return
|
||||
snapshot.variables = variables.value
|
||||
snapshot.flags = flags.value
|
||||
snapshot.telemetry = telemetry.value
|
||||
snapshot.currentStateId = currentStateId.value
|
||||
snapshot.communicationLog = communicationLog.value
|
||||
snapshot.flightContext = flightContext.value
|
||||
snapshot.ready = ready.value
|
||||
}
|
||||
|
||||
function activateFlow(slug: string) {
|
||||
const snapshot = ensureSnapshot(slug)
|
||||
if (activeFlowSlug.value && activeFlowSlug.value !== slug) {
|
||||
persistActiveSnapshot()
|
||||
}
|
||||
activeFlowSlug.value = slug
|
||||
tree.value = snapshot.tree
|
||||
assignActiveVariables(snapshot.variables)
|
||||
assignActiveFlags(snapshot.flags)
|
||||
assignActiveTelemetry(snapshot.telemetry)
|
||||
assignCommunicationLog(snapshot.communicationLog)
|
||||
assignFlightContext(snapshot.flightContext)
|
||||
setActiveStateId(snapshot.currentStateId)
|
||||
ready.value = snapshot.ready
|
||||
}
|
||||
const nextCandidates = computed<string[]>(() => {
|
||||
const s = currentState.value
|
||||
if (!s) return []
|
||||
@@ -210,69 +368,137 @@ export default function useCommunicationsEngine() {
|
||||
return tree.value
|
||||
}
|
||||
|
||||
function resetAutoHistory(stateId: string) {
|
||||
autoExecutionHistory.set(stateId, new Set())
|
||||
function resetAutoHistory(stateId: string, slug = activeFlowSlug.value) {
|
||||
if (!slug) return
|
||||
const snapshot = ensureSnapshot(slug)
|
||||
snapshot.autoHistory.set(stateId, new Set())
|
||||
}
|
||||
|
||||
function markAutoExecuted(stateId: string, transitionId: string) {
|
||||
if (!autoExecutionHistory.has(stateId)) {
|
||||
autoExecutionHistory.set(stateId, new Set())
|
||||
function markAutoExecuted(stateId: string, transitionId: string, slug = activeFlowSlug.value) {
|
||||
if (!slug) return
|
||||
const snapshot = ensureSnapshot(slug)
|
||||
if (!snapshot.autoHistory.has(stateId)) {
|
||||
snapshot.autoHistory.set(stateId, new Set())
|
||||
}
|
||||
autoExecutionHistory.get(stateId)!.add(transitionId)
|
||||
snapshot.autoHistory.get(stateId)!.add(transitionId)
|
||||
}
|
||||
|
||||
function hasAutoExecuted(stateId: string, transitionId: string): boolean {
|
||||
const set = autoExecutionHistory.get(stateId)
|
||||
function hasAutoExecuted(stateId: string, transitionId: string, slug = activeFlowSlug.value): boolean {
|
||||
if (!slug) return false
|
||||
const snapshot = ensureSnapshot(slug)
|
||||
const set = snapshot.autoHistory.get(stateId)
|
||||
return set ? set.has(transitionId) : false
|
||||
}
|
||||
|
||||
function resetEngineFromTree(treeData: RuntimeDecisionTree) {
|
||||
tree.value = treeData
|
||||
variables.value = { ...treeData.variables }
|
||||
const baseFlags = (treeData.flags && typeof treeData.flags === 'object') ? { ...treeData.flags } : {}
|
||||
const stack = Array.isArray(baseFlags.stack) ? [...baseFlags.stack] : []
|
||||
flags.value = {
|
||||
in_air: Boolean(baseFlags.in_air),
|
||||
emergency_active: Boolean(baseFlags.emergency_active),
|
||||
current_unit: typeof baseFlags.current_unit === 'string' ? baseFlags.current_unit : 'DEL',
|
||||
stack,
|
||||
off_schema_count: 0,
|
||||
radio_checks_done: 0,
|
||||
...baseFlags,
|
||||
const system: RuntimeDecisionSystem = {
|
||||
main: treeData.slug,
|
||||
order: [treeData.slug],
|
||||
flows: { [treeData.slug]: treeData },
|
||||
}
|
||||
if (!Array.isArray(flags.value.stack)) {
|
||||
flags.value.stack = []
|
||||
resetEngineFromSystem(system, { activeSlug: treeData.slug })
|
||||
}
|
||||
|
||||
function resetEngineFromSystem(system: RuntimeDecisionSystem, options: { activeSlug?: string } = {}) {
|
||||
runtimeSystem.value = system
|
||||
const order = Array.isArray(system.order) && system.order.length
|
||||
? [...system.order]
|
||||
: Object.keys(system.flows)
|
||||
flowOrder.value = order
|
||||
|
||||
for (const key of Object.keys(flowSnapshots)) {
|
||||
delete flowSnapshots[key]
|
||||
}
|
||||
currentStateId.value = treeData.start_state
|
||||
communicationLog.value = []
|
||||
telemetry.value = {
|
||||
altitude_ft: Number(baseFlags.altitude_ft) || 0,
|
||||
speed_kts: Number(baseFlags.speed_kts) || 0,
|
||||
groundspeed_kts: Number(baseFlags.groundspeed_kts) || 0,
|
||||
vertical_speed_fpm: Number(baseFlags.vertical_speed_fpm) || 0,
|
||||
latitude_deg: Number(baseFlags.latitude_deg) || 0,
|
||||
longitude_deg: Number(baseFlags.longitude_deg) || 0,
|
||||
heading_deg: Number(baseFlags.heading_deg) || 0,
|
||||
|
||||
for (const slug of order) {
|
||||
const treeData = system.flows[slug]
|
||||
if (!treeData) continue
|
||||
flowSnapshots[slug] = createSnapshotFromTree(treeData)
|
||||
}
|
||||
|
||||
const preferred = options.activeSlug && system.flows[options.activeSlug]
|
||||
? options.activeSlug
|
||||
: system.main && system.flows[system.main]
|
||||
? system.main
|
||||
: order[0]
|
||||
|
||||
if (preferred) {
|
||||
activateFlow(preferred)
|
||||
ready.value = true
|
||||
const snapshot = ensureSnapshot(preferred)
|
||||
resetAutoHistory(snapshot.currentStateId, preferred)
|
||||
evaluateAutoTransitions()
|
||||
} else {
|
||||
activeFlowSlug.value = ''
|
||||
tree.value = null
|
||||
ready.value = false
|
||||
assignActiveVariables({})
|
||||
assignActiveFlags({
|
||||
in_air: false,
|
||||
emergency_active: false,
|
||||
current_unit: 'DEL',
|
||||
stack: [],
|
||||
off_schema_count: 0,
|
||||
radio_checks_done: 0,
|
||||
})
|
||||
assignActiveTelemetry({
|
||||
altitude_ft: 0,
|
||||
speed_kts: 0,
|
||||
groundspeed_kts: 0,
|
||||
vertical_speed_fpm: 0,
|
||||
latitude_deg: 0,
|
||||
longitude_deg: 0,
|
||||
heading_deg: 0,
|
||||
})
|
||||
assignCommunicationLog([])
|
||||
assignFlightContext(createDefaultFlightContext())
|
||||
setActiveStateId('')
|
||||
}
|
||||
autoExecutionHistory.clear()
|
||||
resetAutoHistory(currentStateId.value)
|
||||
flightContext.value.phase = 'clearance'
|
||||
ready.value = true
|
||||
evaluateAutoTransitions()
|
||||
}
|
||||
|
||||
function loadRuntimeTree(data: RuntimeDecisionTree) {
|
||||
resetEngineFromTree(data)
|
||||
}
|
||||
|
||||
function loadRuntimeSystem(data: RuntimeDecisionSystem, options: { activeSlug?: string } = {}) {
|
||||
resetEngineFromSystem(data, options)
|
||||
}
|
||||
|
||||
const activeFlow = computed(() => activeFlowSlug.value)
|
||||
|
||||
const availableFlows = computed(() => {
|
||||
if (!runtimeSystem.value) return [] as Array<{ slug: string; name: string; description?: string; start: string }>
|
||||
return flowOrder.value
|
||||
.filter((slug) => Boolean(runtimeSystem.value!.flows[slug]))
|
||||
.map((slug) => {
|
||||
const treeData = runtimeSystem.value!.flows[slug]
|
||||
return {
|
||||
slug,
|
||||
name: treeData.name || slug,
|
||||
description: treeData.description,
|
||||
start: treeData.start_state,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function setActiveFlow(slug: string) {
|
||||
if (!slug || !flowSnapshots[slug]) {
|
||||
throw new Error(`Flow snapshot not loaded: ${slug}`)
|
||||
}
|
||||
activateFlow(slug)
|
||||
ready.value = true
|
||||
queueMicrotask(() => evaluateAutoTransitions())
|
||||
}
|
||||
|
||||
async function fetchRuntimeTree(slug = 'icao_atc_decision_tree') {
|
||||
ready.value = false
|
||||
const fetcher: any = (globalThis as any).$fetch
|
||||
if (typeof fetcher !== 'function') {
|
||||
throw new Error('Universal fetch is not available in this context')
|
||||
}
|
||||
const data = await fetcher<RuntimeDecisionTree>(`/api/decision-flows/${slug}/runtime`)
|
||||
resetEngineFromTree(data)
|
||||
const data = await fetcher<RuntimeDecisionSystem>('/api/decision-flows/runtime')
|
||||
const activeSlug = slug && data.flows[slug] ? slug : data.main
|
||||
resetEngineFromSystem(data, { activeSlug })
|
||||
}
|
||||
|
||||
function normalizeComparableValue(value: any): any {
|
||||
@@ -417,7 +643,7 @@ export default function useCommunicationsEngine() {
|
||||
function initializeFlight(fpl: any) {
|
||||
const runtime = ensureTree()
|
||||
// Set variables
|
||||
variables.value = {
|
||||
const nextVariables = {
|
||||
...variables.value,
|
||||
callsign: fpl.callsign || fpl.callsign,
|
||||
acf_type: fpl.aircraft?.split('/')[0] || 'A320',
|
||||
@@ -448,6 +674,7 @@ export default function useCommunicationsEngine() {
|
||||
remarks: 'standard',
|
||||
time_now: new Date().toISOString()
|
||||
}
|
||||
assignActiveVariables(nextVariables)
|
||||
|
||||
// Update flight context
|
||||
Object.assign(flightContext.value, {
|
||||
@@ -455,7 +682,7 @@ export default function useCommunicationsEngine() {
|
||||
phase: 'clearance'
|
||||
})
|
||||
|
||||
flags.value = {
|
||||
const nextFlags: EngineFlags = {
|
||||
...flags.value,
|
||||
in_air: false,
|
||||
emergency_active: false,
|
||||
@@ -464,10 +691,11 @@ export default function useCommunicationsEngine() {
|
||||
off_schema_count: 0,
|
||||
radio_checks_done: 0
|
||||
}
|
||||
assignActiveFlags(nextFlags)
|
||||
|
||||
currentStateId.value = runtime.start_state
|
||||
communicationLog.value = []
|
||||
resetAutoHistory(currentStateId.value)
|
||||
setActiveStateId(runtime.start_state)
|
||||
assignCommunicationLog([])
|
||||
resetAutoHistory(runtime.start_state)
|
||||
}
|
||||
|
||||
function updateFrequencyVariables(update: Partial<Record<FrequencyVariableKey, string>>) {
|
||||
@@ -495,7 +723,7 @@ export default function useCommunicationsEngine() {
|
||||
throw new Error('Decision state unavailable')
|
||||
}
|
||||
const candidates = nextCandidates.value
|
||||
.map(id => ({ id, state: states.value[id] }))
|
||||
.map(id => ({ id, state: states.value[id], flow: runtime.slug }))
|
||||
.filter(candidate => candidate.state)
|
||||
|
||||
return {
|
||||
@@ -506,6 +734,7 @@ export default function useCommunicationsEngine() {
|
||||
flags: { ...flags.value },
|
||||
pilot_utterance: pilotTranscript,
|
||||
tree: runtime.name,
|
||||
flow_slug: runtime.slug,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +759,14 @@ export default function useCommunicationsEngine() {
|
||||
flags.value.stack = decision.stack.slice()
|
||||
}
|
||||
|
||||
if (decision.activate_flow && decision.activate_flow !== activeFlowSlug.value) {
|
||||
try {
|
||||
setActiveFlow(decision.activate_flow)
|
||||
} catch (err) {
|
||||
console.warn('[Engine] Failed to activate flow from decision', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (decision.off_schema) {
|
||||
flags.value.off_schema_count++
|
||||
console.log(`[Engine] Off-schema response #${flags.value.off_schema_count}`)
|
||||
@@ -624,7 +861,7 @@ export default function useCommunicationsEngine() {
|
||||
const fallback = typeof raw === 'number' ? raw : Number(raw)
|
||||
next[key] = Number.isNaN(fallback) ? current : fallback
|
||||
}
|
||||
telemetry.value = next
|
||||
assignActiveTelemetry(next)
|
||||
queueMicrotask(() => evaluateAutoTransitions())
|
||||
}
|
||||
|
||||
@@ -693,7 +930,7 @@ export default function useCommunicationsEngine() {
|
||||
flags.value.stack.push(currentStateId.value)
|
||||
}
|
||||
|
||||
currentStateId.value = stateId
|
||||
setActiveStateId(stateId)
|
||||
resetAutoHistory(stateId)
|
||||
const s = currentState.value
|
||||
if (!s) return
|
||||
@@ -764,7 +1001,8 @@ export default function useCommunicationsEngine() {
|
||||
normalized: normalizeATCText(msg, exposeCtxFlat()),
|
||||
state: stateId,
|
||||
radioCheck: options.radioCheck,
|
||||
offSchema: options.offSchema
|
||||
offSchema: options.offSchema,
|
||||
flow: activeFlowSlug.value || undefined,
|
||||
}
|
||||
communicationLog.value.push(entry)
|
||||
}
|
||||
@@ -855,7 +1093,9 @@ export default function useCommunicationsEngine() {
|
||||
nextCandidates,
|
||||
activeFrequency,
|
||||
communicationLog: readonly(communicationLog),
|
||||
clearCommunicationLog: () => { communicationLog.value = [] },
|
||||
clearCommunicationLog: () => { assignCommunicationLog([]) },
|
||||
activeFlow,
|
||||
availableFlows,
|
||||
|
||||
// pm_alt.vue integration
|
||||
flightContext: readonly(flightContext),
|
||||
@@ -865,7 +1105,9 @@ export default function useCommunicationsEngine() {
|
||||
initializeFlight,
|
||||
updateFrequencyVariables,
|
||||
loadRuntimeTree,
|
||||
loadRuntimeSystem,
|
||||
fetchRuntimeTree,
|
||||
setActiveFlow,
|
||||
isReady,
|
||||
|
||||
// Communication
|
||||
|
||||
Reference in New Issue
Block a user