Developer Guide

The following sections explain, how to consume the Vector SIL Kit library in your own application and how to use the SIL Kit API to communicate with other participants of a simulation.

Getting the SIL Kit

Precompiled SIL Kit packages are regularly released on GitHub (https://github.com/vectorgrp/sil-kit/releases). If you need to build the library yourself, take a look at the readme file of the repository’s root folder.

Architecture

The SIL Kit implements a layered architecture:

../_images/SilKitArchitecture.svg

Prerequisites for Compilation

  • For Windows:
    • Visual Studio 2017 (toolset v141) and higher

  • For Ubuntu 18.04 LTS:
    • GCC 8 or

    • Clang

Writing your first SIL Kit application

This tutorial assumes that you are familiar with CMake (https://cmake.org) and C++.

Using the SIL Kit package

The SIL Kit distribution contains a self-contained and deployable installation in the SilKit directory. The CMake build configuration required is exported to SilKit/lib/cmake/SilKit and defines the SilKit::SilKit target.

From CMake this can be consumed via the find_package(SilKit CONFIG) mechanism. For example, the following CMakeLists.txt imports the SIL Kit library based on its file system path.

Project(SampleSilKit)
cmake_minimum_required(VERSION 3.0)
set(CMAKE_BUILD_TYPE Debug)
find_package(SilKit 
    REQUIRED CONFIG
    PATHS "${CMAKE_CURRENT_LIST_DIR}/SilKit"
)
find_package(Threads REQUIRED)
add_executable(${PROJECT_NAME} simple.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE SilKit::SilKit Threads::Threads)

Properties, like include directories and compile flags, are automatically handled by the imported target. If you use another method to build your software you can directly use the SilKit/include and SilKit/lib directories for C++ headers and libraries.

A simple Publish / Subscribe application

We’ll create a simple, self-contained SIL Kit application that uses Publish/Subscribe to exchange user-defined data between two participants. In our C++ file simple.cpp, we include the headers and define namespaces and constants:

#include <iostream>
#include <thread>

#include "silkit/SilKit.hpp"
#include "silkit/services/all.hpp"
#include "silkit/services/orchestration/string_utils.hpp"

using namespace std::chrono_literals;

const auto registryUri = "silkit://localhost:8500";

SIL Kit participants are created with a configuration that is used to change certain aspects of the simulation without recompiling the application.

Note

SIL Kit applications should be able to run without a configuration file. However, applications should allow users to provide one in case they want to reconfigure the behavior of the application (see The Participant Configuration File for details).

This can be done by loading an existing YAML file. Here, we use the configuration file simple.yaml to configure a logger that logs all error messages to a file:

Logging:
  Sinks:
  - Type: File
    Level: Warn
    LogName: SimpleParticipantLog

We load it in the main function of our code:

int main(int argc, char** argv)
{
    auto config = SilKit::Config::ParticipantConfigurationFromFile("simple.yaml");
    // TODO: Use config to create participants
}

The application will run two participants concurrently, each in its own thread. One thread will act as a publisher by sending a test string to its subscribers:

void publisher_main(std::shared_ptr<SilKit::Config::IParticipantConfiguration> config)
{
    auto participant = SilKit::CreateParticipant(config, "PublisherParticipant", registryUri);

    auto* lifecycleService =
        participant->CreateLifecycleService({SilKit::Services::Orchestration::OperationMode::Coordinated});

    SilKit::Services::PubSub::PubSubSpec pubSubSpec{"DataService", "text/plain"};
    auto* publisher = participant->CreateDataPublisher("PublisherController", pubSubSpec);

    auto* timeSyncService = lifecycleService->CreateTimeSyncService();
    timeSyncService->SetSimulationStepHandler(
        [publisher](std::chrono::nanoseconds now, std::chrono::nanoseconds /*duration*/) {
            // Generate some data
            static auto msgIdx = 0;
            std::string message = "DataService Msg" + std::to_string(msgIdx++);
            std::vector<uint8_t> data{message.begin(), message.end()};

            // Publish the raw bytes of the message to all subscribers
            publisher->Publish(std::move(data));

            // Delay the simulation
            std::this_thread::sleep_for(1s);
        },
        1ms);

    try
    {
        // Run the simulation main loop until stopped by the sil-kit-system-controller
        auto result = lifecycleService->StartLifecycle();
        std::cout << "Publisher: result: " << result.get() << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Publisher exception caught: " << e.what() << std::endl;
    }
}

Initially, the simulation is joined by creating the participant called “PublisherParticipant”. This properly initializes the SIL Kit library; enables the instantiation of Services and offers access to the Lifecycle Service, which controls the orchestration of our simulation. Next, we create a publisher for the DataService topic. Later, we subscribe to the same topic name in our subscriber to enable communication between the participants. The actual simulation is performed in the simulation task. This is a callback that is executed by the SIL Kit runtime whenever the simulation time advances. This callback has to be registered with the time synchronization service’s SetSimulationStepHandler(). We hand over the publisher object in the capture list of our simulation task and use it to send data through its Publish() method.

The subscriber runs in its own thread, too:

void subscriber_main(std::shared_ptr<SilKit::Config::IParticipantConfiguration> config)
{
    auto participant = SilKit::CreateParticipant(config, "SubscriberParticipant", registryUri);

    auto* lifecycleService =
        participant->CreateLifecycleService({SilKit::Services::Orchestration::OperationMode::Coordinated});

    SilKit::Services::PubSub::PubSubSpec pubSubSpec{"DataService", "text/plain"};
    auto receptionHandler = [](auto* subscriber, const auto& dataMessageEvent) {
        std::string message{dataMessageEvent.data.begin(), dataMessageEvent.data.end()};
        std::cout << " <- Received data=\"" << message << "\"" << std::endl;
    };
    auto* subscriber = participant->CreateDataSubscriber("SubscriberController", pubSubSpec, receptionHandler);

    auto* timeSyncService = lifecycleService->CreateTimeSyncService();
    timeSyncService->SetSimulationStepHandler(
        [](std::chrono::nanoseconds /*now*/, std::chrono::nanoseconds /*duration*/) {
            // Simulation task must be defined, even an empty one
        },
        1ms);

    try
    {
        // Run the simulation main loop until stopped by the sil-kit-system-controller
        auto result = lifecycleService->StartLifecycle();
        std::cout << "Subscriber: result: " << result.get() << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Subscriber exception caught: " << e.what() << std::endl;
    }
}

The setup is similar to the publisher, except that we instantiate a subscriber interface. This allows us to register a SetDataMessageHandler() callback to receive data value updates. The simulation task has to be defined, even though no simulation work is performed.

We extend our main function to spawn both threads and join them again once finished. Also, we use a try-catch block here to get proper error handling e.g., if the configuration file cannot be loaded.

int main(int argc, char** argv)
{
    try
    {
        // Load the YAML configuration
        auto config = SilKit::Config::ParticipantConfigurationFromFile("simple.yaml");

        // Launch the participant threads
        std::thread publisher{publisher_main, config};
        std::thread subscriber{subscriber_main, config};

        // Once finished, close the threads
        if (subscriber.joinable())
            subscriber.join();
        if (publisher.joinable())
            publisher.join();
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

The application is built with CMake on the command line (from a build directory) by calling cmake .. to generate and then build via cmake --build .. A more convenient way is to open the folder in an IDE with CMake support. To run this sample, copy the shared library files (e.g., on Windows the SilKit.dll, SilKitd.dll from SilKit/bin) and the simple.yaml next to the compiled executable.

Running the simulation

Our sample needs the utility processes sil-kit-registry and sil-kit-system-controller to run. The registry is required for participant discovery. The sil-kit-system-controller takes the participant names as command line arguments, initializes the connected participants and starts the simulation until the return key is pressed. For convenience and to reduce code duplication, these utility programs are implemented in separate executables and distributed in binary forms.

The final simulation setup can be run through the following commands:

# Start the Middleware Registry
./sil-kit-registry.exe

# Start the System Controller and tell it to wait for PublisherParticipant and SubscriberParticipant
./sil-kit-system-controller.exe PublisherParticipant SubscriberParticipant

# Start the application running the two participants
# Make sure that the SilKit.dll and simple.yaml are available
./SampleSilKit.exe

The complete source code of this sample can be found here: CMakeLists.txt simple.cpp simple.yaml