Howto:Create new subsystems

From FlightGear wiki
Jump to: navigation, search

This page is meant to document the necessary steps involved in creating new C++ subsystems in FlightGear. This is usually required in order to implement completely new components or features, but also in order to create new instrument implementations that are hardcoded in C++, such as the TCAS, NavDisplay or MK-VIII.

Also, implementing new subsystems in C++ is usually the preferred way of implementing computationally intense features or code that needs to be run at very high frequency, possibly even outside the main loop (i.e. using an SGThread-based worker thread), code that may otherwise cause too much CPU load if for example implemented in scripting space using Nasal scripting (e.g. terrain/agradar radar).

On the other hand, many interesting features can already be implemented using Nasal support, and often it is also significantly easier for new developers to enhance the current scripting interface in order to make new APIs available to FlightGear, instead of coming up with a completely new C++ subsystem. The necessary steps to add additional APIs to the scripting system are documented at Howto:Extending Nasal.

In order to work through this tutorial, you will need to be familiar with:

  • building FlightGear from source
  • C++ programming

(If you are reading this in (or after) 2017/2018, you may also want to consider using HLA for these purposes, which should be mature enough by then)

Step by Step

Sim-version-with-python.png
diff --git a/src/Main/options.cxx b/src/Main/options.cxx
index 20039d6..b48dbda 100644
--- a/src/Main/options.cxx
+++ b/src/Main/options.cxx
@@ -231,6 +231,12 @@ void fgSetDefaults ()
     v->setValueReadOnly("build-number", HUDSON_BUILD_NUMBER);
     v->setValueReadOnly("build-id", HUDSON_BUILD_ID);
     v->setValueReadOnly("hla-support", bool(FG_HAVE_HLA));
+#ifdef HAVE_PYTHON
+    v->setValueReadOnly("python-support", true);
+#else
+    v->setValueReadOnly("python-support", false);
+#endif
+
 #if defined(FG_NIGHTLY)
     v->setValueReadOnly("nightly-build", true);
 #else
  • make sure that you can build SG+FG from source Building FlightGear
  • get a sourceforge account and clone the SG/FG repositories FlightGear and Git#Clone and handle Repositories
  • create a new topic branch: FlightGear and Git#Local_Branch
  • Add a new set of cxx/hxx files to $FG_SRC (either using an appropriate existing folder, or by adding a new one)
  • implement the SGSubsystem interface for the new subsystem [1] (if you need to introduce more than just a single subsystem, inherit from SGSubsystemMgr instead)
  • if needed, add your new files to the CMake build system: Developing using CMake#Adding_new_files_to_the_build
  • wrap any optional code blocks in between #ifdef/#endif blocks (including inclusion of headers)
  • consider adding a dedicated log-class for your new subsystem
  • add the new subsystem to the initialization sequence in flightgear/src/Main/fg_init.cxx, wrap everything in between #ifdef/#endif using the cmake build switch, if in doubt, also provide a startup option (instruments are dynamically allocated using the instrument manager, see Howto:Create_a_2D_drawing_API_for_FlightGear#Creating_a_new_instrument)
  • expose availability of your optional feature by setting a boolean property (and if applicable, a version number) so that this can be shown in the about.xml dialog, for troubleshooting purposes, it makes sense to also SG_LOG() that info during startup, so that it shows up in the log file [2]
  • edit $FG_SRC/Main/subsystemFactory.cxx to add your new subsystem there, so that your subsystem can be stopped/started on demand
  • please test removing/creating your subsystem on demand, e.g. using Howto:Reset/re-init Troubleshooting

Inheriting from SGSubsystem

In general, to add a new subsystem you would have to create a derived class from the SimGear class SGSubsystem and define at least a small set of functions:

    #include <subsystem_mgr.hxx> //required header file

    // create a new class, inherit from SGSubsystem
    class MyClass : public SGSubsystem
    {
    public:
 
      MyClass ();
      virtual ~MyClass ();
 
      // re-implement virtual methods required for all SGSubsystems
      virtual void init ();
      virtual void reinit ();
      virtual void bind ();
      virtual void unbind ();
      virtual void update (double dt); // this is the most important one
    }; // end of MyClass

APIs

The init() function should make sure everything is set and ready so the update() function can be run by the main loop.

The reinit() function handles everything in case of a reset by the user. However, at the moment, many subsystems still do not support reinit() properly, which is a known bug (ticket 419) and separately discussed at Reset & re-init. In addition we have a bunch of "legacy" subsystems that do not yet use the SGSubsystem structure at all. The long-term plan is that all subsystems should fully implement the SGSubsystem interface, so that they can be properly reset/re-initialized, but also dynamically instantiated/freed - so that optional subsystems don't need to be running.

SGSubSystemManager::update(dt) is called once per frame, which basically ensures that each subsystem's update function is called once a frame, with delta_time since last update as argument (you can however maintain your own SGTimeStamp-based timer to only process update at lower intervals, i.e. ever 5-10 seconds). Note that you should take care to honor dt properly, because FlightGear supports changing simulation time at runtime. At the moment, most instruments and other subsystems must be considered broken when using that feature, because many of the old subsystems still do not take dt into account. Quickly searching for instruments using "uncorrected" dt for complex mathematical calculations (e.g. low-pass filters) shows more than 15 other instruments which seem affected (such as the altimeter/attitude-/heading-/adf- indicators, the NAV receiver, GPS way point calculations etc.). That's why changing "dt" might be a good idea - though it's a larger change. The current meaning of "dt" is a bit, well, "awkward" :) (ticket 421). New code should be developed such that this is taken into consideration.

The instrumentation subsystem creates instrument subsystems also dynamically according to the instrumentation XML file in addition to the fixed ones (HUD, ODGauge). To learn more about interleaving subsystems with the FDM, see [3].

(Note: The bind() and unbind() functions can be used to tie and untie properties, but tied properties should no longer be used in new code. New code and updated modules using tied properties should instead be using so called Property Objects which are a more future-proof abstraction, that is also suitable to be used in an increasingly distributed architecture using HLA).

Not all of these methods will necessarily need to be fully implemented in the beginning: a simple subsystem prototype might very well work by just implementing the update() method, and leaving the other methods with an empty function body until you really have a need for them.

For example, a very simple "dummy" subsystem might even implement all methods inline:

    #include <subsystem_mgr.hxx> //required header file
    class FGDummy : public SGSubsystem
    {
    private: 
      double m_delay;
      void check() {
        if (m_delay >= 5) { 
          SG_LOG(SG_GENERAL, SG_ALERT, "FGDummy says hi !\n");
          m_delay=0;
         }
      }
    public:
 
      FGDummy ():m_delay(0) {}
      virtual ~FGDummy () {}
 
      virtual void init () {}
      virtual void reinit () {}
      virtual void bind () {}
      virtual void unbind () {}
      virtual void update (double dt) { delay+=dt; check(); }
    };

After that, you can register this class at the subsystem manager, which is implemented in flightgear/src/Main/fg_init.cxx.

Specifically, you'll want to look for the fgInitSubsystems() function:

 // This is the top level init routine which calls all the other
 // initialization routines.  If you are adding a subsystem to flight
 // gear, its initialization call should located in this routine.
 // Returns non-zero if a problem encountered.
 bool fgInitSubsystems() {}

This is where you can add another line reading:

    globals->add_subsystem("dummy", new FGDummy);

The add_subsystem() method supports an optional SGSubsystemMgr::TYPE (defaulted to SGSubsystemMgr::GENERAL)- for details, please see the class SGSubsystemMgr in simgear/simgear/structure/subsystem_mgr.hxx (line 365):

    /**
     * Types of subsystem groups.
     */
    enum GroupType {
        INIT = 0,
        GENERAL,
        FDM,        ///< flight model, autopilot, instruments that run coupled
        POST_FDM,   ///< certain subsystems depend on FDM data
        DISPLAY,    ///< view, camera, rendering updates
        SOUND/*I want to be last!*/,  ///< needs to run AFTER display, to allow concurrent GPU/sound processing
        MAX_GROUPS
    };


Note that you should not unconditionally add new subsystems, but instead make your subsystem configurable through a property flag, so that it can be explicitly disabled during startup - use fgGetBool() to check first if the user disabled your subsystem or not.

Cquote1.png you may also want to provide a startup option to entirely disable Python to help ensure that people can troubleshoot issues and determine if they are affected by Python being active or not - i.e. Python being built-in but adding the subsystem is determined conditionally using a fgGetBool() and or logic in $FG_SRC/Main/options.cxx I am just suggesting that because we have seen countless of posting where Nasal (and its GC) were said to be the culprit, and only when we walked people through removing all Nasal code from the aircraft (think Su15), they finally realied that Nasal was not the issue at all.
Cquote2.png
    if (fgGetBool("/sim/enable-dummy", false))
      globals->add_subsystem("dummy", new FGDummy);

This will check the /sim/enable-dummy property, default it to false, and only add the subsystem if it the property is true. You would then either modify preferences.xml or use a --prop:/sim/enable-dummy=true flag to enable your subsystem.

Similiarly, you'll probably want to check in your update method if the subsystem should be run, or return early (i.e. to save resources).

Depending on your subsystem's dependencies to other subsystems (sound, openal, autopilot, fdm etc), you may want to place your subsystem before or after certain other subsystems to ensure that all required subsystems are available when your new subsystem is invoked.

(Note: If you need to do any "post-initialization" to register new Nasal scripting bindings, please use the property/listener-based signaling-mechanism (see /sim/signals) using properties instead of hard-coding something like postInitNasalFoo()in NasalSys.cxx/FGNasalSys::init() - the point being that resetting/re-initializing the simulator is easier to support using a listener-based initialization sequence: Reset & re-init).

Now the subsystem manager calls the update() function of this class every frame. dt is the time (in seconds) elapsed since the last call.

If you have a number of related subsystems that may inevitably be connected, you may want to checkout the class SGSubsystemGroup to nicely keep them in a group.

Dynamically toggling subsystems on/off

Another increasingly important consideration is designing your subsystem such that it can be safely re-initialized at runtime, using the subsystemFactory wrappers in $FG_SRC/Main and the corresponding fgcommands:

  • add-subsystem
  • remove-subsystem
Cquote1.png However, do note that you need to review/augment $FG_SRC/Main/subsystemFactory.cxx for that to work correctly
Cquote2.png

See this commit for more details: FlightGear commit 8608a480.

Finally, you will also want to edit globals.?xx to ensure that your new subsystem is added to the destructor, so that it will be properly freed upon program termination. Alternatively, consider using SGSharedPtr<> instead of a raw pointer.


Another example taken from Subsystem-level Memory Tracking for FlightGear:

diff -urN a/src/Main/CMakeLists.txt b/src/Main/CMakeLists.txt
--- a/src/Main/CMakeLists.txt 2015-08-20 23:03:15.070835000 +0300
+++ b/src/Main/CMakeLists.txt 2015-08-20 23:26:58.706798388 +0300
@@ -17,12 +17,17 @@
main.cxx
options.cxx
util.cxx
+ ram_usage.cxx
positioninit.cxx
subsystemFactory.cxx
screensaver_control.cxx
${RESOURCE_FILE}
)

+IF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+ list(APPEND SOURCES ram_usage_linux.cxx)
+ENDIF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
set(HEADERS
fg_commands.hxx
fg_init.hxx
@@ -35,12 +40,19 @@
main.hxx
options.hxx
util.hxx
+ ram_usage.hxx
positioninit.hxx
subsystemFactory.hxx
AircraftDirVisitorBase.hxx
screensaver_control.hxx
)

+IF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+ list(APPEND HEADERS ram_usage_linux.hxx)
+ENDIF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
+
+
get_property(FG_SOURCES GLOBAL PROPERTY FG_SOURCES)
get_property(FG_HEADERS GLOBAL PROPERTY FG_HEADERS)

diff -urN a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx
--- a/src/Main/fg_init.cxx 2015-08-20 23:22:52.166804000 +0300
+++ b/src/Main/fg_init.cxx 2015-08-20 23:29:15.074794813 +0300
@@ -141,6 +141,7 @@
#include "globals.hxx"
#include "logger.hxx"
#include "main.hxx"
+#include "ram_usage.hxx"
#include "positioninit.hxx"
#include "util.hxx"
#include "AircraftDirVisitorBase.hxx"
@@ -715,6 +716,10 @@
////////////////////////////////////////////////////////////////////
globals->add_subsystem("properties", new FGProperties);

+ ////////////////////////////////////////////////////////////////////
+ // Add the ram usage statistics system
+ ////////////////////////////////////////////////////////////////////
+ globals->add_subsystem("memory-stats", new MemoryUsageStats, SGSubsystemMgr::INIT, 5.00);

////////////////////////////////////////////////////////////////////
// Add the performance monitoring system.
diff -urN a/src/Main/ram_usage.cxx b/src/Main/ram_usage.cxx
--- a/src/Main/ram_usage.cxx 1970-01-01 03:00:00.000000000 +0300
+++ b/src/Main/ram_usage.cxx 2015-08-20 23:29:53.950793794 +0300
@@ -0,0 +1,22 @@
+#include "ram_usage_linux.hxx"
+
+MemoryUsageStats::MemoryUsageStats() {
+ _mem = new LinuxMemoryInterface(); //FIXME: should be implemented for Win/Mac & Linux
+}
+
+MemoryUsageStats::~MemoryUsageStats() {
+ delete _mem;
+}
+
+void
+MemoryUsageStats::update(double dt) {
+ _mem->update();
+ double swap = _mem->getSwapSize();
+ double total = _mem->getTotalSize();
+ SG_LOG(SG_GENERAL, SG_DEBUG, "Updating Memory Stats:" << total << " kb");
+ fgSetInt("/memory-usage/swap-usage-kb", swap );
+ fgSetInt("/memory-usage/total-usage-kb", total );
+}
+
+
+
diff -urN a/src/Main/ram_usage.hxx b/src/Main/ram_usage.hxx
--- a/src/Main/ram_usage.hxx 1970-01-01 03:00:00.000000000 +0300
+++ b/src/Main/ram_usage.hxx 2015-08-20 23:29:53.950793794 +0300
@@ -0,0 +1,51 @@
+#ifndef __RAM_USAGE
+#define __RAM_USAGE
+
+#include <simgear/timing/timestamp.hxx>
+#include <simgear/structure/subsystem_mgr.hxx>
+
+#include <Main/globals.hxx>
+#include <Main/fg_props.hxx>
+
+#include <string>
+#include <map>
+
+using std::map;
+
+// Linux: /proc/pid/smaps
+// Windows: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682050(v=vs.85).aspx
+
+class MemoryInterface {
+public:
+ MemoryInterface() {}
+ typedef map<const char*, double> RamMap;
+//protected:
+ virtual void update() = 0;
+
+ double getTotalSize() const {return _total_size;}
+ //virtual void setTotalSize(double t) {_total_size=t;}
+
+ double getSwapSize() const {return _swap_size;}
+ //virtual void setSwapSize(double s) {_swap_size=s;}
+protected:
+ RamMap _size;
+ std::string _path;
+ std::stringstream _pid;
+
+ double _total_size;
+ double _swap_size;
+};
+
+class MemoryUsageStats : public SGSubsystem
+{
+public:
+ MemoryUsageStats();
+ ~MemoryUsageStats();
+ virtual void update(double);
+protected:
+private:
+ MemoryInterface* _mem;
+};
+
+#endif
+
diff -urN a/src/Main/ram_usage_linux.cxx b/src/Main/ram_usage_linux.cxx
--- a/src/Main/ram_usage_linux.cxx 1970-01-01 03:00:00.000000000 +0300
+++ b/src/Main/ram_usage_linux.cxx 2015-08-20 23:29:53.950793794 +0300
@@ -0,0 +1,49 @@
+// https://gist.github.com/896026/c346c7c8e4a9ab18577b4e6abfca37e358de83c1
+
+#include "ram_usage_linux.hxx"
+
+#include <cstring>
+#include <string>
+
+#include "Main/globals.hxx"
+
+using std::string;
+
+LinuxMemoryInterface::LinuxMemoryInterface() {
+ _pid << getpid();
+ _path = "/proc/"+ _pid.str() +"/smaps";
+}
+
+void
+LinuxMemoryInterface::OpenProcFile() {
+ file = fopen(_path.c_str(),"r" );
+ if (!file) {
+ throw("MemoryTracker:Cannot open /proc/pid/smaps");
+ }
+ SG_LOG(SG_GENERAL, SG_DEBUG, "Opened:"<< _path.c_str() );
+}
+
+LinuxMemoryInterface::~LinuxMemoryInterface() {
+ if (file) fclose(file);
+}
+
+void LinuxMemoryInterface::update() {
+ OpenProcFile();
+ if (!file) throw("MemoryTracker: ProcFile not open");
+
+ _total_size = 0;
+ _swap_size = 0;
+
+ char line[1024];
+ while (fgets(line, sizeof line, file))
+ {
+ char substr[32];
+ int n;
+ if (sscanf(line, "%31[^:]: %d", substr, &n) == 2) {
+ if (strcmp(substr, "Size") == 0) { _total_size += n; }
+ else if (strcmp(substr, "Swap") == 0) { _swap_size += n; }
+ }
+ }
+ fclose(file);
+}
+
diff -urN a/src/Main/ram_usage_linux.hxx b/src/Main/ram_usage_linux.hxx
--- a/src/Main/ram_usage_linux.hxx 1970-01-01 03:00:00.000000000 +0300
+++ b/src/Main/ram_usage_linux.hxx 2015-08-20 23:29:53.950793794 +0300
@@ -0,0 +1,22 @@
+#ifndef __RAM_USAGE_LINUX
+#define __RAM_USAGE_LINUX
+
+ #include <sys/types.h>
+ #include <unistd.h>
+ #include <stdio.h>
+
+ #include "ram_usage.hxx"
+
+class LinuxMemoryInterface : public MemoryInterface {
+public:
+ LinuxMemoryInterface();
+~LinuxMemoryInterface();
+ virtual void update();
+private:
+ void OpenProcFile();
+ const char* filename;
+ FILE *file;
+};
+
+
+#endif
+

Using Property Listeners

If your subsystem is interested in specific property state, it can register so called property listeners using the SGPropertyChangeListener API, the interface of which needs to be implemented by your subsystem:

 class SGPropertyChangeListener
  {
 public:
   virtual ~SGPropertyChangeListener ();
   virtual void valueChanged (SGPropertyNode * node);
   virtual void childAdded (SGPropertyNode * parent, SGPropertyNode * child);
   virtual void childRemoved (SGPropertyNode * parent, SGPropertyNode * child);
 
 protected:
   friend class SGPropertyNode;
   virtual void register_property (SGPropertyNode * node);
   virtual void unregister_property (SGPropertyNode * node);
  
 private:
   std::vector<SGPropertyNode *> _properties;
  };

There are separate notifications for a change value, and added child, and a removed child, so that user code can take appropriate actions. The protected methods are callbacks from SGPropertyNode::addChangeListener to allow two-way pointers; the destructor will remove all listener references so that there won't be dangling pointers when the object that's listening disappears.

SGPropertyNode now has three separate methods for firing change events:

  • SGPropertyNode::fireValueChanged ()
  • SGPropertyNode::fireChildAdded (SGPropertyNode * child)
  • SGPropertyNode::fireChildRemoved (SGPropertyNode * child)

The child-added and child-removed events will *always* be fired automatically. The value-changed events will be fired automatically unless the property is tied, in which case the controlling code must invoke it specifically (or not).


If you are just interested in being notified once a value is changed, you'll want to implement the valueChanged method, the childAdded and childRemoved methods will be invoked once subnodes are added or removed, this is for example useful if you are interested in all updates to a specific branch within the property tree, so that internal state can be properly updated.

So, if you know that you'll primarily have to know if a certain value is modified, you can concentrate on implementing just the valueChanged method.

In order to "subscribe" to certain properties, you'll want to make use of the register_property() and unregister_property() methods. You'll find a bunch of FG-specific overloaded wrappers in flightgear/src/Main/fg_props.hxx (line 173), namely:

  • fgAddChangeListener (SGPropertyChangeListener * listener, const std::string & path)
  • fgAddChangeListener (SGPropertyChangeListener * listener, const char * path, int index)
  • fgAddChangeListener (SGPropertyChangeListener * listener, const std::string & path, int index)


Referring to our earlier example, it can be basically enhanced like this (all implemented inline for sake of clarity and simplicity):

   #include <subsystem_mgr.hxx> //required header file
   #include <props.hxx> // required for SGPropertyNode stuff
   class FGDummy : public SGSubsystem, public SGPropertyChangeListener
   {
   private: 
     double m_delay;
     SGPropertyNode* m_dummy_prop;
     void check() {
       if (m_delay >= 5) { 
         SG_LOG(SG_GENERAL, SG_ALERT, "FGDummy says hi !\n");
         m_delay=0;
        }
     }
   public:
     FGDummy ():m_delay(0), m_dummy_prop(fgGetNode("/dummy/value"),1) {}
     virtual ~FGDummy () {}
    virtual void valueChanged (SGPropertyNode * node) {SG_LOG(SG_GENERAL,SG_ALERT,"FGDummy:Listener fired");}
     virtual void init () {}
     virtual void reinit () {}
     virtual void bind () {}
     virtual void unbind () {}
     virtual void update (double dt) { delay+=dt; check(); }
   }

This is really just the beginning obviously, because we only touched the fundamentals. There are lots of more sophisticated examples to be found in the source tree (listed in ascending complexity):

  • the telnet system uses listeners to add very basic support for subscribing to property updates
  • the Nasal scripting language exposes the listener API to Nasal space using setlistener()/removelistener(), so that Nasal callbacks can be invoked, too
  • the autopilot system uses listeners to be largely configurable through the property tree
  • the AI system uses listeners to allow AI traffic to be instantiated and controlled just by setting properties
  • the Canvas subsystem (now to be found in SimGear), uses listeners to implement a 2D drawing API on top of the property tree