Howto:Add new fgcommands to FlightGear

From FlightGear wiki
Jump to navigation Jump to search

Background

So called "fgcommands" are FlightGear extension functions which can be invoked in a number of different ways, such as embedded in various XML files (in the form of "action" Bindings), but also via Nasal scripts using the fgcommand() extension function. In addition, it is also possible to invoke fgcommands using the telnet/props interface, so that a simple form of "Remote Procedure Calling" can be implemented, i.e. triggering code via telnet to be run inside the FlightGear process.

Compared to Nasal extension functions (or cppbind bindings), the nice thing about "fgcommands" is that they are not just available to Nasal scripts, but that they can also be used by lots of other FlightGear systems directly without resorting to scripts and registering listeners, such as in the form of GUI bindings, mouse/keyboard handlers and so on.

Of course, there are some disadvantages too: fgcommands always need their arguments wrapped in a property tree structure. This is because they don't have any concept of Nasal and its internal data structures, as they need to work with just XML files and the property tree.

So there is a certain marshaling/conversion overhead involved here. While this is certainly negligible for simple commands that are run rarely, this overhead may add up once a certain fgcommand gets called repeatedly at a high rate. In such cases it may be more efficient to let the command work with argument lists (vectors) rather than just a single argument, so that multiple tasks can be completed by a single invocation.

An overview of fgcommands currently available and supported is provided in $FG_ROOT/Docs/README.commands.

Beginning with FlightGear 2.11+, new fgcommands can now also be implemented in Nasal and registered through new Nasal extension functions:

  • addcommand(name, func)
  • removecommand(name)

This allows you to make up new fgcommands just by implementing them in Nasal:

addcommand("hello", func {
 print("hello");
});
fgcommand("hello");

to process arguments, you can pass a property tree with nodes:

addcommand("hello", func(node) {
 print("hello ", node.getNode('name').getValue() );
});
fgcommand("hello", props.Node.new({'name': 'Johan G'}) );

Obviously, not all fgcommands can be implemented in Nasal, which is why you may sometimes still have to use C++ - creating a new fgcommand using C++ is also fairly simple actually. The source code you need to look at is in flightgear/src/Main/fg_commands.cxx.

Cquote1.png I’d prefer we not multiplex commands. Having more command names costs us nothing, so I’d prefer separate real commands for connect, disconnect and refresh. (eg, ‘multiplayer-connect’, ‘multiplayer-disconnect’)

And then we don’t need a sub-‘command’ argument to a command
Of course I suppose we have other commands that violate this rule, but it would be good to follow it for the future, in my opinion.


Cquote2.png

All commands are listed there, beginning in line 160. The signature of these callback functions is always such that they:

  • have static linkage
  • return a bool (true/false, 1/0)
  • accept one const pointer to a SGPropertyNode (const SGPropertyNode * arg)

Note All FlightGear disk I/O handled via Nasal scripting and/or fgcommands, is subject to access validation via IOrules. This includes the SGPath bindings in FlightGear 2.99+

However, unlike $FG_ROOT, $FG_HOME is generally accessible for writing, consider this example:

#  come up with a path and filename in $FG_HOME, inside the Export sub folder, file name is test.xml
var filename = getprop("/sim/fg-home") ~ "/Export/test.xml";

# use the write_properties() helper in io.nas, which is a wrapper for the savexml fgcommand (see README.commands)
io.write_properties( path: filename, prop: "/sim" );

This will dump the sub branch of the /sim property tree into $FG_HOME/Export/test.xml

For additional examples, see $FG_ROOT/Nasal/io.nas

To learn more about PropertyList processing via loadxml and savexml, please see $FG_ROOT/Docs/README.commands

Internally, all read/write access is validated via an API called fgValidatePath(), for details please see $FG_SRC/Main/util.cxx

The simplest command can be found from line 167, the do_null() command which does nothing:

/**
 * Built-in command: do nothing.
 */
static bool
do_null (const SGPropertyNode * arg)
{
  return true;
}


To see if that's actually doing something, you could add a printf or cout statement and rebuild FlightGear (you'll probably want to use a new git branch for these experiments, in order not to mess up your main branch - when doing that you'll just need to "git add fg_commands.cxx && git commit -m test" after making modifications):

/**
 * Built-in command: do nothing.
 */
static bool
do_null (const SGPropertyNode * arg)
{
  printf("Okay: doing nothing :)");
  return true;
}

Next, you could run the modified fgcommand by using the Nasal console (make sure to watch your console):

 fgcommand("null");

Regarding the syntax of the fgcommand() function: the first parameter is the name of the command that you'd like to run, the second parameter points to an optional property node that contains all the required parameters, that are separately set up.

Argument processing

All callback functions need to accept a single argument, i.e. a const pointer: const SGPropertyNode*. This single argument is used to pass a property sub branch to each fgcommand. This means that each fgcommand is responsible for parsing and processing its function arguments.


For instance, imagine a command that accepts three different parameters to play an audio file:

  • path
  • file
  • volume

These all need to be first set up in the property tree separately, i.e. using setprop:

 setprop("/temp1/path","my path" );
 setprop("/temp1/file","filename" );
 setprop("/temp1/volumen", 1);
 fgcommand("null", "/temp1");

You could just as well use a temporary property tree object, rather than the global property tree:

 var myTree = props.Node.new( {"filename": "stars.jpeg"} );
 fgcommand("null", myTree);

This would turn the hash initialized into a property tree structure, an even shorter version would be:

 fgcommand("null", props.Node.new( {"filename": "stars.jpeg"} ) );

In general, using a temporary property tree object is preferable and recommended if you don't need to access the data in the global property tree otherwise.

So, what the C++ code is doing once it is called, is getting the arguments out of the property argument, see line 1183:

 string path = arg->getStringValue("path");
 string file = arg->getStringValue("file");
 float volume = arg->getFloatValue("volume");

This is accomplished using the Property Tree API, i.e. the methods available in the SGPropertyNode class: http://simgear.sourceforge.net/doxygen/classSGPropertyNode.html

The most frequently used methods are:

  • bool getBoolValue () const: Get a bool value for this node.
  • int getIntValue () const: Get an int value for this node.
  • long getLongValue () const: Get a long int value for this node.
  • float getFloatValue () const: Get a float value for this node.
  • double getDoubleValue () const: Get a double value for this node.
  • const char * getStringValue () const: Get a string value for this node.

However, before actually trying to read in an argument, it is better to first check if the node is actually available, too:

 if (arg->hasValue("path"))
  string path = arg->getStringValue("path");

If an important argument is missing that you cannot set to some sane default value, you should show an error message and return false, to leave the function early:

 if (arg->hasValue("path"))
  string path = arg->getStringValue("path");
  else {
   printf("Cannot play audio file without a file name");
   return false;
  }

There is also a helper macro called SG_LOG available to print debug messages to the console:

 if (arg->hasValue("path"))
  string path = arg->getStringValue("path");
  else {
   SG_LOG(SG_GENERAL, SG_ALERT,"Cannot play audio file without a file name");
   return false;
  }

Exception handling

To be on the safe side, complex fgcommands, which may terminate for one reason or another (such as missing files), should make use of exception handling to catch exceptions and deal with them gracefully, so that they cannot affect the simulator critically.

This is accomplished by using standard C++ try/catch blocks, usually catching an sg_exception. The SG_LOG() macro can be used to print debugging information to the console. If you'd like to display error information using the native FlightGear GUI, you can use the "guiErrorMessage(const char*,sg_exception)" helper.

For example, see the callback function "do_preferences_load" from line 445:

/**
 * Built-in command: (re)load preferences.
 *
 * path (optional): the file name to load the panel from (relative
 * to FG_ROOT). Defaults to "preferences.xml".
 */
static bool
do_preferences_load (const SGPropertyNode * arg)
{
  try {
    fgLoadProps(arg->getStringValue("path", "preferences.xml"),
                globals->get_props());
  } catch (const sg_exception &e) {
    guiErrorMessage("Error reading global preferences: ", e);
    return false;
  }
  SG_LOG(SG_INPUT, SG_INFO, "Successfully read global preferences.");
  return true;
}


Hello World

For example, to implement a new "hello_world" command, that accepts a single parameter named "name":

/**
 * Built-in command: print hello world (name) to the console
 *
 * name (optional): the file name to load the panel from
 * 
 */
static bool
do_hello_world (const SGPropertyNode * arg)
{
  string name = "anonymous"; // default
  if (!arg->hasValue("name")) {
    guiErrorMessage("Cannot say hello without name!: ");
    return false;
  }
  name = arg->getStringValue("name");
  try {
    SG_LOG(SG_GENERAL, SG_ALERT, "Hello World:"<<name.c_str() );
  } catch (const sg_exception &e) {
    guiErrorMessage("Error saying HELLO WORLD: ", e);
    return false;
  }
  return true;
}


After adding the new callback to the callback list and rebuilding FlightGear, you can easily invoke the new "hello_world" fgcommand using the Nasal console like this:

 fgcommand("hello_world", props.Node.new( {"name": "Howard Hughes"} ));


Adding new commands

All new commands must have the previously described signature, the functions should then be added to the list of built-in commands, beginning in line 1552. The list of built-in commands maps the human-readable names used in README.commands to the names of the internal C++ functions implementing them.

Reaching out to subsystems

Quite possibly, you may need to reach out to some other subsystems to implement a certain fgcommand, such as accessing the sound system, the networking/multiplayer system or the FDM system, this is accomplished by using the globals-> pointer, which allows you to access other subsystems easily.

Please refer to flightgear/src/Main/globals.cxx (line 120) and its header file globals.hxx. A number of helpful usage examples can be found by searching fg_commands.cxx for "globals->".

In general, you will always want to add a NULL pointer check to ensure that the corresponding subsystem handle could actually be retrieved:

FGMultiplayMgr * self = (FGMultiplayMgr*) globals->get_subsystem("mp");
if (!self) {
 SG_LOG(SG_NETWORK, SG_WARN, "Multiplayer subsystem not available.");
 return false;
}

Equally, it may be appropriate to wrap code in between try/catch blocks, too.

A list of methods available to access certain subsystems is provided here in globals.hxx, beginning in line 220.

Subsystem specific fgcommands

In addition to directly editing the default initialization routine in fg_init.cxx, you can also dynamically add/remove fgcommands from your SGSubsystem, by getting a handle to the SGCommandMgr singleton, specifying a command name, and a corresponding callback (which can be a static member of your SGSubsystem).

fgcommands having implicit dependencies on other subsystems, should not be added in a hard-coded fashion, but via the ctor/init or postinit() methods of the corresponding subsystem itself - otherwise, such code is conflicting with James' reset/-reinit work - in particular, the very fgcommands intended to allow subsystems to be shut-down and re-initialized.

In particular, see FlightGear commit 8608a480.

Even if such additions don't break FG immediately, they contribute to crippling the reset/re-init effort - and will make it increasingly difficult to allow run-time dependencies to be better established and formalized.

We've had a long discussion about this exact issue WRT to Nasal/CppBind bindings added unconditionally, which is causing the same problem. The general consensus was that subsystem-specific functionality must be initialized and cleaned up by the corresponding subsystem itself, instead of always assuming that all subsystems are available.

#include <simgear/structure/commands.hxx>
...
SGCommandMgr::instance()->addCommand("command-name", &MySGSubsystem::commandCallback);

This is something that would be typically done in your constructor or the SGSubsystem::init() method. Form more detailed examples, just refer to the source tree and look for occurences of "SGCommand":

./src/Model/panelnode.cxx:365:  SGCommandMgr::instance()->addCommand("panel-mouse-click", do_panel_mouse_click);
./src/Environment/realwx_ctrl.cxx:246:    SGCommandMgr::instance()->addCommand("request-metar", commandRequestMetar);
./src/Environment/realwx_ctrl.cxx:247:    SGCommandMgr::instance()->addCommand("clear-metar", commandClearMetar);
./src/Environment/realwx_ctrl.cxx:252:  //SGCommandMgr::instance()->removeCommand("request-metar");
./src/Autopilot/route_mgr.cxx:218:  SGCommandMgr* cmdMgr = SGCommandMgr::instance();
./src/Autopilot/route_mgr.cxx:237:  SGCommandMgr* cmdMgr = SGCommandMgr::instance();
./src/GUI/FGPUIMenuBar.cxx:45:    SGCommandMgr::command_t command;
./src/Scripting/NasalSys.cxx:599:class NasalCommand : public SGCommandMgr::Command
./src/Scripting/NasalSys.cxx:644:    SGCommandMgr::Command* cmd = globals->get_commands()->getCommand(naStr_data(args[0]));
./src/Scripting/NasalSys.cxx:646:  //  SGCommandMgr::Command* cmd = globals->get_commands()->removeCommand(naStr_data(args[0]))
./src/Time/TimeManager.cxx:63:  SGCommandMgr::instance()->addCommand("timeofday", do_timeofday);
./src/Main/subsystemFactory.hxx:26:class SGCommandMgr;
./src/Main/subsystemFactory.hxx:40:void registerSubsystemCommands(SGCommandMgr* cmdMgr);
./src/Main/fg_commands.cxx:1575:  SGCommandMgr::command_t command;
./src/Main/globals.hxx:48:class SGCommandMgr;
./src/Main/globals.hxx:125:    SGCommandMgr *commands;
./src/Main/globals.hxx:249:    inline SGCommandMgr *get_commands () { return commands; }
./src/Main/globals.cxx:152:    commands( SGCommandMgr::instance() ),
./src/Main/subsystemFactory.cxx:283:  SGCommandMgr::command_t command;
./src/Main/subsystemFactory.cxx:293:void registerSubsystemCommands(SGCommandMgr* cmdMgr)

Removing fgcommands

Equally, there's an API for removing fgcommands on demand, e.g. when shutting down a subsystem - which would be typically done inside your SGSubsystem's dtor:

FGMultiplayMgr::~FGMultiplayMgr() 
{
   globals->get_commands()->removeCommand("multiplayer");
   globals->get_commands()->removeCommand("multiplayer-connect");
   globals->get_commands()->removeCommand("multiplayer-disconnect");
   globals->get_commands()->removeCommand("multiplayer-refreshserverlist");
} // FGMultiplayMgr::~FGMultiplayMgr()
//////////////////////////////////////////////////////////////////////

Finally

When you send patches or file merge requests for new fgcommands, please also make sure to send patches for README.commands, too - so that your new commands are documented there. Otherwise, people may have a hard time using/maintaining your fgcommands, possibly years after you added them, without you even being around by then.

Also, depending on the nature of functionality that you would like to add to FlightGear, you may want to use a more sophisticated method of adding new features, such as adding custom Nasal extension functions, or adding a dedicated SGSubsystem, a full property tree wrapper using listeners, or the cppbind framework in Simgear in order to expose full classes/objects.

This was just an introduction intended to get you started, most of the missing information can be learned by referring to the plethora of existing fgcommands and seeing how they are implemented and working.

Please get in touch if you think that there's something missing here, or just add your information directly!