Creating a Simple RTC Component

This tutorial shows how to create and run a simple RTC Component application that is using the Runnable State Machine from the Component Framework.

Note

General information about RTC Components can be found in the section RTC Component and in the RTC Toolkit design document.

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 backends such as CII configuration service, CII OLDB, etc.

To create a simple component, developers first select the required component life-cycle and then 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).

The example component can be instantiated (started) several times using different component instance names and settings. As every other RTC Component, the state of the application can be steered using the toolkit-provided rtctkClient application to send state change commands.

To make life easier for developers, a working example is already provided with the RTC Toolkit source code in the directory _examples/exampleComponent.

Note

The example in _examples/exampleComponent is only provided with the source code. It is not installed when installing using RPM packages.

The example is provided as a waf package that is composed of the following waf modules:

  • app - The Example RTC Component application including the default configuration

  • 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:

$ rtctkExampleComponent.sh run

The example will bring up two RTC component instances and let them step through their life-cycles. 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.

To step, manually, through the component life-cycle use the following sequence of commands:

# Deploy and start the example applications
rtctkExampleComponent.sh provision
rtctkExampleComponent.sh deploy

# From a second terminal, use the client to step through the life-cycle of the respective component instance
rtctkExampleComponent.sh send rtc_component_1 Init
rtctkExampleComponent.sh send rtc_component_1 Enable
rtctkExampleComponent.sh send rtc_component_1 Run
rtctkExampleComponent.sh send rtc_component_1 Idle
rtctkExampleComponent.sh send rtc_component_1 Disable
rtctkExampleComponent.sh send rtc_component_1 Reset

# Gracefully terminate the applcations and clean-up
rtctkExampleComponent.sh undeploy
rtctkExampleComponent.sh unprovision

In the provision phase, the application environment is prepared by resolving environment variables and by copying certain YAML files into the respective run-directory in $INTROOT/run/.

Then, in the deploy phase, 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 like Init, Enable, Run, etc.

In the undeploy and unprovision 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 simple RTC Component application from scratch.

A minimal RTC Component application consists of the following parts:

  • Build Script

  • Business Logic Class with life-cycle definition

  • main.cpp

  • Config Files

Build Script

The wscript contains rules on how to build the example component application.

from wtools.module import declare_cprogram

declare_cprogram(target='rtctkExampleComponent', use='componentFramework.rtcComponent')

Note

To understand and work with waf build scripts, basic knowledge on waf and wtools is required.

Business Logic Class

To create a custom RTC component developers need to specify the desired component life-cycle 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/runtimeRepoIf.hpp>
#include <rtctk/componentFramework/oldbIf.hpp>


namespace rtctk::exampleComponent {

    using namespace rtctk::componentFramework;

    using LifeCycle = 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, const JsonPayload& args) override;
        bool GuardUpdatingAllowed(const JsonPayload& args) override;

    private:
        log4cplus::Logger& m_logger;
        std::string m_name;
        RuntimeRepoIf& m_rtr;
        OldbIf& m_oldb;
    };

}  // namespace rtctk::exampleComponent

The listing above shows the file businessLogic.hpp. The user-provided business logic class BusinessLogic implements the life-cycle of a Runnable<RtcComponent> which makes use of the runnable state machine from the design document.

In the corresponding implementation file - businessLogic.cpp - important contextual information and services, such as the component instance name or a handle to the Runtime Repository, are passed to the Business Logic via the constructor.

Individual life-cycle methods can be implemented by component developers to provide custom behavior. Logging functionality is globally available when including rtctk/componentFramwork/logger.hpp.

BusinessLogic::BusinessLogic(const std::string& name, ServiceContainer& services)
    : m_logger(GetLogger("app"))
    , m_name(name)
    // retrieve handle to the runtime repository adapter from the service container
    , m_rtr(services.Get<RuntimeRepoIf>())
    // retrieve handle to the online database adapter from the service container
    , m_oldb(services.Get<OldbIf>()) {
}

During ActivityInitialising configuration parameters are read from the Runtime Repository and Monitoring Datapoints are created:

void BusinessLogic::ActivityInitialising(StopToken st) {
    // use the runtime repository adapter to read configuration parameters
    DataPointPath cfg_dp_name = DataPointPath("/" + m_name + "/static/parameter");
    auto parameter_value = m_rtr.GetDataPoint<std::string>(cfg_dp_name);
    LOG4CPLUS_INFO(m_logger, "Parameter value: " << parameter_value);

    // use the online database adapter to create and set monitoring datapoints
    DataPointPath mon_dp_name = DataPointPath("/" + m_name + "/mon_dp");
    m_oldb.CreateDataPoint<int32_t>(mon_dp_name);
    m_oldb.SetDataPoint<int32_t>(mon_dp_name, 42);
}

Note

In the businessLogic.cpp file of this example some lines are commented out, this was done on purpose. To enable the functionality there, feel free to comment in these line and recompile.

In ActivityRunning the component fulfills its main job. In this example, it only emits a log message every 500 ms:

void BusinessLogic::ActivityRunning(StopToken st) {
    while (not st.StopRequested()) {
        LOG4CPLUS_INFO(m_logger, "... still Running");
        sleep_for(500ms);
    }
}

main.cpp

In main.cpp the RTC Component is set up and connected with the respective Business Logic class.

To run the Business Logic as an RTC Component, main.cpp must be implemented in a specific way:

#include <rtctk/componentFramework/rtcComponentMain.hpp>
#include "businessLogic.hpp"

using namespace rtctk::componentFramework;
using namespace rtctk::exampleComponent;

void RtcComponentMain(const Args& args) {
    RunAsRtcComponent<BusinessLogic>(args);
}
  • RtcComponentMain() is the main entry point for user code. It is called by main(), which is owned by the toolkit to be able to do arbitrary work before and after user code is being executed.

  • RunAsRtcComponent<T>() is a template function that executes the specified Business Logic as an RTC Component. It will construct the Business Logic class and then call the respective life-cycle methods on command reception.

To provide more flexibility, the function RunAsRtcComponent() can also take a business logic factory method as an optional argument. This sort of dependency injection allows defining a Business Logic class with a custom constructor, which may be useful for separation of concerns and testing.

Note

The provided example code shows different ways to customize the Business Logic class using e.g. dependency injection, inheritance or templates. The examples can be activated by uncommenting the respective #define in main.cpp.

Config Files

To be able to instantiate and run the component some initial configuration is required. Currently such configuration is provided using yaml files.

Here is an example of the service discovery configuration file service_disc.yaml.

common:
    persistent_repo_endpoint: !cfg.type:string cii.config://local/persistent_repo
    runtime_repo_endpoint: !cfg.type:string rtr://127.0.0.1:8000/rtr_topic
    oldb_endpoint: !cfg.type:string cii.oldb:/example_component/mon
rtc_component_1:
    req_rep_endpoint: !cfg.type:string zpb.rr://127.0.0.1:12081/
    pub_sub_endpoint: !cfg.type:string zpb.ps://127.0.0.1:12082/
rtc_component_2:
    req_rep_endpoint: !cfg.type:string zpb.rr://127.0.0.1:12083/
    pub_sub_endpoint: !cfg.type:string zpb.ps://127.0.0.1:12084/

More information about service discovery can be found in section Service Discovery.

In case the component also makes use of the Runtime Repository or the OLDB, further yaml files may be required to create datapoints initially. See also Runtime Configuration Repository.