1
0
Fork 0
fgdata/Aircraft/Instruments-3d/FG1000/Nasal/MFDPages/Surround/Surround.nas
Stuart Buchanan c49c6a38c6 FG1000: GMA1347 Audio Interface and NAV/COM active
The G1000 highlights the active NAV and COM radios in cyan.
In the NAV case, this is the radio being used by the CDI.
In the COM case, this is the COM radio selected on the audio panel.
2021-08-18 20:18:57 +01:00

612 lines
22 KiB
Text

# Copyright 2018 Stuart Buchanan
# 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 <http://www.gnu.org/licenses/>.
#
# MFD Surround
#
# Header fields at the top of the page
#
# PageGroup navigation, displayed in the bottom right of the
# FMS, and controlled by the FMS knob
# Set of pages, references by SVG ID
var PAGE_GROUPS = [
{ label: "MapPageGroupLabel",
group: "MapPageGroup",
pages: [ "NavigationMap", "TrafficMap", "Stormscope", "WeatherDataLink", "TAWSB"],
},
{ label: "WPTGroupLabel",
group: "WPTPageGroup",
pages: [ "AirportInfo", "IntersectionInfo", "NDBInfo", "VORInfo", "UserWPTInfo"],
},
{ label: "AuxGroupLabel",
group: "AuxPageGroup",
pages: [ "TripPlanning", "Utility", "GPSStatus", "XMRadio", "SystemStatus"],
},
{ label: "FPLGroupLabel",
group: "FPLPageGroup",
pages: [ "ActiveFlightPlanNarrow", "FlightPlanCatalog", "StoredFlightPlan"],
},
{ label: "LstGroupLabel",
group: "LstPageGroup",
pages: [ "Checklist"],
},
{ label: "NrstGroupLabel",
group: "NrstPageGroup",
pages: [ "NearestAirports", "NearestIntersections", "NearestNDB", "NearestVOR", "NearestUserWaypoints", "NearestFrequencies", "NearestAirspaces"],
}
];
# Mapping for header labels to specific FMS or ADC messages, and sprintf formatting
# to use
var HEADER_MAPPING = {
"BRG" : { message : "FMSLegBearingMagDeg", format : "%d°"},
"XTK" : { message : "FMSLegCourseError", format : "%.1fnm"},
"DIS" : { message : "FMSDistance", format : "%.1fnm"},
"DTK" : { message : "FMSLegDesiredTrack", format : "%d°"},
"END" : { message : "EnduranceHrs", format : "%.1fhrs"},
"ESA" : { message : "EnRouteSafeAltitude", format : "%dft"}, # TODO
"ETA" : { message : "FMSEstimatedTimeArrival", format : ""}, # TODO
"ETE" : { message : "FMSEstimatedTimeEnroute", format : ""},
"FOD" : { message : "FMSFuelOverDestination", format : "%dgal"},
"FOB" : { message : "FuelOnBoard", format : "%dgal"},
"GS" : { message : "FMSGroundspeed", format : "%dkts"},
"MSA" : { message : "MinimumSafeAltitude", format : "%dft"}, # TODO
"TAS" : { message : "ADCTrueAirspeed", format : "%dkts"},
"TKE" : { message : "FMSLegTrackErrorAngle", format : "%d°"},
"TRK" : { message : "FMSLegTrack", format : "%d°"},
"VSR" : { message : "FMSLegVerticalSpeedRequired", format : "%dfpm"}, # TODO
};
# Style element use for the AP Status indicator. This is normally green text
# on a black background, but is highlighted when disengaged as black text on a yellow
# background for 5 seconds.
var AP_STATUS_STYLE = {
CURSOR_BLINK_PERIOD : 0.5,
HIGHLIGHT_COLOR : "#ffff00",
HIGHLIGHT_TEXT_COLOR : "#000000",
NORMAL_TEXT_COLOR : "#00ff00",
};
# Style element for use by the flight director modes and armed indicators.
# This is normally green text on a black background, but when highlighted is
# black text on a green background
var FD_STATUS_STYLE = {
CURSOR_BLINK_PERIOD : 0.5,
HIGHLIGHT_COLOR : "#00ff00",
HIGHLIGHT_TEXT_COLOR : "#000000",
NORMAL_TEXT_COLOR : "#00ff00",
};
# Style element for use by the NAV and COM frequencies. This is normally white text
# on a black background, but when the COM is enabled by the GMA1347, or the NAV is being
# used by the CDI, then it is green on a black background.
var NAVCOM_FREQ_STYLE = {
CURSOR_BLINK_PERIOD : 0.5,
HIGHLIGHT_COLOR : "#000000",
HIGHLIGHT_TEXT_COLOR : "#00ff00",
NORMAL_TEXT_COLOR : "#ffffff",
};
var Surround =
{
new : func (mfd, myCanvas, device, svg, pfd=0)
{
var obj = { parents : [
Surround,
MFDPage.new(mfd, myCanvas, device, svg, "Surround", ""),
] };
obj.pfd = pfd;
var textElements = [
"Comm1StandbyFreq", "Comm1SelectedFreq",
"Comm2StandbyFreq", "Comm2SelectedFreq",
"CommVolume",
"Nav1StandbyFreq", "Nav1SelectedFreq",
"Nav2StandbyFreq", "Nav2SelectedFreq",
"Nav1ID", "Nav2ID",
"NavVolume",
];
var fdTextElements = ["HeaderAPLateralArmed", "HeaderAPLateralActive", "HeaderAPVerticalArmed", "HeaderAPVerticalActive", "HeaderAPVerticalReference"];
# Labels that show and hide for volume notification
var volumeLabelElements = ["Comm2Label","CommVolumeLabel","Nav2Label","NavVolumeLabel"];
obj.addTextElements(textElements);
obj.addElements(volumeLabelElements);
if (pfd) {
obj.addTextElements(["HeaderFrom", "HeaderTo", "LegDistance", "LegBRG"]);
obj.addTextElements(fdTextElements, FD_STATUS_STYLE);
obj.setTextElements(fdTextElements, "");
obj._apStatus = PFD.TextElement.new(obj.pageName, svg, "HeaderAPStatus", "", AP_STATUS_STYLE);
obj._dto = PFD.HighlightElement.new(obj.pageName, svg, "HeaderDTO", "DTO");
obj._leg = PFD.HighlightElement.new(obj.pageName, svg, "HeaderActiveLeg", "Leg");
obj._old_lateral_armed = nil; # We store the previous armed values so we can detect a transtion from armed to active.
obj._old_vertical_armed = nil;
obj._ap_on = 0;
} else {
obj.addTextElements(["Header1Label", "Header1Value",
"Header2Label", "Header2Value",
"Header3Label", "Header3Value",
"Header4Label", "Header4Value"]);
}
obj._comm1selected = PFD.HighlightElement.new(obj.pageName, svg, "Comm1Selected", "Comm1");
obj._comm2selected = PFD.HighlightElement.new(obj.pageName, svg, "Comm2Selected", "Comm2");
obj._nav1selected = PFD.HighlightElement.new(obj.pageName, svg, "Nav1Selected", "Nav1");
obj._nav2selected = PFD.HighlightElement.new(obj.pageName, svg, "Nav2Selected", "Nav2");
obj._comm1failed = PFD.HighlightElement.new(obj.pageName, svg, "Comm1Failed", "Comm1");
obj._comm2failed = PFD.HighlightElement.new(obj.pageName, svg, "Comm2Failed", "Comm2");
obj._nav1failed = PFD.HighlightElement.new(obj.pageName, svg, "Nav1Failed", "Nav1");
obj._nav2failed = PFD.HighlightElement.new(obj.pageName, svg, "Nav2Failed", "Nav2");
obj._canvas = myCanvas;
obj._menuVisible = 0;
obj._selectedPageGroup = 0;
obj._selectedPage = 0;
obj._selected_comm = 1;
obj._selected_nav = 1;
obj._elements = {};
foreach (var pageGroup; PAGE_GROUPS) {
var group = svg.getElementById(pageGroup.group);
var label = svg.getElementById(pageGroup.label);
assert(group != nil, "Unable to find element " ~ pageGroup.group);
assert(label != nil, "Unable to find element " ~ pageGroup.label);
obj._elements[pageGroup.group] = group;
obj._elements[pageGroup.label] = label;
foreach(var pg; pageGroup.pages) {
var page = svg.getElementById(pg);
assert(page != nil, "Unable to find element " ~ pg);
obj._elements[pg] = page;
}
}
# Timers to control when to hide the menu after inactivity, and when to load
# a new page.
obj._hideMenuTimer = maketimer(3, obj, obj.hideMenu);
obj._hideMenuTimer.singleShot = 1;
obj._hideCommVolumeTimer = maketimer(2, obj, obj.hideCommVolume);
obj._hideCommVolumeTimer.singleShot = 1;
obj._hideNavVolumeTimer = maketimer(2, obj, obj.hideNavVolume);
obj._hideNavVolumeTimer.singleShot = 1;
obj._loadPageTimer = maketimer(0.5, obj, obj.loadPage);
obj._loadPageTimer.singleShot = 1;
obj.hideMenu();
obj.setController(fg1000.SurroundController.new(obj, svg, pfd));
obj.hideCommVolume();
obj.hideNavVolume();
return obj;
},
handleNavComData : func(data) {
foreach(var name; keys(data)) {
var val = data[name];
if (name == "Comm1SelectedFreq") me.setTextElement("Comm1SelectedFreq", sprintf("%0.03f", val));
if (name == "Comm1StandbyFreq") me.setTextElement("Comm1StandbyFreq", sprintf("%0.03f", val));
if (name == "Comm1Serviceable") {
if (val == 1) {
me._comm1failed.setVisible(0);
} else {
me._comm1failed.setVisible(1);
}
}
if (name == "Comm2SelectedFreq") me.setTextElement("Comm2SelectedFreq", sprintf("%0.03f", val));
if (name == "Comm2StandbyFreq") me.setTextElement("Comm2StandbyFreq", sprintf("%0.03f", val));
if (name == "Comm2Serviceable") {
if (val == 1) {
me._comm2failed.setVisible(0);
} else {
me._comm2failed.setVisible(1);
}
}
if (name == "CommSelected") {
if (val == 1) {
me._selected_comm=1;
me._comm1selected.setVisible(1);
me._comm2selected.setVisible(0);
} else {
me._selected_comm=2;
me._comm1selected.setVisible(0);
me._comm2selected.setVisible(1);
}
}
if (name == "CommAudioSelected") {
me.getTextElement("Comm1SelectedFreq").setColor(val == 1 ? NAVCOM_FREQ_STYLE.HIGHLIGHT_TEXT_COLOR : NAVCOM_FREQ_STYLE.NORMAL_TEXT_COLOR);
me.getTextElement("Comm2SelectedFreq").setColor(val == 2 ? NAVCOM_FREQ_STYLE.HIGHLIGHT_TEXT_COLOR : NAVCOM_FREQ_STYLE.NORMAL_TEXT_COLOR);
}
if (name == "Nav1SelectedFreq") me.setTextElement("Nav1SelectedFreq", sprintf("%0.03f", val));
if (name == "Nav1StandbyFreq") me.setTextElement("Nav1StandbyFreq", sprintf("%0.03f", val));
if (name == "Nav1Serviceable") {
if (val == 1) {
me._nav1failed.setVisible(0);
} else {
me._nav1failed.setVisible(1);
}
}
if (name == "Nav2SelectedFreq") me.setTextElement("Nav2SelectedFreq", sprintf("%0.03f", val));
if (name == "Nav2StandbyFreq") me.setTextElement("Nav2StandbyFreq", sprintf("%0.03f", val));
if (name == "Nav2Serviceable") {
if (val == 1) {
me._nav2failed.setVisible(0);
} else {
me._nav2failed.setVisible(1);
}
}
if (name == "NavSelected") {
if (val == 1) {
me._selected_nav=1;
me._nav1selected.setVisible(1);
me._nav2selected.setVisible(0);
} else {
me._selected_nav=2;
me._nav1selected.setVisible(0);
me._nav2selected.setVisible(1);
}
}
if (name == "Nav1ID") {
if (val==0) {
me.setTextElement("Nav1ID", "");
} else {
me.setTextElement("Nav1ID", val);
}
}
if (name == "Nav2ID") {
if (val==0) {
me.setTextElement("Nav2ID", "");
} else {
me.setTextElement("Nav2ID", val);
}
}
# NAV/COM Volume - display the current volume for 2 seconds in place of the
# standby frequency.
if (name == "Nav1Volume" or name == "Nav2Volume") {
me.showNavVolume(val);
}
if (name == "Comm1Volume" or name == "Comm2Volume") {
me.showCommVolume(val);
}
}
},
showCommVolume: func(val) {
var commvol = sprintf("%d%%",int(val*100));
if (me.getTextValue("CommVolume") == commvol) return;
# Hide com2 standby and label
me.getTextElement("Comm2StandbyFreq").setVisible(0);
me.getElement("Comm2Label").setVisible(0);
if (me._selected_comm == 2) me._comm2selected.setVisible(0);
# Set and show COM volume
me.setTextElement("CommVolume",commvol);
me.getTextElement("CommVolume").setVisible(1);
me.getElement("CommVolumeLabel").setVisible(1);
# Start hide timer (2 secs according to the manual)
me._hideCommVolumeTimer.stop();
me._hideCommVolumeTimer.restart(2);
},
hideCommVolume: func() {
# Hide comm vol and restore standby and label
me.getTextElement("CommVolume").setVisible(0);
me.getElement("CommVolumeLabel").setVisible(0);
me.getTextElement("Comm2StandbyFreq").setVisible(1);
me.getElement("Comm2Label").setVisible(1);
if (me._selected_comm == 2) me._comm2selected.setVisible(1);
},
showNavVolume: func(val) {
var navvol = sprintf("%d%%",int(val*100));
if (me.getTextValue("NavVolume") == navvol) return;
# Hide NAV2 standdby
me.getTextElement("Nav2StandbyFreq").setVisible(0);
me.getElement("Nav2Label").setVisible(0);
if (me._selected_nav == 2) me._nav2selected.setVisible(0);
# Set and show NAV volume
me.setTextElement("NavVolume",navvol);
me.getTextElement("NavVolume").setVisible(1);
me.getElement("NavVolumeLabel").setVisible(1);
# Start hide timer (2 secs according to the manual)
me._hideNavVolumeTimer.stop();
me._hideNavVolumeTimer.restart(2);
},
hideNavVolume: func() {
# Hide Nav volume and show NAV2 standby again
me.getTextElement("NavVolume").setVisible(0);
me.getElement("NavVolumeLabel").setVisible(0);
me.getTextElement("Nav2StandbyFreq").setVisible(1);
me.getElement("Nav2Label").setVisible(1);
if (me._selected_nav == 2) me._nav2selected.setVisible(1);
},
# Update Header data with FMS or ADC data.
updateHeaderData : func(data) {
if (me.pfd) {
# From, To, leg distance and leg bearing headers
if (data["FMSLegID"]) {
if (data["FMSLegID"] == "") {
# No Leg, so hide the headers
me.setTextElement("HeaderTo", "");
me.setTextElement("HeaderFrom", "");
me._dto.setVisible(0);
me._leg.setVisible(0);
} else {
me.setTextElement("HeaderTo", data["FMSLegID"]);
me._leg.setVisible(1);
if (data["FMSMode"] == "dto") {
me.setTextElement("HeaderFrom", "");
me._dto.setVisible(1);
} else {
me._dto.setVisible(0);
me.setTextElement("HeaderFrom", data["FMSPreviousLegID"]);
}
}
}
# When the Autopilot Heading or Altitude modes moves from armed to active we flash the appropriate annunicator for 10 seconds.
# Unfortunately as we use a TriggeredPropertyPublisher, we won't have both the HeaderAP[Vertical|Lateral]Active and HeaderAP[Vertical|Lateral]Armed
# values at the same time so have to save off any change in the armed values to check against.
if ((data["AutopilotHeadingMode"] != nil) and
(data["AutopilotHeadingMode"] != me.getTextValue("HeaderAPLateralActive"))) {
me.setTextElement("HeaderAPLateralActive", data["AutopilotHeadingMode"]);
if ((data["AutopilotHeadingMode"] != "") and
((data["AutopilotHeadingMode"] == me._old_lateral_armed) or
(data["AutopilotHeadingMode"] == me.getTextValue("HeaderAPLateralArmed"))))
{
# Transition from an armed mode to a new mode, so flash
me.highlightTextElement("HeaderAPLateralActive", 10);
}
}
if ((data["AutopilotAltitudeMode"] != nil) and
(data["AutopilotAltitudeMode"] != me.getTextValue("HeaderAPVerticalActive"))) {
me.setTextElement("HeaderAPVerticalActive", data["AutopilotAltitudeMode"]);
if ((data["AutopilotAltitudeMode"] != "") and
((data["AutopilotAltitudeMode"] == me._old_vertical_armed) or
(data["AutopilotAltitudeMode"] == me.getTextValue("HeaderAPVerticalArmed"))))
{
# Transition from an armed mode to a new mode, so flash
me.highlightTextElement("HeaderAPVerticalActive", 10);
}
}
if (data["AutopilotHeadingModeArmed"] != nil) {
if (data["AutopilotHeadingModeArmed"] != me.getTextValue("HeaderAPLateralArmed")) me._old_lateral_armed = me.getTextValue("HeaderAPLateralArmed");
me.setTextElement("HeaderAPLateralArmed", data["AutopilotHeadingModeArmed"]);
}
if (data["AutopilotAltitudeModeArmed"] != nil) {
if (data["AutopilotAltitudeModeArmed"] != me.getTextValue("HeaderAPVerticalArmed")) me._old_lateral_armed = me.getTextValue("HeaderAPVerticalArmed");
me.setTextElement("HeaderAPVerticalArmed", data["AutopilotAltitudeModeArmed"]);
}
# When the Autopilot is disengaged, the AP status element flashes for 5 seconds before disappearing
if (data["AutopilotEnabled"] != nil) {
if ((data["AutopilotEnabled"] == 1) and (me._ap_on == 0)) {
# Toggle the AP on, stopping any flashing that might be occurring.
me._apStatus.unhighlightElement();
me._apStatus.setValue("AP");
me._ap_on = 1;
}
if ((data["AutopilotEnabled"] == 0) and me._ap_on) {
# Toggle the AP off, by flashing the AP Status element for 5 seconds before removing it.
# Only do this if we're not already flashing.
me._ap_on = 0;
me._apStatus.highlightElement(5.0, "");
}
}
if (data["AutopilotTargetVertical"] != nil) me.setTextElement("HeaderAPVerticalReference", data["AutopilotTargetVertical"]);
if (data["FMSLegDesiredTrack"]) me.setTextElement("LegBRG", sprintf("%i°", data["FMSLegDesiredTrack"]));
if (data["FMSLegDistanceNM"]) me.setTextElement("LegDistance", sprintf("%.1fnm", data["FMSLegDistanceNM"]));
} else {
# MFD - 4 configurable Headers
var headers = ["Header1", "Header2", "Header3", "Header4"];
foreach (var header; headers) {
# Get the currently configured heading and set the surround to display it.
var label = me.mfd.ConfigStore.get("MFD" ~ header);
assert(label != nil, "No header configured in ConfigStore for " ~ header);
me.setTextElement(header ~ "Label", label);
# Determine how it maps to Emesary data notifications
var mapping = HEADER_MAPPING[label];
assert(mapping != nil, "No header mapping for " ~ label);
if (data[mapping.message] != nil) {
# Format and display the value
var value = sprintf(mapping.format, data[mapping.message]);
if (mapping.message == "FMSEstimatedTimeEnroute") {
# Special case to format time strings.
var hrs = int(data[mapping.message]);
var mins = int(60*(data[mapping.message] - hrs));
var secs = int(3600*(data[mapping.message] - hrs - mins/60));
if (hrs == 0) {
value = sprintf("%d:%02d", mins, secs);
} else {
value = sprintf("%d:%02d", hrs, mins);
}
}
me.setTextElement(header ~ "Value", value);
}
}
}
if (data["AutopilotNAVSource"] != nil) {
# Set highlighting of the NAV radios based on the NAV Source.
var src = data["AutopilotNAVSource"];
me.getTextElement("Nav1SelectedFreq").setColor(src == "NAV1" ? NAVCOM_FREQ_STYLE.HIGHLIGHT_TEXT_COLOR : NAVCOM_FREQ_STYLE.NORMAL_TEXT_COLOR);
me.getTextElement("Nav2SelectedFreq").setColor(src == "NAV2" ? NAVCOM_FREQ_STYLE.HIGHLIGHT_TEXT_COLOR : NAVCOM_FREQ_STYLE.NORMAL_TEXT_COLOR);
}
},
getCurrentPage : func()
{
var currentpage = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
return me.getMFD().getPage(currentpage);
},
# Go to a define page in the MFD. Only valid for MFDs, and mainly used as
# a useability shortcut to avoid having to use the FMS knobs.
goToPage : func(group, page)
{
# Not valid for the PFD.
if (me.pfd) return;
# Values may be passed as names or indices.
if (int(group) == nil) {
for (var i = 0; i < size(PAGE_GROUPS); i = i + 1) {
if (group == PAGE_GROUPS[i].group) {
me._selectedPageGroup = i;
}
}
} else {
assert(group < size(PAGE_GROUPS), "Page Group index " ~ group ~ " out of bounds");
me._selectedPageGroup = group;
}
if (int(page) == nil) {
for (var j = 0; j < size(PAGE_GROUPS[me._selectedPageGroup].pages); j = j + 1) {
if (page == PAGE_GROUPS[me._selectedPageGroup].pages[j]) {
me._selectedPage = j;
}
}
} else {
assert(page < size(PAGE_GROUPS[me._selectedPageGroup].pages), "Page Group index " ~ group ~ " out of bounds");
me._selectedPage = page;
}
# Now we've updated the selected pages, then load it
me.loadPage();
},
# Function to change a page based on the selection
loadPage : func()
{
# Not valid for the PFD.
if (me.pfd) return;
var pageToLoad = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
var page = me.getMFD().getPage(pageToLoad);
assert(page != nil, "Unable to find page " ~ pageToLoad);
me.device.selectPage(page);
},
incrPageGroup : func(val) {
var incr_or_decr = (val > 0) ? 1 : -1;
me._selectedPageGroup = math.mod(me._selectedPageGroup + incr_or_decr, size(PAGE_GROUPS));
me._selectedPage = 0;
},
incrPage : func(val) {
var incr_or_decr = (val > 0) ? 1 : -1;
me._selectedPage = math.mod(me._selectedPage + incr_or_decr, size(PAGE_GROUPS[me._selectedPageGroup].pages));
},
showMenu : func()
{
# Not valid for the PFD.
if (me.pfd) return;
foreach(var pageGroup; PAGE_GROUPS)
{
if (PAGE_GROUPS[me._selectedPageGroup].label == pageGroup.label)
{
# Display the page group and highlight the label
me._elements[pageGroup.group].setVisible(1);
me._elements[pageGroup.label].setVisible(1);
me._elements[pageGroup.label].setColor(0.7,0.7,1.0);
foreach (var page; pageGroup.pages)
{
# Highlight the current page.
if (pageGroup.pages[me._selectedPage] == page) {
me._elements[page].setColor(0.7,0.7,1.0);
} else {
me._elements[page].setColor(0.7,0.7,0.7);
}
}
}
else
{
# Hide the pagegroup and unhighlight the label on the bottom
me._elements[pageGroup.group].setVisible(0);
me._elements[pageGroup.label].setVisible(1);
me._elements[pageGroup.label].setColor(0.7,0.7,0.7);
}
}
me._menuVisible = 1;
me._hideMenuTimer.stop();
me._hideMenuTimer.restart(3);
me._loadPageTimer.stop();
me._loadPageTimer.restart(0.5);
},
hideMenu : func()
{
foreach(var pageGroup; PAGE_GROUPS)
{
me._elements[pageGroup.group].setVisible(0);
me._elements[pageGroup.label].setVisible(0);
}
me._menuVisible = 0;
},
isMenuVisible : func()
{
return me._menuVisible;
},
};