This example (targets Xtu v 3.0 or higher) is a simple navigation application that demonstrates how to implement:
The application consists of three domains; the GPS domain, the Navigation domain and the HMI domain (Human-Machine Interface). The GPS domain is a simple GPS simulator that generates new position acquisitions at a rate of 1 Hz. The Navigation domain receives position updates and calculates navigational attributes such as speed and traveled distance. Information that is important to a human user (e.g. a car driver or a ship's captain) is managed and communicated by the HMI domain. The domains are connected via three bridges. Each bridge supports a defined set of signals, some of which carry a payload.
The use case is that the system starts up and the GPS domain begins to send new position acquisitions to the Navigation domain. After a short while, the GPS sensor shuts down in order to simulate a loss of satellite signals. The Navigation domain reacts to lack of recent navigation data and provides feedback to a human operator via the HMI-domain. Running the application will generate the following console (HMI) output:
Setting up
Starting up
GPS is on line.
Current position is 59.327037 degrees latitude and 18.075262 degrees longitude.
Current speed is 102.206436 m/s.
Traveled distance is 102.206726 m.
Current position is 59.327142 degrees latitude and 18.077052 degrees longitude.
Current speed is 102.206123 m/s.
Traveled distance is 204.413132 m.
Current position is 59.327247 degrees latitude and 18.078842 degrees longitude.
Current speed is 102.205757 m/s.
Traveled distance is 306.619232 m.
Current position is 59.327352 degrees latitude and 18.080632 degrees longitude.
Current speed is 102.205582 m/s.
Traveled distance is 408.825043 m.
Current position is 59.327456 degrees latitude and 18.082422 degrees longitude.
Current speed is 102.205208 m/s.
Traveled distance is 511.030518 m.
Current position is 59.327561 degrees latitude and 18.084212 degrees longitude.
Current speed is 102.204956 m/s.
Traveled distance is 613.235657 m.
Current position is 59.327666 degrees latitude and 18.086003 degrees longitude.
Current speed is 102.204529 m/s.
Traveled distance is 715.440552 m.
Current position is 59.327771 degrees latitude and 18.087793 degrees longitude.
Current speed is 102.204323 m/s.
Traveled distance is 817.645081 m.
Current position is 59.327875 degrees latitude and 18.089583 degrees longitude.
Current speed is 102.203903 m/s.
Traveled distance is 919.849304 m.
Current position is 59.327980 degrees latitude and 18.091373 degrees longitude.
Current speed is 102.203690 m/s.
Traveled distance is 1022.053223 m.
GPS is off line.
Navigator: -I have received no more position updates. I AM LOST!!!
Shutting down execution contexts.
Bye.
In xUML there are 4 different kinds of domains; Application, Service, Architecture and Implementation domains. To Xtu, a domain is a domain and there is no difference between the four. However, there is a difference between those domains that shall be realized and those that shall be not. The Application domain is a typical example of a domain that is usually not realized (i.e., it has no PSM counterpart).
A domain is realized by inheriting the class template DomainT<>:
#include "BuildUtils.h"
#include "PsmConfiguration.h"
#include "DomainT.h"
// Include the xtu namespace in order to reduce typing
using namespace xtu;
// ClassT<>-classes that need to be included in the domain from start
// have to be forward declared. Otherwise there will be a
// circular dependency between the domain and the ClassT<> template.
class GpsSensor;
class GpsDomain : public DomainT<GpsDomain>
{
public:
GpsDomain();
virtual ~GpsDomain();
// Static CRTP methods to be implemented
static bool implInitialize();
static bool implPrepareShutDown();
static void implShutDown();
// Members
static GpsSensor mGpsSensor;
};
The first template parameter of DomainT<> is the type of the derived domain, i.e. the type GpsDomain in the code above (see CRTP). DomainT<> also has a second, default template parameter not visible in the example above. That parameter defines the scheduler type of the execution context being in charge of the domain execution.
All domain realizations are required to be pure static classes. Since there can be only one instance of each particular domain type for a given, non-distributed application, it makes no real sense to instantiate domain objects. This eliminates a layer of indirection and object instance management. A domain can thus be accessed directly by using the domain's type name only. The domains NavigationDomain and HmiDomain are realized in a similar manner.
An effect of static domain classes is that access to domain members also have to go via static members, one way or another. The mGpsSensor member is a non-trivial xUML class (it has a life cycle model) simulating the actual GPS sensor.
All realized domains has to implement 3 CRTP-inherited methods; implInitialize(), implPrepareShutDown() and implShutDown(). These methods belong to the PSM and deals with startup/shut down of the domain. The implementations are located in the file GpsDomain.cpp:
bool GpsDomain::implInitialize()
{
static EventT<GpsSensor::EI, GpsSensor::EI::TurnOn> turnOnEvt;
// Start GPS sensor
mGpsSensor.post(turnOnEvt);
// Done
return (true);
}
bool GpsDomain::implPrepareShutDown()
{
static EventT<GpsSensor::EI, GpsSensor::EI::TurnOff> turnOffEvt;
// Shut down GPS sensor;
mGpsSensor.post(turnOffEvt);
// Done
return(true);
}
void GpsDomain::implShutDown()
{
}
GpsDomain::implInitialize() initializes the domain by posting the event to turn on the GPS sensor. Note that the event is only queued in the scheduler's working queue and won't be processed until the hosting execution context's start() method is issued. The initialization methods utilizes the fact that non-payloaded events are entirely stateless and therefore can be reused ad infinitum. By defining the event turnOnEvt static, the overhead of event allocation is eliminated. However, the event generation method EventT<>::generate(), and it's aliases, does the same thing under the hood. There is thus seldom any reason the explicitly define static event variables. When a domain has successfully bee initialized, implInitialize() shall return true.
GpsDomain::implPrepareShutDown() is called by the scheduler when it's time to shut down a domain in a controlled fashion. The actual shut-down is triggered by calling DomainBase::requestShutDown(). In the GPS domain the natural thing is to turn off the GPS sensor. If shut-down, for some reason, can't be completed the implPrepareShutDown() method must return false.
The GpsDomain::implShutDown() method is the last possibility to perform final clean-up at PSM-level. The domain is already shut down but in some instances it may be useful to postpone certain PSM tasks as long as possible. Not so for the GPS domain, which has an empty implShutDown().
Non-trivial classes are xUML classes having a life cycle that is more complex than the ordinary C++ Construction -> Destruction sequence. The life cycle of such classes are, as per the xUML formalism, modelled by an event-driven finite state machine (FSM). The only FSM type supported by Xtu is the Moore machine.
Using the GpsSensor class in the GPS domain as an example:
The two class attributes holds the last acquired GPS position as well as the time at which the position was acquired. The GpsSensor FSM has the following design:
In all, the FSM has 5 states, 2 of which are wait states (UPPER CASE NAMES by convention) and 4 events. Before the GpsSensor class can be implemented using Xtu, its state and event identifiers/names have to be specified using C++ enums:
// Holds definitions of PsmStateIdType & PsmEventIdType
#include "PsmConfiguration.h"
// Define the IDs for GpsSensor's states. The enum has to end with _END so that the
// state table can calculate how many states there are.
enum class GpsStateIDs: PsmStateIdType
{
OFF,
TurningOn,
TurningOff,
IDLE,
AcquiringPosition,
_END
};
// Define IDs for GpsSensor's events. The enum has to end with _END so that the
// state table can calculate how many events there are.
enum class GpsEventIDs: PsmEventIdType
{
TurnOn,
TurnOff,
Acquire,
Done,
_END
};
PsmStateIdType and PsmEventIdType are two configurable PSM types. The default setting is unsigned int but for memory restricted systems, a smaller unsigned integer type may be used, e.g. unsigned char. There are two requirements the enums must fulfill:
C++ doesn't support counting the number of items in an enum so there is no way of knowing how large the FSM transition table has to be to hold all state-event combinations. By placing an _END-item as the end of the enum, the table size can be calculated at compile time by the StateTableT<> class template. Once the state and event ID enums are defined, the actual GpsSensor class can be defined. This is done by inheriting the ClassT<> template. The template has the 4 parameters:
tDerivedClassType is the type of the inheriting class (see CRTP), tDomainType defines the type of the domain to which the class belongs, tStateIdType defines the class' state IDs (from the enum above) and tEventIdType defines the event IDs (from above).
#include "BuildUtils.h"
#include "PsmConfiguration.h"
#include "ClassT.h"
#include "GpsDomain.h"
#include "Gps2NavigationBridge.h"
#include "Gps2HmiBridge.h"
#include "Position.h"
// Include the xtu namespace in order to reduce typing
using namespace xtu;
enum class GpsStateIDs: PsmStateIdType
{
...
};
enum class GpsEventIDs: PsmEventIdType
{
...
};
// Define GpsSensor to be an xtu-class, i.e. an xUML class whose life cycle is defined by a finite state machine.
// This class is a simple GPS sensor simulator that send a number of positions and then shuts down.
class GpsSensor : public ClassT<GpsSensor, GpsDomain, GpsStateIDs, GpsEventIDs>
{
public:
GpsSensor();
virtual ~GpsSensor();
//
// Members / Attributes
//
Position mPosition;
// Members for running the simulated GPS sensor
unsigned int mMaxAcquisitions;
unsigned int mAcquisitionCount;
PsmDelayTimeType mAcqDelayMs;
Position mStartPosition;
Position mFinishPosition;
float mLatStepSize;
float mLonStepSize;
// FSM Setup
static const StateBase *implSetupStateTable();
...
}
A difference from the UML definition of the GpsSensor class is that the Position data type holds both the position and its time stamp. The reason is that Xtu signals preferably carry one pay load parameter. Thus, when sending the latest position acquisition to an outgoing GPS domain bridge, the Position data type can be used as is. Another way of implementing single-data-type pay loads is to wrap signal parameters in a std::tuple and then use the tuple data type as a signal's pay load.
The GpsSensor class also has to implement the CRTP inherited implSetupStateTable() method. This is used for initialization and setup of the static FSM state transition table. Before this can be done, the class' actual states must be defined.
A state is essentially a stateless container hosting the code to execute upon state entry. As such, it can be reused ad-infinitum, having no side effects. States are therefore implemented as singleton classes. This is done by inheriting the template StateT<>. The template has 3 parameters:
tClassType defines the type of the class whose life cycle the state is part of (in this example the GpsSensor class). tStateId declares the FSM name/identifier of the state. This is used for indexing in the FSM state-event transition table. In the case of the GpsSensor class, tStateId must be of the type enum class GpsStateIDs (see above). Finaly, tPayLoadType declares the type of the event payload, when such exists. When a state doesn't require a payload, tPayLoadType is ommited. States can be declared/defined in separate compilation units or as nested subclass to the Xtu class whose life cycle the state is part of. The GpsSensor class declares its states as nested subclasses in the GpsSensor.h file:
...
class GpsSensor : public ClassT<GpsSensor, GpsDomain, GpsStateIDs, GpsEventIDs>
{
...
//
// States
//
class OFF : public StateT<GpsSensor, StateIDs::OFF>
{
public:
XTU_INLINE void entry(Self &fSelf) const override final
{
// Do nothing but wait
}
};
class TurningOn : public StateT<GpsSensor, StateIDs::TurningOn>
{
public:
XTU_INLINE void entry(Self &fSelf) const override final
{
// Turn on GPS
// ...
// Signal to other domains that GPS is on line
Gps2HmiBridge::post(Gps2HmiBridge::GpsIsOnLine::generate());
// Done
fSelf.send(generate<EventIDs::Done>());
}
};
class TurningOff : public StateT<GpsSensor, StateIDs::TurningOff>
{
public:
XTU_INLINE void entry(Self &fSelf) const override final
{
// Turn off GPS
// ...
// Signal to other domains that GPS is off line
Gps2HmiBridge::post(Gps2HmiBridge::GpsIsOffLine::generate());
// Done
fSelf.send(generate<EventIDs::Done>());
}
};
class IDLE : public StateT<GpsSensor, StateIDs::IDLE>
{
public:
XTU_INLINE void entry(Self &fSelf) const override final
{
const static EventT<EventIDs, EventIDs::TurnOff> evtTurnOff;
// Check if to do another acquisition
if (fSelf.mAcquisitionCount < fSelf.mMaxAcquisitions)
{
// Acquire a new position after a delay of fSelf.mAcqDelayMs milliseconds.
// This specific delayed event is generated, i.e. acquired from the event pool, and will be automatically memory managed/reused.
fSelf.postDelayed(generate<EventIDs::Acquire>(), fSelf.mAcqDelayMs);
fSelf.mAcquisitionCount++;
}
// Nothing more to do so shut down GPS.
// Here the event is specifically pre-allocated. There is no risk for re-use conflicts since the event has no pay load.
// Non-payloaded events can be reused with no restrictions at all, as long as the event object is still a live object.
// By allocating the event as a static instance (as is done above), it will live as long as the main process lives.
else
{
fSelf.postDelayed(evtTurnOff, fSelf.mAcqDelayMs);
}
}
};
class AcquiringPosition : public StateT<GpsSensor, StateIDs::AcquiringPosition>
{
public:
XTU_INLINE void entry(Self &fSelf) const override final
{
const static EventT<EventIDs, EventIDs::Done> evtDone;
// Acquire a new GPS position. This is simulated by adding a constant step to the previous position
fSelf.mPosition.mTimeStamp = std::chrono::steady_clock::now();
fSelf.mPosition.mLatitude += fSelf.mLatStepSize;
fSelf.mPosition.mLongitude += fSelf.mLonStepSize;
// Send a signal to the outgoing bridge that a new position has been acquired
Gps2NavigationBridge::NewAcquisition &signal = Gps2NavigationBridge::NewAcquisition::generate(fSelf.mPosition);
Gps2NavigationBridge::post(signal);
// Done.
fSelf.send(evtDone);
}
};
};
State implementations can utilize several type aliases defined by Xtu. StateIDs is an alias for enum class GpsStateIDs. The fully quallified name is GpsSensor::StateIDs. Since the state declarations are nested subclasses to the GpsSensor class, it's enough to use StateIDs.
EventIDs is an alias for the event names/identifiers of the GpsSensor class (i.e. enum class GpsEventIDs) and works in the same way as StateIDs.
Finally, Self is a type alias that referes to the class to which a particular state belongs, in our case GpsSensor. fSelf is thus a reference to the particular instance of Self/GpsSensor on which behalf the entry() method is issued.
These aliases are available to all states with the intent to reduce typing and errors.
The CRTP-inherited FSM initialization method (implSetupStateTable()) is a static class method that is called only once, by the constructor of the very first instantiation of the particular non-trivial class type. The main responsibilities of implSetupStateTable() are:
Since state objects are themselves stateless and can be reused by all instances of the particular class, it's only necessary to instantiate one object of each state type.
The FSM behavior is configured using 4 state-event transition rules:
The corresponding methods are:
Since implSetupStateTable() is a static class method, it is preferably implemented in the class' cpp-file (as opposed to the hpp-file).
//
// FSM Setup
//
const GpsSensor::StateBase *GpsSensor::implSetupStateTable()
{
// Define textual names for states. This is usually only useful during debugging. In
// production code, textual state names will only waste memory.
static const ST::StateNameArrayType *stateNames = new ST::StateNameArrayType({"OFF", "TurningOn", "TurningOff", "IDLE", "Acquiring"});
static const ST::EventNameArrayType *eventNames = new ST::EventNameArrayType({"TurnOn", "TurnOff", "Acquire", "Done"});
StateTable::setStateNames(stateNames);
StateTable::setEventNames(eventNames);
// Instantiate static state instances
static const OFF off;
static const TurningOn turningOn;
static const TurningOff turningOff;
static const IDLE idle;
static const AcquiringPosition aquiringPosition;
// Initialize state table and set initial state
StateTable::initStateTable(off);
// Setup FSM rules
// ST is a short form of the type alias ClassT<>::StateTable (== GpsStateIDs)
// EI is a short form of the type alias ClassT<>::EventIDs (== GpsEventIDs)
ST::setTransitionRule(off, EI::TurnOn, turningOn);
ST::setIgnoreRule(off, EI::TurnOff);
ST::setIgnoreRule(off, EI::Acquire);
ST::setTransitionRule(turningOn, EI::Done, idle);
ST::setTransitionRule(turningOff, EI::Done, off);
ST::setTransitionRule(idle, EI::TurnOff, turningOff);
ST::setTransitionRule(idle, EI::Acquire, aquiringPosition);
ST::setTransitionRule(aquiringPosition, EI::Done, idle);
// Return initial state
return(StateTable::getInitState());
}
The three domains are deployed in two separate execution contexts. The GPS and Navigation domains are deployed in the same, threaded context and the HMI domain is deployed in the main process context.
In a non-distributed deployment, an Xtu application is always executed by a main process context and a number (>= 0) of of thereto associated thread contexts. The process execution context is the first to start, usually when the C/C++ main()-function is called. It is thus convenient to use the main process context to create, setup and make the deployment environment, including threaded contexts, ready for execution.
The selected deployment has a direct impact on the type of bridges used to connect the domains. From the domains' perspective, all bridges behave in the same manner but from a deployment perspective, there are significant differences. Since the GPS and Navigation domains execute in the same context, the Gps2Navigation bridge will be a realized by a simple, single threaded bridge. Such bridge is defined by inheriting the SingleBridgeT<> class template:
#include "SingleBridgeT.h"
#include "SignalT.h"
#include "EventT.h"
#include "GpsDomain.h"
#include "NavigationDomain.h"
#include "Navigator.h"
#include "Position.h"
// Include the xtu name space to reduce typing
// (SingleBridgeT<...> instead of xtu.SingleBridgeT<...>)
using namespace xtu;
class Gps2NavigationBridge : public SingleBridgeT<Gps2NavigationBridge, GpsDomain, NavigationDomain>
{
public:
...
};
The first template parameter is the type name of the bridge itself (see CRTP). The second parameter is the domain type issuing signals and/or operations (the Requiring domain) and the third template parameter is the domain type responding to sent signals or operations (the Providing domain). Thus, the bridge definition above constructs a bridge that can send signals and/or operations from the GPS domain to the Navigation domain. If there was a need to send signals in the other direction, it would require a separate bridge type where the GPS and Navigation domains switched places in the SingleBridgeT<> template.
Now that we have a bridge, the next step is to defined what signals/operations the bridge shall support. Signals (asynchronous) and operations (synchronous) are defined by inheriting the class templates SignalT<> and OperationT<> respectively. Using nested classes, a bridge's supported signals/operations can be included in the same C++ code module as the bridge. This provides a good overview of the entire bridge interface. However, if there is an absolute need to have static objects/variables in a bridge, signal or operation, it's best to move those to the cpp-file to reduce memory footprint and to prevent Static Initialization Order Fiasco. Bridges, signals and operations are by themselves state-less classes/instances and therefore not sensitive to order of instantiation.
...
class Gps2NavigationBridge : public SingleBridgeT<Gps2NavigationBridge, GpsDomain, NavigationDomain>
{
public:
// Signals
class NewAcquisition : public SignalT<NewAcquisition, Gps2NavigationBridge, Position>
{
public:
void process() override final
{
// Update navigator with a new position-lock event
// This way of generating an event is the most raw, unaided way requiring all types specified.
// A shorter way of doing the same is:
//
// ... = Navigator::generate<Navigator::EventIDs::NewPositionLock, Position>(mPayLoad);
//
// or, using another one of the several type aliases for the event id enumeration:
//
// ... = Navigator::generate<Navigator::EI::NewPositionLock, Position>(mPayLoad);
//
// Thus:
//
// Navigator::EventIDs == Navigator::EI == The event ID enumeration provided by the Navigator class definition
//
// An even shorter way of BOTH generating and posting the event is:
//
// NavigationDomain::mNavigator.post(Navigator::generate<Navigator::EI::NewPositionLock, Position>(mPayLoad));
//
const EventT<Navigator::EventIDs, Navigator::EventIDs::NewPositionLock, Position> &event = EventT<Navigator::EventIDs, Navigator::EventIDs::NewPositionLock, Position>::generate(this->mPayLoad);
NavigationDomain::mNavigator.post(event);
}
};
};
The first template parameter of SignalT<> is the signal itself (see CRTP). The second specifies the bridge that will transfer the signal and the third parameter is the signal's pay load type (if any). The NewAcquisition signal is now bound the Gps2NavigationBridge in a type safe manner. In other words, only the Gps2NavigationBridge can transfer the NewAcquisition signal and it will only be transfered from the GPS domain to the Navigation domain. Any attempt to send the NewAcquisition using another bridge type will generate a compile time error. The payload is accessible by this->mPayLoad in the signal's process() method.
A bridge provides semantic translation between domains. As such, it is an example (though more generic) of the Dependency inversion principle. Thus, the name of a signal, as well as its potential payload, provides meaning only to the requiring domain. When a signal is received and processed by the providing domain, the code in the signal's process() method is executed and the signal can be translated to something that is meaningfull to the providing domain - in this case the generation & posting of an event to an instance of the Navigator class.
The Navigation2Hmi bridge crosses the boundary between two execution contexts (as does the Gps2Hmi bridge). Sending and receiving signals/operations is thus a concurrent scenario and the bridge has to handle this in a predictable way (see Leslie Lamport's paper "Time, clocks, and the ordering of events in a distributed system"). The template ThreadedBridgeT<> provides all this under the hood using a single-producer-single-consumer, thread safe FIFO buffer that guarantees correct ordering and concurrent operation.
The Navigation2Hmi bridge is defined in a similar way to the Gps2Navigation bridge:
#include <stdio.h>
#include "ThreadedBridgeT.h"
#include "NavigationDomain.h"
#include "HmiDomain.h"
#include "EventT.h"
#include "MessageTemplate.h"
#include "Console.h"
#include "Position.h"
// Include the xtu namespace in order to reduce typing
using namespace xtu;
class Navigation2HmiBridge : public ThreadedBridgeT<Navigation2HmiBridge, NavigationDomain, HmiDomain, SpScQueueSize::_3_SLOTS>
{
public:
...
}
The first 3 template parameters of ThreadedBridgeT<> are the same as for SingleBridgeT<>, i.e. the bridge type itself, the requiring domain type and the providing domain type. In addition, ThreadedBridgeT<> has a fourth, optional parameter for defining the effective size of the FIFO buffer. The default buffer size is 3 slots, which is also redundantly specified in the example above. One slot can store either a signal or an operation.
Signals for Navigation2HmiBridge are defined in the same way as for Gps2NavigationBridge:
...
class Navigation2HmiBridge : public ThreadedBridgeT<Navigation2HmiBridge, NavigationDomain, HmiDomain, SpScQueueSize::_3_SLOTS>
{
public:
class CurrentPosition : public SignalT<CurrentPosition, Navigation2HmiBridge, Position>
{
public:
XTU_INLINE void process() override final
{
const std::string &msgTemplate = PD::cMessageTemplate.mTemplate[static_cast<const unsigned int>(MessageTemplate::ID::CURRENT_POSITION)];
char buffer[512];
// Generate the text message
sprintf(buffer, msgTemplate.c_str(), this->mPayLoad.mLatitude, this->mPayLoad.mLongitude);
// Post the text message to the console
PD::cConsole.post(Console::generate<Console::EI::Print, std::string>(std::string(buffer)));
}
};
class CurrentSpeed : public SignalT<CurrentSpeed, Navigation2HmiBridge, float>
{
public:
XTU_INLINE void process() override final
{
const std::string &msgTemplate = PD::cMessageTemplate.mTemplate[static_cast<const unsigned int>(MessageTemplate::ID::CURRENT_SPEED)];
char buffer[512];
// Generate the text message
sprintf(buffer, msgTemplate.c_str(), this->mPayLoad);
// Post the text message to the console
PD::cConsole.post(Console::generate<Console::EI::Print, std::string>(std::string(buffer)));
}
};
class TraveledDistance : public SignalT<TraveledDistance, Navigation2HmiBridge, float>
{
public:
XTU_INLINE void process() override final
{
const std::string &msgTemplate = PD::cMessageTemplate.mTemplate[static_cast<const unsigned int>(MessageTemplate::ID::TRAVELED_DISTANCE)];
char buffer[512];
// Generate the text message
sprintf(buffer, msgTemplate.c_str(), this->mPayLoad);
// Post the text message to the console
PD::cConsole.post(Console::generate<Console::EI::Print, std::string>(std::string(buffer)));
}
};
class IAmLost : public SignalT<IAmLost, Navigation2HmiBridge>
{
public:
XTU_INLINE void process() override final
{
const std::string &msg = PD::cMessageTemplate.mTemplate[static_cast<const unsigned int>(MessageTemplate::ID::I_AM_LOST)];
// Post the text message to the console
PD::cConsole.post(Console::generate<Console::EI::Print, std::string>(msg));
}
};
};
Some additional notes:
An execution context is the context in which one or several domains are commonly executed, sharing hardware resources such as memory, CPU, etc. The most fundamental execution context used by Xtu is the Process, which is the same as a Posix process. Consequently, the entry point of an Xtu application is usually the C/C++ main()-function.
The simplest way of defining a PSM-specific process context is to inherit the class DefaultProcessContext. In the Navigator example, this looks like:
#include "DefaultProcessContext.h"
// Include the xtu namespace in order to reduce typing
using namespace xtu;
class MainProcessContext : public DefaultProcessContext
{
public:
MainProcessContext() : DefaultProcessContext("MainProcessContext") { }
explicit MainProcessContext(const std::string &fName) : DefaultProcessContext(fName) { }
virtual ~MainProcessContext() { };
...
}
A Process context can also host several Thread contexts, that can run in parallel on multi-core or multi-tasking platforms. A thread context is the same as a Posix thread and can be defined in almost the same way as a process context:
#include "DefaultThreadContext.h"
// Include the xtu namespace in order to reduce typing
using namespace xtu;
class GpsNavExecContext : public DefaultThreadContext
{
public:
GpsNavExecContext() : DefaultThreadContext("GpsNavExecContext") { };
explicit GpsNavExecContext(const std::string &fName) : DefaultThreadContext(fName) { };
virtual ~GpsNavExecContext() { };
...
}
Any execution context is responsible for the creation and configuration all the parts and pieces necessary for that particular context to execute the PSM as intended. In other words, an execution context must provide the init-code necessary to bring the context up and running. That does, however, not mean that the setup code must be executed by the context itself. It's actually often better to have all PSM-related setup/init code to be executed by a single path of execution, i.e. the Process context's main()-function.
DefaultProcessContext and DefaultThreadContext both inherit the class template ExecContextT<>. The template defines several virtual methods, that the instantiating context has to fill with suitable code, for creation, configuration and shut down. These methods are:
namespace xtu
{
template <class tSchedulerType = DefaultScheduler>
class ExecContextT : public ExecContextBase
{
...
// To be implemented by inheriting class
virtual bool setupDomains() = 0;
virtual bool setupBridges() = 0;
virtual bool initialize() = 0;
virtual void start() = 0;
virtual void idle() = 0;
virtual bool prepareShutDown() = 0;
virtual void shutDownContext() = 0;
...
}
}
The idle()-method is an additional method which a scheduler can use to signal to the particular platform that there is no work to be done at the moment. The exact Start-up / Shut down sequence can be found at the Xtu web site. This brings us to the main()-function of the Navigator example's process context:
#include <iostream>
#include "MainProcessContext.h"
#include "GpsNavExecContext.h"
int main(void)
{
/*********************************************
* *
* This part is 100% platform specific code. *
* *
*********************************************/
// Create contexts
MainProcessContext mainCtx;
GpsNavExecContext gpsNavCtx;
// Setup domains
std::cout << "Setting up\n" << std::flush;
if (mainCtx.setupDomains() == true)
{
if (gpsNavCtx.setupDomains() == true)
{
// Setup bridges
if (mainCtx.setupBridges() == true)
{
if (gpsNavCtx.setupBridges() == true)
{
// Initialize
if (mainCtx.initialize() == true)
{
if (gpsNavCtx.initialize() == true)
{
// Start up. Start Threaded context first. mainCtx.start() won't
// return until it's ready to be shut down.
std::cout << "Starting up\n\n" << std::flush;
gpsNavCtx.start(); // Gps and Nav thread context is up and running in a separate thread
mainCtx.start(); // Hmi main process context is up and running
// Time to shut down. Wait for threaded context to die first
std::cout << "\nShutting down execution contexts.\n" << std::flush;
while (gpsNavCtx.isReadyToDie() == false)
{
// Idle for a while
mainCtx.idle();
}
// Join the gps/nav context to guarantee that is has been killed.
gpsNavCtx.join();
// Ok to die
std::cout << "\nBye.";
return(0);
}
}
}
}
}
}
// Something went wrong
return(-1);
}
The exact initialization sequence is obviously specific for each PSM. The Navigator example's main() function may be regarded as a template of a typical, not so complex initialization. The steps are:
CRTP - Curiously Recurring Template Pattern
FSM - Finite state machine
PIM - Platform-Independent Model
PSM - Platform-Specific Model
xUML - Executable UML