# cpdlc.nas --- CPDLC library # Copyright (C) 2020 Henning Stahlke # # This file is part of FlightGear. # # FlightGear is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # FlightGear is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with FlightGear. If not, see . # # Author: Henning Stahlke # Created: 2020-11-14 # #-------------------------------------------------------------------------- # Note: this library is work in progress. Once it is stable, it should be # added to FGDATA #-------------------------------------------------------------------------- #print(caller(0)[2]); #-------------------------------------------------------------------------- # fgcommands # cpdlc-connect # cpdlc-send # cpdlc-next-message # cpdlc-disconnect #-------------------------------------------------------------------------- # Example: # var tx = "/network/cpdlc/input/message"; # fgcommand("cpdlc-connect", props.Node.new( {atc: "EDDHgnd"} )); # fgcommand("cpdlc-send", props.Node.new( {message: "TXTD-1 Hello"} )); # fgcommand("cpdlc-send", props.Node.new( {property: tx} )); # fgcommand("cpdlc-disconnect"); #-------------------------------------------------------------------------- var ARG_FL_ALT = 1; var ARG_SPEED = 2; var ARG_NAVPOS = 3; var ARG_ROUTE = 4; var ARG_XPDR = 5; var ARG_CALLSIGN = 6; var ARG_FREQ = 7; var ARG_TIME = 8; var ARG_DIRECTION = 9; var ARG_DEGREES = 10; var ARG_ATIS_CODE = 11; var ARG_DEVIATION_TYPE = 12; var ARG_ENDURANCE = 13; #remaining fuel as time in seconds var ARG_LEGTYPE = 14; var ARG_TEXT = 15; var ARG_INTEGER = 16; #keys according to tables in ICAO doc 4444 var responses = { # W/U in Doc 4444 w: {id: "RSPD-1", txt: "WILCO"}, u: {id: "RSPD-2", txt: "UNABLE"}, # A/N in Doc 4444 a: {id: "RSPD-5", txt: "AFFIRM"}, n: {id: "RSPD-6", txt: "NEGATIVE"}, s: {id: "RSPD-3", txt: "STANDBY"}, r: {id: "RSPD-4", txt: "ROGER"}, # need clarification # single Y in Doc 4444 means any? }; #-- messages from ATC to aircraft -- # do not add "s" to r_opts, it is added automatically var uplink_messages = { "RTEU-2": { txt: "PROCEED DIRECT TO $1", args: [ARG_NAVPOS], r_opts: ["w","u"] }, "RTEU-3": { txt: "AT TIME $1 PROCEED DIRECT TO $2", args: [ARG_TIME, ARG_NAVPOS], r_opts: ["w","u"] }, "RTEU-4": { txt: "AT $1 PROCEED DIRECT TO $2", args: [ARG_NAVPOS, ARG_NAVPOS], r_opts: ["w","u"] }, "RTEU-6": { txt: "CLEARED TO $1 VIA $2", args: [ARG_NAVPOS, ARG_ROUTE], r_opts: ["w","u"] }, "RTEU-7": { txt: "CLEARED $1", args: [ARG_ROUTE], r_opts: ["w","u"] }, "RTEU-11": { txt: "AT $1 HOLD INBOUND TRACK $2 $3 TURNS $4 LEGS", args: [ARG_NAVPOS, ARG_DEGREES, ARG_DIRECTION, ARG_LEGTYPE], r_opts: ["w","u"] }, "RTEU-12": { txt: "AT $1 HOLD AS PUBLISHED", args: [ARG_NAVPOS], r_opts: ["w","u"] }, "RTEU-13": { txt: "EXPECT FURTHER CLEARANCE AT $1", args: [ARG_TIME], r_opts: ["w","u"] }, "RTEU-16": { txt: "REQUEST POSITION REPORT", args: [], r_opts: ["w","u"] }, "RTEU-17": { txt: "ADVISE ETA $1", args: [ARG_NAVPOS], r_opts: ["w","u"] }, "LATU-9": { txt: "RESUME OWN NAVIGATION", args: [], r_opts: ["w","u"] }, "LATU-11": { txt: "TURN $1 HEADING $2", args: [ARG_DIRECTION, ARG_DEGREES], r_opts: ["w","u"] }, "LATU-12": { txt: "TURN $1 GROUND TRACK $2", args: [ARG_DIRECTION, ARG_DEGREES], r_opts: ["w","u"] }, "LATU-14": { txt: "CONTINUE PRESENT HEADING", args: [], r_opts: ["w","u"] }, "LATU-16": { txt: "FLY HEADING $1", args: [ARG_DEGREES], r_opts: ["w","u"] }, "LATU-19": { txt: "REPORT PASSING $1", args: [ARG_NAVPOS], r_opts: ["w","u"] }, "LVLU-5": { txt: "MAINTAIN $1", args: [ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-6": { txt: "CLIMB TO $1", args: [ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-7": { txt: "AT TIME $1 CLIMB TO $2", args: [ARG_TIME, ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-8": { txt: "AT $1 CLIMB TO $2", args: [ARG_NAVPOS, ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-9": { txt: "DESCENT TO $1", args: [ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-10": { txt: "AT TIME $1 DESCENT TO $2", args: [ARG_TIME, ARG_FL_ALT], r_opts: ["w","u"] }, "LVLU-11": { txt: "AT $1 DESCENT TO $2", args: [ARG_NAVPOS, ARG_FL_ALT], r_opts: ["w","u"] }, "CSTU-1": { txt: "CROSS $1 AT $2", args: [ARG_NAVPOS, ARG_FL_ALT], r_opts: ["w","u"] }, "CSTU-2": { txt: "CROSS $1 AT OR ABOVE $2", args: [ARG_NAVPOS, ARG_FL_ALT], r_opts: ["w","u"] }, "CSTU-3": { txt: "CROSS $1 AT OR BELOW $2", args: [ARG_NAVPOS, ARG_FL_ALT], r_opts: ["w","u"] }, "CSTU-4": { txt: "CROSS $1 AT TIME $2", args: [ARG_NAVPOS, ARG_TIME], r_opts: ["w","u"] }, "CSTU-5": { txt: "CROSS $1 BEFORE TIME $2", args: [ARG_NAVPOS, ARG_TIME], r_opts: ["w","u"] }, "CSTU-6": { txt: "CROSS $1 AFTER TIME $2", args: [ARG_NAVPOS, ARG_TIME], r_opts: ["w","u"] }, "CSTU-7": { txt: "CROSS $1 BETWEEN TIME $2 AND TIME $3", args: [ARG_NAVPOS, ARG_TIME, ARG_TIME], r_opts: ["w","u"] }, "SPDU-4": { txt: "MAINTAIN $1", args: [ARG_SPEED], r_opts: ["w","u"] }, "SPDU-5": { txt: "MAINTAIN PRESENT SPEED", args: [], r_opts: ["w","u"] }, "SPDU-9": { txt: "INCREASE SPEED TO $1", args: [ARG_SPEED], r_opts: ["w","u"] }, "SPDU-11": { txt: "REDUCE SPEED TO $1", args: [ARG_SPEED], r_opts: ["w","u"] }, "SPDU-13": { txt: "RESUME NORMAL SPEED", args: [], r_opts: ["w","u"] }, "ADVU-2": { txt: "SERVICE TERMINATED", args: [], r_opts: ["w","u"] }, "ADVU-3": { txt: "IDENTIFIED $1", args: [], r_opts: ["w","u"] }, "ADVU-4": { txt: "IDENTIFICATION LOST", args: [], r_opts: ["w","u"] }, "ADVU-5": { txt: "ATIS $1", args: [ARG_ATIS_CODE], r_opts: ["w","u"] }, "ADVU-9": { txt: "SQUAWK $1", args: [ARG_XPDR], r_opts: ["w","u"] }, "ADVU-15": { txt: "SQUAWK IDENT", args: [], r_opts: ["w","u"] }, "ADVU-19": { txt: "$1 DEVIATION DETECTED. VERIFY AND ADVISE", args: [ARG_DEVIATION_TYPE], r_opts: ["w","u"] }, "COMU-1": { txt: "CONTACT $1 $2", args: [ARG_CALLSIGN, ARG_FREQ], r_opts: ["w","u"] }, "COMU-2": { txt: "AT $1 CONTACT $2 $3", args: [ARG_NAVPOS, ARG_CALLSIGN, ARG_FREQ], r_opts: ["w","u"] }, "COMU-5": { txt: "MONITOR $1 $2", args: [ARG_CALLSIGN, ARG_FREQ], r_opts: ["w","u"] }, "COMU-9": { txt: "CURRENT ATC UNIT $1", args: [ARG_CALLSIGN], r_opts: [] }, "EMGU-1": { txt: "REPORT ENDURANCE AND POB", args: [], r_opts: ["y"] }, "RSPU-1": { txt: "UNABLE", args: [], r_opts: [] }, "RSPU-2": { txt: "STANDBY", args: [], r_opts: [] }, "RSPU-4": { txt: "ROGER", args: [], r_opts: [] }, "RSPU-5": { txt: "AFFIRM", args: [], r_opts: [] }, "RSPU-6": { txt: "NEGATIVE", args: [], r_opts: [] }, "TXTU-1": { txt: "$1", args: [ARG_TEXT], r_opts: ["r"] }, "TXTU-4": { txt: "$1", args: [ARG_TEXT], r_opts: ["w","u"] }, "TXTU-5": { txt: "$1", args: [ARG_TEXT], r_opts: ["a","n"] }, }; #-- messages from aircraft to ATC -- var downlink_messages = { "RTED-1": { txt: "REQUEST DIRECT TO $1", args: [ARG_NAVPOS], r_opts: ["y"] }, "RTED-3": { txt: "REQUEST CLEARANCE $1", args: [ARG_ROUTE], r_opts: ["y"] }, "RTED-5": { txt: "POSITION REPORT $1", args: [ARG_NAVPOS], r_opts: ["y"] }, "RTED-6": { txt: "REQUEST HEADING $1", args: [ARG_DEGREES], r_opts: ["y"] }, "RTED-7": { txt: "REQUEST GROUND TRACK $1", args: [ARG_DEGREES], r_opts: ["y"] }, "RTED-8": { txt: "WHEN CAN WE EXPECT BACK ON ROUTE", args: [], r_opts: ["y"] }, "RTED-10": { txt: "ETA $1 TIME $2", args: [ARG_NAVPOS, ARG_TIME], r_opts: ["n"] }, "LATD-3": { txt: "CLEAR OF WEATHER", args: [], r_opts: ["n"] }, "LATD-4": { txt: "BACK ON ROUTE", args: [], r_opts: ["n"] }, "LATD-8": { txt: "PASSING $1", args: [ARG_NAVPOS], r_opts: ["n"] }, "LVLD-1": { txt: "REQUEST LEVEL $1", args: [ARG_FL_ALT], r_opts: ["y"] }, "LVLD-6": { txt: "WHEN CAN WE EXPECT LOWER LEVEL", args: [], r_opts: ["y"] }, "LVLD-7": { txt: "WHEN CAN WE EXPECT HIGHER LEVEL", args: [], r_opts: ["y"] }, "LVLD-8": { txt: "LEAVING LEVEL $1", args: [ARG_FL_ALT], r_opts: ["n"] }, "LVLD-9": { txt: "MAINTAINING LEVEL $1", args: [ARG_FL_ALT], r_opts: ["n"] }, "SPDD-1": { txt: "REQUEST SPEED $1", args: [ARG_SPEED], r_opts: ["y"] }, "RSPD-1": { txt: "WILCO", args: [], r_opts: [] }, "RSPD-2": { txt: "UNABLE", args: [], r_opts: [] }, "RSPD-3": { txt: "STANDBY", args: [], r_opts: [] }, "RSPD-4": { txt: "ROGER", args: [], r_opts: [] }, "RSPD-5": { txt: "AFFIRM", args: [], r_opts: [] }, "RSPD-6": { txt: "NEGATIVE", args: [], r_opts: [] }, "COMD-1": { txt: "REQUEST VOICE CONTACT $1", args: [ARG_FREQ], r_opts: ["y"] }, "EMGD-1": { txt: "PAN PAN PAN", args: [], r_opts: ["y"] }, "EMGD-2": { txt: "MAYDAY MAYDAY MAYDAY", args: [], r_opts: ["y"] }, "EMGD-3": { txt: "$1 ENDURANCE AND $2 POB", args: [ARG_ENDURANCE, ARG_INTEGER], r_opts: ["y"] }, "EMGD-4": { txt: "CANCEL EMERGENCY", args: [], r_opts: ["y"] }, "TXTD-1": { txt: "$1", args: [ARG_TEXT], r_opts: ["y"] }, }; # # TODO: # add message buffer for old messages # support multi part messages # var CPDLCMessageHandler = { _msg_separator: "|", new: func(prop = "/network/cpdlc/rx/message") { var obj = { parents: [me], mid: "", mtxt: "", margs: [], r_opts: [], valid: 0, pMessage: props.getNode(prop, 1), }; return obj; }, # validate incoming message # returns 'decoded' message parseSingleMessage: func(message) { me.margs = split(" ", message); me.mid = me.margs[0]; # basic validation: 4th char is "U" (for uplink) followed by "-" if (size(me.mid) >= 5 and chr(me.mid[3]) == "U" and chr(me.mid[4]) == "-") { if (contains(uplink_messages, me.mid)) { var descriptor = uplink_messages[me.mid]; me.mtxt = descriptor.txt; me.r_opts = descriptor.r_opts; #always add standby as reply option (should not be given in the message definitions!) append(me.r_opts, "s"); # next assumes a constant number of args per message id if (size(me.margs)-1 != size(descriptor.args)) { print(DEV_ALERT, "CPDLC arg count mismatch ("~me.mid~") "~ "have "~(size(me.margs)-1)~" expecting "~size(descriptor.args)); } # replace $i with args for (var i=1; i <= size(descriptor.args); i += 1) { me.mtxt = string.replace(me.mtxt, "$"~i, me.margs[i]); } me.valid = 1; } else { #unknown message id me.mtxt = "[unknown id]"~message; me.r_opts = []; me.valid = 0; } } elsif (size(message)) { me.mtxt = "[raw]"~message; } else { me.mtxt = ""; } return me.mtxt; }, getMessage: func() { var raw_msg = split(me._msg_separator, me.pMessage.getValue()); var friendly_msg = ""; foreach (var part; raw_msg) { friendly_msg ~= me.parseSingleMessage(part) ~ me._msg_separator; } friendly_msg = left(friendly_msg, size(friendly_msg)-size(me._msg_separator)); return friendly_msg; }, getMessageId: func() { return me.mid; }, getReplyOptions: func() { return me.r_opts; }, getReplyOptionByIndex: func(i) { if (int(i) != nil and i < size(me.r_opts)) { return me.r_opts[i]; } else return -1; }, reply: func(r) { if (!me.valid) return; if (vecindex(me.r_opts, r)) { return responses[r]; } }, };