Howto:Add new features to the route manager system

From FlightGear wiki
Jump to navigation Jump to search
Note  Whenever possible, please refrain from modeling complex systems, like an FDM, autopilot or Route Manager with Nasal. This is primarily to help reduce Nasal overhead (especially GC overhead). It will also help to unify duplicated code. The FlightGear/SimGear code base already contains fairly generic and efficient systems and helpers, implemented in C++, that merely need to be better generalized and exposed to Nasal so that they can be used elsewhere. For example, this would enable Scripted AI Objects to use full FDM implementations and/or built-in route manager systems.

Technically, this is also the correct approach, as it allows us to easily reuse existing code that is known to be stable and working correctly, .

For details on exposing these C++ systems to Nasal, please refer to Nasal/CppBind. If in doubt, please get in touch via the mailing list or the forum first.

Objective

Add new features to the C++ code that implements the route manager subsystem.

Background

The route manager system in FlightGear is the backbone for all "autoflight" features in FlightGear. It is written in C++ and designed to be entirely controllable and configurable using the built-in property tree. This means that the route manager can be easily controlled using a number of different ways (such as the property browser, the GUI, XML bindings for cockpit hotspots, Nasal scripts, http browser access and telnet).

The relevant source code is to be found in the "Autopilot" directory of the source tree, i.e. $FG_SRC/Autopilot. The most important files are:

The constructor of the route manager is to be found in flightgear/flightgear/next/src/Autopilot/route_mgr.cxx#l208, this is also where all built-in "commands" are registered, as you can see:

FGRouteMgr::FGRouteMgr() :

  _currentIndex(0),

  input(fgGetNode( RM "input", true )),

  mirror(fgGetNode( RM "route", true ))

{

  listener = new InputListener(this);
  input->setStringValue("");
  input->addChangeListener(listener);

  SGCommandMgr::instance()->addCommand("load-flightplan", commandLoadFlightPlan);
  SGCommandMgr::instance()->addCommand("save-flightplan", commandSaveFlightPlan);
  SGCommandMgr::instance()->addCommand("activate-flightplan", commandActivateFlightPlan);
  SGCommandMgr::instance()->addCommand("clear-flightplan", commandClearFlightPlan);
  SGCommandMgr::instance()->addCommand("set-active-waypt", commandSetActiveWaypt);
  SGCommandMgr::instance()->addCommand("insert-waypt", commandInsertWaypt);
  SGCommandMgr::instance()->addCommand("delete-waypt", commandDeleteWaypt);
}

the addCommand() method merely maps an ASCII string (i.e. the commands plain text name) to a C++ callback function.

These "commands" are pretty much standard fgcommands, so they have the standard fgcommand signature. So, if you haven't already, it's a good idea to first read Howto: Add new fgcommands to FlightGear.

 static bool command(const SGPropertyNode*);

The code implementing each callback/command is to be found in flightgear/flightgear/next/src/Autopilot/route_mgr.cxx#l63, see for example:

static bool commandLoadFlightPlan(const SGPropertyNode* arg)
{
  FGRouteMgr* self = (FGRouteMgr*) globals->get_subsystem("route-manager");
  SGPath path(arg->getStringValue("path"));
  return self->loadRoute(path);
}


As can be seen here, all commands follow a fairly similar pattern:

  • first, they acquire a temporary pointer to the global route manager system
  • next, they start parameter validation and processing, i.e. turning the argument into an argument usable by the method that will be called.
  • finally, the pointer to the route manager system is used to directly invoke the required C++ method


In addition, the route manager system also implements the interface of the SGPropertyChangeListener SimGear class to create a property-tree based control interface on top of the property tree.

This means that C++ code can be easily invoked by setting a bunch of "control properties", to which the route manager subsystem holds registered listeners. This is very much similar to Qt's signals/slot mechanism.

Once some FlightGear system writes to such a listened-to property, the registered callbacks of the route manager system are automatically invoked by the listener to process, validate and handle the "command" properly.

This control interface is implemented beginning in flightgear/flightgear/next/src/Autopilot/route_mgr.cxx#l688:

// command interface /autopilot/route-manager/input:

//
//   @CLEAR             ... clear route
//   @POP               ... remove first entry
//   @DELETE3           ... delete 4th entry
//   @INSERT2:KSFO@900  ... insert "KSFO@900" as 3rd entry
//   KSFO@900           ... append "KSFO@900"
//

void FGRouteMgr::InputListener::valueChanged(SGPropertyNode *prop)

{

    const char *s = prop->getStringValue();

    if (strlen(s) == 0) {

      return;

    }

    

    if (!strcmp(s, "@CLEAR"))

        mgr->clearRoute();

    else if (!strcmp(s, "@ACTIVATE"))

        mgr->activate();

    else if (!strcmp(s, "@LOAD")) {

      SGPath path(mgr->_pathNode->getStringValue());

      mgr->loadRoute(path);

    } else if (!strcmp(s, "@SAVE")) {

      SGPath path(mgr->_pathNode->getStringValue());

      mgr->saveRoute(path);

    } else if (!strcmp(s, "@NEXT")) {

      mgr->jumpToIndex(mgr->_currentIndex + 1);

    } else if (!strcmp(s, "@PREVIOUS")) {

      mgr->jumpToIndex(mgr->_currentIndex - 1);

    } else if (!strncmp(s, "@JUMP", 5)) {

      mgr->jumpToIndex(atoi(s + 5));

    } else if (!strncmp(s, "@DELETE", 7))

        mgr->removeWayptAtIndex(atoi(s + 7));

    else if (!strncmp(s, "@INSERT", 7)) {

        char *r;

        int pos = strtol(s + 7, &r, 10);

        if (*r++ != ':')

            return;

        while (isspace(*r))

            r++;

        if (*r)

            mgr->insertWayptAtIndex(mgr->waypointFromString(r), pos);

    } else if (!strncmp(s, "@ROUTE", 6)) {

      char* r;

      int endIndex = strtol(s + 6, &r, 10);

      RouteType rt = (RouteType) mgr->_routingType->getIntValue();

      mgr->routeToIndex(endIndex, rt);

    } else if (!strcmp(s, "@AUTOROUTE")) {

      mgr->autoRoute();

    } else if (!strcmp(s, "@POSINIT")) {

      mgr->initAtPosition();

    } else

      mgr->insertWayptAtIndex(mgr->waypointFromString(s), -1);

}

void FGRouteMgr::initAtPosition()

{

  if (isRouteActive()) {

    return; // don't mess with the active route

  }

  

  if (haveUserWaypoints()) {

    // user has already defined, loaded or entered a route, again

    // don't interfere with it

    return; 

  }

  

  if (airborne->getBoolValue()) {

    SG_LOG(SG_AUTOPILOT, SG_INFO, "initAtPosition: airborne, clearing departure info");

    _departure = NULL;

    departure->setStringValue("runway", "");

    return;

  }

  

// on the ground

  SGGeod pos = SGGeod::fromDegFt(lon->getDoubleValue(), 

    lat->getDoubleValue(), alt->getDoubleValue());

  if (!_departure) {

    _departure = FGAirport::findClosest(pos, 20.0);

    if (!_departure) {

      SG_LOG(SG_AUTOPILOT, SG_INFO, "initAtPosition: couldn't find an airport within 20nm");

      departure->setStringValue("runway", "");

      return;

    }

  }

  

  std::string rwy = departure->getStringValue("runway");

  if (!rwy.empty()) {

    // runway already set, fine

    return;

  }

  

  FGRunway* r = _departure->findBestRunwayForPos(pos);

  if (!r) {

    return;

  }

  

  departure->setStringValue("runway", r->ident().c_str());

  SG_LOG(SG_AUTOPILOT, SG_INFO, "initAtPosition: starting at " 

    << _departure->ident() << " on runway " << r->ident());

}