Creating a Custom RTC Component¶
This advanced tutorial explains how to create and run a RTC Component application with a custom component life-cycle that extends the basic RTC Component life-cycle.
Important
The creation of custom life-cycle extensions is something that should happen only rarely in scope of instrument RTC projects. While this mechanism provides additional flexibility it also makes indiviudal instrument RTCs more different and thus more difficult to maintain. If the need to create a custom life-cycle extension arises we advise to discuss first with the RTC Toolkit team whether the life-cycle extension can be provided in scope of the toolkit so that it can be reused by other instruments. This would enable us to produce more similar and thus maintainable systems.
Before reading this tutorial make sure that you have read and understood the basic tutorial Creating a Simple RTC Component.
To create a RTC component application that makes use of of a custom life-cycle developers first need to create a life-cycle extension. Then they select the required component life-cycle and provide a custom business logic class that implements the behavior for the different stages of the life-cycle (i.e. it implements the activity methods).
A working example is provided with the RTC Toolkit in the directory _examples/exampleCustom
,
the waf package is composed of the following waf modules:
app
- A custom RTC Component application including configurationclient
- A custom RTC Client Applicationinterface
- Custom CII MAL interface definitionsscripts
- Helper scripts for deployment and control
Running the Example¶
After installing the RTC Toolkit (see Installation) the executables of the working example
should be present in $INTROOT/bin
and they should be globally available via $PATH
.
To run the example simply use the following command:
rtctkExampleCustom.sh run
The example will bring up a single RTC component instance and let it step through its life-cycle.
You can follow the output on console or tail -f
individual log files in $INTROOT/logsink
.
After about 35 seconds the example will terminate gracefully.
To step manually through the component life-cycle use the following sequence of commands:
# Deploy and start the example applications
rtctkExampleCustom.sh deploy
rtctkExampleCustom.sh start
# Use the client to step through the life-cycle of the respective component instance
rtctkExampleCustom.sh send Init
rtctkExampleCustom.sh send Enable
rtctkExampleCustom.sh send Run
rtctkExampleCustom.sh send Optimise 42
rtctkExampleCustom.sh send Foo
rtctkExampleCustom.sh send Bar 43
rtctkExampleCustom.sh send Idle
rtctkExampleCustom.sh send Disable
rtctkExampleCustom.sh send Reset
# Gracefully terminate the applcations and clean-up
rtctkExampleCustom.sh stop
rtctkExampleCustom.sh undeploy
In the deploy
phase the application environment is prepared by resolving environment variables
and by copying certain YAML files into the respective run-directory in $INTROOT/run/
.
In phase start
the respective applications are started with the correct command line arguments.
While the applications are running they can be steered through their life-cycles using commands
Init
, Enable
, Run
, etc.
In the stop
and undeploy
phases the application processes are terminated gracefully and
the run-directory is cleared again.
Note
The example shell scripts were introduced to make running rtctk applications simpler, to hide complexity and to fake functionality that is not yet provided by the CII. They are very likely to be changed or removed at some later point.
Development Guide¶
This section explains how to create a RTC Component application that combines toolkit provided life-cycle extension with a custom life-cycle. Aspects that are already explained in tutorial Creating a Simple RTC Component are not repeated here.
To make typing namespaces less painful we define the short alias cfw
for namespace
rtctk::componentFramework
.
Since this example introduces custom commands that the default client application does not understand, a custom client application needs to be provided as well. This custom client can send new application specific commands to the custom component.
MAL Interface Definition¶
Custom commands need to be defined in a CII MAL ICD file according to the syntax defined in the CII MAL manuals. This file is needed by both the component application and the client application.
The listing below shows an example interface definition rtctkexif
that defines custom
commands Foo
and Bar
. It can be found in file rtctkexif.xml
in the interface module.
<?xml version="1.0" encoding="UTF-8"?>
<types xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schemas/icd_type_definition.xsd">
<include href="svif.xml"/>
<package name="rtctkexif">
<interface name="CustomCmds">
<method name="Foo" returnType="string" throws="::stdif::ExceptionErr"/>
<method name="Bar" returnType="string" throws="::stdif::ExceptionErr">
<argument name="args" type="string"/>
</method>
</interface>
</package>
</types>
RTC Component Application¶
If custom commands are required instrument RTC developers need to provide a life-cycle extension that defines when these commands are accepted and to which action or activities in the business logic interface they need to be mapped. Since the component life-cycle is defined using a state machine model, creating a life-cycle extension means extending the toolkit provided standard state machines according to specific needs.
Event Definition¶
Life-cycle extension that introduce new commands also require the definition of corresponding
state machine events in file customEvents.rad.ev
.
# Event definitions
version: "1.0"
namespace: events
includes:
- rtctk/componentFramework/exceptions.hpp
- rad/mal/request.hpp
- Rtctkexif.hpp
events:
Foo:
payload: rad::cii::Request<std::string>
Bar:
payload: rad::cii::Request<std::string, std::string>
CustomDone:
context: statemachine
CustomError:
payload: rtctk::componentFramework::RtctkException
context: statemachine
Event Injection¶
In addition, the definition of a supporting class is necessary that receives custom commands and
translates them into state machine events, it can be found in file customCmdsImpl.hpp
.
namespace cfw = componentFramework;
class CustomCmdsImpl : public rtctkexif::AsyncCustomCmds
{
public:
static void Register(cfw::CommandReplier& replier, cfw::StateMachineEngine& engine) {
std::shared_ptr<elt::mal::rr::RrEntity> rr_service = std::make_shared<CustomCmdsImpl>(engine);
replier.RegisterService<rtctkexif::AsyncCustomCmds>("CustomCmds", rr_service);
}
CustomCmdsImpl(cfw::StateMachineEngine& engine) : m_engine(engine) { }
::elt::mal::future<std::string> Foo() override {
return cfw::InjectReqRepEvent<events::Foo>(this->m_engine);
}
::elt::mal::future<std::string> Bar(std::string const& args) override {
return cfw::InjectReqRepEvent<events::Bar>(this->m_engine, args);
}
private:
cfw::StateMachineEngine& m_engine;
};
Life-cycle Extension¶
An example life-cycle extension that implements commands Foo
and Bar
and that maps the
commands to concrete methods in the BusinessLogic interface can be found in file customLifeCycle.hpp
.
Life-cycle extensions are implemented using Mixin Layers
that encompass four major classes.
The ModelBuilder
class is responsible for adjusting the state machine model. New regions,
states transitions, events, guards, actions and activities can be introduced using a model
manipulation API from class ModelManipulator
to realise the desired behaviour of the extension.
class ModelBuilder : public Super::ModelBuilder
{
public:
ModelBuilder(cfw::StateMachineEngine& engine) : Super::ModelBuilder(engine)
{
using namespace cfw;
this->RegisterLayer({"CustomLifeCycle", { }});
this->mm.AddState(Composite, "On.Operational.RegionCustom", "On.Operational");
this->mm.AddState(Initial, "On.Operational.CustomInitial", "On.Operational.RegionCustom");
this->mm.AddState(Simple, "On.Operational.CustomIdle", "On.Operational.RegionCustom");
this->mm.AddState(Simple, "On.Operational.CustomFoo", "On.Operational.RegionCustom", "ActivityFoo" ,"ActionFooEntry");
this->mm.AddState(Simple, "On.Operational.CustomBar", "On.Operational.RegionCustom", "ActivityBar" ,"ActionBarEntry");
this->mm.AddTrans("On.Operational.CustomInitial" , "On.Operational.CustomIdle");
this->mm.AddTrans("On.Operational.CustomIdle" , "On.Operational.CustomFoo" , "events.Foo" ,"");
this->mm.AddTrans("On.Operational.CustomFoo" , "On.Operational.CustomIdle" , "events.CustomDone" ,"" ,"ActionFooDone" );
this->mm.AddTrans("On.Operational.CustomFoo" , "On.Operational.CustomIdle" , "events.CustomError" ,"" ,"ActionFooFailed" );
this->mm.AddTrans("On.Operational.CustomIdle" , "On.Operational.CustomBar" , "events.Bar" ,"");
this->mm.AddTrans("On.Operational.CustomBar" , "On.Operational.CustomIdle" , "events.CustomDone" ,"" ,"ActionBarDone" );
this->mm.AddTrans("On.Operational.CustomBar" , "On.Operational.CustomIdle" , "events.CustomError" ,"" ,"ActionBarFailed" );
}
};
The InputStage
, registers class CustomCmdsImpl
to enable the component to receive custom
commands and translate them into state machine events.
class InputStage : public Super::InputStage
{
public:
using Super::InputStage::InputStage;
void Start() override {
Super::InputStage::Start();
CustomCmdsImpl::Register(this->m_replier, this->m_engine);
}
};
The BizLogicIf
class adds methods to the business logic interface that need to be implemented
later in the user-provided BusinessLogic class.
class BizLogicIf : public Super::BizLogicIf
{
public:
virtual void ActivityFoo(cfw::StopToken st) {}
virtual void ActivityBar(cfw::StopToken st, cfw::JsonPayload const& args) {}
};
The OutputStage
, implements actions, guards and activities, sends command replies and delegates
activities to the BusinessLogic. Depending on the complexity of the state machine model this class
can become quite complex. To reduce complexity it can be split into multiple classes if necessary.
class OutputStage : public Super::OutputStage
{
public:
OutputStage(cfw::StateMachineEngine& engine, BizLogicIf& bl) : Super::OutputStage(engine, bl)
{
using namespace cfw;
// Handlers ###################################################################
engine.RegisterRejectHandler<events::Foo>();
engine.RegisterRejectHandler<events::Bar>();
m_custom_success_handler = [this] {
this->m_engine.PostEvent(std::make_unique<events::CustomDone>());
};
m_custom_error_handler = [this](const std::runtime_error& ex) {
std::string text = "Exception in state '" + this->m_engine.GetState() + "' with text '" + ex.what() + "'";
std::runtime_error enriched_ex(text);
this->m_engine.PostEvent(std::make_unique<events::CustomError>(enriched_ex));
};
// Actions #####################################################################
engine.RegisterActionStatic<events::Foo>("ActionFooEntry",
[this](auto const& ev) {
assert(not m_tmp_foo_request);
m_tmp_foo_request = ev.GetPayload();
});
engine.RegisterActionStatic<events::CustomDone>("ActionFooDone",
[this](auto const&) {
assert(m_tmp_foo_request);
m_tmp_foo_request->SetReplyValue("OK");
m_tmp_foo_request.reset();
});
engine.RegisterActionStatic<events::CustomError>("ActionFooFailed",
[this](auto const& ev) {
auto const& ex = ev.GetPayload();
LOG4CPLUS_ERROR(GetLogger(), " exception info " << ex.what());
assert(m_tmp_foo_request);
m_tmp_foo_request->SetException(ex);
m_tmp_foo_request.reset();
});
engine.RegisterActionStatic<events::Bar>("ActionBarEntry",
[this](auto const& ev) {
assert(not m_tmp_bar_request);
m_tmp_bar_request = ev.GetPayload();
});
engine.RegisterActionStatic<events::CustomDone>("ActionBarDone",
[this](auto const&) {
assert(m_tmp_bar_request);
m_tmp_bar_request->SetReplyValue("OK");
m_tmp_bar_request.reset();
});
engine.RegisterActionStatic<events::CustomError>("ActionBarFailed",
[this](auto const& ev) {
auto const& ex = ev.GetPayload();
LOG4CPLUS_ERROR(GetLogger(), " exception info " << ex.what());
assert(m_tmp_bar_request);
m_tmp_bar_request->SetException(ex);
m_tmp_bar_request.reset();
});
// Activities #####################################################################
engine.RegisterActivity("ActivityFoo",
[this](StopToken stop_token) {
static_cast<BizLogicIf&>(this->m_logic).ActivityFoo(stop_token);
}, m_custom_success_handler, m_custom_error_handler);
engine.RegisterActivity("ActivityBar",
[this](StopToken stop_token) {
std::string args = m_tmp_bar_request->GetRequestPayload();
JsonPayload arg = JsonPayload::parse(args);
static_cast<BizLogicIf&>(this->m_logic).ActivityBar(stop_token, arg);
}, m_custom_success_handler, m_custom_error_handler);
}
protected:
std::optional<rad::cii::Request<std::string>> m_tmp_foo_request;
std::optional<rad::cii::Request<std::string, std::string>> m_tmp_bar_request;
std::function<void()> m_custom_success_handler;
std::function<void(std::runtime_error const&)> m_custom_error_handler;
};
Custom Business Logic¶
To create an RTC component that makes use of the life-cycle extension instrument RTC developers
need to specify the desired component life-cycle in the life-cycle expression and then implement
the resulting Business Logic Interface accordingly. In the provided example this
is done in file businessLogic.hpp
.
#include "rtctk/componentFramework/rtcComponent.hpp"
#include "rtctk/componentFramework/runnable.hpp"
#include "rtctk/componentFramework/optimisable.hpp"
#include "customLifeCycle.hpp"
using namespace rtctk::componentFramework;
namespace rtctk::exampleCustom {
// This is the life-cycle expression!
using LifeCycle = CustomLifeCycle<Optimisable<Runnable<RtcComponent>>>;
class BusinessLogic : public LifeCycle::BizLogicIf
{
public:
using ComponentType = LifeCycle;
BusinessLogic(const std::string& name, ServiceContainer& services);
virtual ~BusinessLogic() = default;
void ActivityStarting(StopToken st) override;
void ActivityInitialising(StopToken st) override;
void ActivityEnabling(StopToken st) override;
void ActivityDisabling(StopToken st) override;
void ActivityGoingRunning(StopToken st) override;
void ActivityGoingIdle(StopToken st) override;
void ActivityRunning(StopToken st) override;
void ActivityRecovering(StopToken st) override;
void ActivityUpdating(StopToken st, Payload arg) override;
bool GuardUpdatingAllowed(Payload args) override;
void ActivityOptimising(StopToken st, JsonPayload const& arg) override;
bool GuardOptimisingAllowed(JsonPayload const& arg) override;
void ActivityFoo(StopToken st) override;
void ActivityBar(StopToken st, JsonPayload const& arg) override;
};
} // closing namespace
The listing above shows file businessLogic.hpp
. The user-provided business logic class
BusinessLogic
implements life-cycle CustomLifeCycle<Optimisable<Runnable<RtcComponent>>>
.
By adding the life-cycle extension Optimisable
additional commands, an orthogonal region with
new states and the virtual life-cycle methods ActivityOptimising
and GuardOptimisingAllowed
are added. By adding the CustomLifeCycle
the commands and activities from the custom life-cycle
extension become available.
The individual life-cycle methods are then implemented by component developers to provide custom
behavior. Here an example implementation for method ActivityOptimising
:
void BusinessLogic::ActivityOptimising(StopToken st, JsonPayload const& arg)
{
while(not st.StopRequested()) {
LOG4CPLUS_INFO(GetLogger(), "... still Optimising");
sleep_for(1s);
}
}
Compliance Testing¶
To ensure that the extended component life-cycle is still compatible with the default RTC component life-cycle the RTC Toolkit provides reusable unit tests that can be used for life-cycle compliance testing.
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "businessLogic.hpp"
using TypesToTest = testing::Types<rtctk::exampleCustom::LifeCycle>;
#include "rtctk/componentFramework/test/testOptimisableCompliance.hpp"
#include "rtctk/componentFramework/test/testRtcComponentCompliance.hpp"
#include "rtctk/componentFramework/test/testRunnableCompliance.hpp"
#include "testCustomLifeCycleCompliance.hpp"
The listing above shows an example unit test from file testLifeCycleCompliance.cpp
that makes
use of toolkit provided compliance tests to validate that the life-cycle of the custom component
rtctk::exampleCustom::LifeCycle
is indeed compatible with the RtcComponent
life-cycle
and also with the toolkit provided life-cycle extensions Runnable
and Optimisable
.
If required, developers can also include other pre-defined compliance tests. If the test succeeds
developers can be confident that adding a new life-cycle extension did not break compatibility
with the standard behaviour.
Instrument RTC developers are also invited to write compliance tests for their own life-cycle extensions in similar fashion as the toolkit provided compliance tests.
RTC Client Application¶
A custom client application needs to be created so that custom commands Foo
and Bar
can be
send to the custom component.
To develop a custom client application instrument RTC developers need to specialise the toolkit
provided CommandRequestor
class and register it to the toolkit provided client runner
method as a template method.
#include "rtctk/client/clientLib.hpp"
#include "rtctk/componentFramework/commandRequestor.hpp"
#include "Rtctkexif.hpp"
class MyCommandRequestor : public rtctk::componentFramework::CommandRequestor
{
public:
MyCommandRequestor(
const elt::mal::Uri& uri,
std::optional<std::chrono::milliseconds> timeout = std::nullopt)
: CommandRequestor(uri, timeout)
, m_custom_if(MakeInterface<rtctkexif::CustomCmdsAsync>("CustomCmds"))
{
RegisterCommand("Foo", [this](auto const& args) { return m_custom_if->Foo(); });
RegisterCommand("Bar", [this](auto const& args) { return m_custom_if->Bar(args); });
}
private:
std::unique_ptr<rtctkexif::CustomCmdsAsync> m_custom_if;
};
int main(int argc, char** argv)
{
return rtctk::client::RunClient<MyCommandRequestor>(argc, argv);
}
The listing above shows file main.cpp
of the custom client application. Instrument RTC developers
provide a custom CommandRequestor
such as class MyCommandRequestor
that instantiates the
MAL interface with MakeInterface
and registers commands individually with RegisterCommand
.
In main
the class is passed to the component runner function RunClient
as a template argument.