>xabsl   The Extensible Agent Behavior Specification Language

The xabsl::Engine Class Library

The xabsl::Engine is the XABSL runtime system. It is platform and application independent and written in plain ANSI C++. Due to that, XABSL can be easily employed on any robotic platform. To run the engine in a specific software environment only two classes (for file access and error handling) have to be derived from abstract classes.

The engine parses and executes the intermediate code that was generated from XABSL documents. It links the symbols of a XABSL specification to variables and functions of the agent platform. Therefore, for each used symbol an entity in the software environment has to be registered to the engine. While options and their states are represented in XABSL, basic behaviors are written in C++. They have to be derived from a common base class and registered to the engine.

The engine provides extensive debugging interfaces for monitoring the option and state activations, the values of the symbols and the parameters of options and basic behaviors. Instead of executing the engine from the root option, single options or basic behaviors can be tested separately.

Files of the xabsl::Engine

XabslAction.cpp [code]Implementation of class Action and helper classes
XabslAction.h [code]Definition of class Action and Helper classes
XabslAgent.cpp [code]Implementation of class Agent
XabslAgent.h [code]Definition of class Agent
XabslArray.h [code]Declaration and implementation of template class NamedArray
XabslBasicBehavior.h [code]Declaration class BasicBehavior
XabslBehavior.h [code]Definition of class Behavior
XabslBooleanExpression.cpp [code]Implementation of BooleanExpression and derivates
XabslBooleanExpression.h [code]Definition of BooleanExpression and derivates
XabslCoopState.h [code]Definition of class CoopState and Helper classes
XabslDecimalExpression.cpp [code]Implementation of DecimalExpression and derivates
XabslDecimalExpression.h [code]Definition of DecimalExpression and derivates
XabslEngine.cpp [code]Implementation of class Engine
XabslEngine.h [code]Declaration class Engine
XabslEnumeratedExpression.cpp [code]Implementation of EnumeratedExpression and derivates
XabslEnumeratedExpression.h [code]Definition of EnumeratedExpression and derivates
XabslOption.cpp [code]Implementation of class Option and helper classes
XabslOption.h [code]Definition of class Option and Helper classes
XabslParameters.cpp [code]Implementation of class Parameters
XabslParameters.h [code]Definition of class Parameters
XabslState.cpp [code]Implementation of class State and helper classes
XabslState.h [code]Definition of class State and Helper classes
XabslStatement.cpp [code]Implementation of class Statement and helper classes
XabslStatement.h [code]Definition of class Statement and Helper classes
XabslSymbols.cpp [code]Implementation of class Symbols and helper classes
XabslSymbols.h [code]Definition of class Symbols and helper classes
XabslTeamMessage.h [code]Definition of class TeamMessage
XabslTools.cpp [code]Implementation of several helper classes for the Engine
XabslTools.h [code]Definition of several helper classes for the Engine

See the Doxygen-generated source code documentation of the class Engine for more details.

Running the xabsl::Engine on a specific Target Platform

First, one has to declare a message and error handling class that is derived from ErrorHandler. This class has to implement the printMessage() and printError() function. E.g.:

class MyErrorHandler : public xabsl::ErrorHandler
{
public:
  MyErrorHandler();
	
  virtual void printError(const char* text) {cout << "error: " << text << endl;}
	
  virtual void printMessage(const char* text){cout << text << endl;}
};

The Boolean variable "errorsOccurred" can be used to determine if there occurred errors during the creation or execution of the engine.

Then, a class that gives the engine a read access to the intermediate code has to be derived from InputSource. These pure virtual functions have to be implemented:

  • open(): opens the file containing the intermediate code. Note that the code doesn't need to be read from a file. It is also possible to read it from a memory buffer or any other stream.
  • close(): is called by the engine after having read the data.
  • readValue(): reads a numeric value from the file.
  • readString(): reads a string from the file.

An example:

class MyFileInputSource : public xabsl::InputSource
{
public:
  MyFileInputSource(const char* fileName) : file(0), theChar(' ')
  { strcpy(filename,fileName); }

  ~MyFileInputSource() {if (file!=0) delete file;}

  virtual bool open() {file = new std::ifstream(filename); return(file!=0);}
  virtual void close() {if (file!=0) delete file; file = 0;}

  virtual double readValue()
  { char buf[20]; readFromFile(buf); return atof(buf); }

  virtual bool readString(char* destination, int maxLength)
  { readFromFile(destination); return true; }
	
private:
  char filename[200];
  std::ifstream* file;
  char theChar;

  void readFromFile(char* value)
  {
    while(!file->eof() && isWhitespace())
    {
      if (theChar == '/')
        while(!file->eof() && theChar != '\n') file->read(&theChar,1);
      file->read(&theChar,1);
    }

    while(!file->eof() && !isWhitespace())
    { *value++ = theChar; if(!file->eof()) file->read(&theChar,1); }
    *value = 0;
  }

  bool isWhitespace()
  { return theChar == ' ' || theChar == '/' || theChar == '\n'
           || theChar == '\r' || theChar == '\t'; }
};

Please note that the file contains comments (//...) that have to be skipped by the read functions:

// divide (7)
7
// multiply (6)
6
// decimal value (0): 52.5
0 52.5
// reference to decimal symbol (1) ball.y:
1 13

The comments have to be treated as in C++ files. (New line ends a comment.) In the example only "7 6 0 52.5 1 13" should be read from the file.

At last, a static function that returns the system time in milliseconds has to be defined, e.g.:

static unsigned long getSystemTime() {
  timeb sysTime;
  ftime(&sysTime);
  return (sysTime.time * 1000 + sysTime.millitm);
}

Creating a New Engine

First, an instance of the adapted ErrorHandler has to be created:

MyErrorHandler errorHandler;

Then, the engine can be created, passing a reference to the error handler and a pointer to the time function as parameters:

xabsl::Engine* pMyEngine = new xabsl::Engine(errorHandler,&getSystemTime);

Now all the symbols and basic behaviors have to be registered to the engine. Note that this has to be done before the option graph is created.

Registering Symbols

As the behaviors written in XABSL use symbols to interact with the agent's software environment, for each of these symbols the corresponding variable or function have to be registered to the engine. The following example registers the variables aDoubleInputVariable and aDoubleOutputVariable to the symbols "a-decimal-input-symbol" and "a-decimal-output-symbol" which were defined in the XABSL agent:

pMyEngine->registerDecimalInputSymbol("a-decimal-input-symbol",&aDoubleInputVariable);
pMyEngine->registerDecimalOutputSymbol("a-decimal-output-symbol",&aDoubleOutputVariable);

If the value for the symbol is not represented by a variable but by a function, this function has to be registered to the engine. Note that this function has do be static:

class MySymbols 
{
public:
  static double doubleReturningFunction();
  static void doubleAcceptingFunction(double value);
};

...

MySymbols mySymbols;

pMyEngine->registerDecimalInputSymbol("a-decimal-input-symbol",
	    &MySymbols::doubleReturningFunction);
pMyEngine->registerDecimalOutputSymbol("a-decimal-output-symbol",
	    &MySymbols::doubleAcceptingFunction);


The registration of Boolean symbols works in a similar way:

pMyEngine->registerBooleanInputSymbol("a-boolean-input-symbol",&aBooleanInputVariable);
pMyEngine->registerBooleanOutputSymbol("a-boolean-output-symbol",&aBooleanOutputVariable);

Or:

class MySymbols 
{
public:
  static bool booleanReturningFunction();
  static void booleanAcceptingFunction(bool value);
};

...

MySymbols mySymbols;

pMyEngine->registerBooleanInputSymbol("a-boolean-input-symbol",
	    &MySymbols::booleanReturningFunction);
pMyEngine->registerBooleanOutputSymbol("a-boolean-output-symbol",
	    &MySymbols::booleanAcceptingFunction,
	    &MySymbols::booleanReturningFunction);


Enumerated input or output symbols have to be registered similarily:

class MySymbols
{
public:
  enum MyEnum { element1, element2, element3 } anEnumInputVariable, anEnumOutputVariable;
  static MyEnum enumReturningFunction();
  static void enumAcceptingFunction(MyEnum value);
};

...

MySymbols mySymbols;

pMyEngine->registerEnumeratedInputSymbol("an-enumerated-input-symbol", "my-enum",
	    (int*)&mySymbols.anEnumInputVariable);
pMyEngine->registerEnumeratedOutputSymbol("an-enumerated-output-symbol", "my-enum",
	    (int*)&mySymbols.anEnumOutputVariable);

Or:

pMyEngine->registerEnumeratedInputSymbol("an-enumerated-input-symbol", "my-enum",
	    &MySymbols::enumReturningFunction);
pMyEngine->registerEnumeratedOutputSymbol("an-enumerated-symbol", "my-enum",
	    &MySymbols::enumAcceptingFunction,
        &MySymbols::enumReturningFunction);

For the enumerations used in enumerated symbols, each enum element that was defined in the XABSL agent has to be registered to its corresponding value:

pMyEngine->registerEnumElement("my-enum", "my-enum.element1", MySymbols::element1);
pMyEngine->registerEnumElement("my-enum", "my-enum.element2", MySymbols::element2);
pMyEngine->registerEnumElement("my-enum", "my-enum.element3", MySymbols::element3);


If input symbols have parameters, these have to be declared and registered additionally:

class MySymbols
{
public:
  static double parameter1, parameter2;
  static double myFunction() { return (parameter1 + parameter2) / 2; }
};

...

MySymbols mySymbols;

pMyEngine->registerDecimalInputSymbol("a-decimal-input-symbol",
	    &MySymbols::myFunction);
pMyEngine->registerDecimalInputSymbolDecimalParameter("a-decimal-input-symbol",
	    "a-decimal-input-symbol.parameter1", mySymbols.parameter1);
pMyEngine->registerDecimalInputSymbolDecimalParameter("a-decimal-input-symbol",
	    "a-decimal-input-symbol.parameter2", mySymbols.parameter2);

For different symbol and parameter types there are different functions for registering parameters. These functions are: registerDecimalInputSymbolDecimalParameter, registerDecimalInputSymbolBooleanParameter, registerDecimalInputSymbolEnumeratedParameter, registerBooleanInputSymbolDecimalParameter, registerBooleanInputSymbolBooleanParameter, registerBooleanInputSymbolEnumeratedParameter, registerEnumeratedInputSymbolDecimalParameter, registerEnumeratedInputSymbolBooleanParameter, and registerEnumeratedInputSymbolEnumeratedParameter.

Registering Basic Behaviors

All basic behaviors have to be derived from the class xabsl::BasicBehavior and have to implement the pure virtual function execute(). The name of the basic behavior has to be passed to the constructor of the base class. The decimal, boolean, or enumerated parameters of the basic behavior have to be declared as members of the class and registered using parameters->registerDecimal/registerBoolean/registerEnumerated inside the implementation of the function registerParameters():

class MyBasicBehavior : public xabsl::BasicBehavior
{
public:
  double parameter1;
  bool parameter2;
  MyEnum parameter3;

  MyBasicBehavior(XabslErrorHandler& errorHandler)
  : xabsl::BasicBehavior("a-basic-behavior",errorHandler)
  {}

  virtual void registerParameters()
  {
    parameters->registerDecimal("a-basic-behavior.parameter1", parameter1);
    parameters->registerBoolean("a-basic-behavior.parameter2", parameter2);
    parameters->registerEnumerated("a-basic-behavior.parameter3", "my-enum", parameter3);
  }
	
  virtual void execute()
  {
    // do the requested action using parameter1, parameter2, and parameter3
  }
};

Then, for each basic behavior class an instance has to be registered to the engine:

MyBasicBehavior myBasicBehavior(errorHandler);

pMyEngine->registerBasicBehavior(myBasicBehavior);

Creating the Option Graph

After the registration of all symbols and basic behaviors, the intermediate code can be parsed:

MyFileInputSource input("path_to_the_intermediate_code.dat");
	
pMyEngine->createOptionGraph(input);

If the engine detects an error during the execution of the option graph, the error handler is invoked. This can happen when the intermediate code contains a symbol or a basic behavior that was not registered before. Whether the option graph was created successfully or not can be checked like this:

if (errorHandler.errorsOccurred)
{
  // do some backup behavior
  delete pMyEngine;
}

Executing the Engine

If no errors occurred during the creation, the engine can be executed this way:

pMyEngine->execute();

This function executes the option graph only a single time. Starting from the selected root option, the state machine of each option is carried out to determine the next active state. Then for the subsequent option of this state the state machine becomes carried out and so on until the subsequent behavior is a basic behavior, which is executed then, too. After that the output symbols that were set during the execution of the option graph become applied to the software environment.

In the execute() function the execution starts from the selected root option, which is in the beginning the root option of the first agent. The agent can be switched using this function:

pMyEngine->setSelectedAgent("name-of-the-agent");

Debugging Interfaces

Instead of executing the option graph beginning with the root option of the currently selected agent, the function

pMyEngine->setRootAction("name-of-an-option-or-basic-behavior", isOption);

can be called to select a different root action. This is useful to test a single option or basic behavior. With

pMyEngine->getRootAction(0)->getParameters()->setDecimalParameter("name-of-the-decimal-parameter", 42);
pMyEngine->getRootAction(0)->getParameters()->setBooleanParameter("name-of-the-boolean-parameter", true);
pMyEngine->getRootAction(0)->getParameters()->setEnumeratedParameter("name-of-the-enumerated-parameter", element2);

the parameters of the executed option or basic behavior can be set.


There is a number of functions to trace the current state of the option graph, the option activation tree, the option parameters, and the selected basic behavior:

const xabsl::Action* getRootAction (int index) const;
const xabsl::Array <xabsl::Action*> getRootActions () const;
const char* getSelectedAgentName ();

The member functions of the xabsl::Action object returned can be used to retrieve this information.


For tracing the values of symbols, the engine provides access to the symbols stored:

xabsl::DecimalInputSymbol* decimalInputSymbol = pMyEngine->decimalInputSymbols["name-of-a-symbol"];
xabsl::BooleanInputSymbol* booleanInputSymbol = pMyEngine->booleanInputSymbols["name-of-a-symbol"];
xabsl::EnumeratedInputSymbol* enumeratedInputSymbol = pMyEngine->enumeratedInputSymbols["name-of-a-symbol"];
xabsl::DecimalOutputSymbol* decimalOutputSymbol = pMyEngine->decimalOutputSymbols["name-of-a-symbol"];
xabsl::BooleanOutputSymbol* booleanOutputSymbol = pMyEngine->booleanOutputSymbols["name-of-a-symbol"];
xabsl::EnumeratedOutputSymbol* enumeratedOutputSymbol = pMyEngine->enumeratedOutputSymbols["name-of-a-symbol"];

Note that these operators crash if the requested symbol does not exist. The existence of symbols can be checked using the exists method:

pMyEngine->decimalInputSymbols.exists("name-of-a-symbol");
pMyEngine->booleanInputSymbols.exists("name-of-a-symbol");
pMyEngine->enumeratedInputSymbols.exists("name-of-a-symbol");
pMyEngine->decimalOutputSymbols.exists("name-of-a-symbol");
pMyEngine->booleanOutputSymbols.exists("name-of-a-symbol");
pMyEngine->enumeratedOutputSymbols.exists("name-of-a-symbol");