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.

Important

Before reading this tutorial make sure that you have read and understood the basic tutorial Creating a Simple RTC Component.

Note

For simplicity reasons, the example uses the file based implementation of OLDB, Persistent and Runtime Configuration Repositories; as well as file based service discovery. This means that the underlying format of configuration and data points is different than the one used when the above mentioned services are used with the standard back-ends such as CII configuration service, CII OLDB, etc.

To create a RTC component application that makes use 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 configuration

  • client - A custom RTC Client Application

  • interface - Custom CII MAL interface definitions

  • scripts - Helper scripts for deployment and control

Running the Example

After installing the RTC Toolkit (see Installation) the executables of the working example 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 $RTC_LOGS or if it is not set in: $INTROOT/logsink. After about 35 seconds the example will terminate gracefully.

Note

The commands indicated above, e.g. when using undeploy, may generate the following output:

rtctkExampleCustom: no process found

This is expected and should not be treated as an indication of failure. The same applies for similar commands in the rest of this tutorial.

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="rtctkif.xml"/>
  <package name="rtctkexif">
    <interface name="CustomCmds">
      <method name="Foo" returnType="string" throws="::rtctkif::ExceptionErr"/>
      <method name="Bar" returnType="string" throws="::rtctkif::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: std::exception_ptr
                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](std::exception_ptr eptr) {
            this->m_engine.PostEvent(std::make_unique<events::CustomError>(eptr));
        };

        // 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& eptr = ev.GetPayload();
                assert(m_tmp_foo_request);
                m_tmp_foo_request->SetException(
                    RequestFailed(NestedExceptionPrinter(eptr).Str()));
                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& eptr = ev.GetPayload();
                assert(m_tmp_bar_request);
                m_tmp_bar_request->SetException(
                    RequestFailed(NestedExceptionPrinter(eptr).Str()));
                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::exception_ptr)> 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 is an example implementation for the 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.