Using listeners and signals with Nasal: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
(Created page with "{{Template:Nasal Navigation}} == Listeners and Signals == The important thing to keep in mind is that custom listeners are generally not about loading or running Nasal files,...")
 
Line 6: Line 6:


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.  
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() ===
===setlistener() vs. _setlistener() ===

Revision as of 12:19, 20 August 2012


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

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. (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".

Examples of properties where setlistener won't work:

  • /position/elevation-ft
  • /ai/models/aircraft/orientation/heading-deg
  • 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>]]);

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 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 {
     if (cmdarg().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.

If you have set a callback function named myCallbackFunc via setlistener (setlistener(myNode, myCallbackFunc)), you can use this syntax in the callback function:

myCallbackFunc ([<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, 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 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 ... 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.