Difference between revisions of "Canvas MFD Framework"

From FlightGear wiki
Jump to: navigation, search
(Introduction)
(Update to cover the new UI Elements for the PFD)
Line 9: Line 9:
 
This works with a SVG file that defines the menu label locations and has a group for each page. The framework manages the menu hierarchy and displays labels corresponding to the buttons.  
 
This works with a SVG file that defines the menu label locations and has a group for each page. The framework manages the menu hierarchy and displays labels corresponding to the buttons.  
  
This is based around the SpaceShuttle displays which are in turn based on the http://wiki.flightgear.org/McDonnell_Douglas_F-15_Eagle#MPCD  
+
This is based around the SpaceShuttle displays which are in turn based on the http://wiki.flightgear.org/McDonnell_Douglas_F-15_Eagle#MPCD.
  
The code as below will allow multiple instances of a particular display to be created. Towards the end of the code an array is populated with one MFD_device instance per display. All use the same SVG and do the same job - but can be on different devices (left, right, etc) in the cockpit. The 3d model must have the frame, bezel and buttons for each display. The model_index selects which buttons to use. The buttons are named sim/model/f15/controls/PFD/button-pressed0 - where this property contains the index of the button, and there is one property per display.
+
For a straightforward example of this in use, see https://sourceforge.net/p/flightgear/fgaddon/HEAD/tree/trunk/Aircraft/F-15/Nasal/MPCD/MPCD_main.nas
 +
 
 +
For a more complex example as part of the FG1000 glass cockpit, see https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Aircraft/Instruments-3d/FG1000/Nasal/MFD.nas
  
 
[[File:f-15-cockpit-mpcd-armament.jpg|300px|MCPD Armament Top Level Menu]]
 
[[File:f-15-cockpit-mpcd-armament.jpg|300px|MCPD Armament Top Level Menu]]
  
== Implementation example ==
+
== Design ==
  
This uses a 3d rectangle called "MPCDImage". If there is likely to be more than one instance of an MFD then all of this logic should be wrapped up inside a class
+
The PFD/MFD contains multiple pages, each of which can be referenced by buttons and displayed.
  
<syntaxhighlight lang="nasal">
+
Emesary is used to pass input information from the buttons into the MFD. This allows MP support hardware interfaces.
# F-15 Canvas MPCD (Multi-Purpose-Colour-Display)
+
# ---------------------------
+
# MPCD has many pages; the classes here support multiple pages, menu
+
# operation and the update loop.
+
# 2016-05-17: Refactor to use Nasal/canvas/MFD_Generic.nas
+
# ---------------------------
+
# Richard Harrison: 2015-01-23 : rjh@zaretto.com
+
# ---------------------------
+
  
#for debug: setprop ("/sim/startup/terminal-ansi-colors",0);
+
To make it easier to use SVG files for UI elements, a number of UI primitives are supported which support highlighting, editing in a generic way (see https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Nasal/canvas/PFD/).
  
var MPCD_Station =
+
== UI Primitives ==
{
+
new : func (svg, ident)
+
    {
+
var obj = {parents : [MPCD_Station] };
+
  
        obj.status = svg.getElementById("PACS_L_"~ident);
+
===UIElement===
        if (obj.status == nil)
+
Abstract element, all other elements inherit from this and have the following methods:
            print("Failed to load PACS_L_"~ident);
+
  
        obj.label = svg.getElementById("PACS_V_"~ident);
+
* getName : func() { return me._name; },
        if (obj.label == nil)
+
* setValue : func(value) { me._value = value; },
            print("Failed to load PACS_V_"~ident);
+
* getValue : func() { return me._value; },
 +
* highlightElement : func() { },
 +
* unhighlightElement : func() { },
 +
* isEditable : func () { return 0; },
 +
* isInEdit : func() { return me._edit; },
 +
* enterElement : func() { me._edit = 0; return me._value; },
 +
* clearElement : func() { me._edit = 0; },
 +
* editElement : func() { me._edit = 1; },
 +
* setVisible : func(vis) { },
 +
* incrSmall : func(value) { },
 +
* incrLarge : func(value) { },
  
        obj.selected = svg.getElementById("PACS_R_"~ident);
 
        if (obj.selected == nil)
 
            print("Failed to load PACS_R_"~ident);
 
  
        obj.selected1 = svg.getElementById("PACS_R1_"~ident);
+
===TextElement===
        if (obj.selected1 == nil)
+
Simple text element, can have values set (setValue) and retrieved (getValue).
            print("Failed to load PACS_R1_"~ident);
+
  
        obj.prop = "payload/weight["~ident~"]";
+
In the SVG file, this will have the name [pagename][name].
        obj.ident = ident;
+
  
        setlistener(obj.prop~"/selected", func(v)
+
Constructor:  new : func (pagename, svg, name, value="", style=nil)
                    {
+
* pagename - the name of the page this belongs to, as referenced by the MFD.  This is the prefix for the SVG elements themselves.
                        obj.update();
+
* svg - reference the svg Group element that contains the element.
                    });
+
* name - name of the element.  In the SVG file this will be [pagename][name].
        setlistener("sim/model/f15/controls/armament/weapons-updated", func
+
* value - initial value
                    {
+
* style - style hash
                        obj.update();
+
                    });
+
  
        obj.update();
+
===ArrowElement===
        return obj;
+
An SVG element that is made visible or hidden to indicate that an element is selected, rather than highlighting the text.
    },
+
  
    update: func
+
In the SVG file, this will have the name [pagename][name].
    {
+
        var weapon_mode = getprop("sim/model/f15/controls/armament/weapon-selector");
+
        var na = getprop(me.prop~"/selected");
+
        var sel = 0;
+
        var mode = "STBY";
+
        var sel_node = "sim/model/f15/systems/external-loads/station["~me.ident~"]/selected";
+
        var master_arm=getprop("sim/model/f15/controls/armament/master-arm-switch");
+
  
        if (na != nil and na != "none")
+
Constructor:  new : func (pagename, svg, name, value="", style=nil)
        {
+
* pagename - the name of the page this belongs to, as referenced by the MFD.  This is the prefix for the SVG elements themselves.
            if (na == "AIM-9")
+
* svg - reference the svg Group element that contains the element.
            {
+
* name - name of the element. In the SVG file this will be [pagename][name].
                na = "9L";
+
* value - initial value
                if (weapon_mode == 1)
+
* style - style hash
                {
+
                    sel = getprop(sel_node);
+
                    if (sel and master_arm)
+
                        mode = "RDY";
+
                }
+
                else mode = "SRM";
+
            }
+
            elsif (na == "AIM-120")
+
            {
+
                na = "120A";
+
                if (weapon_mode == 2)
+
                {
+
                    sel = getprop(sel_node);
+
                    if (sel and master_arm)
+
                        mode = "RDY";
+
                }
+
                else mode = "MRM";
+
            }
+
            elsif (na == "AIM-7")
+
            {
+
                na = "7M";
+
                if (weapon_mode == 2)
+
                {
+
                    sel = getprop(sel_node);
+
                    if (sel and master_arm)
+
                        mode = "RDY";
+
                }
+
                else mode = "MRM";
+
            }
+
            me.status.setText(mode);
+
            me.label.setText(na);
+
  
            me.selected1.setVisible(sel);
+
===ScrollElement===
            if (mode == "RDY")
+
Element that can take a number of pre-defined values.   
            {
+
                me.selected.setVisible(sel);
+
                me.status.setColor(0,1,0);
+
            }
+
            else
+
            {
+
                me.selected.setVisible(0);
+
                me.status.setColor(1,1,1);
+
            }
+
        }
+
        else
+
        {
+
            me.status.setText("");
+
            me.label.setText("");
+
            me.selected.setVisible(0);
+
            me.selected1.setVisible(0);
+
        }
+
    },
+
};
+
 
+
var MPCD_Device =
+
{
+
#
+
# create new MFD device. This is the main interface (from our code) to the MFD device
+
# Each MFD device will contain the underlying PFD device object, the SVG, and the canvas
+
# Parameters
+
# - designation - Flightdeck Legend for this
+
# - model_element - name of the 3d model element that is to be used for drawing
+
# - model_index - index of the device
+
    new : func(designation, model_element, model_index=0)
+
    {
+
        var obj = {parents : [MPCD_Device] };
+
        obj.designation = designation;
+
        obj.model_element = model_element;
+
        var dev_canvas= canvas.new({
+
                "name": designation,
+
                          "size": [1024,1024],
+
                          "view": [740,680],                     
+
                    "mipmapping": 1   
+
                    });                         
+
 
+
        dev_canvas.addPlacement({"node": model_element});
+
        dev_canvas.setColorBackground(0.003921,0.1764,0, 0);
+
# Create a group for the parsed elements
+
        obj.PFDsvg = dev_canvas.createGroup();
+
        var pres = canvas.parsesvg(obj.PFDsvg, "Nasal/MPCD/MPCD_0_0.svg");
+
# Parse an SVG file and add the parsed elements to the given group
+
        printf("MPCD : %s Load SVG %s",designation,pres);
+
        obj.PFDsvg.setTranslation (270.0, 197.0);
+
#
+
# create the object that will control all of this
+
        obj.num_menu_buttons = 20;
+
        obj.PFD = PFD_Device.new(obj.PFDsvg, obj.num_menu_buttons, "MI_", dev_canvas);
+
        obj.PFD._canvas = dev_canvas;
+
        obj.PFD.designation = designation;
+
        obj.mfd_device_status = 1;
+
        obj.model_index = model_index; # numeric index (1 to 9, left to right) used to connect the buttons in the cockpit to the display
+
 
+
        obj.addPages();
+
        return obj;
+
    },
+
 
+
    addPages : func
+
    {
+
        me.p1_1 = me.PFD.addPage("Aircraft Menu", "p1_1");
+
 
+
        me.p1_1.update = func
+
        {
+
            var sec = getprop("instrumentation/clock/indicated-sec");
+
            me.page1_1.time.setText(getprop("sim/time/gmt-string")~"Z");
+
            var cdt = getprop("sim/time/gmt");
+
 
+
            if (cdt != nil)
+
                me.page1_1.date.setText(substr(cdt,5,2)~"/"~substr(cdt,8,2)~"/"~substr(cdt,2,2)~"Z");
+
        };
+
 
+
        me.p1_1 = me.PFD.addPage("Aircraft Menu", "p1_1");
+
        me.p1_2 = me.PFD.addPage("Top Level PACS Menu", "p1_2");
+
        me.p1_3 = me.PFD.addPage("PACS Menu", "p1_3");
+
        me.p1_3.S0 = MPCD_Station.new(me.PFDsvg, 0);
+
        #1 droptank
+
        me.p1_3.S2 = MPCD_Station.new(me.PFDsvg, 2);
+
        me.p1_3.S3 = MPCD_Station.new(me.PFDsvg, 3);
+
        me.p1_3.S4 = MPCD_Station.new(me.PFDsvg, 4);
+
        #5 droptank
+
        me.p1_3.S6 = MPCD_Station.new(me.PFDsvg, 6);
+
        me.p1_3.S7 = MPCD_Station.new(me.PFDsvg, 7);
+
        me.p1_3.S8 = MPCD_Station.new(me.PFDsvg, 8);
+
        #9 droptank
+
        me.p1_3.S10 = MPCD_Station.new(me.PFDsvg, 10);
+
 
+
        me.pjitds_1 =  PFD_NavDisplay.new(me.PFD,"Situation", "mpcd-sit", "pjitds_1", "jtids_main");
+
        # use the radar range as the ND range.
+
 
+
        me.p_spin_recovery = me.PFD.addPage("Spin recovery", "p_spin_recovery");
+
        me.p_spin_recovery.cur_page = nil;
+
 
+
        me.p1_1.date = me.PFDsvg.getElementById("p1_1_date");
+
        me.p1_1.time = me.PFDsvg.getElementById("p1_1_time");
+
 
+
        me.p_spin_recovery.p_spin_cas = me.PFDsvg.getElementById("p_spin_cas");
+
        me.p_spin_recovery.p_spin_alt = me.PFDsvg.getElementById("p_spin_alt");
+
        me.p_spin_recovery.p_spin_alpha = me.PFDsvg.getElementById("p_spin_alpha");
+
        me.p_spin_recovery.p_spin_stick_left  = me.PFDsvg.getElementById("p_spin_stick_left");
+
        me.p_spin_recovery.p_spin_stick_right  = me.PFDsvg.getElementById("p_spin_stick_right");
+
        me.p_spin_recovery.update = func
+
        {
+
            me.p_spin_alpha.setText(sprintf("%d", getprop ("orientation/alpha-indicated-deg")));
+
            me.p_spin_alt.setText(sprintf("%5d", getprop ("instrumentation/altimeter/indicated-altitude-ft")));
+
            me.p_spin_cas.setText(sprintf("%3d", getprop ("instrumentation/airspeed-indicator/indicated-speed-kt")));
+
 
+
            if (math.abs(getprop("fdm/jsbsim/velocities/r-rad_sec")) > 0.52631578947368421052631578947368
+
                or math.abs(getprop("fdm/jsbsim/velocities/p-rad_sec")) > 0.022)
+
            {
+
                me.p_spin_stick_left.setVisible(1);
+
                me.p_spin_stick_right.setVisible(0);
+
            }
+
            else
+
            {
+
                me.p_spin_stick_left.setVisible(0);
+
                me.p_spin_stick_right.setVisible(1);
+
            }
+
        };
+
 
+
        #
+
        # Page 1 is the time display
+
        me.p1_1.update = func
+
        {
+
            var sec = getprop("instrumentation/clock/indicated-sec");
+
            me.time.setText(getprop("sim/time/gmt-string")~"Z");
+
            var cdt = getprop("sim/time/gmt");
+
 
+
            if (cdt != nil)
+
                me.date.setText(substr(cdt,5,2)~"/"~substr(cdt,8,2)~"/"~substr(cdt,2,2)~"Z");
+
        };
+
 
+
        #
+
        # armament page gun rounds is implemented a little differently as the menu item (1) changes to show
+
        # the contents of the magazine.
+
        me.p1_3.gun_rounds = me.p1_3.addMenuItem(1, sprintf("HIGH\n%dM",getprop("sim/model/f15/systems/gun/rounds")), me.p1_3);
+
 
+
        setlistener("sim/model/f15/systems/gun/rounds", func(v)
+
                    {
+
                        if (v != nil) {
+
                            me.p1_3.gun_rounds.title = sprintf("HIGH\n%dM",v.getValue());
+
                            me.PFD.updateMenus();
+
                        }
+
                    }
+
            );
+
        me.PFD.selectPage(me.p1_1);
+
        me.mpcd_button_pushed = 0;
+
        # Connect the buttons - using the provided model index to get the right ones from the model binding
+
        setlistener("sim/model/f15/controls/MPCD/button-pressed", func(v)
+
                    {
+
                        if (v != nil) {
+
                            if (v.getValue())
+
                                me.mpcd_button_pushed = v.getValue();
+
                            else {
+
                                printf("%s: Button %d",me.designation, me.mpcd_button_pushed);
+
                                me.PFD.notifyButton(me.mpcd_button_pushed);
+
                                me.mpcd_button_pushed = 0;
+
                            }
+
                        }
+
                    }
+
            );
+
 
+
        # Set listener on the PFD mode button; this could be an on off switch or by convention
+
        # it will also act as brightness; so 0 is off and anything greater is brightness.
+
        # ranges are not pre-defined; it is probably sensible to use 0..10 as an brightness rather
+
        # than 0..1 as a floating value; but that's just my view.
+
        setlistener("sim/model/f15/controls/PFD/mode"~me.model_index, func(v)
+
                    {
+
                        if (v != nil) {
+
                            me.mfd_device_status = v.getValue();
+
                            print("MFD Mode ",me.designation," ",me.mfd_device_status);
+
                            if (!me.mfd_device_status)
+
                                me.PFDsvg.setVisible(0);
+
                            else
+
                                me.PFDsvg.setVisible(1);
+
                        }
+
                    }
+
            );
+
 
+
        me.mpcd_button_pushed = 0;
+
        me.setupMenus();
+
        me.PFD.selectPage(me.p1_1);
+
    },
+
 
+
    # Add the menus to each page.
+
    setupMenus : func
+
    {
+
#
+
# Menu Id's
+
# 0          5           
+
# 1          6           
+
# 2          7           
+
# 3          8           
+
# 4          9           
+
#
+
# Top: 10 11 12 13 14
+
# Bot: 15 16 17 18 19
+
        me.mpcd_spin_reset_time = 0;
+
 
+
        me.p1_1.addMenuItem(0, "ARMT", me.p1_2);
+
        me.p1_1.addMenuItem(1, "BIT", me.p1_2);
+
        me.p1_1.addMenuItem(2, "SIT", me.pjitds_1);
+
        me.p1_1.addMenuItem(3, "WPN", me.p1_2);
+
        me.p1_1.addMenuItem(4, "DTM", me.p1_2);
+
 
+
        me.p1_2.addMenuItem(1, "A/A", me.p1_3);
+
        me.p1_2.addMenuItem(2, "A/G", me.p1_3);
+
        me.p1_2.addMenuItem(3, "CBT JETT", me.p1_3);
+
        me.p1_2.addMenuItem(4, "WPN LOAD", me.p1_3);
+
        me.p1_2.addMenuItem(9, "M", me.p1_1);
+
 
+
        me.p1_3.addMenuItem(2, "SIT", me.pjitds_1);
+
        me.p1_3.addMenuItem(3, "A/G", me.p1_3);
+
        me.p1_3.addMenuItem(4, "2/2", me.p1_3);
+
        me.p1_3.addMenuItem(8, "TM\nPWR", me.p1_3);
+
        me.p1_3.addMenuItem(9, "M", me.p1_1);
+
        me.p1_3.addMenuItem(10, "PYLON", me.p1_3);
+
        me.p1_3.addMenuItem(12, "FUEL", me.p1_3);
+
        me.p1_3.addMenuItem(14, "PYLON", me.p1_3);
+
        me.p1_3.addMenuItem(15, "MODE S", me.p1_3);
+
 
+
        me.pjitds_1.addMenuItem(9, "M", me.p1_1);
+
    },
+
 
+
    update : func
+
    {
+
    # see if spin recovery page needs to be displayed.
+
    # it is displayed automatically and will remain for 5 seconds.
+
    # this page provides (sort of) guidance on how to recover from a spin
+
    # which is identified by the yar rate.
+
        if (!wow and math.abs(getprop("fdm/jsbsim/velocities/r-rad_sec")) > 0.52631578947368421052631578947368)
+
        {
+
            if (me.PFD.current_page != me.p_spin_recovery)
+
            {
+
                me.p_spin_recovery.cur_page = me.PFD.current_page;
+
                me.PFD.selectPage(me.p_spin_recovery);
+
            }
+
            me.mpcd_spin_reset_time = getprop("instrumentation/clock/indicated-sec") + 5;
+
        }
+
        else
+
        {
+
            if (me.mpcd_spin_reset_time > 0 and getprop("instrumentation/clock/indicated-sec") > me.mpcd_spin_reset_time)
+
            {
+
                me.mpcd_spin_reset_time = 0;
+
                if (me.p_spin_recovery.cur_page != nil)
+
                {
+
                    me.PFD.selectPage(me.p_spin_recovery.cur_page);
+
                    me.p_spin_recovery.cur_page = nil;
+
                }
+
            }
+
        }
+
 
+
        if (me.mfd_device_status)
+
            me.PFD.update();
+
    },
+
};
+
 
+
#
+
# Mode switch is day/night/off. we just do on/off
+
setlistener("sim/model/f15/controls/MPCD/mode", func(v)
+
            {
+
                if (v != nil)
+
                {
+
                    MPCD.mpcd_mode = v.getValue();
+
#    if (!mpcd_mode)
+
#        MPCDcanvas.setVisible(0);
+
#    else
+
#        MPCDcanvas.setVisible(1);
+
                }
+
            });
+
 
+
 
+
#
+
# Connect the radar range to the nav display range.
+
setprop("instrumentation/mpcd-sit/inputs/range-nm", getprop("instrumentation/radar/radar2-range"));
+
setlistener("instrumentation/radar/radar2-range",
+
            func(v)
+
            {
+
                setprop("instrumentation/mpcd-sit/inputs/range-nm", v.getValue());
+
            });
+
 
+
var MFD_array = [];
+
 
+
#
+
#
+
# Create and append all of the MFDs in the cockpit.
+
# - MFD_Device.new( Identity, Canvas3dSurface, model index)
+
#               
+
append(MFD_array, MPCD_Device.new("F15-MPCD", "MPCDImage",0));
+
 
+
# update only one display per frame to reduce load. This can easily be changed
+
# to update all by looping around all of the displays in the MFD_array
+
var frame_device_update_id = 0;
+
 
+
var MFD_rtExec_update = func
+
{
+
    if (frame_device_update_id >= size(MDU_array))
+
        frame_device_update_id = 0;
+
 
+
    if (frame_device_update_id < size(MDU_array))
+
        MDU_array[frame_device_update_id].update();
+
 
+
    frame_device_update_id += 1;
+
    MFD_rtExec_timer.restart(0.02);
+
}
+
 
+
var MFD_rtExec_timer = maketimer(6, MFD_rtExec_update);
+
MFD_rtExec_timer.restart(6);
+
 
+
</syntaxhighlight>
+
 
+
== Background ==
+
{{Note|Please also see [[Howto:Coding a simple Nasal Framework]]}}
+
 
+
{{FGCquote
+
|1= I wish we could share more of our development on these things. Having lots of logic for route management and performance data and autopilot stuff re-implemented in every new "realistic" plane kinda sucks. It would be nice if we could build up the built-in system that covers 90% of the cases and any specialiation can be handled by refining the built-in system. Adding a new aircraft should be throw up a couple of displays and live with the built-in default ND and MFD in the first iteration, and then progressively refine it to match the real deal.
+
|2= {{cite web
+
  | url    = http://forum.flightgear.org/viewtopic.php?p=277481#p277481
+
  | title = <nowiki>Re: FMC</nowiki>
+
  | author = <nowiki>alge</nowiki>
+
  | date  = Feb 24th, 2016
+
  | added  = Feb 24th, 2016
+
  | script_version = 0.25
+
  }}
+
}}
+
 
+
{{cquote
+
  |<nowiki>If the coders here at FG spent their time (1000 hrs) making the apps / tools that take all the coding out of making stuff, then the modders those with little coding knowledge would spend the 3000 hrs doing the rest.</nowiki>
+
  |{{cite web |url=http://forum.flightgear.org/viewtopic.php?p=211072#p211072
+
    |title=<nowiki>Re: Does FlightGear has Multiplayer Combat mode?</nowiki>
+
    |author=<nowiki>Bomber</nowiki>
+
    |date=<nowiki>Thu May 29</nowiki>
+
  }}
+
}}
+
 
+
{{FGCquote
+
  |as long as you keep coding with a generic mindset, i.e. to support use-cases like:<br/>
+
<br/>
+
<ul>
+
<li> multiple independent instances per instrument/layer </li>
+
<li> different instruments/aircraft (i.e. with different AP/RM or NAV/COM dependencies)</li>
+
<li> the GUI use-case (see map-canvas.xml in $FG_ROOT/gui/dialogs)</li>
+
<li> styling (custom colors, fonts, symbols etc)</li>
+
<li> different position sources (aircraftpos.controller/NDSourceDriver)</li>
+
</ul>
+
<br/>
+
None of this is rocket science or even very difficult - these are fairly basic things, but most people fail to keep all of them in mind, so that they end up coming up with instruments that only support a single use-case unfortunately.<br/>
+
<br/>
+
But as long as people keep following a handful of guidelines, we can even easily support new use-cases, including even a standalone FGCanvas mode, without it involving a ton of porting work.  Likewise, GUI dialogs for configuring weather or showing a GUI console, would then automatically be able to also support holding patterns, vertical profiles (VSD) and weather overlays.<br/>
+
<br/>
+
And you really only need to know a subset of Nasal to apply these guidelines, and it will get you pretty far.
+
  |{{cite web |url=http://forum.flightgear.org/viewtopic.php?p=216427#p216427
+
    |title=<nowiki>Re: A330-200 with Canvas and other features</nowiki>
+
    |author=<nowiki>Hooray</nowiki>
+
    |date=<nowiki>Mon Aug 11</nowiki>
+
  }}
+
}}
+
'''Hooray:''' Basically, my idea was to generalize the ND code some more and come up with helper classes for 1) SGSubsystems, 2) MFD instruments and 3) image generators, 4) displays and display switches/selectors. Introducing a handful of helper classes would allow us to support all important use-cases that are common in modern glass cockpits, including switching between different FMCs or PFDs/NDs. I would really prefer to closely coordinate things here - we really don't want to have people come up with hugely different approaches for different types of MFDs.
+
 
+
A common framework in the background (with some common elements like "rotating" numbers, HSI logic, trend vector calculations, etc.) is definitely a good idea.
+
 
+
Canvas MFD displays are just canvas textures, which are just represented as "properties", or rather, "property branches" - in the form of /canvas/by-index/texture[x] - you can use the property tree browser to check how these work - but basically, each canvas texture can have multiple "placements", such as "aircraft", "scenery" or GUI (dialog/window) - these placements maintain a reference count internally.
+
 
+
Toggling between different image sources is also accomplished by supporting recursion through "nested canvases" - i.e. a canvas can "include" (reference) another canvas and use it as the image source (raster image).
+
 
+
Thus, technically, the only thing involved would be coming up with two classes for "displays" and "image generators" - where each would be internally mapped to a canvas texture, the image generator would be "black box" where rendering takes place - while the "display" would be just a simple canvas that references the proper canvas texture, based on the currently selected switch/mode.
+
Meanwhile, I would prefer coming up with a real MFD framework that manages different displays/screens and image sources.
+
 
+
That should then also help with PFD/ND/CDU and EICAS/EFIS stuff.
+
 
+
Philosopher's MapStructure framework has been specifically designed to support the notion of controllers for these things, so need to add any heavy hacks to the code - we should better work together and ensure that MapStructure ends up in fgdata soon enough ...
+
Gijs already started working on a MFD creation framework for the 744, and as previously mentioned, certain features are going to be identical - regardless of aircraft, i.e. bizjet, boeing, airbus etc - most MFDs will have knobs to adjust brightness or change video sources - so I'd rather keep the general design in mind here, and not implement such things specifically for a certain aircraft. Ultimately, it really just boils down to mapping a few properties to the corresponding canvas properties.
+
 
+
 
+
 
+
{{cquote|I also need a better way to switch pages on the lower EICAS. Right now I delete/re-create the Canvas with this code. It doesn't work well though; at times no page is loaded at all. Of course I cannot delete a Canvas when I have it displayed in a dialog, so this method is probably doomed...
+
<ref>{{cite web |url=http://forum.flightgear.org/viewtopic.php?f=19&t=7867&hilit=switch+mfd&start=75#p191702
+
|title=The making of the Queen
+
|author=Gijs |date= Sun Oct 13, 2013 9:04 am}}</ref>|Gijs}}
+
 
+
 
+
{{cquote|I'm also doing some work on my C-130J cockpit and therefore have got nearly the same problems^^ There are currently five screens with a lot of pages which can be freely placed on any of the screens. I'm not yet sure on how to setup this system in detail. If displays/windows/etc. show exactly the same thing they should also use the same canvas. One approach would be to use a canvas for each page and add one ore more placements to it depending on where it should be displayed.
+
Another approach would be to use a canvas for each screen and either reload each page on switching or after loading once hide the according group.
+
A completely different approach (which probably also will require some core changes) is to allow moving groups between different canvasses and also just to a storage location to move pages around as needed.
+
 
+
<ref>{{cite web |url=http://forum.flightgear.org/viewtopic.php?f=19&t=7867&hilit=switch+mfd&start=90#p191764
+
|title=The making of the Queen
+
|author=TheTom |date= Mon Oct 14, 2013 5:02 am}}</ref>|TheTom}}
+
 
+
 
+
{{cquote|It would probably be a good idea to look at existing airliners in FG, such as the 744, 777 and then come up with a simple Nasal-space framework to manage image sources and screens, so that a screen selector would ideally only manage placements, while supporting different MFDs for each pilot - analogous to how A661 has the concept of an image generator (IG) and a cockpit display system (CDS).
+
 
+
For most modern jets it would make sense to introduce some intermediate layer that wraps the main canvas system, so that different displays (PFD, ND, EICAS, M/CDU etc) can be conveniently managed.
+
 
+
Basically, we only need to add a handful of Nasal wrapper classes that provide the building blocks for any kind of EFIS, i.e. generic components such as:
+
 
+
* display (CRT/LCD)
+
* source selector: http://www.meriweather.com/flightdeck/747/ctr-747.html
+
* image source (a Nasal class that simply wraps a canvas)
+
* display settings (brightness etc)
+
 
+
cockpit developers would then ideally use existing components or add new ones as required, for different types of EFIS (777, 747, A320, A380).
+
 
+
PFD/ND and EICAS/ECAM or MCDUs would be built on top of these.<ref>{{cite web |url=http://forum.flightgear.org/viewtopic.php?f=19&t=7867&hilit=switch+mfd&start=90#p191764
+
|title=The making of the Queen
+
|author=Hooray |date= Mon Oct 14, 2013 5:02 am}}</ref>|Hooray}}
+
 
+
{{cquote
+
  |<nowiki>Canvas & Nasal are still fairly low-level for most aircraft developers, to come up with good -and fast displays (code)- people still need to be experienced coders, and familiar with FlightGear scripting and Canvas technologies/elements and the way performance is affected through certain constructs. So far, we now have the means to create the corresponding visuals, but there's still quite some work ahead to re-implement existing hard-coded displays - but to implement a compelling jet fighter, including a credible cockpit, you would need more than "just" the visuals, i.e. lots of handbooks/manuals, building blocks for creating systems and components, and scripting-space frameworks to help with the latter.
+
 
+
The best option to pave the way for this is to keep on generalizing existing code, so that instruments support multiple instances, multiple aircraft, and multiple "sensors". Here, galvedro's work is really promising. But for the "visual" side of this, we really need to generalize our NavDisplay code much more - so that we can factor out MFD related functionality, and reuse it on fighters like the m2000-5.</nowiki>
+
  |{{cite web |url=http://forum.flightgear.org/viewtopic.php?p=211061#p211061
+
    |title=<nowiki>Re: Does FlightGear has Multiplayer Combat mode?</nowiki>
+
    |author=<nowiki>Hooray</nowiki>
+
    |date=<nowiki>Thu May 29</nowiki>
+
  }}
+
}}
+
 
+
<references/>
+
 
+
== Design ==
+
{{Out of date}}
+
<!--
+
{{Note|[[File:Gpsmap196-panel-page-gui-dialog.png|thumb|F-JJTH's gpsmap196 GUI dialog showing the panel page]]
+
This is currently being worked on by F-JJTH & Hooray as part of working on [[Garmin GPSMap 196]]}}
+
  
{{Note|Also see [[File:Extra500canvas.png|thumb|[[Avidyne Entegra R9]] ]]}}
+
In addition to the [PageName][name] SVG Text element, [PageName][name]Left and [PageName][name]Right SVG elements used to indicate if there are any further values to scroll through.
-->
+
  
* Screen
+
Constructor:  new : func (pageName, svg, name, values, initialIndex=0, style=nil)
* Image Source
+
* pagename - the name of the page this belongs to, as referenced by the MFD.  This is the prefix for the SVG elements themselves.
* Switch/Selector
+
* svg - reference the svg Group element that contains the element.
* Placement Manager
+
* name - name of the element.  In the SVG file this will be [pagename][name].
 +
* values - array of values that this scroll element can scroll through
 +
* initialIndex - index into the array of values.
 +
* style - style hash
  
From am design point of view, I would probably introduce a handful of helpers to help with all these tasks:
+
Additional methods:
* SGSubsystem wrapper for Nasal-based subystems
+
* setValues(values_array) sets the values of the element to values_array
* MFDScreen (wrapper for canvases referenced as raster images)
+
* MFDImageGenerator (wrapper for a canvas rendering context)
+
* CockpitButton (Switch, Button, Selector)
+
* MFDSourceSelector (wrapper for assigning different image generators to a single canvas screen)
+
* NavDisplay (wip)
+
* PrimaryFlightDisplay
+
  
<syntaxhighlight lang="nasal">
+
===DataEntryElement
 +
Element that allows the user to enter input, e.g. entering an ICAO airport name to search for.  incrLarge() is used to move the cursor between characters, incrSmall() is used to change the current character, or if the DataEntryElement is not being edited, to start editing it.
  
# wrapper for a cockpit placement
+
In addition to a [PageName][name] SVG Text element containing the string, there must also be a set of [PageName][name][0...size] elements, each representing a single character for data entry.
var Screen = {
+
};
+
  
# wrapper for any Nasal class managing a canvas
+
Constructor: new : func (pagename, svg, name, value, size, charSet, style=nil)
var ImageSource = {
+
* pagename - the name of the page this belongs to, as referenced by the MFD.  This is the prefix for the SVG elements themselves.
};
+
* svg - reference the svg Group element that contains the element.
 +
* name - name of the element.  In the SVG file this will be [pagename][name].
 +
* value - initial value
 +
* size - the number of characters in the DataEntryElement
 +
* charSet - string containing the set of valid characters, e.g. "ABDCEFG...."
 +
* style - style hash
  
var SourceSelector = {
 
};
 
  
var MFDisplay = {}; # top-level MFD helper
+
===GroupElement===
 +
A set of elements used to display a paged list containing lines of elements.  incrSmall() is used to scroll through the list, one line at a time.  Pagination happens automatically.
  
var MFDMode = {}; # implement different modes
+
If there are more values than space to display them, then a scrollbar can be displayed.
var MFDPage = {}; # implement pages for each mode
+
  
var MFDSwitch = {}; # handle switches & buttons
+
The SVG file will contain
 +
* [pageName][elementNames ....][0 .... size] elements for the lines of elements to display
 +
* optionally, [pageName][scrollTroughElement] and [pageName][scrollThumbElement] to display a scrollbar.
  
</syntaxhighlight>
+
Constructor: new : func (pageName, svg, elementNames, size, highlightElement, arrow=0, scrollTroughElement=nil, scrollThumbElement=nil, scrollHeight=0, style=nil)
 +
* pagename - the name of the page this belongs to, as referenced by the MFD.  This is the prefix for the SVG elements themselves.
 +
* svg - reference the svg Group element that contains the element.
 +
* elementNames - a list of element names that define a single line within the group. 
 +
* size - the number of elements that are displayed on each page.
 +
* highlightElement - the name of the element (in elementNames) that will be highlighted to indicate the selected line.
 +
* arrow - whether the highlightElement is a TextElement (and will be highlighted normally) or an ArrowElement (and will be shown or hidden)
 +
* scrollTroughElement/scrollThumbElement, the names (without pagename prefix) that are used to scroll through the list.  If neither are defined, then no scrollbar will be displayed, though the list can still scroll.
 +
* scrollHeight - the total height through which the scrollThumbElement will move.  Typically this will be the height of the scrollTroughElement minus the height of the scrollThumbElement.
 +
* style - style hash

Revision as of 12:29, 31 December 2017

This article is a stub. You can help the wiki by expanding it.


Introduction

There is a simple implementation of the basic support for an MFD (or MPCD, or PFD), or any device that is basically a set of pages with buttons around the outside to select the page that is displayed.

This works with a SVG file that defines the menu label locations and has a group for each page. The framework manages the menu hierarchy and displays labels corresponding to the buttons.

This is based around the SpaceShuttle displays which are in turn based on the http://wiki.flightgear.org/McDonnell_Douglas_F-15_Eagle#MPCD.

For a straightforward example of this in use, see https://sourceforge.net/p/flightgear/fgaddon/HEAD/tree/trunk/Aircraft/F-15/Nasal/MPCD/MPCD_main.nas

For a more complex example as part of the FG1000 glass cockpit, see https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Aircraft/Instruments-3d/FG1000/Nasal/MFD.nas

MCPD Armament Top Level Menu

Design

The PFD/MFD contains multiple pages, each of which can be referenced by buttons and displayed.

Emesary is used to pass input information from the buttons into the MFD. This allows MP support hardware interfaces.

To make it easier to use SVG files for UI elements, a number of UI primitives are supported which support highlighting, editing in a generic way (see https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Nasal/canvas/PFD/).

UI Primitives

UIElement

Abstract element, all other elements inherit from this and have the following methods:

  • getName : func() { return me._name; },
  • setValue : func(value) { me._value = value; },
  • getValue : func() { return me._value; },
  • highlightElement : func() { },
  • unhighlightElement : func() { },
  • isEditable : func () { return 0; },
  • isInEdit : func() { return me._edit; },
  • enterElement : func() { me._edit = 0; return me._value; },
  • clearElement : func() { me._edit = 0; },
  • editElement : func() { me._edit = 1; },
  • setVisible : func(vis) { },
  • incrSmall : func(value) { },
  • incrLarge : func(value) { },


TextElement

Simple text element, can have values set (setValue) and retrieved (getValue).

In the SVG file, this will have the name [pagename][name].

Constructor: new : func (pagename, svg, name, value="", style=nil)

  • pagename - the name of the page this belongs to, as referenced by the MFD. This is the prefix for the SVG elements themselves.
  • svg - reference the svg Group element that contains the element.
  • name - name of the element. In the SVG file this will be [pagename][name].
  • value - initial value
  • style - style hash

ArrowElement

An SVG element that is made visible or hidden to indicate that an element is selected, rather than highlighting the text.

In the SVG file, this will have the name [pagename][name].

Constructor: new : func (pagename, svg, name, value="", style=nil)

  • pagename - the name of the page this belongs to, as referenced by the MFD. This is the prefix for the SVG elements themselves.
  • svg - reference the svg Group element that contains the element.
  • name - name of the element. In the SVG file this will be [pagename][name].
  • value - initial value
  • style - style hash

ScrollElement

Element that can take a number of pre-defined values.

In addition to the [PageName][name] SVG Text element, [PageName][name]Left and [PageName][name]Right SVG elements used to indicate if there are any further values to scroll through.

Constructor: new : func (pageName, svg, name, values, initialIndex=0, style=nil)

  • pagename - the name of the page this belongs to, as referenced by the MFD. This is the prefix for the SVG elements themselves.
  • svg - reference the svg Group element that contains the element.
  • name - name of the element. In the SVG file this will be [pagename][name].
  • values - array of values that this scroll element can scroll through
  • initialIndex - index into the array of values.
  • style - style hash

Additional methods:

  • setValues(values_array) sets the values of the element to values_array

===DataEntryElement Element that allows the user to enter input, e.g. entering an ICAO airport name to search for. incrLarge() is used to move the cursor between characters, incrSmall() is used to change the current character, or if the DataEntryElement is not being edited, to start editing it.

In addition to a [PageName][name] SVG Text element containing the string, there must also be a set of [PageName][name][0...size] elements, each representing a single character for data entry.

Constructor: new : func (pagename, svg, name, value, size, charSet, style=nil)

  • pagename - the name of the page this belongs to, as referenced by the MFD. This is the prefix for the SVG elements themselves.
  • svg - reference the svg Group element that contains the element.
  • name - name of the element. In the SVG file this will be [pagename][name].
  • value - initial value
  • size - the number of characters in the DataEntryElement
  • charSet - string containing the set of valid characters, e.g. "ABDCEFG...."
  • style - style hash


GroupElement

A set of elements used to display a paged list containing lines of elements. incrSmall() is used to scroll through the list, one line at a time. Pagination happens automatically.

If there are more values than space to display them, then a scrollbar can be displayed.

The SVG file will contain

  • [pageName][elementNames ....][0 .... size] elements for the lines of elements to display
  • optionally, [pageName][scrollTroughElement] and [pageName][scrollThumbElement] to display a scrollbar.

Constructor: new : func (pageName, svg, elementNames, size, highlightElement, arrow=0, scrollTroughElement=nil, scrollThumbElement=nil, scrollHeight=0, style=nil)

  • pagename - the name of the page this belongs to, as referenced by the MFD. This is the prefix for the SVG elements themselves.
  • svg - reference the svg Group element that contains the element.
  • elementNames - a list of element names that define a single line within the group.
  • size - the number of elements that are displayed on each page.
  • highlightElement - the name of the element (in elementNames) that will be highlighted to indicate the selected line.
  • arrow - whether the highlightElement is a TextElement (and will be highlighted normally) or an ArrowElement (and will be shown or hidden)
  • scrollTroughElement/scrollThumbElement, the names (without pagename prefix) that are used to scroll through the list. If neither are defined, then no scrollbar will be displayed, though the list can still scroll.
  • scrollHeight - the total height through which the scrollThumbElement will move. Typically this will be the height of the scrollTroughElement minus the height of the scrollThumbElement.
  • style - style hash