Using listeners and signals with Nasal

From FlightGear wiki
Jump to navigation Jump to search


Behind the Scenes of Listeners

Repeatedly calling getprop() to update a property is called polling. Doing that at frame rate (each time a frame is updated) can become pretty expensive and often isn't necessary, especially for properties that are rarely updated.

Polling is only really recommended for properties that change per frame or really often. Otherwise, you will probably want to use listeners or less-frequent polling using a custom timer.

For example, a listener would be quite appropriate for an on/off switch or similar, an update loop for everything that needs to be updated each frame/update (i.e. engine rpm), and if something is changing every frame anyways or is a tied property, you might as well use the update loop for several reasons: besides the fact that it might be necessary, it linearizes your data flow and moves it closer to the use of the data, it makes use of the current known efficiency of getprop(), and I would venture that it seems most familiar to you. I really wouldn't worry about listener efficiency here

The props.Node wrappers are slower than getprop/setprop because there's more Nasal-space overhead. Intuitively, the props.Node stuff should be faster, because of the cached reference - but the getprop()/setprop() code is native C code that uses a heavily optimized algorithm to look up properties (this may change once the props bindings start using the new cppbind framework).

There will certainly be a break-even point, depending on how often properties change - and how many properties are involved. But usually, listeners should be superior to polling at frame rate for properties that do not change per frame.

Keep in mind that listeners are not "background processes" at all - a listener will be triggered by the property tree once a node is accessed, which will invoke the Nasal callback. Timers and listeners are NOT background "processes". They are just invoked by different subsystems, i.e. the property tree (listeners) or the events subsystem (timers). There are other subsystems that can also invoke Nasal handlers, such as the GUI system or the AI code. This all takes place inside the FG main loop (=main thread), not some separate background/worker thread. Which is also the reason why all the Nasal APIs are safe to be used.

It is important to understand that a "listener" is a passive thing, i.e. a "list" (array) of optional functions that are to be invoked whenever a property is modified/written to - thus, once you modify the property, the property tree code will check the size of the "callback list" that contains callbacks that are to be notified (called) when the property is written - and then calls each callback in a foreach() loop.

Which is to say that those performance monitor stats are not necessarily representative when it comes to Nasal callbacks invoked as timers/listeners, which also applies to C++ code using these two APIs (timers & listeners).


Listeners are not actively "listening" at all - there's no "listener watch" running - unlike timers, listeners are totally passive instead - it works basically like this:

  • register a listener for some property named "foo", to call some Nasal code
  • which just adds a callback to a property specific vector, NOTHING else.
  • once setprop("foo", value) is called
  • the property tree is updated
  • next the property tree checks if any listeners are registered for that branch
  • if there are listeners attached, they are all called (there's basically a vector of listeners)

So listeners are not really "processes" at all - neither background nor foreground: Merely their *callbacks* become processes after being invoked. Otherwise, they're just a vector of Nasal callbacks - which are only ever called if the property is modified. In other words, there's basically zero cost. Listener overhead is mainly determined by the callback's payload - not by listening (which is just checking vector.size() != 0 and calling each element), unless the property is updated frequently (in terms of frame rate)

Admittedly, having many callbacks/listeners attached, could also add up quickly.

So for benchmarking purposes, you can just use a closure to wrap your callback and update your systime() object accordingly, you could provide a separate "timed_listener" function or just override setlistener().

Listeners and Signals

The important thing to keep in mind is that custom listeners are generally not about loading or running Nasal files, most of the Nasal files are loaded and executed implicitly during startup. Only the Nasal sub modules (i.e. inside their own $FG_ROOT/Nasal/ directory) are dynamically loaded using a listener). So listeners are really just a simple way to talk to the property tree: Hey property tree, would you please be so kind to call this Nasal function whenever this property is modified?

So, 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.

To learn more about managing resources like timers and listeners, please see Developing and debugging Nasal code#Managing timers and listeners.

setlistener() vs. _setlistener()

You are requested not to use the raw _setlistener() function, except in files in $FG_ROOT/Nasal/ when they are needed immediately. Only then the raw function is required, as it doesn't rely on props.nas. Using setlistener() once props.nas is loaded allows using high-level objects to reference properties, instead of raw C-objects (called "ghosts").

Note: Once cppbind is used to replace props.nas, _setlistener() will be deprecated because the builtin function will be effectively using the same mechanism as what the wrapper function (the current setlistener()) does right now.

When listeners don't work

Unfortunately, listeners don't work on so-called "tied" properties when the node value isn't set via property methods. Tied properties are a semi-deprecated API to allow C++ code to handle the value directly and control getting/setting directly, usually avoiding the property tree altogether. (You can spot such tied properties by Ctrl-clicking the "." entry in the property browser: they are marked with a "T".) The problem comes when the C++ value is written to outside of the property tree, which means that the property tree doesn't receive a notification, even though normal sets via the property tree would still fire the listeners. Most of the FDM properties are "tied", and a few in other subsystems.

Examples of properties where setlistener won't work:

  • /position/elevation-ft
  • /ai/models/aircraft/orientation/heading-deg
  • /instrumentation/marker-beacon/[inner|middle|outer]
  • Any property node created as an alias
  • Lots of others

Before working to create a listener, always check whether a listener will work with that property node by control-clicking the "." in property browser to put it into verbose mode, and then checking whether the property node for which you want to set up a listener is marked with a "T" or not.

If you can't set a listener for a particular property, the alternative is to use settimer to set up a timer loop that checks the property value regularly.

Listeners are most efficient for properties that change only occasionally. No code is called at all during frames where the listener function is not called. If the property value changes every frame, setting up a settimer loop with time=0 will execute every frame, just the same as setlistener would, and the settimer loop is more efficient than setting a listener. This is one reason the fact the setlistener doesn't work on certain tied and FDM properties is not a great loss. See the section on timer loops below.

setlistener()

Syntax:

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

<property> 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.

<function> The second argument is a function object (not a function call!). The function you pass here will be called with the target property node as its sole argument as soon as someone writes to the property.

<startup=0> The third argument is optional. If it is non-zero, then it causes the listener to be called initially (but not if runtime is 1). This is useful to let the callback function pick up the node value at startup.

<runtime=1> 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 is important for cases where a property is written to once per frame, no matter if the value changed or not. YASim, for example, does that for /gear/gear/wow or /gear/launchbar/state. So, 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.

For both optional flags 0 means less calls, and 1 means more calls. The first is for startup behavior, and the second for runtime behavior.

Here's a real-life example:

setlistener("/gear/launchbar/state", func (node) {
    if (node.getValue() == "Engaged")
        setprop("/sim/messages/copilot", "Engaged!");
}, 1, 0);

YASim writes once per frame the string "Disengaged" to property /gear/launchbar/state. When an aircraft on deck of the aircraft carrier locks into the catapult, this changes to "Engaged", which is then written again in every frame, until the aircraft leaves the catapult. Because the locking in is a bit difficult -- one has to target the sensitive area quite exactly --, it was desirable to get some quick feedback: a screen message that's also spoken by the Festival speech synthesis. With the args 1 and 0, this is done initially (for the unlikely case that we are locked in from the beginning), and then only when the node changes from an arbitrary value to "Engaged".

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.

The third is a indication of the operation: 0 for changing the value, -1 for removing a child node, and +1 for adding a child.

The fourth indicates whether the event occurred on the node that was listened to and is always 0 if the previous argument is not 0.

Here is the syntax supposing you have set a callback function named myCallbackFunc via setlistener (setlistener(myNode, myCallbackFunc)):

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

You cannot refer to OOP methods directly -- to do so, enclose the function call in a func() { } block as such:

signal.listener = setlistener(node, func() { me.phaseFunc(); }, 0, 0);

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);
});
Cquote1.png For those not following all the cvs logs: I've added a new function to Nasal a few days ago: removelistener(). It takes one argument -- the unique id number of a listener as returned by setlistener(): var foo = setlistener("/sim/foo", die); ... removelistener(foo); This can be used to remove all listeners in an <unload> part that were set by the <load> part of a scenery object: <load> listener = []; append(listener, setlistener("/sim/foo", die)); append(listener, setlistener("/sim/bar", func {}); ... </load> <unload> foreach (l; listener) { removelistener(l) } </unload> screen.nas stores all relevant listener ids in a hash, so that other parts can, for example, remove the mapping of pilot messages to screen and voice): removelistener(screen.listener["pilot"]); The id is 0 for the first listener, 1 for the second etc. removelistener() returns the total number of remaining listeners, or nil on error (i.e. if there was no listener known with this id). This can be used for statistics: id = setlistener("/sim/signals/quit", func {}); # let's not count this one num = removelistener(id); print("there were ", id, " Nasal listeners attached since fgfs was started"); print("of which ", num, " are still active"); m.
— Melchior FRANZ (Mar 2nd, 2006). [Flightgear-devel] Nasal: new command "removelistener()".
(powered by Instant-Cquotes)
Cquote2.png

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, optionally, access up to four parameters which are handed over via regular function arguments. Many times none of these parameters is used at all, as in the above example.

Most often, only the first parameter is used--which gives the node of the changed value.

The following code attaches the monitor_course() function to a gps property, using the argument course to get the node with the changed value.

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

Here is code that accesses two arguments--the changed node and the listened-to node (these may be different when monitoring all children of a certain node)--and also shows how to monitor changes to a node including changes to children:

var monitor_course = func(course, flightinfo) {
    print("One way to get the course setting: ", flightinfo.leg-course-deviation-deg.getValue());
    print("Another way to get the same setting ", course.getValue());
}
var i = setlistener("instrumentation/gps/wp", monitor_course, 0, 2);

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

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

Beware, however, that the contents of a function defined within the setlistener call are not evaluated until the call is actually made. If, for instance, local variables change before the setlistener call happens, the call will reflect the current value of those variables at the time the callback function is called, not the value at the time the listener was set.

For example, with this loop, the function will always return the value 10--even if mynode[1], mynode[2], mynode[3] or any of the others is the one that changed. It is because the contents of the setlistener are evaluated after the loop has completed running and at that point, i=10:

var output = func(number) {
    print("mynode", number, " has changed!"); #This won't work!
}
for(i=1; i <= 10; i = i+1) {
   var i = setlistener("mynode["~i~"]", func{ output (i); });
}

You can also access the four available function properties (or just one, two, or three of them as you need) in your anonymous function. Here is an example that accesses the first value:

for(i=1; i <= 10; i = i+1) {
    var i = setlistener("mynode["~i~"]", func (changedNode) { print (changedNode.getPath() ~ " : " ~ changedNode.getValue()); });
}

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 repositioning the aircraft, and to "false" when the teleport has been completed
  • /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 ... triggered 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.

Nasal code coupled to the autopilot system

Caution
Cquote1.png it's relatively easy to do bad things unintentionally. Like tie a bit of code to an FDM property and run updates of a display 120 times per second rather than the 30 times you actually need. Like start a loop multiple times so that you update the same property 30 times per frame rather than the one time you actually need. It's actually pretty hard to catch these things, because the code is formally okay, does the right thing and just eats more performance than necessary, and there's no simple output telling you that you're running 30 loops rather than the one you expect.
Cquote2.png

Some people have a need to run Nasal code at the same rate as the simulation/FDM. Currently, without modifying the source code for FlightGear, the only way to do this is to find a property updated at the right time in the simulation cycle and set a listener on it. From a code quality standpoint, this is less than ideal.

Autopilot rules, FDM and important instruments run at fixed rate of 120Hz and are already _independant_ of frame rate (Note: this does not help those who try to implement APs manually using Nasal, since Nasal can only run at frame rate. But please do use the "autopilot property rule" system for the fast control part of the autopilot - and only do slow stuff in Nasal (such as switching between autopilot modes), which does not require a close coupling to the FDM/autopilot. The 777 is a good example showing this: dynamic part of AP is done by property rules; switching between AP modes, like "hold glideslope" => "flare" is done in Nasal).

The FDM runs 120 times per second (if so configured), but it runs all iterations for a frame one after the other, then waits until the next frame. The FDM runs at 120 hertz and with a fixed time step.

However, we play one small trick to make that happen. We take the time that has elapsed since the last frame, compute how many whole iterations of the FDM will fit in that time slice (at 1/120th of a second per iteration.) Then we invoke the FDM that many times with a time step of 1/120th of a second. Finally we save out the remainder and add that into the next time slice.

This can produce a small amount of temporal jitter between the graphics and the fdm if the graphics frame rates are not a diviser of 120. In the best case scenario, you've locked your graphics frame rate to 60 hz so the FDM runs exactly 2 iterations every time it is invoked and there is no temporal jitter at all, ever.

One thing to keep in mind is that handing a different size time slice to the FDM every frame (and sometimes that time slice could be 1 second or more?) can lead to instabilities in the math. So our approach is intended to avoid that potential problem. As far as the FDM is concerned, it *is* running asyncronously, at a fixed time step. But, we are playing a little trick on the FDM (it doesn't care) in order to handle the unfortunate possibility of non-fixed and highly variable frame rates on PC hardware running consumer grade operating systems.

Some FDM stuff would like to be tied to the FDM update rate, and that's a desirable goal. What about a callback function then? The FDM subsystem would set /sim/signals/fdm-update, and you could attach a listener to that which does all the things that should interact with the FDM, such as AP, FCS, etc. The rest of Nasal would keep running with the frame rate.

There's just one (minor) problem at the moment. There's no generic FDM update() function where one could put a sig.setDoubleValue(dt).This would have to be done in all FDMs.

Another possibility is to extend the declarative expression logic, which is already supported by the autopilot components, to allow a Nasal expression. Then you mix the declarative components (which you're going to want for most autopilot laws) with some scripted ones. Since the expression evaluation would be driven by the autopilot subsystem, it would run at whatever frame-rate that itself runs at - which is currently in lock-step with the FDM [1].

There are several other options, such as 1) Run a second events system and add an additional parameter to Nasal's settimer allowing you to use this new events system. 2) Add in a signal that is fired each simulation step, probably right before the Autopilot system is run:

setlistener("/sim/signals/fdm-update", func(n) {
       var dt = n.getValue();
       # ... and whatever needs to be done at fdm rate
   });

Some core developers are fairly opposed to the whole idea, i.e. want to avoid *any* Nasal in the fast simulation loop of the FDM, because Nasal execution is slow and non-deterministic because of its GC issue. Running it in the fast simulation loop is the last thing they want[2].

In addition, the developer and maintainer of the AP system is planning on adding Nasal bindings to the AP code to allow runtime instantiation of ap/property rules and has a Nasal binding for that in mind[3].

You can then use Nasal for the high level stuff, and enable/disable/switch the individual controller elements (e.g. in order to automatically switch the autopilot mode when capturing the ILS). There are some nice examples with fgdata/Git aircraft. You could look at the 777.

The big advantage of the property rules is that they don't produce garbage that a garbage collector has to clean up. But as nothing in life comes for free (except FlightGear, of course) XML tends to be much more verbose.

Guideline

  • Computing properties from a well defined set of other properties once per frame: use a property rule.
  • If there is no other way to get it done: use Nasal.

This is also how such things are done in the real world: controllers aren't implemented in imperative programming languages these days - especially not in scripting languages. People use model-based design and connect controller elements - using graphical tools like MATLAB/Simulink. Obviously, FG is missing a graphical interface to specify the controller rules - but the idea of specifying through XML is the same and specification is straight forward.

Creating an autopilot (or any GNC or system model, for that matter) can be done very effectively with discrete objects such as summers, gains, controllers, filters, switches, etc., much as JSBSim has done with the system components. This is a standard approach in industry, as exemplified by Mathwork's $imulink product.

Scilab/Scicos is similar in concept. Control system topologies are often diagrammed in a way that can lead to a one-to-one correspondence between a block and a control system object that can be referenced in an XML file, if the control system component library has been defined properly. This, again, is the way that JSBSim has approached the solution.

Some benefits to such an approach include better testability, more predictability, and easier interface (someday) with a GUI tool, should one materialize. The downside is that XML can be verbose.

All that being said, it is definitely possible to run Nasal in the FDM update loop, so to make up your own mind, you could try this: Alternatively, as a short-term solution that does not rely on editing C++ code, you could just trigger off an internal autopilot property. Which is a pretty clever workaround: trigger a boolean property for each iteration, so that the Nasal listener can pick it up, which will get invoked at FDM update rate that way.

Perhaps we could set up a trigger that is fired by the Autopilot subsystem, immediately before the autopilot executes. However, someone more familiar with FG's code base than me can probably come up with a more elegant solution. I feel like adding a new events system and a parameter to settimer is cleaner than a signal -- and that if a signal is added, the Autopilot subsystem is probably not the place to add it.

Also see: