1. Introduction¶
This User Manual describes how to build C++ applications for the ELT using the Rapid Application Development (RAD) toolkit.
RAD is an application framework that enables the development of event-driven distributed applications based on state machines.
The rest of the document describes:
How an application based on RAD looks like.
RAD libraries and tools.
How to configure the user development environment, retrieve, build, and install RAD.
How to create an application based on RAD from templates.
Examples of applications based on RAD.
COMODO tool for UML/SysML model transformations
2. RAD Based Applications¶
An application based on RAD toolkit reacts to internal or external events by invoking actions and/or starting activities as specified in a State Machine model.
2.1. Events¶
RAD based applications reacts to events. Events can be:
Requests
Replies
Topics
Timeouts
Unix signals (CTRL-C, etc.)
Internal events (events generated by the application itself)
Events are implemented by C++ classes containing an event identifier and a payload. To facilitate the application development, it is possible to define in a text file with extension .rad.ev the list of events (the identifier and the payload data structure). This file is then processed at compile time by the RAD tool codegen to generate the C++ classes (see for example events.rad.ev).
2.2. Event Loop¶
The event loop is responsible for continuously listening to requests, replies, topics, timeouts, UNIX signals, etc. and for invoking the associated callback. The callback creates an event which is inject it into the State Machine Engine. The State Machine engine depending on the current state and the injected event, selects which actions to invoke, which activities to start and to which state to move in.
In RAD the event loop is implemented using BOOST ASIO.
2.3. Actions¶
Actions represent short lasting tasks (ideally lasting 0 time) implemented using methods of a C++ class. They are similar to callback functions invoked when an event occurs and the application is in a given state.
2.4. Activities¶
Activities represent long lasting tasks. They are started when entering a given state and are stopped when exiting the state. They can be implemented by:
classes with a run() method which is executed on a separate thread.
classes implementing co-routines.
2.5. State Machine Model¶
When to invoke an action or to start/stop an activity is defined in the State Machine model. The model describes for each state and event which action to invoke and which activity to start/stop. The State Machine model is specified using a domain specific language: StateChartXML (SCXML). SCXML is a W3C recommendation that allows to specify a State Machine using XML (for an example see sm.xml). The SCXML State Machine model can be executed at run-time using an SCXML interpreter. RAD provides scxml4cpp library as SCXML interpreter. Note that events, actions, and activities C++ implementation have an identifier that should match the names in the SCXML model.
2.6. Application Development¶
In order to develop a RAD based application, the developer as to provide:
A text file with extension .rad.ev containing the list of internal and external events processed by the application.
A text file in SCXML format containing the State Machine model.
C++ implementation of the actions and activities classes.
C++ implementation of the application configuration and runtime data classes.
RAD provides a fast way to create an application using Cookiecutter templates. By running the template(s) a fully working application with a basic State Machine model, events, and actions are generated.
See the tutorial Tutorial 1: Creating an Application with RAD + CII for detailed information on how to develop an application using RAD.
3. RAD Libraries and Tools¶
RAD libraries provide transparent access and integration with the Software Platform services. They also group functionalities common to all applications and not provided by the Software Platform.
3.1. Application Stack¶
ELT applications based on RAD are built on top of the following application stack:
Level |
Application Stack |
Description |
---|---|---|
4 |
Application |
Your application(s) |
3 |
Application Framework |
RAD Libraries and Tools |
2 |
Software Platform |
Core Integration Infrastructure |
1 |
Development Env. |
Linux CentOS, GNU C++, waf, etc. |
0 |
Hardware or VM |
Servers |
The ground level of the application stack are the ESO standard servers and Virtual Machines (VM). They are installed with the ELT Development Environment.
The ELT Development Environment, level 1, is based on Linux Cent OS and includes the GNU C++ compiler, waf building tool, and many other libraries such the Google Unit Tests, Robot framework for the integration tests, etc. (see: Guide to Developing Software for the EELT).
The Software Platform, level 2, is a set of libraries, running on top of the Development Environment, that provides common services such as: Error Handling, Logging, Messaging, Configuration, In-memory DB (Online-DB), Alarms, etc. The official ELT Software Platform is the Core Integration Infrastructure (CII). Since there was the need to start developing applications before the introduction of CII and since not all CII services have been released yet, a Prototype SW platform (made of ZeroMQ, Google Protocol Buffers, C++ exceptions, EasyLogging, Redis in-memory DB, YAML configuration files) can also be used.
The currently official services to be used are listed in the following table.
Service |
Description |
---|---|
Error Handling |
C++ Exceptions |
Logging |
CII logging API based on log4cplus |
Messaging |
CII/MAL ZPB Req/Rep and Pub/Sub |
Configuration |
Based on files using YAML |
Online-DB |
Redis in-memory key/value DB |
The application framework, level 3, can be used to develop State Machine based applications that use the services described in the table above.
Warning
RAD will use more CII services as soon as they become available, therefore applications developed with the current version of RAD may have to be ported.
3.2. Libraries¶
RAD is made of the following libraries:
utils
core
events
mal
services
sm
All RAD classes and functions are declared within the rad namespace. Classes and functions using CII specific features have an additional namespace: rad::cii. For example: rad::Helper, rad::cii::Publisher.
For detailed information on the libraries classes and methods see the online RAD Doxygen documentation.
3.2.1. utils¶
Library providing common utility classes and functions. It does not depend on other RAD libraries.
Class |
Description |
---|---|
Helper |
Helper class providing static methods such as: GetHostname(), FindFile(), FileExists(), GetEnvVar(), CreateIdentity(), SplitAddrPort(), GetVersion(). |
The following free functions are going to be replaced by the Time Library once available (see ESO-331947).
Function |
Description |
---|---|
GetTime |
Get time of the day as double. |
ConvertToIsoTime |
Covert time of the day to ISO time string. |
GetTimestamp |
Get current time in ISO format. |
3.2.2. core¶
Library providing error handling and logging services. It depends on utils library.
Class |
Description |
---|---|
ErrorCategory |
Class representing RAD errors. |
Exception |
RAD exception. |
Function |
Description |
---|---|
Assert |
Assert a condition. If the condition is false, it logs a fatal error. |
LogInitialize |
Initializes log services. |
LogConfigure |
Load and configure log properties. |
logGetLogger |
Returns the default RAD logger (name = “rad”). |
logGetSmLogger |
Returns the RAD State Machine logger (name = “rad.sm”). |
3.2.3. events¶
Library providing events related services. It does not depend on other RAD libraries.
Class |
Description |
---|---|
Event |
Class representing a specific event with template type for the payload. |
AnyEvent |
Class used to represent any event. |
Function |
Description |
---|---|
getPayload |
Return a reference to the event payload. |
3.2.4. mal¶
Library providing CII messaging services. It depends on core library and requires CII.
Class |
Description |
---|---|
Publisher |
Class that can be used to publish a topic using CII/ZPB. |
Subscriber |
Class that can be used to subscribe to topic using CII/ZPB. |
Replier |
Class that can be used to receive commands and send replies using CII/ZPB. |
Requestor |
Class that can be used to send commands and receive replies using CII/ZPB. |
Request |
Class representing a command and the associated [error] reply. |
3.2.5. services¶
Library providing DB, and other services. It depends on core library.
Class |
Description |
---|---|
DbAdapter |
Interface to read/write to a key-value in-memory DB. |
DbAdapterRedis |
Realization of DbAdapter interface for Redis DB. |
Note
This library contains other classes that allows to use the messaging services from the Prototype Software Platform. These classes are now replaced by the mal library.
3.2.6. sm¶
Library providing State Machine services. It depends on services, events, and scxml4cpp libraries.
Class |
Description |
---|---|
ActionCallback |
Class mapping a void class method to an scxml4cpp::Action object. |
GuardCallback |
Class mapping a boolean class method to an scxml4cpp::Action object. |
ActionGroup |
Base class for classes grouping action methods. |
ThreadActivity |
Base class for do-activities implemented as standard C++ threads. |
PthreadActivity |
Base class for do-activities implemented as Posix threads. |
CoroActivity |
Base class for do-activities implemented as Co-routines. |
ActionMgr |
Base class for instantiating actions and do-activities. |
Signal |
Class for deealing with UNIX signals events. |
Timer |
Class for dealing with time-out events. |
SMEvent |
Class to wrap RAD events into SCXML events. |
SMAdapter |
Facade to the SCXML State Machine engine. |
Note
This library contains other classes that allows to use the Prototype Software Platform instead of CII.
3.2.7. scxml4cpp¶
scxml4cpp is an ESO library able to parse and execute an SCXML model. It is made two components: the parser and the engine. The parser is based on xerces-c++ and it is used to parse the XML file containing the SCXML State Machine model. The engine is used to interpret at run-time the SCXML State Machine model following the W3C algorithm.
3.3. Tools¶
The following tools are part of RAD toolkit:
cookiecutters to create C++ skeleton application.
codegen to create events C++ classes.
COMODO to translate State Machine models from SysML/UML to SCXML.
3.3.1. Cookiecutters¶
Cookiecutters is an open-source tool (see Cookiecutter) that is used to generate a RAD based applications from templates. The templates are stored in rad/rad/cpp/templates/config directory.
3.3.2. codegen¶
codegen is an ESO tool that takes as input a YAML text file and generates C++ classes with events implementation. It is invoked by waf at compile time. Generated files are in the build/ directory.
4. RAD Installation¶
4.1. Environment Configuration¶
To configure environment variables LMOD tool (https://europeansouthernobservatory.sharepoint.com/sites/EELT-ICS-SWFW/Wiki/EELT%20LMOD.aspx) is used. It replaces the VLT PECS tool.
LMOD is based on LUA language. The configuration of the env. variables should be stored in the “/modulefiles/private.lua file. For example:
local home = os.getenv("HOME")
local introot = pathJoin(home, "EELT/EELT-INTROOT")
setenv("INTROOT", introot)
setenv("PREFIX", introot)
load("introot")
local cfgpath = pathJoin(home, "EELT/EELT-INTROOT/resource/config")
setenv("CFGPATH", cfgpath)
Note
PREFIX is needed by waf to know where to install binaries and libraries.
INTROOT is usually the same as PREFIX.
CFGPATH can be used to define the paths where applications configuration files are located. It has therefore to include the INTROOT/PREFIX directory.
4.2. Retrieving RAD from GIT¶
RAD is archived in GIT repository: https://gitlab.eso.org/ifw/rad
It can be retrieve with the following command:
>git clone https://gitlab.eso.org/ifw/rad
Note
Username and password have to be provided.
4.3. Building and Installing RAD¶
RAD can be compiled with the following commands:
>waf configure
>waf build
RAD can be installed into the $PREFIX directory by:
>waf install
4.4. Directory Structure¶
RAD project is organized in the following directories:
Directory |
Description |
---|---|
docs |
RAD User Manual. |
rad |
RAD Libraries. |
scxml4cpp |
SCXML State Machine engine for C++. |
scxml4py |
SCXML State Machine engine for Python. |
test |
RAD Integration Tests. |
RAD Libraries are organized in the following directories:
Directory |
Description |
---|---|
rad/codegen |
Code generator to create the event classes. |
rad/cpp/utils |
Library providing common utility functions (e.g. FindFile) |
rad/cpp/core |
Library providing error handling and logging services. |
rad/cpp/events |
Library providing events related services. |
rad/cpp/mal |
Library providing CII messaging services. |
rad/cpp/services |
Library providing ZMQ messaging, DB, and other services. |
rad/cpp/sm |
Library providing State Machine service. |
rad/cpp/templates |
Templates to create RAD projects and applications. |
rad/cpp/_examples |
Examples of applications based on RAD. |
5. RAD Integration Tests¶
RAD has two sets of integration tests located in rad/test directory:
The first set is in rad/test/rad. They use the application described in Examples to test RAD libraries.
The second set is in rad/test/template and are used to test the templates. These integration tests use the Cookiecutter templates illustrated in the tutorials to generate RAD applications and associated interfaces. Then it compiles the generated modules and executes the generated tests to verify RAD libraries.
These tests are executed daily by the ELT Continuous Integration infrastructure using the latest RAD sources. The results can be accesed from Jenkins.
To execute manually the tests:
> cd rad/test/rad/src
> robot *.robot
> cd rad/test/templates/src
> robot *.robot
6. Tutorial 1: Creating an Application with RAD + CII¶
In order to develop an application based on RAD, the following steps are performed:
Generate WAF Project
Generate Interface Module
Generate msgSend Module
Generate Application Module
Generate Integration Test Module
Build and Install Generated Modules
Run Integration Tests
Customize Application, Test, and Interface modules
Note
Steps 1, 2, 3 can be skipped if you are adding your application to an existing project.
6.1. Generate CII WAF Project¶
In order to be able to build, an ELT application needs to be part of a WAF project. This means that there must be a directory (e.g. “hello”) that contains a “wscript” file declaring the root of a WAF project. See WAF User Manual for information on WAF projects.
An “hello” WAF project can be created by executing the following commands and entering the requested information:
> cookiecutter rad/rad/cpp/templates/config/rad-waftpl-malprj
project_name [hello]: hello
modules_name [hellomalif hellomalifsend hellomal]:
The input values to the template are:
project_name the name of the WAF project which is used to create the directory containing the project SW modules.
modules_name the name of the SW modules part of this project.
Note
By pressing enter, the default values (in square brackets) are selected.
From the template Cookiecutter generates the directory hello and inside the file wscript. This file contains the WAF project declaration, the features and libraries required to compile this project, and the name of SW modules to compile.
6.2. Generate CII Interface Module¶
All commands, replies, and topics used to communicate between ELT applications, must be specified in dedicated interface modules. For the CII Software Platform, interfaces are specified using XML.
A CII interface module containing the “standard” commands can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-malapplif
module_name [hellomalif]: hellomalif
parent_package_name [hello]: hello
The input values to the template are:
module_name the name of the SW module to be generated (which contains the interface specification).
parent_package_name the name of the directory that contains the module. In this case it is the project directory.
From the template Cookiecutter generates the directory hellomalif containing the following files:
File |
Description |
---|---|
hellomalif/wscript |
WAF file to compile the SW module. |
hellomalif/src/hellomalif.xml |
CII MAL XML file with the interface definition. |
The file hellomalif.xml looks like:
<?xml version="1.0" encoding="UTF-8"?>
<types xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schemas/icd_type_definition.xsd">
<package name="hellomalif">
<exception name="ExceptionErr">
<member name="desc" type="string"/>
</exception>
<struct name="LogInfo">
<member name="level" type="string"/>
<member name="logger" type="string"/>
</struct>
<interface name="StdCmds">
<method name="Init" returnType="string"/>
<method name="Reset" returnType="string"/>
<method name="Enable" returnType="string"/>
<method name="Disable" returnType="string"/>
<method name="Status" returnType="string"/>
<method name="Config" returnType="string"/>
<method name="Stop" returnType="string"/>
<method name="Exit" returnType="string"/>
<method name="SetLogLevel" returnType="string" throws="ExceptionErr">
<argument name="info" type="nonBasic" nonBasicTypeName="LogInfo"/>
</method>
</interface>
</package>
</types>
It specifies:
the exception ExceptionErr, that can be used for error replies.
the data structure LogInfo, used as parameter in the SetLogLevel command.
nine commands that return a string as normal reply.
the SetLogLevel can raise an exception of type ExceptionErr in case of errors. The SetLogLevel command is the only one taking a parameter of type LogInfo.
For more information on the CII/MAL XML interface definition language, refer to MAL ICD User Manual.
The hellomalif.xml is transformed into C++ code at compile time by the CII/MAL code generator and by Google ProtoBuf compiler. Generated code is located in hello/build directory.
6.3. Generate CII msgSend Module¶
msgSend is a command-line tool that can be used to send the commands defined in the Interface Module(s) to ELT applications and to collect the replies.
The msgSend tool used in this tutorial is not meant to be used for production. It is just a very basic implementation needed to run the examples.
Note
Since interfaces are specified at compile time in the Interface Modules, msgSend must also depend, at compile time, on the Interface Modules. In the VLT, msgSend could be used with the ‘-n’ to send commands not defined in the CDT. This is not possible in the ELT.
A SW module implementing the msgSend tool to send the “standard” commands to CII applications can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-malsend
interface_name [hellomalif]: hellomalif
interface_module [hellomalif]: hellomalif
module_name [hellomalifsend]: hellomalifsend
application_name [hellomalifSend]: hellomalifSend
parent_package_name [hello]: hello
The input values to the template are:
interface_name the name of the CII Interface module (which specifies the commands sent by the msgSend tool and is defined in Generate CII Interface Module).
interface_module fully qualified name of CII Interface library.
module_name the name of the SW module to be generated (which contains the msgSend tool).
application_name the name of the binary to be produced when compiling the generated module.
parent_package_name the name of the directory that contains the tool. In this case it is the project directory.
From the template Cookiecutter generates the directory hellomalifsend containing the following files:
File |
Description |
---|---|
hellomalifsend/wscript |
WAF file to compile the SW module. |
hellomalifsend/src/main.cpp |
Implementation of msgSend. |
The tool can be invoked by:
hellomalifSend [-v] <serviceURI> <request> <parameters>
-v verbose with debug info.
<serviceURI> destination of the command (e.g. zpb.rr://127.0.0.1:12081/StdCmds)
<request> command to be sent (e.g. hellomalif.Status)
<parameters> parameters of the command
6.4. Generate CII Application Module¶
RAD provides the templates to create a simple server application implementing the standard ELT State Machine model using the CII Software Platform services.
An application that uses the services of the CII Software Platform can be created by executing the following commands and entering the required information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-malappl
module_name [hellomal]: hellomal
application_name [hellomal]: hellomal
parent_package_name [hello]: hello
interface_name [hellomalif]: hellomalif
interface_module [hellomalif]: hellomalif
The input values to the template are:
module_name the name of the SW module to generate.
application_name the name of the binary to be produced when compiling the generated module.
parent_package_name the name of the directory that contains the SW module. In this case it is the project directory.
interface_name the name of SW module containing the interface specification.
interface_module the fully qualified name name of the interface library.
From the template Cookiecutter generates the directory hellomal containing the following files:
File |
Description |
---|---|
wscript |
WAF file to build the application. |
resource/config/config.yaml |
YAML application configuration file. |
resource/config/sm.xml |
SCXML file with the State Machine model. |
resource/config/log.properties |
Logging configuration file. |
src/events.rad.ev |
List of events processed by the application. |
src/actionMgr.[hpp|cpp] |
Class responsible for instantiating actions and activities/ |
src/actionsStd.[hpp|cpp] |
Class implementing standard action methods. |
src/config.[hpp|cpp] |
Class loading YAML configuration file. |
src/dataContext.[hpp|cpp] |
Class used to store application run-time data shared between action classes. |
src/dbInterface.[hpp|cpp] |
Class interfacing to the in-memory DB. |
src/logger.[hpp|cpp] |
Default logger definition. |
src/stdCmdsImpl.hpp |
Class implementing CII/MAL asynchronous server interface. |
src/main.cpp |
Application entry function. |
6.4.1. wscript¶
This file is used by WAF to build the application binary.
from wtools.module import declare_cprogram
declare_cprogram(target='hellomal',
features='radgen',
use='log4cplus yaml-cpp hiredis protobuf xerces-c
rad.cpp.core rad.cpp.services rad.cpp.events
rad.cpp.sm rad.cpp.utils rad.cpp.mal hellomal-cxx')
It specifies the target binary name hellomal, which tools to use for building (e.g. radgen to transform events.rad.ev into C++ classes), and which libraries to link:
log4cplus for logging.
yaml-cpp to load YAML configuration files.
hiredis to access Redis DB.
protobuf to serialize/deserialize Google Protocol Buffers messages.
xerces-c required to parse SCXML State Machine Model.
rad.cpp.core, … RAD libraries.
hellomal-cxx CII/MAL generated library.
6.4.2. config.yaml¶
This file contains the application configuration in YAML format.
cfg.req.endpoint : "zpb.rr://127.0.0.1:12081/"
cfg.db.timeout_sec : 2
cfg.sm.scxml : "hellomal/sm.xml"
cfg.log.properties : "hellomal/log.properties"
The fields are mapped to the configuration attributes defined in the Config class (see config.hpp|cpp) which is responsible for loading the config.yaml file.
cfg.req.endpoint CII/MAL endpoint to be used to receive commands.
cfg.db.timeout_sec Timeout when accessing Redis DB.
cfg.sm.scxml SCXML State Machine model file path (see sm.xml).
cfg.log.properties Logging configuration file path (see log.properties).
6.4.3. log.properties¶
Logging APIs are provided by log4cplus library. Logging service can be configured via the following configuration file.
log4cplus.rootLogger=INFO, console
log4cplus.logger.rootLogger.rad=INFO, console
log4cplus.logger.rootLogger.rad.sm=INFO, console
log4cplus.logger.rootLogger.hellomal=INFO, console
log4cplus.appender.console=log4cplus::ConsoleAppender
log4cplus.appender.console.layout=log4cplus::PatternLayout
log4cplus.appender.console.layout.ConversionPattern={{'[%D{%H:%M:%S:%q}][%-5p][%c] %m%n'}}
In the file it is possible to specify the log level for each logger (rootLogger, rad, hellomal), where the logs should be published (appenders, e.g. console) and the format for each appender.
Default application logger is specified in logger.hpp|cpp files.
6.4.4. sm.xml¶
This file contains the SCXML representation of the standard ELT State Machine model.
<?xml version="1.0" encoding="us-ascii"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" xmlns:customActionDomain="http://my.custom-actions.domain/CUSTOM"
version="1.0" initial="State">
<state id="On">
<initial>
<transition target="NotOperational"/>
</initial>
<state id="NotOperational">
<initial>
<transition target="NotReady"/>
</initial>
<state id="NotReady">
<transition event="Events.Init" target="Ready">
<customActionDomain:ActionsStd.Init name="ActionsStd.Init"/>
</transition>
</state>
<state id="Ready">
<transition event="Events.Enable" target="Operational">
<customActionDomain:ActionsStd.Enable name="ActionsStd.Enable"/>
</transition>
</state>
</state>
<state id="Operational">
<initial>
<transition target="Idle"/>
</initial>
<state id="Idle">
<transition event="Events.Stop" target="Ready">
<customActionDomain:ActionsStd.Stop name="ActionsStd.Stop"/>
</transition>
</state>
<transition event="Events.Disable" target="Ready">
<customActionDomain:ActionsStd.Disable name="ActionsStd.Disable"/>
</transition>
</state>
<transition event="Events.Init">
<customActionDomain:ActionsStd.Init name="ActionsStd.Init"/>
</transition>
...
<transition event="Events.Exit" target="Off">
<customActionDomain:ActionsStd.Exit name="ActionsStd.Exit"/>
</transition>
<transition event="Events.CtrlC" target="Off">
<customActionDomain:ActionsStd.ExitNoReply name="ActionsStd.ExitNoReply"/>
</transition>
</state>
<final id="Off">
</final>
</scxml>
The State Machine consists of the following states:
A composite outer state On indicating that the application has started.
A composite state On/NotOperational indicating that the application and controlled devices cannot be used yet for operation.
A leaf state On/NotOperational/NotReady indicating that the application and devices have not been fully initialized yet.
A leaf state On/NotOperational/Ready indicating that the application has been initialized (but the devices may not be ready).
A composite state On/Operational indicating that the application and controlled devices provide all operational functionalities.
A leaf state On/Operational/Idle indicating that the application and the controlled devices are available to be used for operation.
A final pseudo-state Off to indicate that the application has terminated.
The State Machine presents the following transitions:
It is possible to move from On/NotOperational/NotReady to On/NotOperational/Ready via the Events.Init event.
It is possible to move from On/NotOperational/Ready to On/Operational/Idle via the Events.Enable event.
It is possible to move from On/Operational/Idle to On/NotOperational/Ready via the Events.Disable event.
It is possible to move from any state back to On/NotOperational/NotReady via the Events.Reset event.
It is possible to terminate the application from any state via the Events.Exit or the Events.CtrlC events.
Note
The SCXML State Machine model is Software Platform independent. The same model is used for the CII Software Platform and for the Prototype Software Platform. The SCXML engine is also Software Platform independent: scxml4cpp is also used in WSF2 for the VLT Software Platform.
6.4.5. events.rad.ev¶
This file contains the definition of the events triggering the transitions in the State Machine model.
# Event definitions for hellomal application
version: "1.0"
namespace: Events
includes:
- boost/exception_ptr.hpp
- rad/mal/request.hpp
- Hellomalif.hpp
events:
Exit:
doc: Event for the Exit request message.
payload: rad::cii::Request<std::string>
Status:
payload: rad::cii::Request<std::string>
Stop:
payload: rad::cii::Request<std::string>
Reset:
payload: rad::cii::Request<std::string>
Init:
payload: rad::cii::Request<std::string>
Enable:
payload: rad::cii::Request<std::string>
Disable:
payload: rad::cii::Request<std::string>
Config:
payload: rad::cii::Request<std::string>
SetLogLevel:
payload: rad::cii::Request<std::string, std::shared_ptr<Hellomalif::LogInfo>>
CtrlC:
doc: Event representing the CTRL-C (SIGINT) linux signal.
For each event it is possible to specify an optional event description and event payload type.
For CII commands, the event payload is associated to the command and reply payloads. RAD provides a wrapper class, rad::cii::Request, that allows to access the command payload and to set the replay payload. The wrapper class takes therefore two templates parameters:
the data type of the reply parameter
the data type of the command parameter (optional)
For example the SetLogLevel event is associated to the CII/MAL SetLogLevel command and reply. The reply payload data type is std::string while the command takes as parameter a shared pointer of the LogInfo data structure defined in CII Interface module.
Other events like Exit, …, Config are associated to commands without parameters and with std::string as reply data type.
Finally CtrlC event has no payload and it is not associated to any command.
From this file, if the feature “radgen” is specified in the wscript, the events.rad.ev.hpp files are generated in the build/ directory and compiled as part of the WAF build process.
The events.rad.hpp file, generated from events.rad.ev, looks like:
#ifndef EVENTS_EVENTS_RAD_HPP
#define EVENTS_EVENTS_RAD_HPP
#include <rad/AnyEvent.hpp>
#include <boost/exception_ptr.hpp>
#include <rad/mal/request.hpp>
#include <Hellomalif.hpp>
namespace Events {
class Init final : public rad::AnyEvent {
public:
static constexpr char const* id = "Events.Init";
static constexpr rad::EventInfo::Context ctx = rad::EventInfo::Context::any;
using payload_t = rad::cii::Request<std::string>;
...
private:
rad::cii::Request<std::string> m_payload;
};
...
6.4.6. actionsStd.hpp|cpp¶
The ActionsStd class implements the actions defined in the State Machine model (see sm.xml).
For example the action ActionStd.Init executed when the event Events.Init occurs:
<transition event="Events.Init" target="Ready">
<customActionDomain:ActionsStd.Init name="ActionsStd.Init"/>
</transition>
is mapped to the C++ method defined in actionsStd.hpp which takes the last_event as parameter:
/**
* Implementation of the Init action. This action:
* - replies back for the originator of the ReqInit request.
*
* @param[in] lastEvent Last event received which should be a Init event
* (the Init event is triggered by a ReqInit request).
*/
void Init(const rad::AnyEvent& last_event);
The method’s implementation in actionsStd.cpp extracts from the last_event the event’s payload (defined in events.rad.ev) which is of type std::shared_ptr<rad::cii::Request<std::string>>>, sets the OK reply message and returns.
void ActionsStd::Init(const rad::AnyEvent& last_event) {
RAD_TRACE(GetLogger());
auto req = rad::getPayload_nothrow< Events::Init > (last_event);
if (req == nullptr) {
LOG4CPLUS_ERROR(GetLogger(), "Init event has no associated request!");
return;
}
req->SetReplyValue("OK");
}
The one above is just the simple case where the actions does nothing but replying to the originator of the Init command. More elaborated actions implementation can be found in the Examples section.
6.4.7. actionMgr.hpp|cpp¶
The ActionMgr class is responsible, via the CreateActions method, for instantiating the classes implementing the actions methods (e.g. ActionsStd). This method sets up also the callbacks to be invoked by the State Machine engine when an action has to be executed. The callback mechanism is based on the a function call stored in the ActionCallback object.
void ActionMgr::CreateActions(boost::asio::io_service& ios,
rad::SMAdapter& sm,
DataContext& the_data) {
RAD_TRACE(GetLogger());
/* Create action group classes */
ActionsStd* actions_std = new ActionsStd(ios, sm, the_data);
if (actions_std == nullptr) {
LOG4CPLUS_ERROR(GetLogger(), "Cannot create actions_std object.");
return;
}
AddActionGroup(actions_std);
/* Create SM call-backs */
scxml4cpp::Action* the_action = nullptr;
using std::placeholders::_1;
the_action = new rad::ActionCallback("ActionsStd.Init",
std::bind(&ActionsStd::Init, actions_std, _1));
AddAction(the_action);
the_action = new rad::ActionCallback("ActionsStd.Status",
std::bind(&ActionsStd::Status, actions_std, _1));
AddAction(the_action);
...
}
Moreover the ActionMgr class is responsible for instantiating the classes implementing the do-activities as threads or co-routines.
void ActionMgr::CreateActivities(rad::SMAdapter& sm,
DataContext& the_data) {
RAD_TRACE(GetLogger());
/*
* Instantiate here rad::ThreadActivity,
* rad::PthreadActivity, or rad::CoroActivity.
*/
/*
* For example:
* rad::ThreadActivity activity = nullptr;
* activity = new ActivityMoving("ActivityMoving", sm, the_data);
* AddActivity(activity);
*/
}
The CreateActions and CreateActivities methods take as parameters:
a reference to rad::SMAdapter which is used to post internal events to the State Machine engine.
a reference to the application DataContext to be able to share run-time data among actions and do-activities.
In addition, the CreateActions method takes a reference to the boost::asio::io_service to allow the ActionsStd::Exit and ActionsStd::ExitNoReply methods to stop the BOOST ASIO event loop.
The ActionMgr object is instantiated in main.cpp.
6.4.8. config.hpp|cpp¶
The Config class is responsible for providing (read-only) access to the application configuration. The configuration can come from:
Default values defined in config.hpp
Environment Variables
Command line parameters
Application configuration file config.yaml
The Config class constructor initializes the configuration attributes with the default values and the environment variables (if available). The class provides the methods ParseOptions() to read the command line parameters and LoadConfig() to load the configuration file.
Configuration parameters can be obtained via the Get methods.
Note
Configuration files are loaded from the current directory or from any directory listed in the CFGPATH environment variable.
The Config object is instantiated in main.cpp.
6.4.9. dbInterface.hpp|cpp¶
The DbInterface class is responsible for getting/setting application information in the in-memory DB via the Get/Set methods.
The header file provides the key definitions to be used when writing key-value pairs in the DB or when reading the values associated to given keys.
The generated class provides some Get and Set methods to read/write the application state and configuration:
GetControlState(), GetControlSubstate()
SetControlState(), SetControlSubstate()
SetConfig(Config& cfg)
The developer can add all the Get/Set methods required by the application.
The DbInterface constructor takes as parameters:
A string representing the prefix to be added to all the “keys” before writing in DB
A reference to an object that allows to talk to the Online DB. This object should implement the rad::DbAdapter interface. RAD provides a class which is specialized to talk to Redis DB: rad::DbAdapterRedis.
The DbInterface object instantiated in main.cpp and used by the DataContext class.
6.4.10. dataContext.hpp|cpp¶
The DataContext class provides (read/write) access to actions and activities to the application run-time information. This class allows to also to write the run-time information to the in-memory DB via the DbInterface class.
Since run-time information may need to be initialized with application configuration data, a reference to the Config class is passed via the constructor. The class provides also a method to reload the application configuration (ReloadConfig()).
The DataContext is instantiated in main.cpp.
6.4.11. logger.hpp|cpp¶
log4cplus library provides the possibility to associated logs to different loggers. This features allows to set the log level (and therefore enable/disable logging) for given loggers. For example it is possible to enable logging for an application secondary thread and disable the logging for the main thread by using different loggers: one associated to the main thread and one associated to the secondary thread.
RAD suggests to use for the main application thread a common global logger which takes the name from the SW module name and it is defined in the logger.hpp file.
const std::string LOGGER_NAME = "hellomal";
The logger can be obtained from the free function implemented in logger.cpp:
log4cplus::Logger& GetLogger() {
static log4cplus::Logger logger = log4cplus::Logger::getInstance(LOGGER_NAME);
return logger;
}
For secondary threads (e.g. Activity classes) or in case of loggers dedicated to given classes, it is suggested to declare the logger as class attribute and use it in the logging macros. The name of specialized logger should use the SW module name as prefix (e.g. “hellomal.ActivityName”) to allow an easy enabling/disabling of all application logs.
Note
There is an overhead in using the log4cplus::Logger::getInstance(loggerName) method and therefore it is preferable to avoid calling that method every we need to log.
The RAD_ASSERT macros use the rootLogger.
6.4.12. stdCmdsImpl.hpp¶
The StdCmdsImpl class implements the CII/MAL/ZPB interface specified in CII Interface module. In the constructor it takes a reference to the rad::SMAdapter used to inject the event associated to the command into the State Machine engine.
class StdCmdsImpl : public hellomalif::AsyncStdCmds {
public:
explicit StdCmdsImpl(rad::SMAdapter& sm) : m_sm(sm) {
RAD_TRACE(GetLogger());
}
virtual elt::mal::future<std::string> Init() override {
RAD_TRACE(GetLogger());
auto ev = std::make_shared<Events::Init>();
m_sm.PostEvent(ev);
return ev->getPayload().GetReplyFuture();
}
...
virtual elt::mal::future<std::string> SetLogLevel(const std::shared_ptr<hellomalif::LogInfo>& info) override {
RAD_TRACE(GetLogger());
auto ev = std::make_shared<Events::SetLogLevel>(info->clone());
m_sm.PostEvent(ev);
return ev->getPayload().GetReplyFuture();
}
private:
rad::SMAdapter& m_sm;
};
The Init() method is invoke when the hellomalif.Init command is sent to the hellomal application (for example using the msgSend application). This method creates the event Events::Init associated to the command and post it to the State Machine engine via the rad::SMAdapter::PostEvent() method. Note that the Init command has no parameters and the reply has std::string as payload. Finally the Init() method return a future of type std::string to the CII/MAL server.
The SetLogLevel() method is similar to the Init() but, in addition, it takes a parameter of type hellomalif::LogInfo as argument. The parameter becomes the payload of the Events::SetLogLevel event.
Note
The methods of the StdCmdsImpl class are invoked from the CII/MAL server thread and not from the main application thread.
The StdCmdsImpl object is instantiated in the main.cpp as part of the CII/MAL server initialization.
6.4.13. main.cpp¶
The main() function of an application based on RAD is responsible for creating all the required objects, initializing the services and start the event loop.
It starts by initializing the logging library and loading the ZeroMQ+ProtoBuf CII/MAL middleware. Follows the loading of the application configuration via the Config class (see config.hpp|cpp) and the creation of the DataContext used to exchange run-time information between actions and activities.
int main(int argc, char *argv[]) {
rad::LogInitialize();
LOG4CPLUS_INFO(hellomal::GetLogger(), "Application hellomal started.");
try {
/* Init CII/MAL middleware */
rad::cii::LoadMiddlewares({"zpb"});
/* Read only configuration */
hellomal::Config config;
if (config.ParseOptions(argc, argv) == false) {
// request for help
return EXIT_SUCCESS;
}
config.LoadConfig();
rad::LogConfigure(rad::Helper::FindFile(config.GetLogProperties()));
/* Runtime DB */
rad::DbAdapterRedis redis_db;
/* Runtime data context */
hellomal::DataContext data_ctx(config, redis_db);
...
At this point the event loop based on BOOST ASIO (io_service) and the State Machine engine related objects (external_events, state_machine_ctx, and state_machine) are created.
The state_machine_ctx is passed by the State Machine engine to any invoked actions.
The external_events is the event queue processed by the State Machine engine.
The state_machine is the RAD facade to the State Machine engine and it is used to load the State Machine model, register event/state listeners, and inject events.
Before loading the State Machine model, the actions and activities objects are created via the ActionMgr (see actionMgr.hpp|cpp).
...
/* Create event loop */
boost::asio::io_service io_service;
/* State Machine related objects */
scxml4cpp::EventQueue external_events;
scxml4cpp::Context state_machine_ctx;
rad::SMAdapter state_machine(io_service,
&state_machine_ctx,
external_events);
hellomal::ActionMgr action_mgr;
action_mgr.CreateActions(io_service, state_machine, data_ctx);
action_mgr.CreateActivities(state_machine, data_ctx);
state_machine.Load(config.GetSmScxmlFilename(), &action_mgr.GetActions(),
&action_mgr.GetActivities());
// Register SM Status & Event notification callbacks
hellomal::ActionsStd* actionsStd =
dynamic_cast<hellomal::ActionsStd*>(
action_mgr.FindActionGroup("ActionsStd"));
RAD_ASSERTPTR(actionsStd);
state_machine.AddStatusListener(actionsStd);
state_machine.AddEventListener(actionsStd);
...
The last part of the main() function is dedicated to start:
the CII/MAL server
the State Machine engine (state_machine.Start())
the BOOST ASIO event loop (io_service.run())
Note
The event loop io_service.run() methods returns only when there are no more callbacks registered or when it is stopped via the io_service.stop() method (see ActionsStd::Exit() method).
...
/* Create CII/MAL replier */
rad::cii::Replier malReplier(elt::mal::Uri(config.GetMsgReplierEndpoint()));
malReplier.RegisterService<{{cookiecutter.interface_name}}::AsyncStdCmds>("StdCmds",
std::make_shared<hellomal::StdCmdsImpl>(state_machine));
/* Start event loop */
state_machine.Start();
io_service.run();
state_machine.Stop();
} catch (rad::Exception& e) {
LOG4CPLUS_ERROR(hellomal::GetLogger(), e.what());
return EXIT_FAILURE;
} catch (...) {
LOG4CPLUS_ERROR(hellomal::GetLogger(), boost::current_exception_diagnostic_information());
return EXIT_FAILURE;
}
// to avoid valgrind warnings on potential memory loss
google::protobuf::ShutdownProtobufLibrary();
LOG4CPLUS_INFO(hellomal::GetLogger(), "Application hellomal terminated.");
return EXIT_SUCCESS;
}
6.5. Generate CII Integration Test Module¶
RAD provides templates to generate some basic integration tests based on Robot Framework. They verify the “standard” commands and memory leaks.
A module containing some basic integration tests to verify applications using CII Software Platform can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-robtpl-maltest/
module_name [hellomaltest]: hellomaltest
module_to_test [hellomal]: hellomal
application_to_test [hellomal]: hellomal
interface_prefix [hellomalif]: hellomalif
application_to_send [hellomalifSend]: hellomalifSend
The input values to the template are:
module_name the name of the SW module to be generated (which contains the tests).
module_to_test the name of the SW module to test.
application_to_test the name of application to test.
interface_prefix the name of the interface module.
application_to_send the name of the msgSend application to use in the tests.
From the template Cookiecutter generates the directory hellomaltest containing the following files:
File |
Description |
---|---|
hellomaltest/etr.yaml |
Configuration file to be able to run the tests with ETR tool. |
hellomaltest/src/genStdcmds.robot |
Tests verifying the “standard” commands. |
hellomaltest/src/genMemleaks.robot |
Similar to genStdcmds.robot tests but executed with Valgrind tool to check for memory leaks. |
hellomaltest/src/genUtilities.txt |
Utility functions and configuration parameters used by the tests. |
6.6. Build and Install CII Generated Modules¶
Generated code can be compiled and installed by executing the following commands:
> cd hello
> waf configure
> waf install
Note
Make sure that the PREFIX environment variable is set to the installation directory (which usually coincides with the INTROOT).
6.7. CII Applications Execution¶
In order to execute the generated application, the DB must be started first:
> redis-server
Note
It is possible to monitor the content of Redis DB via a textual client like redis-cli or a graphical one dbbrowser.
After the DB has started, the generated CII application can be executed and its current state can be queries by:
> hellomal -c hellomal/config.yaml -l DEBUG&
> hellomalifSend zpb.rr://127.0.0.1:12081/StdCmds hellomalif.Status ""
The default application command line options are as follow:
-h [ --help ] Print help messages
-n [ --proc-name ] arg Process name
-l [ --log-level ] arg Log level: ERROR, WARNING, STATE, EVENT, ACTION, INFO, DEBUG, TRACE
-c [ --config ] arg Configuration filename
-d [ --db-host ] arg In-memory DB host (ipaddr:port)
Note
Make sure that the CFGPATH environment variable contains the path(s) where the configuration files are located.
Redis IP address and port number must be either the default one (127.0.0.1:6379), or specified as command line parameter with the option -d, or defined in the DB_HOST environment variable, or defined in the application configuration file.
To terminate the application it is enough to send an Exit command or press Ctrl-C:
> hellomalifSend zpb.rr://127.0.0.1:12081/StdCmds hellomalif.Exit ""
6.8. Execute CII Integration Tests¶
Integration tests can be executed via Extensible Test Runner (ETR) tool (see ETR User Manual) or directly using Robot Framework.
In the first case:
> cd hellomaltest
> etr
Note
ETR may not be part of the ELT DevEnv and therefore it has be installed separately.
Using Robot directly:
> cd hellomaltest/src
> robot *.robot
7. Tutorial 2: Customizing an Application with RAD + CII¶
This tutorial explains how to customize an application created in Tutorial 1: Creating an Application with RAD + CII. It shows how to add a custom command with associated actions, an activity, and run-time data to be shared between actions and activities.
The resulting application is similar to the exmalserver example that can be found in rad/rad/cpp/_examples directory.
7.1. Add a Command¶
As example, we introduce a new Preset command that should emulate the pointing of a telescope.
In order to add a new command to the application the following files have to be updated/created:
update hellomalif/src/hellomalif.xml (CII Interface Module)
update hellomalifsend/src/main.cpp (CII msgSend Module)
update hellomal/src/events.rad.ev (CII Application Module)
update hellomal/src/stdCmdsImpl.hpp (CII Application Module)
update hellomal/resource/config/sm.xml (CII Application Module)
create hellomal/src/actionsPreset.hpp|cpp (CII Application Module)
update hellomal/src/actionMgr.cpp (CII Application Module)
7.1.1. Update CII Interface Module¶
If a command has to be added (modified, or removed), the file hellomalif/src/hellomalif.xml has to be edited.
For example, in order to introduce a Preset command that takes 2 parameters (e.g. ra and dec), a new data structure (TelPosition) and the new interface method can be added to the CII interface specification:
...
<struct name="TelPosition">
<member name="ra" type="float" />
<member name="dec" type="float" />
</struct>
...
<interface name="StdCmds">
...
<method name="Preset" returnType="string">
<argument name="pos" type="nonBasic" nonBasicTypeName="TelPosition" />
</method>
...
</interface>
...
7.1.2. Update CII msgSend Module¶
The msgSend tool has to be updated to introduce the possibility of sending the new Preset command. In the file hellomalifsend/src/main.cpp the following code can be added:
...
#include <boost/tokenizer.hpp>
...
} else if (command == "hellomalif.Preset") {
// default values
double ra = 0.0;
double dec = 0.0;
boost::char_separator<char> separator(" ");
boost::tokenizer<boost::char_separator<char>> tokens(params, separator);
auto param_it = tokens.begin();
if (param_it != tokens.end()) {
ra = std::stof(std::string(*param_it));
param_it++;
}
if (param_it != tokens.end()) {
dec = std::stof(std::string(*param_it));
}
auto mal = client->getMal();
auto p = mal->createDataEntity<::hellomalif::TelPosition>();
p->setRa(ra);
p->setDec(dec);
auto reply = client->Preset(p);
std::cout << reply << std::endl;
} ...
7.1.3. Update CII Application Module¶
7.1.3.1. Update events.rad.ev¶
In the application we need to define a new Preset event associated to the new command in hellomal/src/events.rad.ev file:
events:
...
Preset:
doc: event triggered when the Preset command is received.
payload: rad::cii::Request<std::string, std::shared_ptr<hellomalif::TelPosition>>
...
7.1.3.2. Update stdCmdsImpl.hpp¶
The MAL interface implementation must be updated with the introduction of a new method Preset() which creates the corresponding event and inject it into the State Machine engine.
virtual elt::mal::future<std::string> Preset(const std::shared_ptr<hellomalif::TelPosition>& pos) override {
RAD_TRACE(GetLogger());
auto ev = std::make_shared<Events::Preset>(pos->clone());
m_sm.PostEvent(ev);
return ev->GetPayload().GetReplyFuture();
}
7.1.3.3. Update sm.xml¶
The State Machine model can be updated by adding a new Presetting state. This state indicates that a preset command is being executed. Since Preset involves moving the telescope axes, the Presetting state is added as substate of Operational and is can be reached once the system has been initialized (e.g. from On/Operation/idle). The resulting State Machine model looks like:
...
<state id="Operational">
<initial>
<transition target="Idle"/>
</initial>
<state id="Idle">
<transition event="Events.Preset" target="Presetting"/>
</state>
<state id="Presetting">
<onentry>
<customActionDomain:ActionsPreset.Start name="ActionsPreset.Start"/>
</onentry>
</state>
</state>
...
Note that when entering the state Presetting, the new action ActionsPreset.Start will be executed. This action should be responsible for initiating the preset of the telescope.
7.1.3.4. Create actionsPreset.hpp|cpp¶
Instead of implementing the Preset action by adding a new method to the existing ActionsStd class, we introduce a new class: ActionsPreset. To create the class, simply copy actionsStd.hpp and actionsStd.cpp files, rename them, and remove the superfluous code. The header hellomal/src/actionsPreset.hpp should look like:
#include <rad/actionGroup.hpp>
#include <rad/smAdapter.hpp>
#include <events.rad.hpp>
namespace hellomal {
class DataContext;
class ActionsPreset : public rad::ActionGroup {
public:
ActionsPreset(rad::SMAdapter& sm,
DataContext& data);
void Start(const rad::AnyEvent& last_event);
ActionsPreset(const ActionsPreset&) = delete; //! Disable copy constructor
ActionsPreset& operator=(const ActionsPreset&) = delete; //! Disable assignment operator
private:
rad::SMAdapter& m_sm;
DataContext& m_data;
};
} // namespace hellomal
The source file hellomal/src/actionsPreset.cpp implementing the action should look like:
#include "actionsPreset.hpp"
#include "dataContext.hpp"
#include "logger.hpp"
#include <rad/mal/request.hpp>
#include <rad/getPayload.hpp>
namespace hellomal {
ActionsPreset::ActionsPreset(rad::SMAdapter& sm,
DataContext& data)
: rad::ActionGroup("ActionsPreset"),
m_sm(sm),
m_data(data) {
}
void ActionsPreset::Start(const rad::AnyEvent& last_event) {
auto req = rad::getPayloadNothrow< Events::Preset > (last_event);
if (req == nullptr) {
LOG4CPLUS_ERROR(GetLogger(), "Preset event has no associated request!");
return;
}
auto reqParams = req->GetRequestPayload();
float ra = reqParams->getRa();
float dec = reqParams->getDec();
LOG4CPLUS_DEBUG(GetLogger(), "Received Preset to RA " << ra << " DEC " << dec);
req->SetReplyValue("Preset Started");
}
} // namespace hellomal
The action implementation prints the RA/DEC and replies back to the originator of the Preset command.
7.1.3.5. Update actionMgr.cpp¶
Once the action has been implemented it can be added to the ActionMgr class so that it is created at start-up. The method CreateActions() in hellomal/src/actionMgr.cpp can be updated as follows:
#include "actionsPreset.hpp"
...
void ActionMgr::CreateActions(boost::asio::io_service& ios,
rad::SMAdapter& sm,
DataContext& the_data) {
...
ActionsPreset* actions_preset = new ActionsPreset(sm, the_data);
if (actions_preset == nullptr) {
LOG4CPLUS_ERROR(GetLogger(), "Cannot create ActionsPreset object.");
return;
}
AddActionGroup(actions_preset);
...
the_action = new rad::ActionCallback(
"ActionsPreset.Start",
std::bind(&ActionsPreset::Start, actions_preset, _1));
AddAction(the_action);
...
}
7.2. Add an Activity¶
After having added the Preset command (see Add a Command), we introduce the an activity that is started after entering the Presetting state and simulate the moving telescope axis. The activity takes some time (long lasting task) and when it is completed, it triggers an internal event, MoveDone, to go back to the Idle state. This behavior can be achieved by updating/creating the following files:
update hellomal/src/events.rad.ev (CII Application Module)
update hellomal/resource/config/sm.xml (CII Application Module)
create hellomal/src/activityMoving.hpp|cpp (CII Application Module)
update hellomal/src/actionMgr.cpp (CII Application Module)
7.2.1. Update CII Application Module¶
7.2.1.1. Update events.rad.ev¶
TheMoveDone event (without payload) is added in hellomal/src/events.rad.ev file to indicate that the activity has terminated:
events:
...
MoveDone:
doc: event triggered when the ActivityMoving has terminated.
...
7.2.1.2. Update sm.xml¶
The State Machine model is updated with the invocation of the activity ActivityMoving and the new transition from Presetting to Idle on event MoveDone:
<state id="Operational">
<initial>
<transition target="Idle"/>
</initial>
<state id="Idle">
<transition event="Events.Preset" target="Presetting"/>
</state>
<state id="Presetting">
<onentry>
<customActionDomain:ActionsPreset.Start name="ActionsPreset.Start"/>
</onentry>
<invoke id="ActivityMoving"/>
</state>
<transition event="Events.MoveDone" target="Idle"/>
</state>
...
7.2.1.3. Create activityMoving.hpp|cpp¶
The activity that simulates the telescope axes movement can be implemented using a dedicated thread. The thread is implemented by the Run() method of the ActivityMoving class.
#include "logger.hpp"
#include <rad/activity.hpp>
#include <rad/smAdapter.hpp>
#include <string>
namespace hellomal {
class DataContext;
class ActivityMoving : public rad::ThreadActivity {
public:
ActivityMoving(const std::string& id,
rad::SMAdapter& sm,
DataContext& data);
virtual ~ActivityMoving();
void Run() override;
ActivityMoving(const ActivityMoving&) = delete; //! Disable copy constructor
ActivityMoving& operator=(const ActivityMoving&) = delete; //! Disable assignment operator
private:
log4cplus::Logger m_logger = log4cplus::Logger::getInstance(LOGGER_NAME + ".ActivityMoving");
rad::SMAdapter& m_sm;
DataContext& m_data;
};
} // namespace hellomal
The Run() method waits 10s and then trigger the MoveDone event.
#include "activityMoving.hpp"
#include "dataContext.hpp"
#include <events.rad.hpp>
namespace hellomal {
ActivityMoving::ActivityMoving(const std::string& id,
rad::SMAdapter& sm,
DataContext& data)
: rad::ThreadActivity(id),
m_sm(sm),
m_data(data) {
}
ActivityMoving::~ActivityMoving() {
}
void ActivityMoving::Run() {
int i = 0;
const int maxIterations = 10;
while (IsStopRequested() == false) {
LOG4CPLUS_DEBUG(m_logger, "Moving ALT/AZ ...");
using namespace std::chrono;
std::this_thread::sleep_for(1s);
if (i == maxIterations) {
LOG4CPLUS_INFO(m_logger, "Target position reached.");
m_sm.PostEvent(rad::UniqueEvent(new Events::MoveDone()));
break;
}
i++;
}
}
} // namespace hellomal
7.2.1.4. Update actionMgr.cpp¶
In order to have the activity created at application start-up, the ActionMgr::CreateActivities() method has to be updated as follows:
#include "activityMoving.hpp"
...
void ActionMgr::CreateActivities(rad::SMAdapter& sm, DataContext& the_data) {
rad::ThreadActivity* the_activity = nullptr;
the_activity = new ActivityMoving("ActivityMoving", sm, the_data);
AddActivity(the_activity);
}
7.3. Add Data Attributes¶
The telescope axes movement can be made more realistic by logging the intermediate telescope positions. We need therefore a way to inform the ActivityMoving about the target RA/DEC. This can be achieved by sharing the RA/DEC via the DataContext class adding the SetRaDec() and GetRaDec() methods. The DataContext::SetRaDec() method can be used by the ActionsPreset::Start() to store the target position while ActivityMoving::Run() uses the DataContext::GetRaDec() to compute the current position.
To make debugging easier, the target RA/DEC are also written in the DB and therefore the class DbInterface needs also to be updated.
This behavior can be achieved by updating the following files:
update hellomal/src/dbInterface.hpp|cpp (CII Application Module)
update hellomal/src/dataContext.hpp|cpp (CII Application Module)
update hellomal/src/actionsPreset.cpp (CII Application Module)
update hellomal/src/activityMoving.cpp (CII Application Module)
7.3.1. Update CII Application Module¶
7.3.1.1. Update dbInterface.hpp|cpp¶
The DbInterface class is updated with a method to write in the DB the RA and DEC attributes and the associated keys.
const std::string KEY_CONTROL_RA = "ctr.ra";
const std::string KEY_CONTROL_DEC = "ctr.dec";
class DbInterface {
public:
...
void SetRaDec(const float ra, const float dec) {
std::vector < std::string > kvs;
kvs.push_back(m_prefix + KEY_CONTROL_RA);
kvs.push_back(std::to_string(ra));
kvs.push_back(m_prefix + KEY_CONTROL_DEC);
kvs.push_back(std::to_string(dec));
m_runtime_db.MultiSet(kvs);
}
...
}
7.3.1.2. Update dataContext.hpp|cpp¶
Two member attributes, mRa and mDec, and the associated getter and setter methods are added to the DataContext class. The setter method is also for writing the new target position to the DB.
class DataContext {
public:
...
void GetTargetRaDec(float& ra, float& dec) {
ra = m_ra;
dec = m_dec;
}
void SetTargetRaDec(const float ra, const float dec) {
m_ra = ra;
m_dec = dec;
m_db_interface.SetRaDec(ra, dec);
}
private:
...
float m_ra = 0.0;
float m_dec = 0.0;
};
7.3.1.3. Update actionsPreset.cpp¶
The ActionsPreset::Start() method is updated with the writing into the DataContext of the pointing target coordinates.
void ActionsPreset::Start(const rad::AnyEvent& last_event) {
auto req = rad::getPayloadNothrow< Events::Preset > (last_event);
if (req == nullptr) {
LOG4CPLUS_ERROR(GetLogger(), "Preset event has no associated request!");
return;
}
auto reqParams = req->GetRequestPayload();
float ra = reqParams->getRa();
float dec = reqParams->getDec();
LOG4CPLUS_DEBUG(GetLogger(), "Received Preset to RA " << ra << " DEC " << dec);
m_data.SetTargetRaDec(ra, dec);
req->SetReplyValue("Preset Started");
}
7.3.1.4. Update activityMoving.cpp¶
Finally the AcitivityMoving::Run() method can be re-factored to take into account the real target coordinates.
void ActivityMoving::Run() {
float target_ra = 0.0;
float target_dec = 0.0;
m_data.GetTargetRaDec(target_ra, target_dec);
int i = 0;
const int max_iterations = 10;
float cur_ra = 0.0;
float cur_dec = 0.0;
float step_ra = target_ra / max_iterations;
float step_dec = target_dec / max_iterations;
while (IsStopRequested() == false) {
cur_ra = i * step_ra;
cur_dec = i * step_dec;
LOG4CPLUS_DEBUG(m_logger, "Moving ALT/AZ: RA = " << cur_ra << " DEC = " << cur_dec);
using namespace std::chrono;
std::this_thread::sleep_for(1s);
if (i == max_iterations) {
LOG4CPLUS_INFO(m_logger, "Reached target position RA = " << target_ra
<< " DEC = " << target_dec);
m_sm.PostEvent(rad::UniqueEvent(new Events::MoveDone()));
break;
}
i++;
}
}
8. Tutorial 3: Creating an Application with RAD + Prototype (obsolete)¶
The steps to build an application with RAD and the Prototype Software Platform are identical to the ones defined in Tutorial 1: Creating an Application with RAD + CII. The differences lie:
on the name of the templates (they do not have mal postfix),
the way the application interface is specified (see Generate Prototype Interface Module),
the RAD classes used by the application, in particular the ones related to the middleware services.
Warning
RAD still provides the templates to create application using the Prototype Software Platform however these templates will be declared obsolete as soon as the complete CII Software Platform is delivered and integrated in RAD.
8.1. Generate Prototype WAF Project¶
Similar to Generate CII WAF Project but using rad/rad/cpp/templates/config/rad-waftpl-prj template:
> cookiecutter rad/rad/cpp/templates/config/rad-waftpl-prj
project_name [hello]: hello
modules_name [helloif helloifsend hello]:
The input values to the template are:
project_name the name of the WAF project which is used to create the directory containing the project SW modules.
modules_name the name of the SW modules part of this project.
8.2. Generate Prototype Interface Module¶
All commands, replies, and topics used to communicate between ELT applications, must be specified in dedicated interface modules. The Prototype Software Platform uses Google Protocol Buffers to specify the data structures exchanged by the application via request/reply (parameters) and pub/sub (topics).
An interface module containing the “standard” commands can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-applif
module_name [helloif]: helloif
library_name [helloif]: helloif
package_name [examples]: hello
The input values to the template are:
module_name the name of the SW module to be generated (which contains the interface specification).
library_name the name of the binary to be produced when compiling the generated module.
package_name the name of the directory that contains the module. In this case it is the project directory.
From the template Cookiecutter generates the directory helloif containing the following files:
File |
Description |
---|---|
helloif/wscript |
WAF file to compile the SW module. |
helloif/interface/helloif/requests.proto |
Google ProtoBuf data structures. |
The requests.proto file contains the definition data structures used to send requests and replies, for example the Init command (without parameters) and the related reply (with a string parameter):
syntax = "proto3"
package helloif;
message ReqInit {
}
message RepInit {
string reply = 1;
}
The .proto files are complied by the protoc compiler which generates, in the build directory the following C++ files:
hello/build/…/helloif.pb.cpp
hello/build/…/helloif.ph.h
These files are used by the application to send/receive commands/replies. Generated files contain the C++ classes representing the data structures. These classes provide the methods to deserialize (parse) message payloads and to serialize.
The protoc compiler is invoked by waf every time you compile (and the .proto files have been modified).
8.3. Generate Prototype msgSend Module¶
A SW module implementing the msgSend tool to send the “standard” commands to applications based on Prototype Software Platform can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-send/
interface_name [helloif]: helloif
interface_module [helloif]: helloif
module_name [helloifsend]: helloifsendhelloifsend
application_name [helloifSend]: helloifSend
parent_package_name [hello]: hello
The input values to the template are:
interface_name the name of the Prototype Interface module (which specifies the commands sent by the msgSend tool and it was defined in Generate Prototype Interface Module).
interface_module fully qualified name of Prototype Interface library.
module_name the name of the SW module to be generated (which contains the msgSend tool).
application_name the name of the binary to be produced when compiling the generated module.
parent_package_name the name of the directory that contains the tool. In this case it is the project directory.
From the template Cookiecutter generates the directory helloifsend containing the following files:
File |
Description |
---|---|
helloifsend/wscript |
WAF file to compile the SW module. |
helloifsend/src/main.cpp |
Implementation of msgSend. |
The tool can be invoked by:
helloifSend <timeout> <IP> <port> <command> <parameters>
<timeout> reply timeout in msec
<IP> IP address
<port> port
<command> command to be sent (e.g. helloif.ReqStatus)
<parameters> parameters of the command
8.4. Generate Prototype Application Module¶
RAD provides the templates to create a simple server application implementing the standard ELT State Machine model using the Prototype Software Platform services.
A SW module implementing implementing a server application able to process the “standard” commands using ZeroMQ and ProtoBuf services can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter ../rad/rad/cpp/templates/config/rad-cpptpl-appl
module_name [hello]:
application_name [hello]:
package_name [examples]: hello
interface_name [helloif]:
libs [cpp._examples.helloif]: helloif
From the template Cookiecutter generates the directory hello containing the following files:
File |
Description |
---|---|
wscript |
WAF file to build the application. |
resource/config/config.yaml |
YAML application configuration file. |
resource/config/sm.xml |
SCXML file with the State Machine model. |
resource/config/log.properties |
Logging configuration file. |
src/events.rad.ev |
List of events processed by the application. |
src/actionMgr.[hpp|cpp] |
Class responsible for instantiating actions and activities/ |
src/actionsStd.[hpp|cpp] |
Class implementing standard action methods. |
src/config.[hpp|cpp] |
Class loading YAML configuration file. |
src/dataContext.[hpp|cpp] |
Class used to store application run-time data shared between action classes. |
src/dbInterface.[hpp|cpp] |
Class interfacing to the in-memory DB. |
src/logger.[hpp|cpp] |
Default logger definition. |
src/msgParsers.[hpp|cpp] |
Classes parsing the ZeroMQ commands/topics. |
src/main.cpp |
Application entry function. |
The generated application is very similar to the application generated for CII Software Platform (see Generate CII Application Module). Instead of getting the commands via the realization of the CII/MAL interface (stdCmdsImpl.hpp) the “naked” ZMQ messages are parsed by the MsgParsers and TopicParsers classes defined in msgParsers.[hpp|cpp] and injected into the State Machine Engine in form of events..
8.5. Generate Prototype Integration Test Module¶
A module containing some basic integration tests to verify applications using Prototype Software Platform can be created by executing the following commands and entering the requested information:
> cd hello
> cookiecutter rad/rad/cpp/templates/config/rad-robtpl-test/
module_name [hellotest]: hellotest
module_to_test [hello]: hello
application_to_test [hello]: hello
interface_prefix [helloif]: helloif
application_to_send [helloifSend]: helloifSend
The input values to the template are:
module_name the name of the SW module to be generated (which contains the tests).
module_to_test the name of the SW module to test.
application_to_test the name of application to test.
interface_prefix the name of the interface module.
application_to_send the name of the msgSend application to use in the tests.
From the template Cookiecutter generates the directory hellotest containing the following files:
File |
Description |
---|---|
hellotest/etr.yaml |
Configuration file to be able to run the tests with ETR tool. |
hellotest/src/genStdcmds.robot |
Tests verifying the “standard” commands. |
hellotest/src/genMemleaks.robot |
Similar to genStdcmds.robot tests but executed with Valgrind tool to check for memory leaks. |
hellotest/src/genUtilities.txt |
Utility functions and configuration parameters used by the tests. |
8.6. Build and Install Generated Prototype Modules¶
Generated code can be compiled and installed by executing the following commands:
> cd hello
> waf configure
> waf install
Note
Make sure that the PREFIX environment variable is set to the installation directory (which usually coincides with the INTROOT).
8.7. Prototype Applications Execution¶
In order to execute the generated application, the DB must be started first:
> redis-server
Note
It is possible to monitor the content of Redis DB via a textual client like redis-cli or a graphical one dbbrowser.
After the DB has started, the generated CII application can be executed and its current state can be queries by:
> hello -c hello/config.yaml -l DEBUG&
> helloifSend 5000 127.0.0.1 5588 helloif.ReqStatus ""
The default application command line options are as follow:
-h [ --help ] Print help messages
-n [ --proc-name ] arg Process name
-l [ --log-level ] arg Log level: ERROR, WARNING, STATE, EVENT, ACTION, INFO, DEBUG, TRACE
-c [ --config ] arg Configuration filename
-d [ --db-host ] arg In-memory DB host (ipaddr:port)
Note
Make sure that the CFGPATH environment variable contains the path(s) where the configuration files are located.
Redis IP address and port number must be either the default one (127.0.0.1:6379), or specified as command line parameter with the option -d, or defined in the DB_HOST environment variable, or defined in the application configuration file.
To terminate the application it is enough to send an Exit command or press Ctrl-C:
> helloifSend 5000 127.0.0.1 5588 helloif.ReqExit ""
8.8. Execute Prototype Integration Tests¶
Integration tests can be executed via Extensible Test Runner (ETR) tool (see ETR User Manual) or directly using Robot Framework.
In the first case:
> cd hellotest
> etr
Note
ETR may not be part of the ELT DevEnv and therefore it has be installed separately.
Using Robot directly:
> cd hellotest/src
> robot *.robot
8.9. Adding New Command¶
In order to add a new command to the application the following steps have to be performed:
Add the request and related reply in the interface module (e.g. helloif/interface/helloif/Requests.proto file)
Add the event corresponding to the request in the event definition file (e.g. hello/src/Events.rad.ev file)
Update the SCXML model with the transition dealing with the new request (e.g. hello/config/hello/sm.yaml)
Add a new method in the ActionsStd class or add a new actions class
Update the ActionsMgr class with the registration of the new action
9. Examples¶
This section contains some example applications created using RAD.
Examples are located in rad/rad/cpp/_examples/ directory.
9.1. Example Using Prototype Software Platform¶
9.1.1. exif¶
This is an example of interface module with the definition of the commands, replies, topics used by the example applications.
It contains requests.proto for the definition of the requests/replies and topics.proto for the definition of the pub/sub topics.
9.1.2. exsend¶
This is an example of how to build a utility application (similar to the VLT msgSend) able to send requests and receive replies defined in exif interface module. This application is using some RAD libraries but it is not generated from the RAD templates.
The exsend module implements the exSend application that can be used as follow:
exSend <timeout> <IP> <port> <command> <parameters>
where:
<timeout> reply timeout in msec
<IP> IP address
<port> port <command> command to be sent to the server (e.g. exif.ReqInit)
<parameters> parameters of the command
for example to query the status (with 5 sec timeout) of an application running on the same local host on port 5577:
exSend 5000 127.0.0.1 5577 exif.ReqStatus ""
9.1.3. server¶
This is an example that shows how to create RAD based applications that uses request/reply, pub/sub, timers, Linux signals. It uses the interface defined in exif interface module and can be controlled by sending the commands via the exSend application (see exsend module).
The server module has two possible configuration files and state machines:
server/config/radServer/config.yaml server/config/radServer/sm.xml
server/config/radServer/config1.yaml server/config/radServer/sm1.xml
The first configuration (config.yaml and sm.xnl) is used to instantiate a prsControl application that is able to process exif.ReqPreset commands. When a exif.ReqPreset command is received, the application executes the ActionPreset::Start action which sends a exif.ReqMove to a second application (altazControl) that simulate the movement of the axes of a telescope. While waiting for the completion of the preset, it monitors the axes position by subscribing to topic XYMeas topic published on port 5560. The topic is processed by the XYMeas topic is processed by the ActionPreset::Monitor action. See the picture below for a more complete overview of the behaviour of prsControl application.
The second application (altazControl) is configured using config1.yaml and sm1.xml files. It receives the exif.ReqMove command and executes the ActionsMove::Start action and starts a do-activity: the ActivityMoving thread. The thread simulates the movement of the axes and publishes the intermediate positions via the XYMeas topic. When the target position is reached, the do-activity terminates and a reply (exif.RepMove) to the originator of the exif.ReqMove command is sent by the ActionsMove::Done action. See the picture below for a more complete overview of the behaviour of prsControl application. See the picture below for a more complete overview of the behaviour of altazControl application.
Note that both applications store the configuration, status, and telescope position information in the Redis runtime DB.
The sequence of messages to initialize, enable, and start the preset is shown below.
The sequence of messages to preset the axes is shown below.
In order to run the server example refer to the RAD integration test section.
9.1.4. hellorad + server¶
This is an example that shows how a Python client can talk to a C++ server. The client sends a ReqTest request containing “Ping pong” text, the server receives the requests and replies with a RepTest reply. To run the example first start the server:
radServer -c radServer/config.yaml -l DEBUG &
and the start the client:
hellorad client --req-endpoint='tcp://localhost:5577'
9.2. Example Using CII Software Platform¶
9.2.1. exmalif¶
This is the porting of exif interface definition using CII MAL XML language.
9.2.2. exmalsend¶
This tool is similar to exsend and allows to send the commands specified in exmalif to a CII server (see exmalsever below) implementing the exmalif interface.
It shows how to send synchronous CII/MAL/ZPB requests and get the related replies.
9.2.3. exmalserver¶
This is the porting of the server example application to CII.
It shows how to implement a server that is able to:
reply asynchronously to commands including error replies and partial replies.
send commands synchronously and get replies asynchronously.
publish topics and subscribe to topics.