Howto:Coding a simple Nasal Framework

From FlightGear wiki
Revision as of 06:20, 17 May 2014 by Hooray (Talk | contribs) (Modularization)

Jump to: navigation, 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.

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 or NDs)
  • be aircraft/use-case agnostic (e.g. work without hard-coded properties)
  • configurable without touching back-end code (e.g. via configuration hashes)
  • modular (use separate files for splitting things up)

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


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  This is currently being worked on by F-JJTH & Hooray as part of working on Garmin GPSMap 196
F-JJTH's gpsmap196 GUI dialog showing the panel page

Styling