OpenTracker

An Open Architecture for Reconfigurable Tracking based on XML | Contact

Programmers Guide

This guide gives pointers about extending OpenTracker. It describes the core interfaces that hold the library together and how to use them to implement new functionality in the form of additional nodes and modules.

Introduction

OpenTracker's functionality is provided by nodes that describe sources, transformations and sinks of tracking data. Nodes are in turn supported by modules that implement any special functions such as device drivers, computations and network code. Extending OpenTracker is achieved by implemented such nodes and modules. The blue words are names from source objects, such as classes, methods, members and functions. Details can be found in the source documentation, generated by doxygen.

Tracking Data

Tracking data is stored in objects of class State. It represents the a full state of a tracked object at a certain point in time. This is a simple data structure type object with some convenience methods. It is passed between nodes via the different interfaces defined by them. Currently it can store the following data :

position
an array of 3 floats encoding the position in x, y, z.
orientation
an array of 4 floats encoding the orientation as a quaternion in the format x ,y ,z ,w.
button
an unsigned int, storing button state in the indiviual bits. LSB is button 1, etc...
confidence
a float encoding a value between 0 and 1 to represent a confidence value in the state's data.
time
a double representing the timestamp of the state, in milliseconds from 1.1.1970.

Any part of the library ( such as a source node ) may only use or create a subset of this data. In this case the values should be set to reasonable default values (e.g. 0,0,0,1 for the orientation). The time value is mandatory and must be set on all created states correctly ! To simplify the handling of states some utility functions are available :

State::State()
copy constructor, copies the values of the passed state into the new State object.
=operator()
overloaded operator to set values of one state to anothers' state.
State::timestamp()
sets the time value to the current system time, useful for source nodes, creating new events.
State::null
a static null state, used to compare against return values of functions to provide a typesafe null object.

Nodes and Interfaces

Nodes are connected by edges in the data flow graph. There are different types of edges and accordingly nodes have to implement different interfaces to be connectable with other nodes via certain types of edges. The following three edge types and interfaces are possible :

Note that state objects returned by the various functions of these interfaces must not be changed ! Any node has to make a copy and change that to perform any computations etc.

The type restrictions are typically enforced in the DTD of the XML configuration files. Therefore wrong combinations at runtime are not possible. There are some exceptions, however. The Ref element can be used as a child of any parent element and therefore it is possible to link nodes supporting different interfaces with each other. It also allows to create loops in the event processing, which are not caught and result in infinite loops ! Therefore RefNodes should be used with care.

A node can be queried at runtime whether it supports on of the three interfaces. The three methods isEventGenerator(), isEventQueue() and isTimeDependend() return either 0 or 1 corresponding to the node's support. This can be used to check correct typing at runtime. Moreover new nodes derived from Node have to overload the member functions and return the correct values to show that they support a certain interface.

EventGenerator

Node::isEventGenerator()
Test method to check at runtime, whether a node supports the EventGenerator interface. This shoud be overriden by deriving nodes and implemented to return 1, if the node implements the interface.
Node::updateObservers()
When a node has generated a new event by creating new data and setting a State object to the data, it notifies its parent and any RefNodes referencing it using this method. The implementation checks whether the node is really an EventGenerator via Node::isEventGenerator(), so any nodes making use of this method, have to override the former too.

The following method is not part of the EventGenerator interface put closely related.

Node::onEventGenerated()
Whenever a child generates a new event, it notifies its parent and any RefNodes referencing it about a new event generated (hence the name). In this case this method is called on the parent node to notify it of the new event. It is therefore not really part of the EventGenerator interface, but used for observers of this interface.

EventQueue

Node::isEventQueue()
Test method to check at runtime, whether a node supports the EventQueue interface. This shoud be overriden by deriving nodes and implemented to return 1, if the node implements the interface.
Node::getSize()
returns the number of events currently stored in the queue.
Node::getEvent()
returns a certain event, by number. The default parameter is 0, which returns the last (i.e. newest) event. If the index is out of range the typesafe State::null reference is returned.
Node::getEventNearTime()
returns the event that is closest to a certain point in time, the point in time being the parameter of the method.

TimeDependend

Node::isTimeDependend()
Test method to check at runtime, whether a node supports the TimeDependend interface. This shoud be overriden by deriving nodes and implemented to return 1, if the node implements the interface.
Node::getStateAtTime()
returns a State object, representing the value of the node at a given point in time, being the parameter of this method. The states timestamp should have the same value as the requested time.

Children, Parents and Trees

The data flow graphs in OpenTracker are directed trees (or woods) with a single root node. Therefore each node besides the root node has one parent, and may have one or more children. A special type of node ( RefNode ) can be used to relax this constraint and add additional edges between nodes. Moreover children can be marked, to distinugish between different children. To access its parent and children the Node class (superclass of all nodes) implements the following API :

Node::countChildren()
returns the number of direct children, that is children not contained in a wrapper element to mark them.
Node::getChild()
takes an integer between 0 and the value returned by Node::countChildren()-1 and returns a pointer to the indicated child. The children appear in the order they are defined in the configuration file.
Node::countWrappedChildren()
takes a string containing the name of a wrapper element and returns the number of children containted in the specified wrapper element.
Node::getWrappedChild()
takes a string specifying the wrapper element and an integer indicating the wrapped child. It works like Node::getChild() but for a certain wrapper.
Node::isWrapperNode()
returns whether the node is a wrapper node or not. This is interesting for Node::onEventGenerated(), where the wrapper node will appear as the originating node and not a wrapped node. This way events from different inputs can be distringuished.
WrapperNode::getTagName()
returns the tag name (element name) of a wrapper node, indicating which input it corresponds to. Within Node::onEventGenerated(), typical code to distinguish between different inputs looks something like this :
if( generator.isWrapperNode() == 1 )  // this should always be the case
{
	WrapperNode & wrap = (WrapperNode &)generator;
	if( wrap.getTagName().compare("Input1") == 0 )
	{
		// ... do something specific for Input1
	}
	else if( wrap.getTagName().compare("Input2") == 0 )
	{
		// ... do something specific for Input2
	}
}

The Life of a Node

Modules

A module is the basic element of functionality in the OpenTracker framework. It implements anything that is needed by the application. All module classes are derived from Module. They override several of the virtual methods according to their needs.

The Life of a Module

A module is going throught several steps during a program run. First it is instantiated at some point, usually during initialisation. The global function initializeContext() is such a point, it instantiates all known modules and adds them to a Context object.

In the next step the modules Module::init() method is called. The parameters are data from the configuration section of the configuration file. They are a StringTable containing the attributes of the configuration element and any subtrees of children nodes, stored in ConfigNode objects.

After parsing of the configuration file is finished and the DAG structure of nodes has been built, the method Module::start() is called on all modules. This is a good place for any initialisations that depend on configuration parameters and instantiated nodes.

Now the main loop is entered and for all modules the methods Module::pushState(), Module::pullState() and Module::stop() are called. The first allows the generation of new state update events and introduction into the tree via EventGenerator nodes. The second is to retrieve states from the shared DAG structure via other interfaces such as the EventQueue or TimeDependend interface.

Module::stop() tests whether a module wants to exit the main loop and stop the program. If so the main loop is stopped and Module::close() is called on all modules. Then the program exits.

Implementing a Module

To implement a new module, decide about what your module will do in each of these phases and which methods you need to override. Then derive your module from the class Module and you are done. To use in the standard contexts, instantiate a module of your class in initializeContext() and add it to the context passed to the method. Then your module is available to the simple standalone program.

A multi-threaded Module

If your module has some tasks that require more time than the event loop should take, you are advised to implement your extension using multiple threads. A simple helper class is ThreadModule that takes care of starting an additional worker thread for your module and provides synchronisation methods. Override the ThreadModule::run() method and implement your tasks. Then use ThreadModule::lock() and ThreadModule::unlock() to synchronize access to your data between the work thread and the main loop calling pushState() or pullState().

The NodeFactory, Module and Context Triangle

The three classes typically are in a close relation. The NodeFactory is an interface that allows a parser to create nodes for elements by delegating the actuall parsing of element names and attributes to the NodeFactory that knows about the node.

Because the NodeFactory is the place a node is created, it is simple to keep track of new nodes by implementing the NodeFactory in the same class as the Module. This way a module can very easily find out about the nodes it is interested in.

A Context pulls all the different functionality together. It contains a ConfigurationParser object and stores the modules and nodefactories needed for the application. Furthermore it implements the main loop. The global helper function initializeContext() keeps the Context class general by keeping the actual module instantiations outside of the class.

Examples

A good starting point for your own modules and nodes are the following source files :

copyright (c) Graz University of Technology | Webmaster