Update NAV/COMM frequencies from properties
- Add new Emesary notification type for NAV/COM data - Create Update/Publish interfaces using Emesary from properties - Use interfaces to drive updates to EIS and NAV/COM frequencies - Change the PageGroupController to a "proper" MFD page
This commit is contained in:
parent
ad77dc2f9c
commit
73424c1791
18 changed files with 1327 additions and 2635 deletions
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 81 KiB |
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 91 KiB |
|
@ -1,68 +0,0 @@
|
|||
# FG1000 Engine Information Display Default Driver
|
||||
#
|
||||
# Driver values in the EIS.
|
||||
|
||||
var PropMap =
|
||||
{
|
||||
new : func(name, property)
|
||||
{
|
||||
var obj = { parents : [ PropMap ] };
|
||||
obj._name = name;
|
||||
obj._val = 0;
|
||||
obj._prop = globals.props.getNode(property, 1);
|
||||
obj._val = obj._prop.getValue();
|
||||
return obj;
|
||||
},
|
||||
|
||||
update : func()
|
||||
{
|
||||
me._val = me._prop.getValue();
|
||||
},
|
||||
|
||||
getValue : func()
|
||||
{
|
||||
return me._val;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var EISDriver =
|
||||
{
|
||||
new : func()
|
||||
{
|
||||
var obj = { parents : [ EISDriver ] };
|
||||
|
||||
# Hack to handle most aircraft not having proper engine hours
|
||||
setprop("/engines/engine[0]/hours", 157.0);
|
||||
|
||||
obj._propmap = {
|
||||
"RPM" : PropMap.new("RPM", "/engines/engine[0]/rpm"),
|
||||
"MBusVolts" : PropMap.new("MBusVolts", "/systems/electrical/volts"),
|
||||
"EngineHours" : PropMap.new("EngineHours", "/engines/engine[0]/hours"),
|
||||
"FuelFlowGPH" : PropMap.new("FuelFlowGPH", "/engines/engine[0]/fuel-flow-gph"),
|
||||
"OilPressurePSI" : PropMap.new("OilPressurePSI", "/engines/engine[0]/oil-pressure-psi"),
|
||||
"OilTemperatureF" : PropMap.new("OilTemperatureF", "/engines/engine[0]/oil-temperature-degf"),
|
||||
"EGTNorm" : PropMap.new("EGTNorm", "/engines/engine[0]/egt-norm"),
|
||||
"VacuumSuctionInHG" : PropMap.new("VacuumSuctionInHG", "/systems/vacuum/suction-inhg"),
|
||||
|
||||
"LeftFuelUSGal" : PropMap.new("LeftFuelUSGal", "/consumables/fuel/tank[0]/indicated-level-gal_us"),
|
||||
"RightFuelUSGal" : PropMap.new("RightFuelUSGal", "/consumables/fuel/tank[1]/indicated-level-gal_us"),
|
||||
|
||||
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
|
||||
getValue : func(name)
|
||||
{
|
||||
if (me._propmap[name] == nil) print("Unknown Driver Key in EISDriver.nas " ~ name);
|
||||
return me._propmap[name].getValue();
|
||||
},
|
||||
|
||||
update: func () {
|
||||
|
||||
foreach (var tp; keys(me._propmap)) {
|
||||
me._propmap[tp].update();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -30,21 +30,21 @@ var EIS =
|
|||
},
|
||||
|
||||
updateData : func(engineData) {
|
||||
obj.setTextElement("RPMDisplay", sprintf("%i", engineData.RPM));
|
||||
obj.setTextElement("MBusVolts", sprintf("%.01f", engineData.MBusVolts));
|
||||
obj.setTextElement("EBusVolts", sprintf("%.01f", engineData.MBusVolts)); # TODO: Include Emergency Bus
|
||||
obj.setTextElement("EngineHours", sprintf("%.01f", engineData.EngineHours));
|
||||
me.setTextElement("RPMDisplay", sprintf("%i", engineData.RPM));
|
||||
me.setTextElement("MBusVolts", sprintf("%.01f", engineData.MBusVolts));
|
||||
me.setTextElement("EBusVolts", sprintf("%.01f", engineData.MBusVolts)); # TODO: Include Emergency Bus
|
||||
me.setTextElement("EngineHours", sprintf("%.01f", engineData.EngineHours));
|
||||
|
||||
obj._fuelFlowPointer.setValue(engineData.FuelFlowGPH);
|
||||
obj._oilPressurePointer.setValue(engineData.OilPressurePSI);
|
||||
obj._oilTempPointer.setValue(engineData.OilTemperatureF);
|
||||
obj._EGTPointer.setValue(engineData.EGTNorm);
|
||||
obj._EGTCylinder.setValue(engineData.EGTNorm);
|
||||
obj._vacPointer.setValue(engineData.VacuumSuctionInHG);
|
||||
obj._leftFuelPointer.setValue(engineData.LeftFuelUSGal);
|
||||
obj._rightFuelPointer.setValue(engineData.RightFuelUSGal);
|
||||
me._fuelFlowPointer.setValue(engineData.FuelFlowGPH);
|
||||
me._oilPressurePointer.setValue(engineData.OilPressurePSI);
|
||||
me._oilTempPointer.setValue(engineData.OilTemperatureF);
|
||||
me._EGTPointer.setValue(engineData.EGTNorm);
|
||||
me._EGTCylinder.setValue(engineData.EGTNorm);
|
||||
me._vacPointer.setValue(engineData.VacuumSuctionInHG);
|
||||
me._leftFuelPointer.setValue(engineData.LeftFuelUSGal);
|
||||
me._rightFuelPointer.setValue(engineData.RightFuelUSGal);
|
||||
|
||||
obj._RPMPointer.setValue(engineData.RPM);
|
||||
me._RPMPointer.setValue(engineData.RPM);
|
||||
},
|
||||
|
||||
# Menu tree . engineMenu is referenced from most pages as softkey 0:
|
||||
|
|
|
@ -38,7 +38,8 @@ var EISController =
|
|||
}
|
||||
|
||||
# Display it
|
||||
me.page.upateData(data);
|
||||
me._page.updateData(data);
|
||||
return emesary.Transmitter.ReceiptStatus_OK;
|
||||
},
|
||||
|
||||
RegisterWithEmesary : func(transmitter = nil){
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# EIS Driver using Emesary for a single engined aircraft, e.g. Cessna 172.
|
||||
var GenericEISPublisher =
|
||||
{
|
||||
|
||||
new : func (frequency=0.25, transmitter = nil) {
|
||||
var obj = {
|
||||
parents : [ GenericEISPublisher, PropertyPublisher.new(frequency, transmitter) ],
|
||||
};
|
||||
|
||||
# Hack to handle most aircraft not having proper engine hours
|
||||
if (getprop("/engines/engine[0]/hours") == nil) setprop("/engines/engine[0]/hours", 157.0);
|
||||
|
||||
obj.addPropMap("RPM", "/engines/engine[0]/rpm");
|
||||
obj.addPropMap("MBusVolts", "/systems/electrical/volts");
|
||||
obj.addPropMap("EngineHours", "/engines/engine[0]/hours");
|
||||
obj.addPropMap("FuelFlowGPH", "/engines/engine[0]/fuel-flow-gph");
|
||||
obj.addPropMap("OilPressurePSI", "/engines/engine[0]/oil-pressure-psi");
|
||||
obj.addPropMap("OilTemperatureF", "/engines/engine[0]/oil-temperature-degf");
|
||||
obj.addPropMap("EGTNorm", "/engines/engine[0]/egt-norm");
|
||||
obj.addPropMap("VacuumSuctionInHG", "/systems/vacuum/suction-inhg");
|
||||
|
||||
obj.addPropMap("LeftFuelUSGal", "/consumables/fuel/tank[0]/indicated-level-gal_us");
|
||||
obj.addPropMap("RightFuelUSGal", "/consumables/fuel/tank[1]/indicated-level-gal_us");
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
publish : func() {
|
||||
var engineData0 = {};
|
||||
|
||||
foreach (var propmap; me._propmaps) {
|
||||
var name = propmap.getName();
|
||||
engineData0[name] = propmap.getValue();
|
||||
}
|
||||
|
||||
var engineData = [];
|
||||
append(engineData, engineData0);
|
||||
|
||||
me.notify(notifications.PFDEventNotification.EngineData, engineData);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
# NavCom Interface using Emesary for a simple dual Nav/Com system using standard properties
|
||||
var GenericNavComPublisher =
|
||||
{
|
||||
new : func (frequency=0.25, transmitter = nil) {
|
||||
var obj = {
|
||||
parents : [ GenericNavComPublisher, PropertyPublisher.new(frequency, transmitter) ],
|
||||
};
|
||||
|
||||
# Hack to handle cases where there is no selected COMM or NAV frequency
|
||||
if (getprop("/instrumentation/com-selected") == nil) setprop("/instrumentation/com-selected", 1);
|
||||
if (getprop("/instrumentation/nav-selected") == nil) setprop("/instrumentation/nav-selected", 1);
|
||||
|
||||
obj.addPropMap("Comm1SelectedFreq", "/instrumentation/comm/frequencies/selected-mhz");
|
||||
obj.addPropMap("Comm1StandbyFreq", "/instrumentation/comm/frequencies/selected-mhz");
|
||||
obj.addPropMap("Comm1AirportID", "/instrumentation/comm/airport-id");
|
||||
obj.addPropMap("Comm1StationName", "/instrumentation/comm/station-name");
|
||||
obj.addPropMap("Comm1StationType", "/instrumentation/comm/station-type");
|
||||
obj.addPropMap("Comm1Volume", "/instrumentation/comm/volume");
|
||||
obj.addPropMap("Comm1Serviceable", "/instrumentation/comm/serviceable");
|
||||
|
||||
obj.addPropMap("Comm2SelectedFreq", "/instrumentation/comm[1]/frequencies/selected-mhz");
|
||||
obj.addPropMap("Comm2StandbyFreq", "/instrumentation/comm[1]/frequencies/selected-mhz");
|
||||
obj.addPropMap("Comm2AirportID", "/instrumentation/comm[1]/airport-id");
|
||||
obj.addPropMap("Comm2StationName", "/instrumentation/comm[1]/station-name");
|
||||
obj.addPropMap("Comm2StationType", "/instrumentation/comm[1]/station-type");
|
||||
obj.addPropMap("Comm2Volume", "/instrumentation/comm[1]/volume");
|
||||
obj.addPropMap("Comm2Serviceable", "/instrumentation/comm[1]/serviceable");
|
||||
|
||||
obj.addPropMap("CommSelected", "/instrumentation/com-selected");
|
||||
|
||||
obj.addPropMap("Nav1SelectedFreq", "/instrumentation/nav/frequencies/selected-mhz");
|
||||
obj.addPropMap("Nav1StandbyFreq", "/instrumentation/nav/frequencies/selected-mhz");
|
||||
obj.addPropMap("Nav1ID", "/instrumentation/nav/nav-id");
|
||||
obj.addPropMap("Nav1Serviceable", "/instrumentation/nav/serviceable");
|
||||
|
||||
obj.addPropMap("Nav2SelectedFreq", "/instrumentation/nav[1]/frequencies/selected-mhz");
|
||||
obj.addPropMap("Nav2StandbyFreq", "/instrumentation/nav[1]/frequencies/selected-mhz");
|
||||
obj.addPropMap("Nav2ID", "/instrumentation/nav[1]/nav-id");
|
||||
obj.addPropMap("Nav2Serviceable", "/instrumentation/nav[1]/serviceable");
|
||||
|
||||
obj.addPropMap("NavSelected", "/instrumentation/nav-selected");
|
||||
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
publish : func() {
|
||||
var data = {};
|
||||
|
||||
foreach (var propmap; me._propmaps) {
|
||||
var name = propmap.getName();
|
||||
data[name] = propmap.getValue();
|
||||
}
|
||||
|
||||
me.notify(notifications.PFDEventNotification.NavComData, data);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
# Generic PropertyPublisher class for the FG1000 MFD using Emesary
|
||||
# Publishes property values to Emesary for consumption by the MFD
|
||||
|
||||
var PropertyPublisher =
|
||||
{
|
||||
|
||||
PropMap : {
|
||||
new : func(name, property)
|
||||
{
|
||||
var obj = { parents : [ PropertyPublisher.PropMap ] };
|
||||
obj._name = name;
|
||||
obj._prop = globals.props.getNode(property, 1);
|
||||
return obj;
|
||||
},
|
||||
|
||||
getName : func() { return me._name; },
|
||||
getValue : func() { return me._prop.getValue(); },
|
||||
},
|
||||
|
||||
new : func (frequency=0.25, transmitter = nil) {
|
||||
var obj = {
|
||||
parents : [ PropertyPublisher ],
|
||||
_transmitter : transmitter,
|
||||
_frequency : frequency,
|
||||
_propmaps : [],
|
||||
};
|
||||
|
||||
if (obj._transmitter == nil) obj._transmitter = emesary.GlobalTransmitter;
|
||||
|
||||
obj._publishTimer = nil;
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
addPropMap : func(name, prop) {
|
||||
append(me._propmaps, PropertyPublisher.PropMap.new(name, prop));
|
||||
},
|
||||
|
||||
publish : func() {
|
||||
},
|
||||
|
||||
notify : func(notification, data) {
|
||||
var notification = notifications.PFDEventNotification.new(
|
||||
"MFD",
|
||||
1,
|
||||
notification,
|
||||
data);
|
||||
|
||||
me._transmitter.NotifyAll(notification);
|
||||
},
|
||||
|
||||
start : func() {
|
||||
me._timer = maketimer(me._frequency, me, me.publish);
|
||||
me._timer.start();
|
||||
},
|
||||
stop : func() {
|
||||
if(me._timer != nil) me._timer.stop();
|
||||
me._timer = nil;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
# Generic class to update properties from Emesary for the MFD
|
||||
#
|
||||
# In the simplest cases where the Emesary EventParameter for the specified
|
||||
# Event ID is a hash whose values can be mapped directly to property values,
|
||||
# it can be used directly. For more complex cases, the handleNotificationEvent
|
||||
# method should be over-ridden, and should return
|
||||
# emesary.Transmitter.ReceiptStatus_OK or equivalent ReceiptStatus value.
|
||||
|
||||
var PropertyUpdater =
|
||||
{
|
||||
|
||||
PropMap : {
|
||||
new : func(name, property)
|
||||
{
|
||||
var obj = { parents : [ PropertyUpdater.PropMap ] };
|
||||
obj._name = name;
|
||||
obj._prop = globals.props.getNode(property, 1);
|
||||
return obj;
|
||||
},
|
||||
|
||||
getName : func() { return me._name; },
|
||||
getValue : func() { return me._prop.getValue(); },
|
||||
},
|
||||
|
||||
new : func (device, notificationType, eventID, transmitter = nil) {
|
||||
var obj = {
|
||||
parents : [ Interface ],
|
||||
_device : device,
|
||||
_notificationType : notificationType,
|
||||
_transmitter : transmitter,
|
||||
_recipient : recipient,
|
||||
_propmaps : {},
|
||||
};
|
||||
|
||||
if (obj._transmitter == nil) obj._transmitter = emesary.GlobalTransmitter;
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
addPropMap : func(name, prop) {
|
||||
me._propmaps[name] = PropertyPublisher.PropMap.new(name, prop);
|
||||
},
|
||||
|
||||
handleNotificationEvent : func(eventParameter) {
|
||||
|
||||
var retval = emesary.Transmitter.ReceiptStatus_NotProcessed;
|
||||
foreach(var name; keys(eventParameters)) {
|
||||
if (me._propmaps[name] != nil) {
|
||||
me._propmaps[name].setValue(eventParameters[name])
|
||||
retval = emesary.Transmitter.ReceiptStatus_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
},
|
||||
|
||||
RegisterWithEmesary : func(){
|
||||
|
||||
if (me._recipient == nil){
|
||||
me._recipient = emesary.Recipient.new("AirportInfoController_" ~ me._page.device.designation);
|
||||
var pfd_obj = me._device;
|
||||
var controller = me;
|
||||
me._recipient.Receive = func(notification)
|
||||
{
|
||||
if (notification.Device_Id == pfd_obj.device_id
|
||||
and notification.NotificationType == me._notificationType) {
|
||||
if (notification.Event_Id == me._eventID
|
||||
and notification.EventParameter != nil)
|
||||
{
|
||||
return me.handleNotificationEvent(notification.EventParameter);
|
||||
}
|
||||
}
|
||||
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
||||
};
|
||||
}
|
||||
transmitter.Register(me._recipient);
|
||||
me.transmitter = transmitter;
|
||||
},
|
||||
DeRegisterWithEmesary : func(transmitter = nil){
|
||||
# remove registration from transmitter; but keep the recipient once it is created.
|
||||
if (me.transmitter != nil)
|
||||
me.transmitter.DeRegister(me._recipient);
|
||||
me.transmitter = nil;
|
||||
},
|
||||
|
||||
start : func() {
|
||||
me.RegisterWithEmesary();
|
||||
},
|
||||
stop : func() {
|
||||
me.DeRegisterWithEmesary();
|
||||
},
|
||||
|
||||
};
|
|
@ -11,6 +11,7 @@ var nasal_dir = getprop("/sim/fg-root") ~ "/Aircraft/Instruments-3d/FG1000/Nasal
|
|||
io.load_nasal(nasal_dir ~ '/MFDPage.nas', "fg1000");
|
||||
|
||||
var MFDPages = [
|
||||
"Surround",
|
||||
"NavigationMap",
|
||||
"EIS",
|
||||
"TrafficMap",
|
||||
|
@ -58,8 +59,6 @@ foreach (var page; MFDPages) {
|
|||
io.load_nasal(nasal_dir ~ page ~ '/' ~ page ~ 'Controller.nas', "fg1000");
|
||||
}
|
||||
|
||||
io.load_nasal(nasal_dir ~ 'PageGroupController.nas', "fg1000");
|
||||
|
||||
var MFD =
|
||||
{
|
||||
new : func (myCanvas)
|
||||
|
@ -68,8 +67,7 @@ var MFD =
|
|||
parents : [ MFD ],
|
||||
EIS : nil,
|
||||
NavigationMap: nil,
|
||||
|
||||
|
||||
Surround : nil,
|
||||
};
|
||||
|
||||
obj._svg = myCanvas.createGroup("softkeys");
|
||||
|
@ -101,26 +99,28 @@ var MFD =
|
|||
# Surround dynamic elements
|
||||
obj._pageTitle = obj._svg.getElementById("PageTitle");
|
||||
|
||||
# Controller for the display on the bottom left which allows selection
|
||||
# Controller for the header and display on the bottom left which allows selection
|
||||
# of page groups and individual pages using the FMS controller.
|
||||
obj._pageGroupController = fg1000.PageGroupController.new(myCanvas, obj._svg, obj._MFDDevice);
|
||||
obj.Surround = fg1000.Surround.new(obj, myCanvas, obj._MFDDevice, obj._svg);
|
||||
obj._pageGroupController = obj.Surround.controller;
|
||||
|
||||
# Engine Information System. A special case as it's always displayed on the MFD.
|
||||
obj.EIS = obj._pageGroupController.addPage("EIS", fg1000.EIS.new(obj, myCanvas, obj._MFDDevice, obj._svg));
|
||||
obj.EIS = obj.Surround.addPage("EIS", fg1000.EIS.new(obj, myCanvas, obj._MFDDevice, obj._svg));
|
||||
|
||||
# The NavigationMap page is a special case, as it is displayed with the Nearest... pages as an overlay
|
||||
obj.NavigationMap = obj._pageGroupController.addPage("NavigationMap", fg1000.NavigationMap.new(obj, myCanvas, obj._MFDDevice, obj._svg));
|
||||
obj.NavigationMap = obj.Surround.addPage("NavigationMap", fg1000.NavigationMap.new(obj, myCanvas, obj._MFDDevice, obj._svg));
|
||||
obj.NavigationMap.topMenu(obj._MFDDevice, obj.NavigationMap, nil);
|
||||
|
||||
foreach (var page; MFDPages) {
|
||||
if (page != "NavigationMap") {
|
||||
var code = "obj._pageGroupController.addPage(\"" ~ page ~ "\", fg1000." ~ page ~ ".new(obj, myCanvas, obj._MFDDevice, obj._svg));";
|
||||
if ((page != "NavigationMap") and (page != "EIS")) {
|
||||
var code = "obj.Surround.addPage(\"" ~ page ~ "\", fg1000." ~ page ~ ".new(obj, myCanvas, obj._MFDDevice, obj._svg));";
|
||||
var addPageFn = compile(code);
|
||||
addPageFn();
|
||||
}
|
||||
}
|
||||
|
||||
# Display the EIS and NavMap and the appropriate top level on startup.
|
||||
# Display the Surround, EIS and NavMap and the appropriate top level on startup.
|
||||
obj.Surround.setVisible(1);
|
||||
obj.EIS.setVisible(1);
|
||||
obj.EIS.ondisplay();
|
||||
obj._MFDDevice.selectPage(obj.NavigationMap);
|
||||
|
@ -142,6 +142,7 @@ var MFD =
|
|||
{
|
||||
me._MFDDevice.current_page.offdisplay();
|
||||
me._MFDDevice.DeRegisterWithEmesary();
|
||||
me._pageGroupController.del();
|
||||
|
||||
},
|
||||
setPageTitle: func(title)
|
||||
|
|
|
@ -72,6 +72,7 @@ var NearestAirportsController =
|
|||
handleCRSR : func() {
|
||||
me._crsrToggle = (! me._crsrToggle);
|
||||
if (me._crsrToggle) {
|
||||
me.page.topMenu(me.page.device, me.page, nil);
|
||||
me.page.selectAirports();
|
||||
me.selectAirports();
|
||||
} else {
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
# Controller for the 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: [ "ActiveFlightPlanWide", "FlightPlanCatalog", "StoredFlightPlan"],
|
||||
},
|
||||
|
||||
{ label: "LstGroupLabel",
|
||||
group: "LstPageGroup",
|
||||
pages: [ "Checklist1", "Checklist2", "Checklist3", "Checklist4", "Checklist5"],
|
||||
},
|
||||
|
||||
{ label: "NrstGroupLabel",
|
||||
group: "NrstPageGroup",
|
||||
pages: [ "NearestAirports", "NearestIntersections", "NearestNDB", "NearestVOR", "NearestUserWaypoints", "NearestFrequencies", "NearestAirspaces"],
|
||||
}
|
||||
];
|
||||
|
||||
var PageGroupController =
|
||||
{
|
||||
new : func (myCanvas, svg, device)
|
||||
{
|
||||
var obj = { parents : [ PageGroupController ] };
|
||||
obj._canvas = myCanvas;
|
||||
obj._svg = svg;
|
||||
obj._device = device;
|
||||
obj._menuVisible = 0;
|
||||
obj._selectedPageGroup = 0;
|
||||
obj._selectedPage = 0;
|
||||
|
||||
# List of pages to be controllers. Keys are the pages in PAGE_GROUPS;
|
||||
obj._pageList = {};
|
||||
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._loadPageTimer = maketimer(0.5, obj, obj.loadPage);
|
||||
obj._loadPageTimer.singleShot = 1;
|
||||
|
||||
obj.hideMenu();
|
||||
return obj;
|
||||
},
|
||||
|
||||
addPage : func(name, page)
|
||||
{
|
||||
me._pageList[name] = page;
|
||||
},
|
||||
|
||||
getPage : func(name)
|
||||
{
|
||||
return me._pageList[name];
|
||||
},
|
||||
|
||||
hideMenu : func()
|
||||
{
|
||||
foreach(var pageGroup; PAGE_GROUPS)
|
||||
{
|
||||
me._elements[pageGroup.group].setVisible(0);
|
||||
me._elements[pageGroup.label].setVisible(0);
|
||||
}
|
||||
me._menuVisible = 0;
|
||||
},
|
||||
|
||||
# Function to change a page based on the selection
|
||||
loadPage : func()
|
||||
{
|
||||
var pageToLoad = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
|
||||
var page = me._pageList[pageToLoad];
|
||||
|
||||
assert(page != nil, "Unable to find page " ~ pageToLoad);
|
||||
me._device.selectPage(page);
|
||||
},
|
||||
showMenu : func()
|
||||
{
|
||||
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);
|
||||
|
||||
},
|
||||
|
||||
handleFMSOuter : func(val)
|
||||
{
|
||||
if (me._menuVisible == 1) {
|
||||
# Change page group
|
||||
var incr_or_decr = (val > 0) ? 1 : -1;
|
||||
me._selectedPageGroup = math.mod(me._selectedPageGroup + incr_or_decr, size(PAGE_GROUPS));
|
||||
me._selectedPage = 0;
|
||||
}
|
||||
me.showMenu();
|
||||
return emesary.Transmitter.ReceiptStatus_Finished;
|
||||
},
|
||||
|
||||
handleFMSInner : func(val)
|
||||
{
|
||||
if (me._menuVisible == 1) {
|
||||
# Change page group
|
||||
var incr_or_decr = (val > 0) ? 1 : -1;
|
||||
me._selectedPage = math.mod(me._selectedPage + incr_or_decr, size(PAGE_GROUPS[me._selectedPageGroup].pages));
|
||||
}
|
||||
me.showMenu();
|
||||
return emesary.Transmitter.ReceiptStatus_Finished;
|
||||
|
||||
},
|
||||
};
|
254
Aircraft/Instruments-3d/FG1000/Nasal/Surround/Surround.nas
Normal file
254
Aircraft/Instruments-3d/FG1000/Nasal/Surround/Surround.nas
Normal file
|
@ -0,0 +1,254 @@
|
|||
# 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: [ "ActiveFlightPlanWide", "FlightPlanCatalog", "StoredFlightPlan"],
|
||||
},
|
||||
|
||||
{ label: "LstGroupLabel",
|
||||
group: "LstPageGroup",
|
||||
pages: [ "Checklist1", "Checklist2", "Checklist3", "Checklist4", "Checklist5"],
|
||||
},
|
||||
|
||||
{ label: "NrstGroupLabel",
|
||||
group: "NrstPageGroup",
|
||||
pages: [ "NearestAirports", "NearestIntersections", "NearestNDB", "NearestVOR", "NearestUserWaypoints", "NearestFrequencies", "NearestAirspaces"],
|
||||
}
|
||||
];
|
||||
|
||||
var Surround =
|
||||
{
|
||||
new : func (mfd, myCanvas, device, svg)
|
||||
{
|
||||
var obj = { parents : [
|
||||
Surround,
|
||||
MFDPage.new(mfd, myCanvas, device, svg, "Surround", ""),
|
||||
] };
|
||||
|
||||
var textElements = [
|
||||
"Comm1StandbyFreq", "Comm1SelectedFreq",
|
||||
"Comm2StandbyFreq", "Comm2SelectedFreq",
|
||||
"Nav1StandbyFreq", "Nav1SelectedFreq",
|
||||
"Nav2StandbyFreq", "Nav2SelectedFreq",
|
||||
];
|
||||
|
||||
obj.addTextElements(textElements);
|
||||
|
||||
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;
|
||||
|
||||
# List of pages to be controllers. Keys are the pages in PAGE_GROUPS;
|
||||
obj._pageList = {};
|
||||
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._loadPageTimer = maketimer(0.5, obj, obj.loadPage);
|
||||
obj._loadPageTimer.singleShot = 1;
|
||||
|
||||
obj.hideMenu();
|
||||
|
||||
obj.controller = fg1000.SurroundController.new(obj, svg);
|
||||
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.unhighlightElement()
|
||||
} else {
|
||||
me._comm1failed.highlightElement();
|
||||
}
|
||||
}
|
||||
|
||||
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.unhighlightElement();
|
||||
} else {
|
||||
me._comm2failed.highlightElement();
|
||||
}
|
||||
}
|
||||
|
||||
if (name == "CommSelected") {
|
||||
if (val == 1) {
|
||||
me._comm1selected.highlightElement();
|
||||
me._comm2selected.unhighlightElement();
|
||||
} else {
|
||||
me._comm1selected.unhighlightElement();
|
||||
me._comm2selected.highlightElement();
|
||||
}
|
||||
}
|
||||
|
||||
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.unhighlightElement();
|
||||
} else {
|
||||
me._nav1failed.highlightElement();
|
||||
}
|
||||
}
|
||||
|
||||
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.unhighlightElement();
|
||||
} else {
|
||||
me._nav2failed.highlightElement();
|
||||
}
|
||||
}
|
||||
|
||||
if (name == "NavSelected") {
|
||||
if (val == 1) {
|
||||
me._nav1selected.highlightElement();
|
||||
me._nav2selected.unhighlightElement();
|
||||
} else {
|
||||
me._nav1selected.unhighlightElement();
|
||||
me._nav2selected.highlightElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addPage : func(name, page)
|
||||
{
|
||||
me._pageList[name] = page;
|
||||
},
|
||||
|
||||
getPage : func(name)
|
||||
{
|
||||
return me._pageList[name];
|
||||
},
|
||||
|
||||
# Function to change a page based on the selection
|
||||
loadPage : func()
|
||||
{
|
||||
var pageToLoad = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
|
||||
var page = me._pageList[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()
|
||||
{
|
||||
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;
|
||||
},
|
||||
|
||||
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
# Surround Controller
|
||||
var SurroundController =
|
||||
{
|
||||
new : func (page, svg)
|
||||
{
|
||||
var obj = {
|
||||
parents : [ SurroundController ],
|
||||
_recipient : nil,
|
||||
_page : page,
|
||||
};
|
||||
|
||||
obj.RegisterWithEmesary();
|
||||
return obj;
|
||||
},
|
||||
|
||||
del : func() {
|
||||
me.DeRegisterWithEmesary();
|
||||
},
|
||||
|
||||
handleNavComData : func(values) {
|
||||
# pass straight through the to page - we have nothing to do.
|
||||
me._page.handleNavComData(values);
|
||||
return emesary.Transmitter.ReceiptStatus_OK;
|
||||
},
|
||||
|
||||
# These methods are slightly unusual in that they are called by other
|
||||
# controllers when the CRSR is not active. Hence they aren't referenced
|
||||
# in the RegisterWithEmesary call below.
|
||||
#
|
||||
handleFMSOuter : func(val)
|
||||
{
|
||||
if (me._page.isMenuVisible()) {
|
||||
# Change page group
|
||||
me._page.incrPageGroup(val);
|
||||
}
|
||||
me._page.showMenu();
|
||||
return emesary.Transmitter.ReceiptStatus_Finished;
|
||||
},
|
||||
|
||||
handleFMSInner : func(val)
|
||||
{
|
||||
if (me._page.isMenuVisible()) {
|
||||
# Change page group
|
||||
me.incrPage(val);
|
||||
}
|
||||
me.showMenu();
|
||||
return emesary.Transmitter.ReceiptStatus_Finished;
|
||||
},
|
||||
|
||||
RegisterWithEmesary : func(transmitter = nil){
|
||||
if (transmitter == nil)
|
||||
transmitter = emesary.GlobalTransmitter;
|
||||
|
||||
if (me._recipient == nil){
|
||||
me._recipient = emesary.Recipient.new("PageGroupController_" ~ me._page.device.designation);
|
||||
var pfd_obj = me._page.device;
|
||||
var controller = me;
|
||||
me._recipient.Receive = func(notification)
|
||||
{
|
||||
if (notification.Device_Id == pfd_obj.device_id
|
||||
and notification.NotificationType == notifications.PFDEventNotification.DefaultType) {
|
||||
if (notification.Event_Id == notifications.PFDEventNotification.NavComData
|
||||
and notification.EventParameter != nil)
|
||||
{
|
||||
return controller.handleNavComData(notification.EventParameter);
|
||||
}
|
||||
}
|
||||
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
||||
};
|
||||
}
|
||||
transmitter.Register(me._recipient);
|
||||
me.transmitter = transmitter;
|
||||
},
|
||||
DeRegisterWithEmesary : func(transmitter = nil){
|
||||
# remove registration from transmitter; but keep the recipient once it is created.
|
||||
if (me.transmitter != nil)
|
||||
me.transmitter.DeRegister(me._recipient);
|
||||
me.transmitter = nil;
|
||||
},
|
||||
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
# Surround Options
|
||||
var SurroundOptions =
|
||||
{
|
||||
new : func() {
|
||||
var obj = { parents : [SurroundOptions] };
|
||||
obj.Options= {};
|
||||
obj.loadOptions();
|
||||
return obj;
|
||||
},
|
||||
|
||||
getOption : func(type) {
|
||||
return me.Options[type];
|
||||
},
|
||||
|
||||
setOption : func(type, name, value) {
|
||||
me.Options[type][name] = value;
|
||||
},
|
||||
|
||||
loadOptions : func() {
|
||||
me.clearOptions();
|
||||
me.Options.APS = {};
|
||||
},
|
||||
|
||||
clearOptions : func() {
|
||||
me.Options = {};
|
||||
},
|
||||
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
# Surround Styles
|
||||
var SurroundStyles =
|
||||
{
|
||||
new : func() {
|
||||
var obj = { parents : [ SurroundStyles ]};
|
||||
obj.Styles = {};
|
||||
obj.loadStyles();
|
||||
return obj;
|
||||
},
|
||||
|
||||
getStyle : func(type) {
|
||||
return me.Styles[type];
|
||||
},
|
||||
|
||||
setStyle : func(type, name, value) {
|
||||
me.Styles[type][name] = value;
|
||||
},
|
||||
|
||||
loadStyles : func() {
|
||||
me. clearStyles();
|
||||
me.Styles.XXX = {};
|
||||
},
|
||||
|
||||
clearStyles : func() {
|
||||
me.Styles = {};
|
||||
},
|
||||
|
||||
};
|
|
@ -545,6 +545,7 @@ var PFDEventNotification =
|
|||
# 3 Change softkey button text
|
||||
# 4 hardkey pushed - i.e. non-soft keys that don't change function based on context.
|
||||
# 5 Engine data - e.g. RPM, EGTs, CHTs for display purposes
|
||||
# 6 NavCom data - e.g. frequencies, volume, TX, RX for each of the COM and NAV radios.
|
||||
# _event_param - param related to the event ID. implementation specific.
|
||||
##
|
||||
SoftKeyPushed : 1,
|
||||
|
@ -552,6 +553,7 @@ var PFDEventNotification =
|
|||
ChangeMenuText : 3, #event parameter contains array of { Id: , Text: } tuples
|
||||
HardKeyPushed : 4, #event parameter contains single { Id: , Value: } tuple
|
||||
EngineData : 5, #event parameter contains an array of hashes, each containing information about a given engine.
|
||||
NavComData : 6, #event paramterr contains a hash of updated Nav/Com settings
|
||||
|
||||
DefaultType : "PFDEventNotification",
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
var self = cmdarg();
|
||||
var listeners = [];
|
||||
var mfd = nil;
|
||||
var eisPublisher = nil;
|
||||
var navcomPublisher = nil;
|
||||
]]></open>
|
||||
|
||||
<close><![CDATA[
|
||||
|
@ -22,6 +24,11 @@
|
|||
removelistener(l);
|
||||
setsize(listeners, 0);
|
||||
mfd.del();
|
||||
eisPublisher.stop();
|
||||
eisPublisher = nil;
|
||||
navcomPublisher.stop();
|
||||
navcomPublisher = nil;
|
||||
|
||||
|
||||
]]></close>
|
||||
</nasal>
|
||||
|
@ -362,8 +369,17 @@
|
|||
|
||||
var nasal_dir = getprop("/sim/fg-root") ~ "/Aircraft/Instruments-3d/FG1000/Nasal/";
|
||||
io.load_nasal(nasal_dir ~ 'MFD.nas', "fg1000");
|
||||
io.load_nasal(nasal_dir ~ 'Interfaces/PropertyPublisher.nas', "fg1000");
|
||||
io.load_nasal(nasal_dir ~ 'Interfaces/GenericEISPublisher.nas', "fg1000");
|
||||
io.load_nasal(nasal_dir ~ 'Interfaces/GenericNavComPublisher.nas', "fg1000");
|
||||
mfd = fg1000.MFD.new(myCanvas);
|
||||
|
||||
eisPublisher = fg1000.GenericEISPublisher.new();
|
||||
eisPublisher.start();
|
||||
|
||||
navcomPublisher = fg1000.GenericNavComPublisher.new();
|
||||
navcomPublisher.start();
|
||||
|
||||
softkeypushed = 0;
|
||||
# Connect the buttons - using the provided model index to get the right ones from the model binding
|
||||
var softkey_listener = setlistener("/sim/gui/dialogs/fg1000/softkey-pressed", func(v)
|
||||
|
|
Loading…
Add table
Reference in a new issue