Nasal scripting language

From FlightGear wiki
Jump to navigation Jump to search

Please note that a considerable amount of resources has not yet been incorporated here, you can check these out by going to the "discussion" page, where we are collecting links to webpages and mailing list discussions/postings related to Nasal.


FlightGear offers a powerful scripting language called "Nasal", which supports reading and writing of internal properties, accessing internal data via extension functions, creating dialogs and much more. It uses some of the concepts of ECMA/JavaScript, Python and Perl and implements a simple but complete way of Object Oriented Programming. Nasal code can be run by aircraft configuration files, and it can be embedded in various XML files (dialog files, animation files, bindings for joysticks, keyboard and cockpit controls). Nasal is platform independent.

Note that this page is mostly about FlightGear-specific APIs/extension functions and usage pattern. See the Nasal webpage for core language/library documentation and source code examples.

In addition, the 'Nasal' directory in the FlightGear base package contains a wealth of tested, proven and usually well-commented source code that you may want to check out for additional examples of using the Nasal scripting language in FlightGear [1].


Loops

Nasal has several ways to implement an iteration. Loops using while, for, foreach, and forindex block all of FlightGear's subsystems that run in the main thread, and can, thus, only be used for instantaneous operations that don't take too long. For operations that should continue over a longer period, one needs a non-blocking solution. This is done by letting functions call themselves after a timed delay:

var loop = func {
    print("this line appears once every two seconds");
    settimer(loop, 2);
}

loop();        # start loop

Note that the settimer function expects a function object (loop), not a function call (loop()). The fewer code FlightGear has to execute, the better, so it is desirable to run loops only when they are needed. But how does one stop a loop? A once triggered timer function can't be revoked. But one can let the loop function check an outside variable and refuse calling itself, which makes the loop chain die off:

var running = 1;
var loop = func {
    if (running) {
        print("this line appears once every two seconds");
        settimer(loop, 2);
    }
}

loop();        # start loop ...
...
running = 0;   # ... and let it die

Unfortunately, this method is rather unreliable. What if the loop is "stopped" and a new instance immediately started again? Then the running variable would be 1 again, and a pending old loop call, which should really finish this chain, would happily continue. And the new loop chain would start, too, so that we would end up with two loop chains.

This can be solved by providing each loop chain with a loop identifier and letting the function end itself if the id doesn't match the global loop-id. Self-called loop functions need to inherit the chain id. So, every time the global loop id is increased, all loop chains die, and a new one can immediately be started.

var loopid = 0;
var loop = func(id) {
    id == loopid or return;           # stop here if the id doesn't match the global loop-id
    ...
    settimer(func { loop(id) }, 2);   # call self with own loop id
}

loop(loopid);       # start loop
...
loopid += 1;        # this kills off all pending loops, as none can have this new identifier yet
...
loop(loopid);       # start new chain; this can also be abbreviated to:  loop(loopid += 1);


OOP - Object Oriented Programming

In Nasal, objects ("classes") are regular hashes. Self-reference and inheritance are implemented through special variables me and parents. To get a better understanding of the concept, let's start with the very basics.


Hashes

Hashes, also known as "dictionaries" in Python or "maps" in C++/STL are data structures that hold key/value pairs in a way that allows quick access to a value via its key.

var airport = {
    "LOXZ": "Zeltweg",
    "LOWI": "Innsbruck",
    "LOXL": "Linz Hoersching",     # the last comma is optional
};

print(airport["LOXZ"]);            # prints "Zeltweg"
airport["LOXA"] = "Aigen";         # adds LOXA to the hash

The quotes around keys can be left away in a hash definition if the key is a valid variable name or a number. This works just as well: Inheritance: "parents"

var airport = {
    LOXZ: "Zeltweg",
};

There's also an alternative way to access hash members if the keys are valid variable names: airport.LOXI can be used instead of airport["LOXI"]. There is a difference, though, which is described in the next section.

Note that assigning a hash (or a vector) to another variable does never copy the contents. It only creates another reference to the same data structure. So manipulating the hash via its new name does in fact change the one, original hash.

var a = airport;
a.LOXL = "Linz";
print(airport.LOXL);       # prints now "Linz", not "Linz Hoersching"

(True copies of vectors can be made by assigning a full slice: var copy = vec[:]. There's no such method for hashes.)


Self-reference: "me"

Values stored in a hash can be of any type, even of type function. Member functions ("methods") can reference their own enclosing hash via reserved keyword me. This is comparable to the this keyword in C++ classes, or the self keyword in Python.

var value = "test";

var data = {
    value: 23,                         # scalar member variable
    write1: func { print(value); },    # function member
    write2: func { print(me.value); }, # function member
};

data.write1();     # prints "test"
data.write2();     # prints 23

In the last section we learned that there are two ways to access hash members: data.write2() and data["write2"](). But only in the first case write2 is first searched in data and, if not found, in parent hashes (see next section), whereas in the second case we'd get an error message about the me variable being unknown. It's only usable for accessing direct hash members.

The above example is already a simple form of an object. It has its own variable namespace, its own methods, and it can be passed around by-reference as one unit. Such classes are sometimes called singleton classes, as they are unique, with no independent class instances. They mostly serve as a way to keep data and methods nicely encapsulated within a Nasal module. Often they contain a method for initializing, which is usually called init.


Inheritance: "parents"

What we learned about me in the last section is only the half truth. "me" doesn't only reference an object's own hash, but also one or more parent hashes. parents is another reserved keyword. It denotes a vector referencing other object hashes, which are "inherited" that way.

var parent_object = {
    value: 123,
};

var object = {
    parents: [parent_object],
    write: func { print(me.value) },
};

object.write();    # prints 123

Even though object itself doesn't contain a member value, it finds and uses the one of its parent object. parents is a vector that can contain several parent objects. These are then searched in the order from left to right, until a matching member variable or method is found. Each of the parents can itself have parents, which are all recursively searched.


Creating class instances

With me and parents we can implement a class object and create independent instances from that:

var Class = {
    write:     func { print(me.value); },
    increment: func { me.value += 1; },
};

var instance1 = { parents: [Class], value: 123 };
var instance2 = { parents: [Class], value: 456 };

instance1.write();    # prints 123
instance2.write();    # prints 456

As you can see, the two class instances are separate, independent objects, which share another object as parent -- they "inherit" from object Class. One can now easily change members of any of these three objects. The following will redefine the parent's write method, and all instances will automatically use this new version:

Class.write = func { print("VALUE = " ~ me.value) }

But one can also add a method to just one instance:

instance1.write = func { print("VALUE = " ~ me.value) }

Because instance1 does now have its own write method, the parents won't be searched for one, so Class.write is now overridden by instance1's own method. Nothing changed for instance2 -- it will still only find and use Class.write via its parent.

Note, the we couldn't create a class instance by simple assignment, because, as we learned above, this wouldn't create a separate copy of the Class object. All "instances" would reference the same hash!

var bad_instance1 = Class;   # bad
var bad_instance2 = Class;   # bad

bad_instance1.value = 123;   # sets Class.value to 123
bad_instance2.value = 456;   # sets Class.value to 456

bad_instance1.write();       # prints 456, not 123


Constructor

Defining each class instance by explicitly creating a hash with parents is clumsy. It is nicer to have a function that does that for us. Then we can also use function arguments to initialize members of this instance.

var new_class = func(val) {
    return { parents: [Class], value: val };
}

var instance1 = new_class(123);
var instance2 = new_class(456);

instance1.write();   # prints 123
instance2.write();   # prints 456

Because the class generating function new_class() really belongs to class Class, it would be nicer to put it into the class hash as well. In this case we call it a class "constructor", and as a convention, give it the name new. It could have any name, though, and there could be more than one constructor.

var Class = {
    new: func(val) {
        return { parents: [Class], value: val };
    },
    write: func {
        print("VALUE=" ~ me.value);
    },
};

var instance1 = Class.new(123);
var instance2 = Class.new(456);

As you can see, new() doesn't return a copy of Class, but rather a small hash that contains only a list of parents and one individual member value.

Classes aren't always as simple as in our example. Usually they contain several members, of which some may have yet to be calculated in the constructor. In that case it's easier to create a local object hash first, and to let the constructor finally return it. Such local hashes are often named m (as a short reference to me), or obj.

var Class = {
    new: func(val) {
        var m = { parents: [Class] };
        m.value = val;
        return m;
    },
    write: func {
        print("VALUE=" ~ me.value);
    },
};

This last example is the most frequently used form of class definitions in FlightGear-Nasal.


Destructor

There's no such thing in Nasal. In other languages destructors are automatically called when the class gets destroyed, so that memory and other resources that were allocated by the constructor can be freed. In Nasal that's all done by the Garbage Collector (GC), anyway. In the FlightGear context, however, there are resources that should get freed. Listeners should get removed, self-calling functions ("loops") stopped. For that it's recommended to create a destructor function and to call that manually. Such functions are often called del, similar to Python and to pair nicely with the three-letter constructor name new.


Multiple inheritance

A class can inherit from one or more classes, that is: it can access all methods and class members of all parent classes, but also override them and add additional members.

var A = {                                    # simple class A
    new: func {
        return { parents: [A] };
    },
    alpha: func print("\tALPHA"),
    test: func print("\tthis is A.test"),
};

var B = {                                    # simple class B
    new: func {
        return { parents: [B] };
    },
    bravo: func print("\tBRAVO"),
    test: func print("\tthis is B.test"),
};

var C = {                                    # class C that inherits
    parents: [A, B],                         # from classes A and B
    new: func {
        return { parents: [C] };
    },
    charlie: func print("\tCHARLIE"),
    test: func print("\tthis is C.test"),    # overrides A.test() and B.test()
};


print("A instance");
var a = A.new();
a.alpha();

print("B instance");
var b = B.new();
b.bravo();

print("C instance");
var c = C.new();
c.alpha();                        # use alpha from the A parent
c.bravo();                        # use bravo from the B parent
c.charlie();                      # use charlie from C itself
c.test();                         # use C.test(), which overrides A.test()

Even if a class overrides a method of a parent with the same name, the parent's version can still be accessed via parents vector.

c.parents[0].parents[0].test();   # use A.test()
c.parents[0].parents[1].test();   # use B.test()

Listeners and Signals

Listeners are callback functions that are attached to property nodes. They are triggered whenever the node is written to, or, depending on the listener type, also when children are added or removed, and when children are written to. Unlike polling loops, listeners don't have the least effect on the frame rate when they aren't triggered, which makes them preferable to monitor properties that aren't written to frequently. Unfortunately, listeners don't work on so-called "tied" properties when the node value isn't set via property methods. (You can spot such tied properties by Ctrl-clicking the "." entry in the property browser: they are marked with a "T".) Most of the FDM properties are "tied".


setlistener()

Syntax:

var listener_id = setlistener(<property>, <function> [, <startup=0> [, <runtime=1>]]);

The first argument is a property node object (props.Node() hash) or a property path. Because the node hash depends on the props.nas module being loaded, setlistener() calls need to be deferred when used in an $FG_ROOT/Nasal/*.nas file, usually by calling them in a settimer(func {}, 0) construction. To avoid that, one can use the raw _setlistener() function directly, for which setlistener() is a wrapper. The raw function does only accept node paths (e.g. "/sim/menubar/visibility"), but not props.Node() objects.

The second argument is a function object (not a function call!). The func keyword turns code into a function object.

The third argument is optional. If it is non-null, then it causes the listener to be called initially. This is useful to let the callback function pick up the node value at startup.

The fourth argument is optional, and defaults to 1. This means that the callback function will be executed whenever the property is written to, independent of the value. If the argument is set to 0, then the function will only get triggered if a value other than the current value is written to the node. This should be used for properties that are written to in every frame, although the written value is mostly the same. If the argument is 2, then also write access to children will get reported, as well as the creation and removal of children nodes.

setlistener() returns a unique listener id on success, and nil on error. The id is nothing else than a counter that is 0 for the first Nasal listener, 1 for the second etc. You need this id number to remove the listener. Most listeners are never removed, so that one doesn't assign the return value, but simply drop it.

Listener callback functions can access up to four values via regular function arguments, the first two of which are property nodes in the form of a props.Node() object hash.

Syntax:

callback([<changed_node> [, <listened_to_node> [, <operation> [, <is_child_event>]]]])


removelistener()

Syntax:

var num_listeners = removelistener(<listener id>);

removelistener() takes one argument: the unique listener id that a setlistener() call returned. It returns the number of remaining active Nasal listeners on success, nil on error, or -1 if a listener function applies removelistener() to itself. The fact that a listener can remove itself, can be used to implement a one-shot listener function:

var L = setlistener("/some/property", func {
    print("I can only be triggered once.");
    removelistener(L);
});


Listener Examples

The following example attaches an anonymous callback function to a "signal". The function will be executed when FlightGear is closed.

setlistener("/sim/signals/exit", func { print("bye!") });


Instead of an anonymous function, a named function can be used as well:

var say_bye = func { print("bye") }
setlistener("/sim/signals/exit", say_bye);


Callback functions can access up to four parameters which are handed over via regular function arguments. Usually there's only the first used, or none at all, like in the above example. The following code attaches the monitor_course() function to a gps property.

var monitor_course = func(course) {
    print("Monitored course set to ", course.getValue());
}
var i = setlistener("instrumentation/gps/wp/leg-course-deviation-deg", monitor_course);

# here the listener is active

removelistener(i);                    # remove that listener again

The function object doesn't need to be a separate, external function -- it can also be code that is made a function object directly in the setlistener() call:

setlistener("/sim/signals/exit", func { print("bye") });    # say "bye" on exit

Attaching a function to a node that is specified as props.Node() hash:

var node = props.globals.getNode("/sim/signals/click", 1);
setlistener(node, func { gui.popupTip("don't click here!") });

Sometimes it is desirable to call the listener function initially, so that it can pick up the node value. In the following example a listener watches the view number, and turns the HUD on in cockpit view, and off in all other views. It doesn't only do that on writing to "view-number", but also once when the listener gets attached, thanks to the third argument "1":

setlistener("/sim/current-view/view-number", func(n) {
    setprop("/sim/hud/visibility[0]", n.getValue() == 0);
}, 1);

There's no limit for listeners on a node. Several functions can get attached to one node, just as one function can get attached to several nodes. Listeners may write to the node they are listening to. This will not make the listener call itself causing an endless recursion.


Signals

In addition to "normal" nodes, there are "signal" nodes that were created solely for the purpose of having listeners attached:

/sim/signals/exit ... set to "true" on quitting FlightGear

/sim/signals/reinit ... set to "true" right before resetting FlightGear (Shift-Esc), and to "false" afterwards

/sim/signals/click ... set to "true" after a mouse click at the terrain. Hint that the geo coords for the click spot were updated and can be retrieved from /sim/input/click/{longitude-deg,latitude-deg,elevation-ft,elevation-m}

/sim/signals/screenshot ... set to "true" right before the screenshot is taken, and set to "false" after it. Can be used to hide and reveal dialogs etc.

/sim/signals/nasal-dir-initialized ... set to "true" after all Nasal "library" files in $FG_ROOT/Nasal/ were loaded and executed. It is only set once and can only be used to trigger listener functions that were defined in one of the Nasal files in that directory. After that signal was set Nasal starts loading and executing aircraft Nasal files, and only later are settimer() functions called and the next signal is set:

/sim/signals/fdm-initialized ... set to "true" when then FDM has just finished its initialization

/sim/signals/reinit-gui ... set to "true" when the GUI has just been reset (e.g. via Help menu). This is used by the gui.Dialog class to reload Nasal-loaded XML dialogs.

/sim/signals/frame ... set to "true" at the beginning of each iteration of the main loop (a.k.a. "frame"). This is meant for debugging purposes. Normally, one would just use a settimer() with interval 0 for the same effect. The difference is that the signal is guaranteed to be raised at a defined moment, while the timer call may change when subsystems are re-ordered.


Exception handing

die() aborts a function with an error message:

var divide = func(a, b) {
    if (b == 0)
        die("division by zero");
    return a / b;     # this line won't be reached if b == 0
}

die() is also used internally by Nasal core functions. getprop("/4me"), for example, issues an error message "name must begin with alpha or '_'". Now assume we want to write a dialog where the user can type a property path into an input field, and we display the property's value in a popup dialog. What if the user typed an invalid path and we hand that over to getprop()? We don't want Nasal to abort our code because of that. We want to display a nice error message instead. The call() function can catch die() exceptions:

var value = getprop(property);                                    # dies if 'property' is invalid
var value = call(func getprop(property), nil, var err = []);      # catches invalid-property-exception and continues

The second line calls getprop(property) just like the first, and returns its value. But if 'property' was invalid then the call() function catches the exception (die()) and sets the 'err' vector instead, while that vector remains empty on success.

if (size(err))
    print("ERROR: bad property ", property, " (", err[0], ")");   # err[0] contains the die() message
else
    print("value of ", property, " is ", value);

The first argument of call() is a function object, the second a vector of function arguments (or nil), and the third a vector where the function will return a possible error. For more information on the call() function see the Nasal library documentation.

die() doesn't really care about what its argument is. It doesn't have to be a string, and can be any variable, for example a class. This can be used to pass values through a chain of functions.

var Error = {                                                             # exception class
    new: func(msg, number) {
        return { message: msg, number: number };
    },
};

var A = func(a) {
    if (a < 0)
        die(Error.new("negative argument to A", a));                      # throw Error
    return "A received " ~ a;
}

var B = func(val) {
    var result = A(val);
    print("B finished");      # this line is not reached if A threw an exception
    return result;
}

var value = call(B, [-4], var err = []);                                  # try B(-4)

if (size(err)) {                                                          # catch (...)
    print("ERROR: ", err[0].message, "; bad value was ", err[0].number);
    die(err[0]);                                                          # re-throw
} else {
    print("SUCCESS: ", value);
}

FlightGear extension functions

print()

Concatenates an arbitrary number of arguments to one string, appends a new-line, and prints it to the terminal. Returns the number of printed characters.

print("Just", " a ", "test");


getprop()

Returns the node value for a given path, or nil if the node doesn't exist or hasn't been initialized yet.

getprop(<path>);

Example:

print("The frame rate is ", getprop("/sim/frame-rate"), " FPS");


setprop()

Sets a property value for a given node path string. Always returns nil.

setprop(<path> [, <path>, [...]], <value>);

All arguments but the last are concatenated to a path string. The last value is written to the respective node. If the node isn't writable, then an error message is printed to the console.

Examples:

setprop("/sim/current-view/view-number", 2);
setprop("/controls/engines/engine[", i, "]/reverser", 1);


settimer()

Runs a function after a given simulation (default) or real time in seconds.

settimer(<function>, 

The third argument is optional and defaults to 0, which lets the time argument be interpreted as "seconds simulation time". In this case the timer doesn't run when FlightGear is paused. For user interaction purposes (measuring key press time, displaying popups, etc.) one usually prefers real time.

var copilot_annoyed = func { setprop("/sim/messages/copilot", "Stop it! Immediately!") }
settimer(copilot_annoyed, 10);


systime()

Returns epoch time (time since 1972/01/01 00:00) in seconds as a floating point number with high resolution. This is useful for benchmarking purposes.


carttogeod()

Converts cartesian coordinates x/y/z to geodetic coordinates lat/lon/alt, which are returned as a vector. Units are degree and meter.

var geod = carttogeod(-2737504, -4264101, 3862172);
print("lat=", geod[0], " lon=", geod[1], " alt=", geod[2]);

# outputs
lat=37.49999782141546 lon=-122.6999914632327 alt=998.6042055172776


geodtocart()

Converts geodetic coordinates lat/lon/alt to cartesian coordinates x/y/z. Units are degree and meter.

var cart = geodtocart(37.5, -122.7, 1000); # lat/lon/alt(m)
print("x=", cart[0], " y=", cart[1], " z=", cart[2]);

# outputs
x=-2737504.667684828 y=-4264101.900993474 z=3862172.834656495


geodinfo()

Returns information about geodetic coordinates. Takes two arguments: lat, lon (in degree) and returns a vector with two entries, or nil if no information could be obtained because the terrain tile wasn't loaded. The first entry is the elevation (in meters) for the given point, and the second is a hash with information about the assigned material, or nil if there was no material information available, because there is, for instance, an untextured building at that spot.

var lat = getprop("/position/latitude-deg");
var lon = getprop("/position/longitude-deg");
var info = geodinfo(lat, lon);

if (info != nil) {
    print("the terrain under the aircraft is at elevation ", info[0], " m");
    if (info[1] != nil)
        print("and it is ", info[1].solid ? "solid ground" : "covered by water");
}

A full data set looks like this:

debug.dump(geodinfo(lat, lon));

# outputs
[ 106.9892101062052, { light_coverage : 0, bumpiness : 0.5999999999999999, load_resistance : 1e+30,
solid : 0,  names : [ "Lake", "Pond", "Reservoir", "Stream", "Canal" ], friction_factor : 1, 
rolling_friction : 1.5 } ]


parsexml()

This function is an interface to the built-in expat XML parser. It takes up to five arguments. The first is a mandatory absolute path to an XMl file, the remaining four are optional callback functions, each of which can be nil (which is also the default value).

var ret = parsexml(<path> [, <start-elem> [, <end-elem> [,  [, <pi> ]]]]);

<start-elem>  ... called for every starting tag with two arguments: the tag name, and an attribute hash
<end-elem>    ... called for every ending tag with one argument: the tag name
        ... called for every piece of data with one argument: the data string
<pi>          ... called for every "processing information" with two args: target and data string

<ret>         ... the return value is nil on error, and the <path> otherwise

Example:

var start = func(name, attr) {
    print("starting tag ", name);
    foreach (var a; keys(attr))
        print("\twith attribute ", a, "=", attr[a]);
}
var end = func(name) { print("ending tag ", name) }
var data = func(data) { print("data=", data) }
var pi = func(target, data) { print("processing instruction: target=", target, " data=", data) }
parsexml("/tmp/foo.xml", start, end, data, pi);


Tips

How to play sounds using Nasal script?

The same way one would do animations using Nasal;

Adjust properties in Nasal (they may be "private" properties in your own subtree of the property list, say /tmp/<aircraft>) and let the sound configuration file act on those properties. (from flightgear-devel)


Related content

External links