Howto:Coding a simple Nasal Framework

From FlightGear wiki
Revision as of 19:57, 26 February 2016 by Hooray (talk | contribs)
Jump to navigation Jump to search
WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.

Motivation

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. [1]

The whole point of developing frameworks and encapsulating functionality there is to allow people to merely "request" a certain component (think ND, PFD, map, EICAS) and "configure" it as needed (think fonts, colors, symbols, events, images etc). And once you look at the integration "code" that loads a working ND, you will see that it is merely a mark-up dialect sligthly different from XML (in fact, it could be XML just as well, but it doesn't yet make sense to provide an XML dialect currently). In basic terms, you are merely telling the system that you want a certain component, and how you want it to look - and you are doing that using a "configuration file" that could just as well be in a different format (it merely happens to be using JSON/hash syntax, because that's easy to support using Nasal hashes): Canvas ND Framework[2]

Cquote1.png Equally, even creating/adding new features to the ND code is fairly accessible these days, and people don't need to be proficient coders or even understand how Nasal/Canvas work behind the scenes - for this to work, there are tons of assumptions made on what people want to do, but that is generally a safe thing to do in a use-case specific framework (think for creating PFDs, NDs, HUDs or other MFDs). Under the hood, all that is needed is a simple animation framework to show/hide, update and animate SVG symbols using Nasal timers and listeners.
— Hooray (Jul 7th, 2015). Re: Developing a Canvas Cockpit for the CRJ700.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png the main challenge remains thinking in terms of building blocks and frameworks that are simple and generic enough to satisfy all existing use-cases.

We have a number of people doing heavily related work without any collaboration, or even just coordination, going on here unfortunately. And obviously, skills, experience and expertise vary greatly. Unfortunately, some of the most skilled contributors are least willing to collaborate efficiently, while some of the most active contributors have yet to wrap their heads around important Nasal/Canvas concepts. All this is further complicated by the fact that some people write exceptionally good code that is never generalized and committed, while others write pretty poor code, that is directly committed to fgdata. So there's that, too ...

Organizing this whole mess takes up a lot of energy, and usually turns out to take away all the "fun" for people unfortunately.
But there's at least a dozen people here, with varying Nasal/Canvas expertise, that are doing heavily redundant work due to lack of coordination and collaboration, sometimes it's even up to 60-80% of their work/time that could be saved by communicating up-front and getting in touch with people who've done similar things. Unfortunately, that's something that still has to happen ...


— Hooray (Wed Jul 23). Re: Garmin gns530.
(powered by Instant-Cquotes)
Cquote2.png

Objective

Illustrate the basic thought process required to come up with Nasal/Canvas code that is sufficiently generic to meet the following requirements

  • support multiple independent instances (e.g. PFDs, NDs, EFBs, CDUs)
  • be aircraft/use-case agnostic (e.g. work without hard-coded properties)
  • make aircraft-specific settings configurable, e.g. number of engines, by using vectors and hashes
  • configurable without touching back-end code (e.g. via configuration hashes)
  • support stying and custom properties via configuration hashes
  • modular (use separate files for splitting things up)
  • identify common building blocks that would satisfy all requirements for existing use-cases, and refactor/extract the corresponding code to generalize it

We'll be using the PFD/ND code as an example here, and won't be using any complicated techniques.

Classes as Containers for your Variables

Cquote1.png reading up a little more on OO (object-oriented programming) may help you generalize some of your code a little better - for example, it seems that your code is currently structured such that it only supports a single instance of your EFB ? Once you start using classes and objects, you can easily re-arrange your code to allow your captain/copilot to have independent instances of your EFB, so that they don't affect each other. In fact, you could theoretically have dozens of EFBs running concurrently. This may not seem useful or relevant to you at the moment, but it greatly simplifies coding in the long-term.

Gijs ND/PFD code had the same problem originally - but you will find that it is much easier to write generic code once you start using separate instances/variables for each "version" of your instrument (EFB).

If you'd like to learn more about using classes and objects (instance variables) to accomplish this, see this little tutorial: Howto:Coding_a_simple_Nasal_Framework

Basically, the idea is to get rid of "global" variables, and instead use "instance" variables that are part of an outer scope (hash), such as:


— Hooray (Sat Jun 21). Re: 777 EFB: initial feedback.
(powered by Instant-Cquotes)
Cquote2.png
var EFB = {
 
 # constructor (for making new EFB objects)
 new: func(name) {

 # create a new EFB object, inherited from EFB class
 var m = {parents:[EFB] };

 # add a new field to the class named "name", assign a value to it

 m.name = name;
 # here you can add other fields that shall be specific to the instance/object, e.g. the root property
 
 # and finally return the new object to the caller
 return m; 
 },
 # define a method (class function) that can be called to print out the name of the EFB
 whoami: func() {
  print("EFB owner:", me.name );
 }
};

var CaptainEFB = EFB.new("captain");
var CopilotEFB = EFB.new("copilot");

CaptainEFB.whoami();
CopilotEFB.whoami();
Cquote1.png (You can use the Nasal console to test this)

As you can see, your two EFBs will inherit from the same EFB class, but they will have their own private namespace - i.e. the "name" member in this case. It can be accessed via the "me" prefix. And each instance (object) will have its own scope, referenced via the me keyword.


— Hooray (Sat Jun 21). Re: 777 EFB: initial feedback.
(powered by Instant-Cquotes)
Cquote2.png

Variables

Also see: http://forum.flightgear.org/viewtopic.php?f=71&t=23047#p209281 In order to support independent instances of each instrument, you need to use separate variables, so rather than having something like this at global scope:

var horizon = nil;
var markerBeacon = nil;
var markerBeaconText = nil;
var speedText = nil;
var machText = nil;
var altText = nil;
var selHdgText = nil;
var fdX = nil;
var fdY = nil;

You would instead use a hash, and populate it with your variables:

var PrimaryFlightDisplay= {
 new: func() { return {parents:[PrimaryFlightDisplay],}; },
 # set up fields
 horizon: nil,
 markerBeacon: nil,
 markerBeaconText: nil,
 speedText: nil,
 machText: nil,
 altText: nil,
 selHdgText: nil,
 fdX: nil,
 fdY:nil,
};

The same thing can be accomplished by doing something like this in your constructor, using a temporary object:

var PrimaryFlightDisplay= {
 new: func() {
  var m = {parents:[PrimaryFlightDisplay]}; 
  m.horizon = nil;
  m.markerBeacon = nil;
  m.markerBeaconText = nil;
  m.speedText = nil;
  m.machText = nil;
  m.altText = "Hello World"; 
  m.selHdgText = nil;
  m.fdX = nil;
  m.fdY = nil;
 
  return m;
 },
};

To create a new object, you would then simply have to call the .new() function:

 var myPFD = PrimaryFlightDisplay.new();
 print( myPDF.altText );

By using this method, you can easily create dozens of independent instances of your class:

 var PFDVector = [];
 forindex(var i=0;i<100;i+=1)
  append(PFDVector, PrimaryFlightDisplay.new() );

Initialization / Constructor

Once these changes are in place, you can easily initialize your members/fields using a single foreach() loop, i.e. instead of having something like this:

canvas.parsesvg(pfd, "Aircraft/747-400/Models/Cockpit/Instruments/PFD/PFD.svg", {'font-mapper': font_mapper});

curAlt1 = pfd.getElementById("curAlt1");
curAlt2 = pfd.getElementById("curAlt2");
curAlt3 = pfd.getElementById("curAlt3");
vsPointer = pfd.getElementById("vsPointer");
curAltBox = pfd.getElementById("curAltBox");
curSpd = pfd.getElementById("curSpd");
curSpdTen = pfd.getElementById("curSpdTen");
spdTrend = pfd.getElementById("spdTrend");

You could use this

var PrimaryFlightDisplay {
 new: func() {
 var m = {parents:[PrimaryFlightDisplay]};
 m.pdf = {};
 canvas.parsesvg(m.pfd, "Aircraft/747-400/Models/Cockpit/Instruments/PFD/PFD.svg", {'font-mapper': font_mapper});

 m.symbols = {};
 foreach(var symbol; ['curAlt1','curAlt2','curAlt3','vsPointer','curAltBox','curSpd','curSpdTen','curAlt1','spdTrend',])
  me.symbols[symbol] = m.getElementById(symbol);
}, # new()

} # PrimaryFlightDisplay

All the original foo=nil initialization can now be removed, this is 100% equivalent, and saves you tons of typing and time!

Note that it makes sense to group your elements according to their initialization requirements, i.e. anything that just needs getElementById() called would go into the same vector, while anything that requires getElementById().updateCenter() called, would get into another vector.

These tips alone will reduce the code required in PFD.nas by about 150 lines.

Dealing with Properties

The next complication is dealing with instrument-specific properties. Typically, code will have lots of getprop()/setprop() equivalent calls in many places. These need to be encapsulated, i.e. you don't want to use setprop/getprop (or prop.nas) directly for any properties that are specific to the instrument, otherwise your code would fail once several instances of it are active at the same time, because their property access may be competing/conflicting, such as overwriting properties.

One simple solution for this is to have your own setprop/getprop equivalent, as part of your class:

var PrimaryFlightDisplay = {

 set: func(property, value) {
 },

 get: func(property, default) {
 },
};

Your methods would then never use setprop/getprop directly, but instead use the set/get methods, by calling me.get() or me.set() respectively. The next step is identifying property that are instance-specific, i.e. that must not be shared with other instruments. One convenient way to accomplish this is using a numbered index for each instance, such as: /instrumentation/pfd[0], /instrumentation/pfd[1], /instrumentation/pfd[2] etc.

This would then be the place for your instance-specific properties.

Moving huge conditionals into hash functions

The next problem we want to tackle is getting rid of huge conditional blocks inside the update() method: [1]

if(abs(deflection) < 0.5) { # need to verify 0.5
                locPtr.setTranslation(deflection*300,0);
                risingRwyPtr.setTranslation(deflection*300,0);
                locScaleExp.show();
                locScale.hide();
            } else {
                locPtr.setTranslation(deflection*150,0);
                risingRwyPtr.setTranslation(deflection*150,0);
                locScaleExp.hide();
                locScale.show();
            }

Such conditionals can be usually spit into:

  • condition (predicate)
  • body if true
  • body if false

Configuration

Note  Discuss config hashes

Modularization

Note  Discuss io.include() and io.load_nasal() for moving declarative config hashes out of *.nas files


Note
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

Styling

  1. alge (Feb 24th, 2016). Re: FMC.
  2. Hooray (Jul 7th, 2015). Re: Developing a Canvas Cockpit for the CRJ700.