From 736bebeec3c37564f754f2354c1ffc090601f16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 5 Nov 2024 19:29:43 +0100 Subject: [PATCH 01/12] demos: Use BaseApplication to condense common demo features Demos: Overhaul SilKitApplication Demos: Add signal handling, more command line args, renaming demos: Improve application demo base demos: Remove IApp.hpp, pure virtual methods are defined in BaseApplication demos: Add license headers demos: Fix include demos: Include cctype demos: Add SimpleCan Demo demos: Remove CanDemo, restructure new Can Demos demos: Add sample participant configurations demos: Reorganized common CAN behavior; Review suggestions demos: Fix command line help message demos: CanWriter: Always send CAN FD; ApplicationBase: Fix command line help demos: Use arg struct at construction; Always send CAN FD in demo demos: Always run as-fast-as-possible with --config --- Demos/CMakeLists.txt | 4 +- Demos/Can/CMakeLists.txt | 31 +- Demos/Can/CanDemo.cpp | 287 -------- Demos/Can/CanDemoCommon.hpp | 46 ++ Demos/Can/CanReaderDemo.cpp | 71 ++ Demos/Can/CanWriterDemo.cpp | 104 +++ Demos/Can/NetworkSimulatorConfig.yaml | 5 - Demos/Can/SimpleCan.cpp | 84 +++ .../FileLog_Trace.silkit.yaml | 6 + .../FileLog_Trace_FromRemotes.silkit.yaml | 7 + .../NetworkSimulatorConfig.yaml | 27 + .../Stdout_Info.silkit.yaml} | 4 +- .../Trace_ToRemote.silkit.yaml | 5 + Demos/include/ApplicationBase.hpp | 643 ++++++++++++++++++ Demos/include/CommandlineParser.hpp | 408 +++++++++++ Demos/include/SignalHandler.hpp | 254 +++++++ 16 files changed, 1667 insertions(+), 319 deletions(-) delete mode 100644 Demos/Can/CanDemo.cpp create mode 100644 Demos/Can/CanDemoCommon.hpp create mode 100644 Demos/Can/CanReaderDemo.cpp create mode 100644 Demos/Can/CanWriterDemo.cpp delete mode 100644 Demos/Can/NetworkSimulatorConfig.yaml create mode 100644 Demos/Can/SimpleCan.cpp create mode 100644 Demos/SampleConfigurations/FileLog_Trace.silkit.yaml create mode 100644 Demos/SampleConfigurations/FileLog_Trace_FromRemotes.silkit.yaml create mode 100644 Demos/SampleConfigurations/NetworkSimulatorConfig.yaml rename Demos/{Can/DemoCan.silkit.yaml => SampleConfigurations/Stdout_Info.silkit.yaml} (52%) create mode 100644 Demos/SampleConfigurations/Trace_ToRemote.silkit.yaml create mode 100644 Demos/include/ApplicationBase.hpp create mode 100644 Demos/include/CommandlineParser.hpp create mode 100644 Demos/include/SignalHandler.hpp diff --git a/Demos/CMakeLists.txt b/Demos/CMakeLists.txt index 86f910101..a21576380 100755 --- a/Demos/CMakeLists.txt +++ b/Demos/CMakeLists.txt @@ -97,6 +97,8 @@ else() set(_DEMO_OUTPUT_DIR "${CMAKE_BINARY_DIR}/$") endif() +set(SILKIT_DEMO_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + macro(make_silkit_demo executableName demoSourceFile) add_executable(${executableName} ${demoSourceFile} @@ -124,7 +126,7 @@ macro(make_silkit_demo executableName demoSourceFile) SilKit::SilKit Threads::Threads ) - + target_include_directories(${executableName} PRIVATE ${SILKIT_DEMO_DIR}/include) if(MSVC) target_compile_options(${executableName} PRIVATE /wd4996) endif() diff --git a/Demos/Can/CMakeLists.txt b/Demos/Can/CMakeLists.txt index 4d421494b..84498672e 100644 --- a/Demos/Can/CMakeLists.txt +++ b/Demos/Can/CMakeLists.txt @@ -1,27 +1,8 @@ -# Copyright (c) 2022 Vector Informatik GmbH -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +# +# SPDX-License-Identifier: MIT -make_silkit_demo(SilKitDemoCan CanDemo.cpp) +make_silkit_demo(SilKitDemoSimpleCan SimpleCan.cpp) +make_silkit_demo(SilKitDemoCanWriter CanWriterDemo.cpp) +make_silkit_demo(SilKitDemoCanReader CanReaderDemo.cpp) -target_sources(SilKitDemoCan - PRIVATE DemoCan.silkit.yaml - PRIVATE NetworkSimulatorConfig.yaml -) diff --git a/Demos/Can/CanDemo.cpp b/Demos/Can/CanDemo.cpp deleted file mode 100644 index 5cef427fd..000000000 --- a/Demos/Can/CanDemo.cpp +++ /dev/null @@ -1,287 +0,0 @@ -/* Copyright (c) 2022 Vector Informatik GmbH - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "silkit/SilKit.hpp" -#include "silkit/services/logging/ILogger.hpp" -#include "silkit/services/orchestration/all.hpp" -#include "silkit/services/orchestration/string_utils.hpp" -#include "silkit/services/can/all.hpp" -#include "silkit/services/can/string_utils.hpp" - - -using namespace SilKit::Services::Orchestration; -using namespace SilKit::Services::Can; -using namespace SilKit::Services::Logging; - -using namespace std::chrono_literals; - -namespace std { -namespace chrono { -std::ostream& operator<<(std::ostream& out, nanoseconds timestamp) -{ - auto seconds = std::chrono::duration_cast>>(timestamp); - out << seconds.count() << "s"; - return out; -} -} // namespace chrono -} // namespace std - -void FrameTransmitHandler(const CanFrameTransmitEvent& ack, ILogger* logger) -{ - std::stringstream buffer; - buffer << ">> " << ack.status << " for CAN frame with timestamp=" << ack.timestamp - << " and userContext=" << ack.userContext; - logger->Info(buffer.str()); -} - -void FrameHandler(const CanFrameEvent& frameEvent, ILogger* logger) -{ - std::string payload(frameEvent.frame.dataField.begin(), frameEvent.frame.dataField.end()); - std::stringstream buffer; - buffer << ">> CAN frame: canId=" << frameEvent.frame.canId << " timestamp=" << frameEvent.timestamp << " \"" - << payload << "\""; - logger->Info(buffer.str()); -} - -void SendFrame(ICanController* controller, ILogger* logger) -{ - CanFrame canFrame{}; - canFrame.canId = 3; - canFrame.flags |= static_cast(CanFrameFlag::Fdf) // FD Format Indicator - | static_cast(CanFrameFlag::Brs); // Bit Rate Switch (for FD Format only) - - static int msgId = 0; - const auto currentMessageId = msgId++; - - std::stringstream payloadBuilder; - payloadBuilder << "CAN " << (currentMessageId % 100); - auto payloadStr = payloadBuilder.str(); - - std::vector payloadBytes; - payloadBytes.resize(payloadStr.size()); - std::copy(payloadStr.begin(), payloadStr.end(), payloadBytes.begin()); - - canFrame.dataField = payloadBytes; - canFrame.dlc = static_cast(canFrame.dataField.size()); - - void* const userContext = reinterpret_cast(static_cast(currentMessageId)); - - controller->SendFrame(std::move(canFrame), userContext); - std::stringstream buffer; - buffer << "<< CAN frame sent with userContext=" << userContext; - logger->Info(buffer.str()); -} - -/************************************************************************************************** - * Main Function - **************************************************************************************************/ - -int main(int argc, char** argv) -{ - if (argc < 3) - { - std::cerr << "Missing arguments! Start demo with: " << argv[0] - << " [RegistryUri] [--async]" << std::endl - << "Use \"CanWriter\" or \"CanReader\" as ." << std::endl; - return -1; - } - - if (argc > 5) - { - std::cerr << "Too many arguments! Start demo with: " << argv[0] - << " [RegistryUri] [--async]" << std::endl - << "Use \"CanWriter\" or \"CanReader\" as ." << std::endl; - return -1; - } - - std::string participantName(argv[2]); - - if (participantName != "CanWriter" && participantName != "CanReader") - { - std::cout << "Wrong participant name provided. Use either \"CanWriter\" or \"CanReader\"." << std::endl; - return -1; - } - - try - { - std::string participantConfigurationFilename(argv[1]); - - std::string registryUri = "silkit://localhost:8500"; - - bool runSync = true; - - std::vector args; - std::copy((argv + 3), (argv + argc), std::back_inserter(args)); - - for (auto arg : args) - { - if (arg == "--async") - { - runSync = false; - } - else - { - registryUri = arg; - } - } - - auto participantConfiguration = - SilKit::Config::ParticipantConfigurationFromFile(participantConfigurationFilename); - auto sleepTimePerTick = 1000ms; - - std::cout << "Creating participant '" << participantName << "' with registry " << registryUri << std::endl; - - auto participant = SilKit::CreateParticipant(participantConfiguration, participantName, registryUri); - - auto* logger = participant->GetLogger(); - auto* canController = participant->CreateCanController("CAN1", "CAN1"); - - canController->AddFrameTransmitHandler([logger](ICanController* /*ctrl*/, const CanFrameTransmitEvent& ack) { - FrameTransmitHandler(ack, logger); - }); - canController->AddFrameHandler( - [logger](ICanController* /*ctrl*/, const CanFrameEvent& frameEvent) { FrameHandler(frameEvent, logger); }); - - auto operationMode = (runSync ? OperationMode::Coordinated : OperationMode::Autonomous); - - auto* lifecycleService = participant->CreateLifecycleService({operationMode}); - - // Observe state changes - lifecycleService->SetStopHandler([]() { std::cout << "Stop handler called" << std::endl; }); - lifecycleService->SetShutdownHandler([]() { std::cout << "Shutdown handler called" << std::endl; }); - lifecycleService->SetAbortHandler( - [](auto lastState) { std::cout << "Abort handler called while in state " << lastState << std::endl; }); - - if (runSync) - { - lifecycleService->SetCommunicationReadyHandler([canController, &participantName]() { - std::cout << "Communication ready for " << participantName << std::endl; - canController->SetBaudRate(10'000, 1'000'000, 2'000'000); - canController->Start(); - }); - - auto* timeSyncService = lifecycleService->CreateTimeSyncService(); - - if (participantName == "CanWriter") - { - timeSyncService->SetSimulationStepHandler( - [canController, logger, sleepTimePerTick](std::chrono::nanoseconds now, - std::chrono::nanoseconds duration) { - std::cout << "now=" << now << ", duration=" << duration << std::endl; - SendFrame(canController, logger); - std::this_thread::sleep_for(sleepTimePerTick); - }, - 5ms); - } - else - { - timeSyncService->SetSimulationStepHandler( - [sleepTimePerTick](std::chrono::nanoseconds now, std::chrono::nanoseconds duration) { - std::cout << "now=" << now << ", duration=" << duration << std::endl; - std::this_thread::sleep_for(sleepTimePerTick); - }, 5ms); - } - - auto finalStateFuture = lifecycleService->StartLifecycle(); - auto finalState = finalStateFuture.get(); - - std::cout << "Simulation stopped. Final State: " << finalState << std::endl; - std::cout << "Press enter to end the process..." << std::endl; - std::cin.ignore(); - } - else - { - std::atomic isStopRequested = {false}; - std::thread workerThread; - - std::promise promiseObj; - std::future futureObj = promiseObj.get_future(); - lifecycleService->SetCommunicationReadyHandler([&]() { - std::cout << "Communication ready for " << participantName << std::endl; - canController->SetBaudRate(10'000, 1'000'000, 2'000'000); - - workerThread = std::thread{[&]() { - futureObj.get(); - while (lifecycleService->State() == ParticipantState::ReadyToRun - || lifecycleService->State() == ParticipantState::Running) - { - if (participantName == "CanWriter") - { - SendFrame(canController, logger); - } - std::this_thread::sleep_for(sleepTimePerTick); - } - if (!isStopRequested) - { - std::cout << "Press enter to end the process..." << std::endl; - } - }}; - canController->Start(); - }); - - lifecycleService->SetStartingHandler([&]() { promiseObj.set_value(); }); - - lifecycleService->StartLifecycle(); - std::cout << "Press enter to leave the simulation..." << std::endl; - std::cin.ignore(); - - isStopRequested = true; - if (lifecycleService->State() == ParticipantState::Running - || lifecycleService->State() == ParticipantState::Paused) - { - std::cout << "User requested to stop in state " << lifecycleService->State() << std::endl; - lifecycleService->Stop("User requested to stop"); - } - - if (workerThread.joinable()) - { - workerThread.join(); - } - std::cout << "The participant has shut down and left the simulation" << std::endl; - } - } - catch (const SilKit::ConfigurationError& error) - { - std::cerr << "Invalid configuration: " << error.what() << std::endl; - std::cout << "Press enter to end the process..." << std::endl; - std::cin.ignore(); - return -2; - } - catch (const std::exception& error) - { - std::cerr << "Something went wrong: " << error.what() << std::endl; - std::cout << "Press enter to end the process..." << std::endl; - std::cin.ignore(); - return -3; - } - - return 0; -} diff --git a/Demos/Can/CanDemoCommon.hpp b/Demos/Can/CanDemoCommon.hpp new file mode 100644 index 000000000..a75aa8680 --- /dev/null +++ b/Demos/Can/CanDemoCommon.hpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "silkit/services/can/all.hpp" +#include "silkit/services/can/string_utils.hpp" +#include "silkit/services/logging/ILogger.hpp" + +using namespace SilKit::Services::Can; + +std::ostream& operator<<(std::ostream& out, std::chrono::nanoseconds timestamp) +{ + auto seconds = std::chrono::duration_cast>>(timestamp); + out << seconds.count() << "s"; + return out; +} + +// This is the common behavior used in CanReaderDemo and CanWriterDemo +namespace CanDemoCommon { + +void FrameTransmitHandler(const CanFrameTransmitEvent& canFrameAck, ILogger* logger) +{ + // Log + std::stringstream buffer; + buffer << "Ack CAN frame, canId=" << canFrameAck.canId << ", status='" << canFrameAck.status << "'"; + logger->Info(buffer.str()); +} + +void FrameHandler(const CanFrameEvent& canFrameEvent, ILogger* logger) +{ + // Indicate frame type in log message + std::string frameTypeHint = ""; + if ((canFrameEvent.frame.flags & static_cast(CanFrameFlag::Fdf)) != 0) + { + frameTypeHint = "FD "; + } + + // Log + std::string payloadStr(canFrameEvent.frame.dataField.begin(), canFrameEvent.frame.dataField.end()); + std::stringstream buffer; + buffer << "Receive CAN " << frameTypeHint << "frame, canId=" << canFrameEvent.frame.canId << ", data='" + << payloadStr << "'"; + logger->Info(buffer.str()); +} + +} // namespace CanDemoBehavior diff --git a/Demos/Can/CanReaderDemo.cpp b/Demos/Can/CanReaderDemo.cpp new file mode 100644 index 000000000..db64d380a --- /dev/null +++ b/Demos/Can/CanReaderDemo.cpp @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "ApplicationBase.hpp" +#include "CanDemoCommon.hpp" + +using namespace std::chrono_literals; + +class CanReader: public ApplicationBase +{ +public: + // Inherit constructors + using ApplicationBase::ApplicationBase; + +private: + ICanController* _canController{nullptr}; + + std::string networkName = "CAN1"; + void AddCommandLineArgs() override + { + GetCommandLineParser()->Add( + "network", "n", networkName, "[--network ]", + "-n, --network: Name of the CAN network to use. Defaults to '" + networkName + "'."); + } + + void EvaluateCommandLineArgs() override + { + networkName = GetCommandLineParser()->Get("network").Value(); + } + + void CreateControllers() override + { + _canController = GetParticipant()->CreateCanController("CanController1", networkName); + + _canController->AddFrameTransmitHandler([this](ICanController* /*ctrl*/, const CanFrameTransmitEvent& ack) { + CanDemoCommon::FrameTransmitHandler(ack, GetLogger()); + }); + _canController->AddFrameHandler([this](ICanController* /*ctrl*/, const CanFrameEvent& frameEvent) { + CanDemoCommon::FrameHandler(frameEvent, GetLogger()); + }); + } + + void InitControllers() override + { + _canController->SetBaudRate(10'000, 1'000'000, 2'000'000); + _canController->Start(); + } + + void DoWorkSync(std::chrono::nanoseconds /*now*/) override + { + // NOP + } + + void DoWorkAsync() override + { + // NOP + } +}; + +int main(int argc, char** argv) +{ + Arguments args; + args.participantName = "CanReader"; + args.duration = 5ms; + CanReader app{args}; + app.SetupCommandLineArgs(argc, argv); + + return app.Run(); +} + diff --git a/Demos/Can/CanWriterDemo.cpp b/Demos/Can/CanWriterDemo.cpp new file mode 100644 index 000000000..f3d7c3ae7 --- /dev/null +++ b/Demos/Can/CanWriterDemo.cpp @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "ApplicationBase.hpp" +#include "CanDemoCommon.hpp" + +using namespace std::chrono_literals; + +class CanWriter: public ApplicationBase +{ +public: + // Inherit constructors + using ApplicationBase::ApplicationBase; + +private: + ICanController* _canController{nullptr}; + + std::string networkName = "CAN1"; + void AddCommandLineArgs() override + { + GetCommandLineParser()->Add( + "network", "n", networkName, "[--network ]", "-n, --network: Name of the CAN network to use. Defaults to '" + networkName + "'."); + } + + void EvaluateCommandLineArgs() override + { + networkName = GetCommandLineParser()->Get("network").Value(); + } + + void CreateControllers() override + { + _canController = GetParticipant()->CreateCanController("CanController1", networkName); + + _canController->AddFrameTransmitHandler([this](ICanController* /*ctrl*/, const CanFrameTransmitEvent& ack) { + CanDemoCommon::FrameTransmitHandler(ack, GetLogger()); + }); + _canController->AddFrameHandler([this](ICanController* /*ctrl*/, const CanFrameEvent& frameEvent) { + CanDemoCommon::FrameHandler(frameEvent, GetLogger()); + }); + } + + void InitControllers() override + { + _canController->SetBaudRate(10'000, 1'000'000, 2'000'000); + _canController->Start(); + } + + void SendFrame() + { + // Count up message id per frame + static uint64_t messageId = 0; + messageId++; + + // Build a CAN frame + CanFrame canFrame{}; + canFrame.canId = 3; + + // Cycle between normal and FD frames + canFrame.flags = static_cast(CanFrameFlag::Fdf) // FD Format Indicator + | static_cast(CanFrameFlag::Brs); // Bit Rate Switch (for FD Format only) + + + // Build a payload with the message Id + std::stringstream payloadBuilder; + payloadBuilder << "CAN " << messageId; + auto payloadStr = payloadBuilder.str(); + std::vector payloadBytes(payloadStr.begin(), payloadStr.end()); + canFrame.dataField = payloadBytes; + canFrame.dlc = static_cast(canFrame.dataField.size()); + + // Log + std::stringstream buffer; + buffer << "Send CAN FD frame, canId=" << canFrame.canId << ", data='" << payloadStr + << "' "; + GetLogger()->Info(buffer.str()); + + // Send + _canController->SendFrame(std::move(canFrame)); + } + + void DoWorkSync(std::chrono::nanoseconds /*now*/) override + { + SendFrame(); + } + + void DoWorkAsync() override + { + SendFrame(); + } + +}; + +int main(int argc, char** argv) +{ + Arguments args; + args.participantName = "CanWriter"; + args.duration = 5ms; + CanWriter app{args}; + app.SetupCommandLineArgs(argc, argv); + + return app.Run(); +} + diff --git a/Demos/Can/NetworkSimulatorConfig.yaml b/Demos/Can/NetworkSimulatorConfig.yaml deleted file mode 100644 index 64e7871ec..000000000 --- a/Demos/Can/NetworkSimulatorConfig.yaml +++ /dev/null @@ -1,5 +0,0 @@ -Description: Network simulator configuration for a detailed simulation of the CAN Demo -SchemaVersion: '1' -SimulatedNetworks: - - Name: CAN1 - Type: CAN diff --git a/Demos/Can/SimpleCan.cpp b/Demos/Can/SimpleCan.cpp new file mode 100644 index 000000000..dc5cba0e2 --- /dev/null +++ b/Demos/Can/SimpleCan.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include + +#include "silkit/SilKit.hpp" + +using namespace SilKit::Services::Can; +using namespace std::chrono_literals; + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Wrong number of arguments! Start demo with: " << argv[0] << " " << std::endl; + return -1; + } + std::string participantName(argv[1]); + + try + { + // Setup participant, lifecycle, time synchronization and logging + const std::string registryUri = "silkit://localhost:8500"; + const std::string configString = R"({"Logging":{"Sinks":[{"Type":"Stdout","Level":"Info"}]}})"; + auto participantConfiguration = SilKit::Config::ParticipantConfigurationFromString(configString); + auto participant = SilKit::CreateParticipant(participantConfiguration, participantName, registryUri); + auto* lifecycleService = + participant->CreateLifecycleService({SilKit::Services::Orchestration::OperationMode::Coordinated}); + auto* timeSyncService = lifecycleService->CreateTimeSyncService(); + auto* logger = participant->GetLogger(); + + // CAN controller + auto* canController = participant->CreateCanController("CanController1", "CAN1"); + + canController->AddFrameTransmitHandler([logger](ICanController* /*ctrl*/, const CanFrameTransmitEvent& ack) { + logger->Info("CAN frame transmit acknowledge"); + }); + canController->AddFrameHandler([logger](ICanController* /*ctrl*/, const CanFrameEvent& canFrameEvent) { + std::string payload(canFrameEvent.frame.dataField.begin(), canFrameEvent.frame.dataField.end()); + logger->Info("CAN frame received: '" + payload + "'"); + }); + + // Initialize CAN controller + lifecycleService->SetCommunicationReadyHandler([canController]() { + canController->SetBaudRate(10'000, 1'000'000, 2'000'000); + canController->Start(); + }); + + // Simulation steps + const auto stepSize = 1ms; + timeSyncService->SetSimulationStepHandler( + [participantName, canController, logger](std::chrono::nanoseconds now, std::chrono::nanoseconds duration) { + // Send CAN Frame every 10 seconds + if (now.count() % std::chrono::nanoseconds(10s).count() == 0) + { + logger->Info("--------- T = " + std::to_string(now.count() / 1000000000) + "s ---------"); + + // Create CAN frame with dynamic content + const std::string payloadStr = "Data from " + participantName + ": " + std::to_string(std::rand()); + std::vector payloadBytes(payloadStr.begin(), payloadStr.end()); + CanFrame canFrame{}; + canFrame.canId = 1; + canFrame.dataField = std::move(payloadBytes); + canFrame.dlc = static_cast(canFrame.dataField.size()); + + // Send frame (move ownership of canFrame to avoid additional copy) + logger->Info("Send CAN frame: '" + payloadStr + "'"); + canController->SendFrame(std::move(canFrame)); + } + }, stepSize); + + // Start and wait + auto finalStateFuture = lifecycleService->StartLifecycle(); + finalStateFuture.get(); + } + catch (const std::exception& error) + { + std::cerr << "Something went wrong: " << error.what() << std::endl; + return -3; + } + + return 0; +} diff --git a/Demos/SampleConfigurations/FileLog_Trace.silkit.yaml b/Demos/SampleConfigurations/FileLog_Trace.silkit.yaml new file mode 100644 index 000000000..d4e48e804 --- /dev/null +++ b/Demos/SampleConfigurations/FileLog_Trace.silkit.yaml @@ -0,0 +1,6 @@ +Description: Logging to File with Trace level +Logging: + Sinks: + - Level: Trace + Type: File + LogName: TraceLog diff --git a/Demos/SampleConfigurations/FileLog_Trace_FromRemotes.silkit.yaml b/Demos/SampleConfigurations/FileLog_Trace_FromRemotes.silkit.yaml new file mode 100644 index 000000000..3f0a57d20 --- /dev/null +++ b/Demos/SampleConfigurations/FileLog_Trace_FromRemotes.silkit.yaml @@ -0,0 +1,7 @@ +Description: Logging to file with Trace Level from remotes +Logging: + LogFromRemotes: True + Sinks: + - Level: Trace + Type: File + LogName: TraceLogFromRemotes diff --git a/Demos/SampleConfigurations/NetworkSimulatorConfig.yaml b/Demos/SampleConfigurations/NetworkSimulatorConfig.yaml new file mode 100644 index 000000000..a1152ec3b --- /dev/null +++ b/Demos/SampleConfigurations/NetworkSimulatorConfig.yaml @@ -0,0 +1,27 @@ +SchemaVersion: 1 +Description: Small sample configuration for testing purposes, with Link names from the SILKIT Demos +SimulationStepSizeNanoseconds: 1000000 +SimulatedNetworks: + - Name: CAN1 + Type: CAN + - Name: LIN1 + Type: LIN + - Name: Eth1 + Type: Ethernet + - Name: FS-Port0 + Type: Ethernet + - Name: FS-Port1 + Type: Ethernet + - Name: PowerTrain1 + Type: FlexRay +Switches: + - Name: FrontSwitch + Ports: + - Name: Port0 + Network: FS-Port0 + VlanIds: + - 1 + - Name: Port1 + Network: FS-Port1 + VlanIds: + - 1 diff --git a/Demos/Can/DemoCan.silkit.yaml b/Demos/SampleConfigurations/Stdout_Info.silkit.yaml similarity index 52% rename from Demos/Can/DemoCan.silkit.yaml rename to Demos/SampleConfigurations/Stdout_Info.silkit.yaml index ae291a319..6bc7f10e9 100644 --- a/Demos/Can/DemoCan.silkit.yaml +++ b/Demos/SampleConfigurations/Stdout_Info.silkit.yaml @@ -1,5 +1,7 @@ -Description: Sample configuration for CAN +Description: Logging to Stdout with Info level Logging: Sinks: - Level: Info Type: Stdout + + diff --git a/Demos/SampleConfigurations/Trace_ToRemote.silkit.yaml b/Demos/SampleConfigurations/Trace_ToRemote.silkit.yaml new file mode 100644 index 000000000..1bf06f975 --- /dev/null +++ b/Demos/SampleConfigurations/Trace_ToRemote.silkit.yaml @@ -0,0 +1,5 @@ +Description: Logging to Remote with Trace level +Logging: + Sinks: + - Level: Trace + Type: Remote diff --git a/Demos/include/ApplicationBase.hpp b/Demos/include/ApplicationBase.hpp new file mode 100644 index 000000000..f9d0a8a20 --- /dev/null +++ b/Demos/include/ApplicationBase.hpp @@ -0,0 +1,643 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "silkit/SilKit.hpp" +#include "silkit/services/logging/ILogger.hpp" +#include "silkit/services/orchestration/all.hpp" +#include "silkit/services/orchestration/string_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "SignalHandler.hpp" +#include "CommandlineParser.hpp" + +using namespace SilKit; +using namespace SilKit::Services::Orchestration; +using namespace SilKit::Services::Can; +using namespace SilKit::Services::Logging; +using namespace SilKit::Util; + +using namespace std::chrono_literals; + +// The application arguments. +// Values might be overridden by the command line arguments. +struct Arguments +{ + std::string participantName = "Participant1"; + std::string registryUri = "silkit://localhost:8500"; + bool runAsync{false}; + std::chrono::nanoseconds duration = 1ms; + bool asFastAsPossible{false}; +}; +std::shared_ptr _participantConfiguration{nullptr}; + +class ApplicationBase +{ +public: + ApplicationBase(Arguments args = Arguments{}) + { + _arguments = args; + _commandLineParser = std::make_shared(); + } + +protected: + // Must be implemented by the actual application + + // Add application specific command line arguments. Called during SetupCommandLineArgs. + virtual void AddCommandLineArgs() = 0; + // Evaluate application specific command line arguments. Called during SetupCommandLineArgs. + virtual void EvaluateCommandLineArgs() = 0; + + // Controller creates SIL Kit controllers / services here. + virtual void CreateControllers() = 0; + + // Boot up SIL Kit controllers here. + // Called in the CommunicationReadyHandler to ensure that messages emerging from the + // controller creation reach other involved participants. + virtual void InitControllers() = 0; + + // When running with the '--async' flag, this is called continuously in a separate worker thread. + // This thread is managed by the ApplicationBase. + // The function will trigger as soon as the participant state is 'Running'. + virtual void DoWorkAsync() = 0; + + // When running with time synchronization (no '--async' flag), this is called in the SimulationStepHandler. + virtual void DoWorkSync(std::chrono::nanoseconds now) = 0; + +private: + // Command line parser + std::shared_ptr _commandLineParser; + + // Default command line argument identifiers (to allow exclusion) + enum struct DefaultArg + { + Name, + Uri, + Log, + Config, + Async, + Duration, + AsFastAsPossible + }; + // Names of the default command line arguments. + // Note that the 'short name' (e.g. -n) and description are defined at runtime. + const std::unordered_map defaultArgName = {{DefaultArg::Name, "name"}, + {DefaultArg::Uri, "registry-uri"}, + {DefaultArg::Log, "log"}, + {DefaultArg::Config, "config"}, + {DefaultArg::Async, "async"}, + {DefaultArg::Duration, "duration"}, + {DefaultArg::AsFastAsPossible, "fast"}}; + Arguments _arguments; + + // SIL Kit API + std::unique_ptr _participant; + ILifecycleService* _lifecycleService{nullptr}; + ITimeSyncService* _timeSyncService{nullptr}; + + // For sync: wait for sil-kit-system-controller start/abort or manual user abort + enum struct SystemControllerResult + { + Unknown, + CoordinatedStart, + SystemControllerAbort, + UserAbort + }; + std::atomic _hasSystemControllerResult{false}; + std::promise _waitForSystemControllerPromise; + std::future _waitForSystemControllerFuture; + std::atomic _systemControllerResult{SystemControllerResult::Unknown}; + + // For async: Couple worker thread to SIL Kit lifecycle + std::promise _unleashWorkerThreadPromise; + std::future _unleashWorkerThreadFuture; + std::thread _workerThread; + + // Wait for the SIL Kit lifecycle to end + std::future _participantStateFuture; + ParticipantState _finalParticipantState{ParticipantState::Invalid}; + +private: + void AddDefaultArgs(std::unordered_set excludedCommandLineArgs = {}) + { + _commandLineParser->Add("help", "h", "[--help]", "-h, --help: Get this help."); + + Arguments defaultArgs{}; + + if (!excludedCommandLineArgs.count(DefaultArg::Name)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::Name), "n", defaultArgs.participantName, + "[--" + defaultArgName.at(DefaultArg::Name) + " ]", + "-n, --" + defaultArgName.at(DefaultArg::Name) + + " : The participant name used to take " + "part in the simulation. Defaults to '" + + _arguments.participantName + "'."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Uri)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::Uri), "u", defaultArgs.registryUri, + "[--" + defaultArgName.at(DefaultArg::Uri) + " ]", + "-u, --" + defaultArgName.at(DefaultArg::Uri) + + " : The registry URI to connect to. Defaults to '" + _arguments.registryUri + "'."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Log)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::Log), "l", "info", + "[--" + defaultArgName.at(DefaultArg::Log) + " ]", + "-l, --" + defaultArgName.at(DefaultArg::Log) + + " : Log to stdout with level 'trace', 'debug', 'warn', 'info', 'error', 'critical' or " + "'off'. " + "Defaults to 'info' if the '--" + + defaultArgName.at(DefaultArg::Config) + + "' option is not specified. " + "Set to 'off' to explicitly turn off stdout logging. " + "Cannot be used together with '--" + + defaultArgName.at(DefaultArg::Config) + "'."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Config)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::Config), "c", "", + "[--" + defaultArgName.at(DefaultArg::Config) + " ]", + "-c, --" + defaultArgName.at(DefaultArg::Config) + + " : Path to the Participant configuration YAML or JSON file. " + "Cannot be used together with '--" + + defaultArgName.at(DefaultArg::Log) + "'. Will always run as fast as possible."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Async)) + { + _commandLineParser->Add(defaultArgName.at(DefaultArg::Async), "a", + "[--" + defaultArgName.at(DefaultArg::Async) + "]", + "-a, --" + defaultArgName.at(DefaultArg::Async) + + ": run in asynchronous mode. " + "Cannot be used together with '--" + + defaultArgName.at(DefaultArg::Duration) + "'."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Duration)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::Duration), "d", std::to_string(defaultArgs.duration.count() / 1000000), + "[--" + defaultArgName.at(DefaultArg::Duration) + " ]", + "-d, --" + defaultArgName.at(DefaultArg::Duration) + + " : The step size in microseconds of the participant. " + "Defaults to 1000us. " + "Cannot be used together with '--" + + defaultArgName.at(DefaultArg::Async) + "'."); + } + + if (!excludedCommandLineArgs.count(DefaultArg::AsFastAsPossible)) + { + _commandLineParser->Add( + defaultArgName.at(DefaultArg::AsFastAsPossible), "f", + "[--" + defaultArgName.at(DefaultArg::AsFastAsPossible) + "]", + "-f, --" + defaultArgName.at(DefaultArg::AsFastAsPossible) + + ": Run the simulation as fast as possible. By default, the execution is slowed down " + "to a single work cycle per second. " + "Cannot be used together with '--" + + defaultArgName.at(DefaultArg::Config) + "'."); + } + } + + auto ToLowerCase(std::string s) -> std::string + { + std::for_each(s.begin(), s.end(), [](char& c) { c = static_cast(std::tolower(c)); }); + return s; + } + auto IsValidLogLevel(const std::string& levelStr) -> bool + { + auto logLevel = ToLowerCase(levelStr); + return logLevel == "trace" || logLevel == "debug" || logLevel == "warn" || logLevel == "info" + || logLevel == "error" || logLevel == "critical" || logLevel == "off"; + } + + void ParseArguments(int argc, char** argv) + { + try + { + _commandLineParser->ParseArguments(argc, argv); + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + _commandLineParser->PrintUsageInfo(std::cerr, argv[0]); + exit(-1); + } + } + + void EvaluateDefaultArgs(const std::string& executableName, std::unordered_set excludedCommandLineArgs) + { + if (_commandLineParser->Get("help").Value()) + { + _commandLineParser->PrintUsageInfo(std::cout, executableName); + exit(0); + } + + if (!excludedCommandLineArgs.count(DefaultArg::Name)) + { + if (_commandLineParser->Get(defaultArgName.at(DefaultArg::Name)).HasValue()) + { + _arguments.participantName = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Name)).Value(); + } + } + + if (!excludedCommandLineArgs.count(DefaultArg::Uri)) + { + if (_commandLineParser->Get(defaultArgName.at(DefaultArg::Uri)).HasValue()) + { + _arguments.registryUri = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Uri)).Value(); + } + } + + bool hasAsyncFlag = false; + if (!excludedCommandLineArgs.count(DefaultArg::Async)) + { + _arguments.runAsync = hasAsyncFlag = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Async)).Value(); + } + + bool hasDurationOption = false; + if (!excludedCommandLineArgs.count(DefaultArg::Duration)) + { + hasDurationOption = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Duration)).HasValue(); + if (hasDurationOption) + { + int durationUs = std::stoi( + _commandLineParser->Get(defaultArgName.at(DefaultArg::Duration)) + .Value()); + _arguments.duration = std::chrono::microseconds(durationUs); + } + } + + if (hasAsyncFlag && hasDurationOption) + { + std::cerr << "Error: Options '--" << defaultArgName.at(DefaultArg::Async) << "' and '--" + << defaultArgName.at(DefaultArg::Duration) << "' cannot be used simultaneously" << std::endl; + _commandLineParser->PrintUsageInfo(std::cerr, executableName); + exit(-1); + } + + bool hasAsFastAsPossibleFlag = false; + if (!excludedCommandLineArgs.count(DefaultArg::AsFastAsPossible)) + { + _arguments.asFastAsPossible = hasAsFastAsPossibleFlag = + _commandLineParser->Get(defaultArgName.at(DefaultArg::AsFastAsPossible)) + .Value(); + } + + bool hasLogOption = false; + if (!excludedCommandLineArgs.count(DefaultArg::Log)) + { + hasLogOption = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Log)).HasValue(); + } + + bool hasCfgOption = false; + if (!excludedCommandLineArgs.count(DefaultArg::Config)) + { + hasCfgOption = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Config)).HasValue(); + } + + if (hasLogOption && hasCfgOption) + { + std::cerr << "Error: Options '--" << defaultArgName.at(DefaultArg::Log) << "' and '--" + << defaultArgName.at(DefaultArg::Config) << "' cannot be used simultaneously" << std::endl; + _commandLineParser->PrintUsageInfo(std::cerr, executableName); + exit(-1); + } + + if (hasAsFastAsPossibleFlag && hasCfgOption) + { + std::cerr << "Error: Options '--" << defaultArgName.at(DefaultArg::AsFastAsPossible) << "' and '--" + << defaultArgName.at(DefaultArg::Config) << "' cannot be used simultaneously" << std::endl; + _commandLineParser->PrintUsageInfo(std::cerr, executableName); + exit(-1); + } + + if (hasCfgOption) + { + // --config always runs as fast as possible + _arguments.asFastAsPossible = true; + + const auto configFileName = + _commandLineParser->Get(defaultArgName.at(DefaultArg::Config)).Value(); + try + { + _participantConfiguration = SilKit::Config::ParticipantConfigurationFromFile(configFileName); + } + catch (const SilKit::ConfigurationError& error) + { + std::cerr << "Error: Failed to load configuration '" << configFileName << "', " << error.what() + << std::endl; + + exit(-2); + } + } + else + { + std::string configLogLevel{"Off"}; + if (!excludedCommandLineArgs.count(DefaultArg::Log)) + { + const auto logLevel{ + _commandLineParser->Get(defaultArgName.at(DefaultArg::Log)).Value()}; + if (!IsValidLogLevel(logLevel)) + { + std::cerr << "Error: Argument of the '--" << defaultArgName.at(DefaultArg::Log) + << "' option must be one of 'trace', 'debug', 'warn', 'info', " + "'error', 'critical', or 'off'" + << std::endl; + exit(-1); + } + configLogLevel = logLevel; + } + + configLogLevel[0] = static_cast(std::toupper(configLogLevel[0])); + + std::ostringstream ss; + ss << "{"; + ss << R"("Logging":{"Sinks":[{"Type":"Stdout","Level":")" << configLogLevel << R"("}]})"; + + if (!_arguments.runAsync && !_arguments.asFastAsPossible) + { + // For async: sleep 1s per cycle + // For sync: set the animation factor to 1/duration(s) here, resulting in one simulation step per second + double animationFactor = + (_arguments.duration.count() > 0) ? 1.0 / (1e-9 * _arguments.duration.count()) : 1.0; + ss << R"(,"Experimental":{"TimeSynchronization":{"AnimationFactor":)" << animationFactor << R"(}})"; + } + ss << "}"; + + const auto configString = ss.str(); + try + { + _participantConfiguration = SilKit::Config::ParticipantConfigurationFromString(configString); + } + catch (const SilKit::ConfigurationError& error) + { + std::cerr << "Error: Failed to set configuration from string '" << configString << "', " << error.what() + << std::endl; + exit(-2); + } + } + } + + void SetupSignalHandler() + { + RegisterSignalHandler([&](auto signalValue) { + { + std::ostringstream ss; + ss << "Signal " << signalValue << " received, attempting to stop simulation..."; + _participant->GetLogger()->Info(ss.str()); + + Stop(); + } + }); + } + + void WorkerThread() + { + _unleashWorkerThreadFuture.wait(); + while (_lifecycleService->State() == ParticipantState::ReadyToRun) + { + // Await running state + }; + while (_lifecycleService->State() == ParticipantState::Running) + { + DoWorkAsync(); + if (!_arguments.asFastAsPossible) + { + std::this_thread::sleep_for(1s); + } + } + } + + void SetupParticipant() + { + _participant = + SilKit::CreateParticipant(_participantConfiguration, _arguments.participantName, _arguments.registryUri); + } + + void SetupLifecycle() + { + auto operationMode = (_arguments.runAsync ? OperationMode::Autonomous : OperationMode::Coordinated); + _lifecycleService = _participant->CreateLifecycleService({operationMode}); + + // Handle simulation abort by sil-kit-system-controller + _waitForSystemControllerFuture = _waitForSystemControllerPromise.get_future(); + _lifecycleService->SetAbortHandler([this](ParticipantState /*lastState*/) { + if (!_hasSystemControllerResult) + { + _hasSystemControllerResult = true; + _waitForSystemControllerPromise.set_value(SystemControllerResult::SystemControllerAbort); + } + }); + + // Called during startup + _lifecycleService->SetCommunicationReadyHandler([this]() { + if (!_arguments.runAsync) + { + // Handle valid simulation start by sil-kit-system-controller + if (!_hasSystemControllerResult) + { + _hasSystemControllerResult = true; + _waitForSystemControllerPromise.set_value(SystemControllerResult::CoordinatedStart); + } + } + // App should initialize it's controllers here + InitControllers(); + }); + } + + void SetupDoWork() + { + if (_arguments.runAsync) + { + // Async: Work by the app is done in a separate thread + _unleashWorkerThreadFuture = _unleashWorkerThreadPromise.get_future(); + _workerThread = std::thread{&ApplicationBase::WorkerThread, this}; + _lifecycleService->SetStartingHandler([this]() { _unleashWorkerThreadPromise.set_value(); }); + } + else + { + // Sync: Work by the app is done in the SimulationStepHandler + _timeSyncService = _lifecycleService->CreateTimeSyncService(); + _timeSyncService->SetSimulationStepHandler( + [this](std::chrono::nanoseconds now, std::chrono::nanoseconds /*duration*/) { DoWorkSync(now); }, + _arguments.duration); + } + } + + void Launch() + { + _participantStateFuture = _lifecycleService->StartLifecycle(); + } + + void Stop() + { + auto state = _lifecycleService->State(); + if (state == ParticipantState::Running || state == ParticipantState::Paused) + { + _lifecycleService->Stop("User requested to stop"); + } + else + { + if (!_arguments.runAsync && !_hasSystemControllerResult) + { + _waitForSystemControllerPromise.set_value(SystemControllerResult::UserAbort); + } + } + } + + void WaitUntilDone() + { + if (!_arguments.runAsync) + { + // Allow the application to exit by itself in all cases + _participant->GetLogger()->Info("Waiting for the system controller to start the simulation"); + _systemControllerResult = _waitForSystemControllerFuture.get(); + if (_systemControllerResult == SystemControllerResult::UserAbort) + { + _participant->GetLogger()->Info("Terminated while waiting for coordinated start"); + // Premature user abort, don't wait for _participantStateFuture + return; + } + else if (_systemControllerResult == SystemControllerResult::SystemControllerAbort) + { + _participant->GetLogger()->Info("System Controller aborted the simulation"); + // No return here as a System Controller abort leads to ParticipantState::Shutdown + // and the _participantStateFuture.get() will return a result. + } + } + + _finalParticipantState = _participantStateFuture.get(); + + if (_arguments.runAsync) + { + if (_workerThread.joinable()) + { + _workerThread.join(); + } + } + } + + // Only use inside class +protected: + ILifecycleService* GetLifecycleService() const + { + return _lifecycleService; + } + + ITimeSyncService* GetTimeSyncService() const + { + return _timeSyncService; + } + + IParticipant* GetParticipant() const + { + return _participant.get(); + } + + ILogger* GetLogger() const + { + return _participant->GetLogger(); + } + + // Also accessible in app / main +public: + const Arguments& GetArguments() const + { + return _arguments; + } + + const auto& GetCommandLineParser() + { + return _commandLineParser; + } + + // Setup of default and application command line arguments + void SetupCommandLineArgs(int argc, char** argv, std::unordered_set excludedCommandLineArgs = {}) + { + std::string executableName = argv[0]; + + AddDefaultArgs(excludedCommandLineArgs); + try + { + AddCommandLineArgs(); + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + exit(-1); + } + ParseArguments(argc, argv); + EvaluateDefaultArgs(executableName, excludedCommandLineArgs); + try + { + EvaluateCommandLineArgs(); + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + _commandLineParser->PrintUsageInfo(std::cerr, executableName); + exit(-1); + } + } + + // Run the SIL Kit workflow + int Run() + { + try + { + // Create participant + SetupParticipant(); + + // Stop() on Ctrl-C + SetupSignalHandler(); + + // app: Controller creation + CreateControllers(); + + // React on valid start / simulation abort + // app: Controller initialization in CommunicationReadyHandler + SetupLifecycle(); + + // sync: Create timeSyncService and simulationStepHandler + // app: DoWorkSync + // async: Create thread and couple to starting handler + // app: DoWorkASync + SetupDoWork(); + + // Start lifecycle + Launch(); + + // Wait for lifecycle to end + // async: Join worker thread + WaitUntilDone(); + + return 0; + } + catch (const std::exception& error) + { + std::cerr << "Error: caught a fatal exception: " << error.what() << std::endl; + return -3; + } + } +}; diff --git a/Demos/include/CommandlineParser.hpp b/Demos/include/CommandlineParser.hpp new file mode 100644 index 000000000..16bbb56ef --- /dev/null +++ b/Demos/include/CommandlineParser.hpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: 2024 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + + +namespace SilKit { +namespace Util { + +//! \brief Parse commandline arguments +class CommandlineParser +{ +public: + CommandlineParser() = default; + + //! \brief Declare a new argument + template + auto& Add(Args&&... args) + { + auto argument = std::make_unique(std::forward(args)...); + if (NameExists(argument->Name())) + { + throw std::runtime_error("Argument '" + argument->Name() + "' already exists"); + } + _arguments.push_back(std::move(argument)); + return *this; + } + + /*! \brief Retrieve a commandline argument by its name + * \throw SilKit::std::runtime_error when argument does not exist or is of a different kind + */ + template + auto Get(std::string name) -> TArgument& + { + auto* argument = GetByName(name); + if (!argument) + { + throw std::runtime_error("Unknown argument '" + name + "'"); + } + return *argument; + } + + /*! \brief Output usage info for previously declared parameters to the given stream + */ + void PrintUsageInfo(std::ostream& out, const std::string& executableName) + { + out << "Usage: " << executableName; + for (auto& argument : _arguments) + { + if (argument->IsHidden()) + continue; + out << " " << argument->Usage(); + } + out << std::endl; + out << "Arguments:" << std::endl; + for (auto& argument : _arguments) + { + if (argument->IsHidden()) + continue; + out << argument->Description() << std::endl; + } + } + + /*! \brief Parse arguments based on argc/argv parameters from a main function + * \throw SilKit::std::runtime_error when a parsing error occurs + */ + void ParseArguments(int argc, char** argv) + { + auto positionalArgumentIt = std::find_if(_arguments.begin(), _arguments.end(), [](const auto& el) { + return el->Kind() == ArgumentKind::Positional || el->Kind() == ArgumentKind::PositionalList; + }); + for (auto i = 1; i < argc; ++i) + { + std::string argument{argv[i]}; + + auto arg{argument}; + auto isShortForm{false}; + if (arg.length() >= 3 && arg.substr(0, 2) == "--") + { + arg.erase(0, 2); + } + else if (arg.length() >= 2 && arg.substr(0, 1) == "-") + { + arg.erase(0, 1); + isShortForm = true; + } + else if (positionalArgumentIt != _arguments.end()) + { + if ((*positionalArgumentIt)->Kind() == ArgumentKind::Positional) + { + auto* positionalArgument = static_cast(positionalArgumentIt->get()); + positionalArgument->_value = std::move(arg); + positionalArgumentIt = std::find_if(++positionalArgumentIt, _arguments.end(), [](const auto& el) { + return el->Kind() == ArgumentKind::Positional || el->Kind() == ArgumentKind::PositionalList; + }); + } + else + { + auto* positionalArgument = static_cast(positionalArgumentIt->get()); + positionalArgument->_values.push_back(std::move(arg)); + } + + continue; + } + else + { + throw std::runtime_error("Bad argument '" + argument + "'"); + } + + auto splitPos = std::find(arg.begin(), arg.end(), '='); + if (splitPos != arg.end()) + { + std::string name = {arg.begin(), splitPos}; + std::string value = {splitPos + 1, arg.end()}; + + auto* option = isShortForm ? GetByShortName