Howto:Add new fgcommands to FlightGear: Difference between revisions

 
(27 intermediate revisions by 3 users not shown)
Line 1: Line 1:
{{Extension Mechanisms}}
= Background =
= 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.
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, 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.  
Compared to [[Nasal]] extension functions (or [[Nasal/CppBind|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.
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.
Line 8: Line 9:
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.
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 [http://gitorious.org/fg/fgdata/blobs/master/Docs/README.commands $FG_ROOT/Docs/README.commands].
An overview of fgcommands currently available and supported is provided in {{readme file|commands}}.


Creating a new fgcommand is fairly simple actually. The source code you need to look at is in [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx $FG_SRC/Main/fg_commands.cxx].
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:


All commands are listed there, beginning in [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx#line160 line #160]. The signature of these callback functions is always such that they:
<syntaxhighlight lang="nasal">
addcommand("hello", func {
print("hello");
});
fgcommand("hello");
</syntaxhighlight>
 
to process arguments, you can pass a property tree with nodes:
 
<syntaxhighlight lang="nasal">
addcommand("hello", func(node) {
print("hello ", node.getNode('name').getValue() );
});
fgcommand("hello", props.Node.new({'name': 'Johan G'}) );
</syntaxhighlight>
 
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 file|src/Main/fg_commands.cxx}}.
 
{{FGCquote
  |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<br/>
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.
  |{{cite web |url=http://sourceforge.net/p/flightgear/mailman/message/33586829/
    |title=<nowiki>Re: [Flightgear-devel] [Flightgear-commitlogs] FlightGear branch,
next, updated. f5f82c461f39ce27c2b2a4c2d3310122576e408f</nowiki>
    |author=<nowiki>James Turner</nowiki>
    |date=<nowiki>2015-03-11</nowiki>
  }}
}}
 
All commands are listed there, beginning in {{flightgear file|src/Main/fg_commands.cxx|l=160|t=line 160}}. The signature of these callback functions is always such that they:


* have static linkage
* have static linkage
Line 19: Line 53:
* accept one const pointer to a SGPropertyNode (const SGPropertyNode * arg)
* accept one const pointer to a SGPropertyNode (const SGPropertyNode * arg)


The simplest command can be found in [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx#line167 line 167], the do_null() command which does nothing:
{{IO Restrictions}}
 
The simplest command can be found from {{flightgear file|src/Main/fg_commands.cxx|l=167|t=line 167}}, the do_null() command which does nothing:


<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
Line 49: Line 85:
Next, you could run the modified fgcommand by using the Nasal console (make sure to watch your console):
Next, you could run the modified fgcommand by using the Nasal console (make sure to watch your console):


<syntaxhighlight lang="php">
<syntaxhighlight lang="nasal">
  fgcommand("null");
  fgcommand("null");
</syntaxhighlight>
</syntaxhighlight>
Line 66: Line 102:
These all need to be first set up in the property tree separately, i.e. using setprop:
These all need to be first set up in the property tree separately, i.e. using setprop:


<syntaxhighlight lang="php">
<syntaxhighlight lang="nasal">
  setprop("/temp1/path","my path" );
  setprop("/temp1/path","my path" );
  setprop("/temp1/file","filename" );
  setprop("/temp1/file","filename" );
Line 75: Line 111:
You could just as well use a temporary property tree object, rather than the global property tree:
You could just as well use a temporary property tree object, rather than the global property tree:


<syntaxhighlight lang="php">
<syntaxhighlight lang="nasal">
  var myTree = props.Node.new( {"filename": "stars.jpeg"} );
  var myTree = props.Node.new( {"filename": "stars.jpeg"} );
  fgcommand("null", myTree);
  fgcommand("null", myTree);
Line 82: Line 118:
This would turn the hash initialized into a property tree structure, an even shorter version would be:
This would turn the hash initialized into a property tree structure, an even shorter version would be:


<syntaxhighlight lang="php">
<syntaxhighlight lang="nasal">
  fgcommand("null", props.Node.new( {"filename": "stars.jpeg"} ) );
  fgcommand("null", props.Node.new( {"filename": "stars.jpeg"} ) );
</syntaxhighlight>
</syntaxhighlight>
Line 88: Line 124:
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.
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 [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx#line1183 line 1183]:
So, what the C++ code is doing once it is called, is getting the arguments out of the property argument, see {{flightgear file|src/Main/fg_commands.cxx|l=1183|t=line 1183}}:


<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
Line 136: Line 172:


== Exception handling ==
== 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.  
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.
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" in [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx#line445 line 445]:
For example, see the callback function "do_preferences_load" from {{flightgear file|src/Main/fg_commands.cxx|l=445|t=line 445}}:


<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
Line 167: Line 202:


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


Line 174: Line 208:
  * Built-in command: print hello world (name) to the console
  * Built-in command: print hello world (name) to the console
  *
  *
  * name (optional): the file name to load the panel from (relative
  * name (optional): the file name to load the panel from
  *  
  *  
  */
  */
Line 181: Line 215:
{
{
   string name = "anonymous"; // default
   string name = "anonymous"; // default
  if (!arg->hasValue("name")) {
    guiErrorMessage("Cannot say hello without name!: ");
    return false;
  }
   name = arg->getStringValue("name");
   name = arg->getStringValue("name");
   try {
   try {
Line 193: Line 231:




After rebuilding FlightGear, you can easily invoke the new "hello_world" fgcommand like this:
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:
 
<syntaxhighlight lang="nasal">
fgcommand("hello_world", props.Node.new( {"name": "Howard Hughes"} ));
</syntaxhighlight>
 
 
= 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 {{flightgear file|src/Main/fg_commands.cxx|l=1552|t=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 ==
== 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 <code>globals-></code> pointer, which allows you to access other subsystems easily.


Quite possibly, you may need to reach out to some other subsystems to implement a certain fgcommand, this is accomplished by using the globals-> pointer, which allows you to access other subsystems easily. Please refer to [http://gitorious.org/fg/flightgear/blobs/next/src/Main/globals.cxx#line120 $FG_SRC/Main/globals.cxx] and its header file [http://gitorious.org/fg/flightgear/blobs/next/src/Main/globals.hxx globals.hxx]. A number of helpful usage examples can be found by searching fg_commands.cxx for "globals->".
Please refer to {{flightgear file|src/Main/globals.cxx|l=120}} and its header file {{flightgear file|src/Main/globals.hxx|t=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:


A list of methods available to access certain subsystems is provided here in [http://gitorious.org/fg/flightgear/blobs/next/src/Main/globals.hxx#line220 globals.hxx, beginning in line 220].
<syntaxhighlight lang="cpp">
FGMultiplayMgr * self = (FGMultiplayMgr*) globals->get_subsystem("mp");
if (!self) {
SG_LOG(SG_NETWORK, SG_WARN, "Multiplayer subsystem not available.");
return false;
}
</syntaxhighlight>


= Adding new commands =
Equally, it may be appropriate to wrap code in between try/catch blocks, too.
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 [http://gitorious.org/fg/flightgear/blobs/next/src/Main/fg_commands.cxx#line1467 1467]. 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.
 
A list of methods available to access certain subsystems is provided here in {{flightgear file|src/Main/globals.hxx|l=220|t=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.
 
<syntaxhighlight lang="cpp">
#include <simgear/structure/commands.hxx>
...
SGCommandMgr::instance()->addCommand("command-name", &MySGSubsystem::commandCallback);
</syntaxhighlight>
 
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":
 
<pre>./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)
 
</pre>
 
=== 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:
 
<syntaxhighlight lang="cpp">
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()
//////////////////////////////////////////////////////////////////////
</syntaxhighlight>
 
== 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 [[Howto:Extend Nasal|adding custom Nasal extension functions]], or [[Howto:Create new subsystems|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.


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.
Please get in touch if you think that there's something missing here, or just add your information directly!


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 see how they are implemented and working.
[[Category:Core developer documentation]]
[[Category:Howto]]
[[Category:Hackathon Materials]]