From 63c7e34d2b830306cefaf334c9fe47eed15a9bcc Mon Sep 17 00:00:00 2001 From: Benjamin Morgan Date: Tue, 25 Jun 2024 23:18:50 +0200 Subject: [PATCH] wip: Refactor in order to add probe command --- engine/CMakeLists.txt | 33 +- engine/src/coordinator.cpp | 18 + engine/src/coordinator.hpp | 32 +- engine/src/lua_setup_test.cpp | 11 +- engine/src/main.cpp | 2 + engine/src/main_commands.cpp | 135 ++ engine/src/main_commands.hpp | 30 +- engine/src/main_probe.cpp | 44 +- engine/src/main_run.cpp | 192 +- engine/src/registrar.hpp | 9 +- engine/src/server.cpp | 9 +- engine/src/server.hpp | 25 +- engine/src/server_mock.cpp | 4 + engine/src/simulation.cpp | 1550 ++--------------- engine/src/simulation.hpp | 143 +- engine/src/simulation_actions.hpp | 66 + engine/src/simulation_context.cpp | 75 +- engine/src/simulation_context.hpp | 269 +-- .../time_event.hpp => simulation_events.hpp} | 36 +- engine/src/simulation_machine.hpp | 284 +++ engine/src/simulation_outcome.hpp | 83 + engine/src/simulation_probe.hpp | 91 + engine/src/simulation_result.hpp | 68 + engine/src/simulation_state_abort.cpp | 50 + engine/src/simulation_state_connect.cpp | 685 ++++++++ engine/src/simulation_state_disconnect.cpp | 55 + engine/src/simulation_state_fail.cpp | 34 + engine/src/simulation_state_keep_alive.cpp | 37 + engine/src/simulation_state_pause.cpp | 70 + engine/src/simulation_state_probe.cpp | 82 + engine/src/simulation_state_reset.cpp | 48 + engine/src/simulation_state_resume.cpp | 41 + engine/src/simulation_state_start.cpp | 84 + engine/src/simulation_state_step_begin.cpp | 77 + .../src/simulation_state_step_controllers.cpp | 128 ++ engine/src/simulation_state_step_end.cpp | 73 + .../src/simulation_state_step_simulators.cpp | 82 + engine/src/simulation_state_stop.cpp | 56 + engine/src/simulation_state_success.cpp | 34 + engine/src/simulation_statistics.hpp | 55 + engine/src/simulation_sync.hpp | 106 ++ oak/include/oak/server.hpp | 10 +- plugins/basic/src/basic.cpp | 32 +- runtime/include/cloe/data_broker.hpp | 37 +- runtime/include/cloe/registrar.hpp | 11 + runtime/include/cloe/trigger/nil_event.hpp | 1 + runtime/include/cloe/trigger/set_action.hpp | 12 +- runtime/include/cloe/vehicle.hpp | 9 + tests/test_lua04_schedule_test.lua | 2 +- tests/test_lua14_speedometer_signals.lua | 2 +- 50 files changed, 3163 insertions(+), 1959 deletions(-) create mode 100644 engine/src/main_commands.cpp create mode 100644 engine/src/simulation_actions.hpp rename engine/src/{utility/time_event.hpp => simulation_events.hpp} (85%) create mode 100644 engine/src/simulation_machine.hpp create mode 100644 engine/src/simulation_outcome.hpp create mode 100644 engine/src/simulation_probe.hpp create mode 100644 engine/src/simulation_result.hpp create mode 100644 engine/src/simulation_state_abort.cpp create mode 100644 engine/src/simulation_state_connect.cpp create mode 100644 engine/src/simulation_state_disconnect.cpp create mode 100644 engine/src/simulation_state_fail.cpp create mode 100644 engine/src/simulation_state_keep_alive.cpp create mode 100644 engine/src/simulation_state_pause.cpp create mode 100644 engine/src/simulation_state_probe.cpp create mode 100644 engine/src/simulation_state_reset.cpp create mode 100644 engine/src/simulation_state_resume.cpp create mode 100644 engine/src/simulation_state_start.cpp create mode 100644 engine/src/simulation_state_step_begin.cpp create mode 100644 engine/src/simulation_state_step_controllers.cpp create mode 100644 engine/src/simulation_state_step_end.cpp create mode 100644 engine/src/simulation_state_step_simulators.cpp create mode 100644 engine/src/simulation_state_stop.cpp create mode 100644 engine/src/simulation_state_success.cpp create mode 100644 engine/src/simulation_statistics.hpp create mode 100644 engine/src/simulation_sync.hpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 94caff92c..2eb8b2806 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -106,13 +106,36 @@ add_library(cloe-enginelib STATIC src/simulation.hpp src/simulation_context.cpp src/simulation_context.hpp + src/simulation_actions.hpp + src/simulation_events.hpp + src/simulation_outcome.hpp + src/simulation_result.hpp + src/simulation_probe.hpp + src/simulation_statistics.hpp + src/simulation_sync.hpp src/simulation_progress.hpp + src/simulation_machine.hpp + src/simulation_state_abort.cpp + src/simulation_state_connect.cpp + src/simulation_state_disconnect.cpp + src/simulation_state_fail.cpp + src/simulation_state_keep_alive.cpp + src/simulation_state_pause.cpp + src/simulation_state_probe.cpp + src/simulation_state_reset.cpp + src/simulation_state_resume.cpp + src/simulation_state_start.cpp + src/simulation_state_step_begin.cpp + src/simulation_state_step_controllers.cpp + src/simulation_state_step_end.cpp + src/simulation_state_step_simulators.cpp + src/simulation_state_stop.cpp + src/simulation_state_success.cpp src/utility/command.cpp src/utility/command.hpp src/utility/defer.hpp src/utility/progress.hpp src/utility/state_machine.hpp - src/utility/time_event.hpp ) add_library(cloe::enginelib ALIAS cloe-enginelib) set_target_properties(cloe-enginelib PROPERTIES @@ -172,6 +195,10 @@ if(BUILD_TESTING) src/lua_stack_test.cpp src/lua_setup_test.cpp ) + target_compile_definitions(test-enginelib + PRIVATE + CLOE_LUA_PATH="${CMAKE_CURRENT_SOURCE_DIR}/lua" + ) set_target_properties(test-enginelib PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON @@ -193,11 +220,13 @@ add_subdirectory(vendor/linenoise) add_executable(cloe-engine src/main.cpp src/main_commands.hpp + src/main_commands.cpp src/main_check.cpp src/main_dump.cpp + src/main_probe.cpp src/main_run.cpp - src/main_usage.cpp src/main_shell.cpp + src/main_usage.cpp src/main_version.cpp ) set_target_properties(cloe-engine PROPERTIES diff --git a/engine/src/coordinator.cpp b/engine/src/coordinator.cpp index 3c40dd166..f10e6d8a4 100644 --- a/engine/src/coordinator.cpp +++ b/engine/src/coordinator.cpp @@ -82,6 +82,24 @@ std::shared_ptr Coordinator::trigger_registrar(Source s) return std::make_shared(*this, s); } +[[nodiscard]] std::vector Coordinator::trigger_action_names() const { + std::vector results; + results.reserve(actions_.size()); + for (const auto& [key, _] : actions_) { + results.emplace_back(key); + } + return results; +} + +[[nodiscard]] std::vector Coordinator::trigger_event_names() const { + std::vector results; + results.reserve(events_.size()); + for (const auto& [key, _] : events_) { + results.emplace_back(key); + } + return results; +} + void Coordinator::enroll(Registrar& r) { // clang-format off r.register_api_handler("/triggers/actions", HandlerType::STATIC, diff --git a/engine/src/coordinator.hpp b/engine/src/coordinator.hpp index bace461f5..ea0d5bbdb 100644 --- a/engine/src/coordinator.hpp +++ b/engine/src/coordinator.hpp @@ -49,12 +49,12 @@ class TriggerUnknownAction : public cloe::TriggerInvalid { public: TriggerUnknownAction(const std::string& key, const cloe::Conf& c) : TriggerInvalid(c, "unknown action: " + key), key_(key) {} - virtual ~TriggerUnknownAction() noexcept = default; + ~TriggerUnknownAction() noexcept override = default; /** * Return key that is unknown. */ - const char* key() const { return key_.c_str(); } + [[nodiscard]] const char* key() const { return key_.c_str(); } private: std::string key_; @@ -73,7 +73,7 @@ class TriggerUnknownEvent : public cloe::TriggerInvalid { /** * Return key that is unknown. */ - const char* key() const { return key_.c_str(); } + [[nodiscard]] const char* key() const { return key_.c_str(); } private: std::string key_; @@ -107,15 +107,25 @@ class Coordinator { void register_event(const std::string& key, cloe::EventFactoryPtr&& ef, std::shared_ptr storage); - sol::table register_lua_table(const std::string& field); + [[nodiscard]] sol::table register_lua_table(const std::string& field); - cloe::DataBroker* data_broker() const { return db_; } + [[nodiscard]] cloe::DataBroker* data_broker() const { return db_; } std::shared_ptr trigger_registrar(cloe::Source s); void enroll(cloe::Registrar& r); - cloe::Logger logger() const { return cloe::logger::get("cloe"); } + [[nodiscard]] cloe::Logger logger() const { return cloe::logger::get("cloe"); } + + /** + * Return a list of names of all available actions that have been enrolled. + */ + [[nodiscard]] std::vector trigger_action_names() const; + + /** + * Return a list of names of all available events that have been enrolled. + */ + [[nodiscard]] std::vector trigger_event_names() const; /** * Process any incoming triggers, clear the buffer, and trigger time-based @@ -130,11 +140,11 @@ class Coordinator { void execute_action_from_lua(const cloe::Sync& sync, const sol::object& obj); protected: - cloe::ActionPtr make_action(const sol::object& lua) const; - cloe::ActionPtr make_action(const cloe::Conf& c) const; - cloe::EventPtr make_event(const cloe::Conf& c) const; - cloe::TriggerPtr make_trigger(cloe::Source s, const cloe::Conf& c) const; - cloe::TriggerPtr make_trigger(const sol::table& tbl) const; + [[nodiscard]] cloe::ActionPtr make_action(const sol::object& lua) const; + [[nodiscard]] cloe::ActionPtr make_action(const cloe::Conf& c) const; + [[nodiscard]] cloe::EventPtr make_event(const cloe::Conf& c) const; + [[nodiscard]] cloe::TriggerPtr make_trigger(cloe::Source s, const cloe::Conf& c) const; + [[nodiscard]] cloe::TriggerPtr make_trigger(const sol::table& tbl) const; void queue_trigger(cloe::Source s, const cloe::Conf& c) { queue_trigger(make_trigger(s, c)); } void queue_trigger(cloe::TriggerPtr&& tp); void store_trigger(cloe::TriggerPtr&& tp, const cloe::Sync& sync); diff --git a/engine/src/lua_setup_test.cpp b/engine/src/lua_setup_test.cpp index 0e6966316..2d5fd8372 100644 --- a/engine/src/lua_setup_test.cpp +++ b/engine/src/lua_setup_test.cpp @@ -30,17 +30,24 @@ #include "stack.hpp" // for Stack using namespace cloe; // NOLINT(build/namespaces) +#ifndef CLOE_LUA_PATH +#error "require CLOE_LUA_PATH to be defined in order to find lua directory" +#endif + class cloe_lua_setup : public testing::Test { + std::vector defer_deletion_; + protected: sol::state lua_state; sol::state_view lua; LuaOptions opt; Stack stack; - std::vector defer_deletion_; - cloe_lua_setup() : lua(lua_state.lua_state()) { opt.environment = std::make_unique(); +#ifdef CLOE_LUA_PATH + opt.lua_paths.emplace_back(CLOE_LUA_PATH); +#endif } std::filesystem::path WriteTempLuaFile(std::string_view content) { diff --git a/engine/src/main.cpp b/engine/src/main.cpp index dbc18e84e..918eef30a 100644 --- a/engine/src/main.cpp +++ b/engine/src/main.cpp @@ -201,6 +201,8 @@ int main(int argc, char** argv) { return engine::dump(with_global_options(dump_options), dump_files); } else if (*check) { return engine::check(with_global_options(check_options), check_files); + } else if (*probe) { + return engine::probe(with_global_options(probe_options), probe_files); } else if (*run) { return engine::run(with_global_options(run_options), run_files); } else if (*shell) { diff --git a/engine/src/main_commands.cpp b/engine/src/main_commands.cpp new file mode 100644 index 000000000..1172727eb --- /dev/null +++ b/engine/src/main_commands.cpp @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file main_commands.cpp + */ + +#include "main_commands.hpp" + +#include // for signal +#include // for getenv +#include // for cerr +#include // for tuple + +// NOTE: Unfortunately, includes Boost headers +// that make use of deprecated headers. This is fixed in Boost 1.70.0, but +// we still need to support earlier versions of Boost. +#define BOOST_ALLOW_DEPRECATED_HEADERS + +#include +#include // for lexical_cast +#include // for random_generator +#include + +#include // for logger::get +#include // for read_conf + +#include "error_handler.hpp" // for conclude_error +#include "simulation.hpp" // for Simulation +#include "stack.hpp" // for Stack + +namespace engine { + +// We need a global instance so that our signal handler has access to it. +Simulation* GLOBAL_SIMULATION_INSTANCE{nullptr}; // NOLINT + +void handle_signal(int sig) { + static size_t interrupts = 0; + switch (sig) { + case SIGSEGV: + case SIGABRT: + abort(); + break; + case SIGINT: + default: + std::cerr << "\n" << std::flush; // print newline so that ^C is on its own line + if (++interrupts == 3) { + std::ignore = std::signal(sig, SIG_DFL); // third time goes to the default handler + } + if (GLOBAL_SIMULATION_INSTANCE != nullptr) { + GLOBAL_SIMULATION_INSTANCE->signal_abort(); + } + break; + } +} + +// Set the UUID of the simulation: +template +std::string handle_uuid_impl(const Options& opt) { + std::string uuid; + if (!opt.uuid.empty()) { + uuid = opt.uuid; + } else if (std::getenv(CLOE_SIMULATION_UUID_VAR) != nullptr) { + uuid = std::getenv(CLOE_SIMULATION_UUID_VAR); + } else { + uuid = boost::lexical_cast(boost::uuids::random_generator()()); + } + opt.stack_options.environment->set(CLOE_SIMULATION_UUID_VAR, uuid); + return uuid; +} + +std::string handle_uuid(const RunOptions& opt) { return handle_uuid_impl(opt); } + +std::string handle_uuid(const ProbeOptions& opt) { return handle_uuid_impl(opt); } + +template +std::tuple handle_config_impl(const Options& opt, + const std::vector& filepaths) { + assert(opt.output != nullptr && opt.error != nullptr); + auto log = cloe::logger::get("cloe"); + cloe::logger::get("cloe")->info("Cloe {}", CLOE_ENGINE_VERSION); + + // Load the stack file: + sol::state lua_state; + sol::state_view lua_view(lua_state.lua_state()); + cloe::Stack stack = cloe::new_stack(opt.stack_options); + cloe::setup_lua(lua_view, opt.lua_options, stack); +#if CLOE_ENGINE_WITH_LRDB + if (opt.debug_lua) { + log->info("Lua debugger listening at port: {}", opt.debug_lua_port); + cloe::start_lua_debugger(lua_view, opt.debug_lua_port); + } +#else + if (opt.debug_lua) { + log->error("Lua debugger feature not available."); + } +#endif + cloe::conclude_error(*opt.stack_options.error, [&]() { + for (const auto& file : filepaths) { + if (boost::algorithm::ends_with(file, ".lua")) { + cloe::merge_lua(lua_view, file); + } else { + cloe::merge_stack(opt.stack_options, stack, file); + } + } + }); + + return {std::move(stack), std::move(lua_state)}; +} + +std::tuple handle_config( + const RunOptions& opt, const std::vector& filepaths) { + return handle_config_impl(opt, filepaths); +} + +std::tuple handle_config( + const ProbeOptions& opt, const std::vector& filepaths) { + return handle_config_impl(opt, filepaths); +} + +} // namespace engine diff --git a/engine/src/main_commands.hpp b/engine/src/main_commands.hpp index 6716f9846..910c1a53c 100644 --- a/engine/src/main_commands.hpp +++ b/engine/src/main_commands.hpp @@ -66,8 +66,14 @@ struct ProbeOptions { std::ostream* output = &std::cout; std::ostream* error = &std::cerr; + // Options + std::string uuid; // Not currently used. + // Flags: int json_indent = 2; + + bool debug_lua = false; // Not currently used. + int debug_lua_port = CLOE_LUA_DEBUGGER_PORT; // Not currently used. }; int probe(const ProbeOptions& opt, const std::vector& filepaths); @@ -89,7 +95,6 @@ struct RunOptions { bool write_output = true; bool require_success = false; bool report_progress = true; - bool probe_simulation = false; bool debug_lua = false; int debug_lua_port = CLOE_LUA_DEBUGGER_PORT; @@ -140,4 +145,27 @@ struct VersionOptions { int version(const VersionOptions& opt); +// ------------------------------------------------------------------------- // + +class Simulation; + +extern Simulation* GLOBAL_SIMULATION_INSTANCE; // NOLINT + +/** + * Handle interrupt signals sent by the operating system. + * + * When this function is called, it cannot call any other functions that + * might have set any locks, because it might not get the lock, and then the + * program hangs instead of gracefully exiting. It's a bit sad, true, but + * that's the way it is. + * + * That is why you cannot make use of the logging in this function. You also + * cannot make use of triggers, because they also have a lock. + * + * The function immediately resets the signal handler to the default provided + * by the standard library, so that in the case that we do hang for some + * reasons, the user can force abort by sending the signal a third time. + */ +void handle_signal(int sig); + } // namespace engine diff --git a/engine/src/main_probe.cpp b/engine/src/main_probe.cpp index 8d1afb4f1..03ee86477 100644 --- a/engine/src/main_probe.cpp +++ b/engine/src/main_probe.cpp @@ -16,27 +16,37 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "main_commands.hpp" +#include // for signal + +#include "error_handler.hpp" // for conclude_error +#include "main_commands.hpp" // for ProbeOptions, handle_* +#include "simulation.hpp" // for Simulation +#include "simulation_probe.hpp" // for SimulationProbe namespace engine { +// From main_commands.cpp: +std::string handle_uuid(const ProbeOptions& opt); +std::tuple handle_config(const ProbeOptions& opt, const std::vector& filepaths); + int probe(const ProbeOptions& opt, const std::vector& filepaths) { - RunOptions ropt; - - // Use settings from ProbeOptions: - ropt.stack_options = opt.stack_options; - ropt.lua_options = opt.lua_options; - ropt.output = opt.output; - ropt.error = opt.error; - ropt.json_indent = opt.json_indent; - - // Update defaults: - ropt.allow_empty = true; - ropt.write_output = false; - ropt.report_progress = false; - ropt.probe_simulation = true; - - return run(ropt, filepaths); + try { + auto uuid = handle_uuid(opt); + auto [stack, lua] = handle_config(opt, filepaths); + auto lua_view = sol::state_view(lua.lua_state()); + + // Create simulation: + Simulation sim(std::move(stack), lua_view, uuid); + GLOBAL_SIMULATION_INSTANCE = ∼ + std::ignore = std::signal(SIGINT, handle_signal); + + // Run simulation: + auto result = cloe::conclude_error(*opt.stack_options.error, [&]() { return sim.probe(); }); + *opt.output << cloe::Json(result).dump(opt.json_indent) << "\n" << std::flush; + return as_exit_code(result.outcome, false); + } catch (cloe::ConcludedError& e) { + return EXIT_FAILURE; + } } } // namespace engine diff --git a/engine/src/main_run.cpp b/engine/src/main_run.cpp index 8ad425a6e..0097f6a4c 100644 --- a/engine/src/main_run.cpp +++ b/engine/src/main_run.cpp @@ -16,165 +16,59 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include // for signal -#include // for getenv -#include // for cerr +#include // for signal +#include // for tuple -// NOTE: Unfortunately, includes Boost headers -// that make use of deprecated headers. This is fixed in Boost 1.70.0, but -// we still need to support earlier versions of Boost. -#define BOOST_ALLOW_DEPRECATED_HEADERS - -#include -#include // for lexical_cast -#include // for random_generator -#include - -#include // for logger::get -#include // for read_conf - -#include "error_handler.hpp" // for conclude_error -#include "main_commands.hpp" // for RunOptions, new_stack, new_lua -#include "simulation.hpp" // for Simulation, SimulationResult -#include "stack.hpp" // for Stack +#include "error_handler.hpp" // for conclude_error +#include "main_commands.hpp" // for RunOptions, handle_* +#include "simulation.hpp" // for Simulation +#include "simulation_result.hpp" // for SimulationResult +#include "stack.hpp" // for Stack namespace engine { -void handle_signal(int /*sig*/); - -// We need a global instance so that our signal handler has access to it. -Simulation* GLOBAL_SIMULATION_INSTANCE{nullptr}; // NOLINT +std::string handle_uuid(const RunOptions& opt); +std::tuple handle_config(const RunOptions& opt, + const std::vector& filepaths); int run(const RunOptions& opt, const std::vector& filepaths) { - assert(opt.output != nullptr && opt.error != nullptr); - auto log = cloe::logger::get("cloe"); - cloe::logger::get("cloe")->info("Cloe {}", CLOE_ENGINE_VERSION); - - // Set the UUID of the simulation: - std::string uuid; - if (!opt.uuid.empty()) { - uuid = opt.uuid; - } else if (std::getenv(CLOE_SIMULATION_UUID_VAR) != nullptr) { - uuid = std::getenv(CLOE_SIMULATION_UUID_VAR); - } else { - uuid = boost::lexical_cast(boost::uuids::random_generator()()); - } - opt.stack_options.environment->set(CLOE_SIMULATION_UUID_VAR, uuid); - - // Load the stack file: - sol::state lua_state; - sol::state_view lua_view(lua_state.lua_state()); - cloe::Stack stack = cloe::new_stack(opt.stack_options); - cloe::setup_lua(lua_view, opt.lua_options, stack); -#if CLOE_ENGINE_WITH_LRDB - if (opt.debug_lua) { - log->info("Lua debugger listening at port: {}", opt.debug_lua_port); - cloe::start_lua_debugger(lua_view, opt.debug_lua_port); - } -#else - if (opt.debug_lua) { - log->error("Lua debugger feature not available."); - } -#endif try { - cloe::conclude_error(*opt.stack_options.error, [&]() { - for (const auto& file : filepaths) { - if (boost::algorithm::ends_with(file, ".lua")) { - cloe::merge_lua(lua_view, file); - } else { - cloe::merge_stack(opt.stack_options, stack, file); - } - } - if (!opt.allow_empty) { - stack.check_completeness(); - } - }); + auto uuid = handle_uuid(opt); + auto [stack, lua] = handle_config(opt, filepaths); + auto lua_view = sol::state_view(lua.lua_state()); + + if (!opt.allow_empty) { + stack.check_completeness(); + } + if (!opt.output_path.empty()) { + stack.engine.output_path = opt.output_path; + } + + // Create simulation: + Simulation sim(std::move(stack), lua_view, uuid); + GLOBAL_SIMULATION_INSTANCE = ∼ + std::ignore = std::signal(SIGINT, handle_signal); + + // Set options: + sim.set_report_progress(opt.report_progress); + + // Run simulation: + auto result = cloe::conclude_error(*opt.stack_options.error, [&]() { return sim.run(); }); + if (result.outcome == SimulationOutcome::NoStart) { + // If we didn't get past the initialization phase, don't output any + // statistics or write any files, just go home. + return EXIT_FAILURE; + } + + // Write results: + if (opt.write_output) { + sim.write_output(result); + } + *opt.output << cloe::Json(result).dump(opt.json_indent) << "\n" << std::flush; + return as_exit_code(result.outcome, opt.require_success); } catch (cloe::ConcludedError& e) { return EXIT_FAILURE; } - - if (!opt.output_path.empty()) { - stack.engine.output_path = opt.output_path; - } - - // Create simulation: - Simulation sim(std::move(stack), lua_view, uuid); - GLOBAL_SIMULATION_INSTANCE = ∼ - std::ignore = std::signal(SIGINT, handle_signal); - - // Set options: - sim.set_report_progress(opt.report_progress); - sim.set_probe_simulation(opt.probe_simulation); - - // Run simulation: - auto result = cloe::conclude_error(*opt.stack_options.error, [&]() { return sim.run(); }); - if (result.outcome == SimulationOutcome::NoStart) { - // If we didn't get past the initialization phase, don't output any - // statistics or write any files, just go home. - return EXIT_FAILURE; - } - - // Write results: - if (opt.write_output) { - sim.write_output(result); - } - if (opt.probe_simulation) { - *opt.output << cloe::Json(result.signals) << "\n" << std::flush; - } else { - *opt.output << cloe::Json(result).dump(opt.json_indent) << "\n" << std::flush; - } - - switch (result.outcome) { - case SimulationOutcome::Success: - return EXIT_OUTCOME_SUCCESS; - case SimulationOutcome::Stopped: - return (opt.require_success ? EXIT_OUTCOME_STOPPED : EXIT_OUTCOME_SUCCESS); - case SimulationOutcome::Aborted: - return EXIT_OUTCOME_ABORTED; - case SimulationOutcome::NoStart: - return EXIT_OUTCOME_NOSTART; - case SimulationOutcome::Failure: - return EXIT_OUTCOME_FAILURE; - case SimulationOutcome::Probing: - return EXIT_OUTCOME_SUCCESS; - default: - return EXIT_OUTCOME_UNKNOWN; - } -} - -/** - * Handle interrupt signals sent by the operating system. - * - * When this function is called, it cannot call any other functions that - * might have set any locks, because it might not get the lock, and then the - * program hangs instead of gracefully exiting. It's a bit sad, true, but - * that's the way it is. - * - * That is why you cannot make use of the logging in this function. You also - * cannot make use of triggers, because they also have a lock. - * - * The function immediately resets the signal handler to the default provided - * by the standard library, so that in the case that we do hang for some - * reasons, the user can force abort by sending the signal a third time. - */ -void handle_signal(int sig) { - static size_t interrupts = 0; - switch (sig) { - case SIGSEGV: - case SIGABRT: - abort(); - break; - case SIGINT: - default: - std::cerr << "\n" << std::flush; // print newline so that ^C is on its own line - if (++interrupts == 3) { - std::ignore = std::signal(sig, SIG_DFL); // third time goes to the default handler - } - if (GLOBAL_SIMULATION_INSTANCE != nullptr) { - GLOBAL_SIMULATION_INSTANCE->signal_abort(); - } - break; - } } } // namespace engine diff --git a/engine/src/registrar.hpp b/engine/src/registrar.hpp index aa9b5c9b4..abc47ed5a 100644 --- a/engine/src/registrar.hpp +++ b/engine/src/registrar.hpp @@ -25,11 +25,12 @@ #include // for string #include // for string_view -#include // for cloe::Registrar +#include // for logger::get +#include // for cloe::Registrar +#include "config.hpp" // for CLOE_TRIGGER_PATH_DELIMITER, ... #include "coordinator.hpp" // for Coordinator #include "server.hpp" // for Server, ServerRegistrar -#include "config.hpp" // for CLOE_TRIGGER_PATH_DELIMITER, ... namespace engine { @@ -105,7 +106,9 @@ class Registrar : public cloe::Registrar { } [[nodiscard]] std::string make_signal_name(std::string_view name) const override { - return make_prefix(name, CLOE_SIGNAL_PATH_DELIMITER); + auto sname = make_prefix(name, CLOE_SIGNAL_PATH_DELIMITER); + coordinator_->logger()->debug("Register signal: {}", sname); + return sname; } void register_action(cloe::ActionFactoryPtr&& af) override { diff --git a/engine/src/server.cpp b/engine/src/server.cpp index a8f5c29ae..48b1ec8f7 100644 --- a/engine/src/server.cpp +++ b/engine/src/server.cpp @@ -36,9 +36,8 @@ namespace engine { class ServerRegistrarImpl : public ServerRegistrar { public: - ServerRegistrarImpl( - oak::Registrar static_reg, oak::ProxyRegistrar api_reg) - : static_registrar_(static_reg), api_registrar_(api_reg) {} + ServerRegistrarImpl(const oak::Registrar& static_reg, oak::ProxyRegistrar api_reg) + : static_registrar_(static_reg), api_registrar_(std::move(api_reg)) {} std::unique_ptr clone() const override { return std::make_unique(static_registrar_, api_registrar_); @@ -62,7 +61,7 @@ class ServerRegistrarImpl : public ServerRegistrar { } void register_api_handler(const std::string& endpoint, cloe::HandlerType t, - cloe::Handler h) override { + cloe::Handler h) override { api_registrar_.register_handler(endpoint, t, h); } @@ -174,6 +173,8 @@ class ServerImpl : public Server { } } + std::vector endpoints() const override { return this->server_.endpoints(); } + Defer lock() override { auto lock = locked_api_registrar_.lock(); return Defer([&]() { lock.release(); }); diff --git a/engine/src/server.hpp b/engine/src/server.hpp index c34c5048e..3cfc0cccf 100644 --- a/engine/src/server.hpp +++ b/engine/src/server.hpp @@ -41,9 +41,9 @@ class ServerRegistrar { public: virtual ~ServerRegistrar() = default; - virtual std::unique_ptr clone() const = 0; + [[nodiscard]] virtual std::unique_ptr clone() const = 0; - virtual std::unique_ptr with_prefix(const std::string& static_prefix, + [[nodiscard]] virtual std::unique_ptr with_prefix(const std::string& static_prefix, const std::string& api_prefix) const = 0; virtual void register_static_handler(const std::string& endpoint, cloe::Handler h) = 0; @@ -59,25 +59,29 @@ class ServerRegistrar { */ class Server { public: - Server(const cloe::ServerConf& config) : config_(config) {} + Server(const Server&) = default; + Server(Server&&) = delete; + Server& operator=(const Server&) = default; + Server& operator=(Server&&) = delete; + Server(cloe::ServerConf config) : config_(std::move(config)) {} virtual ~Server() = default; /** * Return the server configuration. */ - const cloe::ServerConf& config() const { return config_; } + [[nodiscard]] const cloe::ServerConf& config() const { return config_; } /** * Return whether the server is alive and listening for requests. */ - virtual bool is_listening() const = 0; + [[nodiscard]] virtual bool is_listening() const = 0; /** * Return whether the server is currently streaming buffer data to a file. * * If it is, expect performance to be bad. */ - virtual bool is_streaming() const = 0; + [[nodiscard]] virtual bool is_streaming() const = 0; /** * Start the web server. @@ -104,7 +108,7 @@ class Server { * Return a new ServerRegistrar that lets you register static content and * API endpoints with the web server. */ - virtual std::unique_ptr server_registrar() = 0; + [[nodiscard]] virtual std::unique_ptr server_registrar() = 0; /** * Refresh and/or start streaming api data to a file. @@ -116,6 +120,11 @@ class Server { */ virtual void refresh_buffer() = 0; + /** + * Return a list of all registered endpoints. + */ + [[nodiscard]] virtual std::vector endpoints() const = 0; + /** * Return a write lock guard on the server. * @@ -124,7 +133,7 @@ class Server { * * \return Lock guard */ - virtual Defer lock() = 0; + [[nodiscard]] virtual Defer lock() = 0; protected: cloe::Logger logger() const { return cloe::logger::get("cloe"); } diff --git a/engine/src/server_mock.cpp b/engine/src/server_mock.cpp index ad47cd991..c1ed47c0d 100644 --- a/engine/src/server_mock.cpp +++ b/engine/src/server_mock.cpp @@ -124,6 +124,10 @@ class ServerImpl : public Server { void refresh_buffer() override { } + std::vector endpoints() const override { + return {}; + } + Defer lock() override { return Defer([]() {}); } diff --git a/engine/src/simulation.cpp b/engine/src/simulation.cpp index ac42d1a2f..f9ef2e7dd 100644 --- a/engine/src/simulation.cpp +++ b/engine/src/simulation.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 Robert Bosch GmbH + * Copyright 2024 Robert Bosch GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,1431 +75,72 @@ #include "simulation.hpp" -#include // for uint64_t #include // for filesystem::path #include // for ofstream -#include // for future<>, async -#include // for stringstream #include // for string -#include // for sleep_for +#include // for vector<> -#include // for Controller -#include // for AsyncAbort -#include // for DataBroker -#include // for DirectCallback -#include // for Simulator -#include // for CommandFactory, BundleFactory, ... -#include // for DEFINE_SET_STATE_ACTION, SetDataActionFactory -#include // for INCLUDE_RESOURCE, RESOURCE_HANDLER -#include // for Vehicle -#include // for pretty_print -#include // for sol::object to_json +#include // for DataBroker +#include // for pretty_print +#include // for sol::object to_json -#include "coordinator.hpp" // for register_usertype_coordinator -#include "lua_action.hpp" // for LuaAction, -#include "lua_api.hpp" // for to_json(json, sol::object) -#include "simulation_context.hpp" // for SimulationContext -#include "utility/command.hpp" // for CommandFactory -#include "utility/state_machine.hpp" // for State, StateMachine -#include "utility/time_event.hpp" // for TimeCallback, NextCallback, NextEvent, TimeEvent - -// PROJECT_SOURCE_DIR is normally exported by CMake during build, but it's not -// available for the linters, so we define a dummy value here for that case. -#ifndef PROJECT_SOURCE_DIR -#define PROJECT_SOURCE_DIR "" -#endif - -INCLUDE_RESOURCE(index_html, PROJECT_SOURCE_DIR "/webui/index.html"); -INCLUDE_RESOURCE(favicon, PROJECT_SOURCE_DIR "/webui/cloe_16x16.png"); -INCLUDE_RESOURCE(cloe_logo, PROJECT_SOURCE_DIR "/webui/cloe.svg"); -INCLUDE_RESOURCE(bootstrap_css, PROJECT_SOURCE_DIR "/webui/bootstrap.min.css"); +#include "coordinator.hpp" // for Coordinator usage +#include "lua_api.hpp" // for luat_cloe_engine_state +#include "server.hpp" // for Server usage +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine +#include "simulation_probe.hpp" // for SimulationProbe +#include "simulation_result.hpp" // for SimulationResult +#include "utility/command.hpp" // for CommandExecuter usage namespace engine { -class SimulationMachine - : private StateMachine, SimulationContext> { - using SimulationState = State; - - public: - SimulationMachine() { - register_states({ - new Connect{this}, - new Start{this}, - new StepBegin{this}, - new StepSimulators{this}, - new StepControllers{this}, - new StepEnd{this}, - new Pause{this}, - new Resume{this}, - new Success{this}, - new Fail{this}, - new Abort{this}, - new Stop{this}, - new Reset{this}, - new KeepAlive{this}, - new Disconnect{this}, - }); - } - - void run(SimulationContext& ctx) { run_machine(CONNECT, ctx); } - - void run_machine(StateId initial, SimulationContext& ctx) { - StateId id = initial; - while (id != nullptr) { - try { - // Handle interrupts that have been inserted via push_interrupt. - // Only one interrupt is stored. - std::optional interrupt; - while ((interrupt = pop_interrupt())) { - id = handle_interrupt(id, *interrupt, ctx); - } - - if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Off) { - // Run state in this thread synchronously. - id = run_state(id, ctx); - continue; - } - - // Run state in a separate thread asynchronously and abort if - // watchdog_timeout is exceeded. - // - // See configuration: stack.hpp - // See documentation: doc/reference/watchdog.rst - std::chrono::milliseconds timeout = ctx.config.engine.watchdog_default_timeout; - if (ctx.config.engine.watchdog_state_timeouts.count(id)) { - auto maybe = ctx.config.engine.watchdog_state_timeouts[id]; - if (maybe) { - timeout = *maybe; - } - } - auto interval = timeout.count() > 0 ? timeout : ctx.config.engine.polling_interval; - - // Launch state - std::future f = - std::async(std::launch::async, [this, id, &ctx]() { return run_state(id, ctx); }); - - std::future_status status; - for (;;) { - status = f.wait_for(interval); - if (status == std::future_status::ready) { - id = f.get(); - break; - } else if (status == std::future_status::deferred) { - if (timeout.count() > 0) { - logger()->warn("Watchdog waiting on deferred execution."); - } - } else if (status == std::future_status::timeout) { - if (timeout.count() > 0) { - logger()->critical("Watchdog timeout of {} ms exceeded for state: {}", - timeout.count(), id); - - if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Abort) { - logger()->critical("Aborting simulation... this might take a while..."); - this->push_interrupt(ABORT); - break; - } else if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Kill) { - logger()->critical("Killing program... this is going to be messy..."); - std::abort(); - } - } - } - } - } catch (cloe::AsyncAbort&) { - this->push_interrupt(ABORT); - } catch (cloe::ModelReset& e) { - logger()->error("Unhandled reset request in {} state: {}", id, e.what()); - this->push_interrupt(RESET); - } catch (cloe::ModelStop& e) { - logger()->error("Unhandled stop request in {} state: {}", id, e.what()); - this->push_interrupt(STOP); - } catch (cloe::ModelAbort& e) { - logger()->error("Unhandled abort request in {} state: {}", id, e.what()); - this->push_interrupt(ABORT); - } catch (cloe::ModelError& e) { - logger()->error("Unhandled model error in {} state: {}", id, e.what()); - this->push_interrupt(ABORT); - } catch (std::exception& e) { - logger()->critical("Fatal error in {} state: {}", id, e.what()); - throw; - } - } - } - - // Asynchronous Actions: - void pause() { this->push_interrupt(PAUSE); } - void resume() { this->push_interrupt(RESUME); } - void stop() { this->push_interrupt(STOP); } - void succeed() { this->push_interrupt(SUCCESS); } - void fail() { this->push_interrupt(FAIL); } - void reset() { this->push_interrupt(RESET); } - void abort() { this->push_interrupt(ABORT); } - - StateId handle_interrupt(StateId nominal, StateId interrupt, SimulationContext& ctx) override { - logger()->debug("Handle interrupt: {}", interrupt); - // We don't necessarily actually go directly to each desired state. The - // states PAUSE and RESUME are prime examples; they should be entered and - // exited from at pre-defined points. - if (interrupt == PAUSE) { - ctx.pause_execution = true; - } else if (interrupt == RESUME) { - ctx.pause_execution = false; - } else { - // All other interrupts will lead directly to the end of the - // simulation. - return this->run_state(interrupt, ctx); - } - return nominal; - } - - friend void to_json(cloe::Json& j, const SimulationMachine& m) { - j = cloe::Json{ - {"states", m.states()}, - }; - } - -#define DEFINE_STATE(Id, S) DEFINE_STATE_STRUCT(SimulationMachine, SimulationContext, Id, S) - public: - DEFINE_STATE(CONNECT, Connect); - DEFINE_STATE(START, Start); - DEFINE_STATE(STEP_BEGIN, StepBegin); - DEFINE_STATE(STEP_SIMULATORS, StepSimulators); - DEFINE_STATE(STEP_CONTROLLERS, StepControllers); - DEFINE_STATE(STEP_END, StepEnd); - DEFINE_STATE(PAUSE, Pause); - DEFINE_STATE(RESUME, Resume); - DEFINE_STATE(SUCCESS, Success); - DEFINE_STATE(FAIL, Fail); - DEFINE_STATE(ABORT, Abort); - DEFINE_STATE(STOP, Stop); - DEFINE_STATE(RESET, Reset); - DEFINE_STATE(KEEP_ALIVE, KeepAlive); - DEFINE_STATE(DISCONNECT, Disconnect); -#undef DEFINE_STATE -}; - -namespace actions { - -// clang-format off -DEFINE_SET_STATE_ACTION(Pause, "pause", "pause simulation", SimulationMachine, { ptr_->pause(); }) -DEFINE_SET_STATE_ACTION(Resume, "resume", "resume paused simulation", SimulationMachine, { ptr_->resume(); }) -DEFINE_SET_STATE_ACTION(Stop, "stop", "stop simulation with neither success nor failure", SimulationMachine, { ptr_->stop(); }) -DEFINE_SET_STATE_ACTION(Succeed, "succeed", "stop simulation with success", SimulationMachine, { ptr_->succeed(); }) -DEFINE_SET_STATE_ACTION(Fail, "fail", "stop simulation with failure", SimulationMachine, { ptr_->fail(); }) -DEFINE_SET_STATE_ACTION(Reset, "reset", "attempt to reset simulation", SimulationMachine, { ptr_->reset(); }) -DEFINE_SET_STATE_ACTION(KeepAlive, "keep_alive", "keep simulation alive after termination", SimulationContext, { ptr_->config.engine.keep_alive = true; }) -DEFINE_SET_STATE_ACTION(ResetStatistics, "reset_statistics", "reset simulation statistics", SimulationStatistics, { ptr_->reset(); }) - -DEFINE_SET_DATA_ACTION(RealtimeFactor, "realtime_factor", "modify the simulation speed", SimulationSync, "factor", double, - { - logger()->info("Setting target simulation speed: {}", value_); - ptr_->set_realtime_factor(value_); - }) - -// clang-format on - -} // namespace actions - -std::string enumerate_simulator_vehicles(const cloe::Simulator& s) { - std::stringstream buffer; - auto n = s.num_vehicles(); - for (size_t i = 0; i < n; i++) { - auto v = s.get_vehicle(i); - buffer << fmt::format("{}: {}\n", i, v->name()); - } - return buffer.str(); -} - -void handle_cloe_error(cloe::Logger logger, const cloe::Error& e) { - if (e.has_explanation()) { - logger->error("Note:\n{}", fable::indent_string(e.explanation(), " ")); - } -} - -// CONNECT ------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Connect::impl(SimulationContext& ctx) { - logger()->info("Initializing simulation..."); - assert(ctx.config.is_valid()); - - // 1. Initialize progress tracking - ctx.progress.init_begin(6); - auto update_progress = [&ctx](const char* str) { - ctx.progress.init(str); - ctx.server->refresh_buffer(); - }; - - { // 2. Initialize loggers - update_progress("logging"); - - for (const auto& c : ctx.config.logging) { - c.apply(); - } - } - - { // 3. Initialize Lua - auto types_tbl = sol::object(cloe::luat_cloe_engine_types(ctx.lua)).as(); - register_usertype_coordinator(types_tbl, ctx.sync); - - cloe::luat_cloe_engine_state(ctx.lua)["scheduler"] = std::ref(*ctx.coordinator); - } - - { // 4. Enroll endpoints and triggers for the server - update_progress("server"); - - auto rp = ctx.simulation_registrar(); - cloe::Registrar& r = *rp; - - // HTML endpoints: - r.register_static_handler("/", RESOURCE_HANDLER(index_html, cloe::ContentType::HTML)); - r.register_static_handler("/index.html", cloe::handler::Redirect("/")); - r.register_static_handler("/cloe_16x16.png", RESOURCE_HANDLER(favicon, cloe::ContentType::PNG)); - r.register_static_handler("/cloe.svg", RESOURCE_HANDLER(cloe_logo, cloe::ContentType::SVG)); - r.register_static_handler("/bootstrap.css", - RESOURCE_HANDLER(bootstrap_css, cloe::ContentType::CSS)); - - // API endpoints: - r.register_api_handler("/uuid", cloe::HandlerType::STATIC, cloe::handler::StaticJson(ctx.uuid)); - r.register_api_handler("/version", cloe::HandlerType::STATIC, - cloe::handler::StaticJson(ctx.version())); - r.register_api_handler("/progress", cloe::HandlerType::BUFFERED, - cloe::handler::ToJson(&ctx.progress)); - r.register_api_handler( - "/configuration", cloe::HandlerType::DYNAMIC, - [&ctx](const cloe::Request& q, cloe::Response& r) { - std::string type = "active"; - auto m = q.query_map(); - if (m.count("type")) { - type = m.at("type"); - } - - if (type == "active") { - r.write(ctx.config.active_config()); - } else if (type == "input") { - r.write(ctx.config.input_config()); - } else { - r.bad_request(cloe::Json{ - {"error", "invalid type value"}, - {"fields", {{"type", "configuration output type, one of: active, input"}}}, - }); - } - }); - r.register_api_handler("/simulation", cloe::HandlerType::BUFFERED, - cloe::handler::ToJson(&ctx.sync)); - r.register_api_handler("/statistics", cloe::HandlerType::BUFFERED, - cloe::handler::ToJson(&ctx.statistics)); - r.register_api_handler("/plugins", cloe::HandlerType::STATIC, - cloe::handler::StaticJson(ctx.plugin_ids())); - - // Coordinator & Server - ctx.server->enroll(r); - ctx.coordinator->enroll(r); - - // Events: - ctx.callback_loop = r.register_event(); - ctx.callback_start = r.register_event(); - ctx.callback_stop = r.register_event(); - ctx.callback_success = r.register_event(); - ctx.callback_failure = r.register_event(); - ctx.callback_reset = r.register_event(); - ctx.callback_pause = r.register_event(); - ctx.callback_resume = r.register_event(); - ctx.callback_time = std::make_shared( - logger(), [this, &ctx](const cloe::Trigger& t, cloe::Duration when) { - static const std::vector eta_names{"stop", "succeed", "fail", "reset"}; - auto name = t.action().name(); - for (std::string x : eta_names) { - // Take possible namespacing of simulation actions into account. - if (ctx.config.simulation.name) { - x = *ctx.config.simulation.name + "/" + x; - } - if (name == x) { - // We are only interested in the earliest stop action. - if (ctx.sync.eta() == cloe::Duration(0) || when < ctx.sync.eta()) { - logger()->info("Set simulation ETA to {}s", cloe::Seconds{when}.count()); - ctx.sync.set_eta(when); - ctx.progress.execution_eta = when; - } - } - } - }); - r.register_event(std::make_unique(), ctx.callback_time); - r.register_event(std::make_unique(), - std::make_shared(ctx.callback_time)); - - // Actions: - r.register_action(this->state_machine()); - r.register_action(this->state_machine()); - r.register_action(this->state_machine()); - r.register_action(this->state_machine()); - r.register_action(this->state_machine()); - r.register_action(this->state_machine()); - r.register_action(&ctx); - r.register_action(&ctx.sync); - r.register_action(&ctx.statistics); - r.register_action(ctx.commander.get()); - r.register_action(ctx.lua); - - // From: cloe/trigger/example_actions.hpp - auto tr = ctx.coordinator->trigger_registrar(cloe::Source::TRIGGER); - r.register_action(tr); - r.register_action(tr); - r.register_action(); - r.register_action(tr); - } - - { // 5. Initialize simulators - update_progress("simulators"); - - /** - * Return a new Simulator given configuration c. - */ - auto new_simulator = [&ctx](const cloe::SimulatorConf& c) -> std::unique_ptr { - auto f = c.factory->clone(); - auto name = c.name.value_or(c.binding); - for (auto d : ctx.config.get_simulator_defaults(name, f->name())) { - f->from_conf(d.args); - } - auto x = f->make(c.args); - ctx.now_initializing = x.get(); - - // Configure simulator: - auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( - std::string("/simulators/") + name); - x->connect(); - x->enroll(*r); - - ctx.now_initializing = nullptr; - return x; - }; - - // Create and configure all simulators: - for (const auto& c : ctx.config.simulators) { - auto name = c.name.value_or(c.binding); - assert(ctx.simulators.count(name) == 0); - logger()->info("Configure simulator {}", name); - - try { - ctx.simulators[name] = new_simulator(c); - } catch (cloe::ModelError& e) { - logger()->critical("Error configuring simulator {}: {}", name, e.what()); - return ABORT; - } - } - - auto r = ctx.simulation_registrar(); - r->register_api_handler("/simulators", cloe::HandlerType::STATIC, - cloe::handler::StaticJson(ctx.simulator_ids())); - } - - { // 6. Initialize vehicles - update_progress("vehicles"); - - /** - * Return a new Component given vehicle v and configuration c. - */ - auto new_component = [&ctx](cloe::Vehicle& v, - const cloe::ComponentConf& c) -> std::shared_ptr { - // Create a copy of the component factory prototype and initialize it with the default stack arguments. - auto f = c.factory->clone(); - auto name = c.name.value_or(c.binding); - for (auto d : ctx.config.get_component_defaults(name, f->name())) { - f->from_conf(d.args); - } - // Get input components, if applicable. - std::vector> from; - for (const auto& from_comp_name : c.from) { - if (!v.has(from_comp_name)) { - return nullptr; - } - from.push_back(v.get(from_comp_name)); - } - // Create the new component. - auto x = f->make(c.args, std::move(from)); - ctx.now_initializing = x.get(); - - // Configure component: - auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( - std::string("/components/") + name); - x->connect(); - x->enroll(*r); - - ctx.now_initializing = nullptr; - return x; - }; - - /** - * Return a new Vehicle given configuration c. - */ - auto new_vehicle = [&](const cloe::VehicleConf& c) -> std::shared_ptr { - static uint64_t gid = 1024; - - // Fetch vehicle prototype. - std::shared_ptr v; - if (c.is_from_simulator()) { - auto& s = ctx.simulators.at(c.from_sim.simulator); - if (c.from_sim.is_by_name()) { - v = s->get_vehicle(c.from_sim.index_str); - if (!v) { - throw cloe::ModelError("simulator {} has no vehicle by name {}", c.from_sim.simulator, - c.from_sim.index_str) - .explanation("Simulator {} has following vehicles:\n{}", c.from_sim.simulator, - enumerate_simulator_vehicles(*s)); - } - } else { - v = s->get_vehicle(c.from_sim.index_num); - if (!v) { - throw cloe::ModelError("simulator {} has no vehicle at index {}", c.from_sim.simulator, - c.from_sim.index_num) - .explanation("Simulator {} has following vehicles:\n{}", c.from_sim.simulator, - enumerate_simulator_vehicles(*s)); - } - } - } else { - if (ctx.vehicles.count(c.from_veh)) { - v = ctx.vehicles.at(c.from_veh); - } else { - // This vehicle depends on another that hasn't been create yet. - return nullptr; - } - } - - // Create vehicle from prototype and configure the components. - logger()->info("Configure vehicle {}", c.name); - auto x = v->clone(++gid, c.name); - ctx.now_initializing = x.get(); - - std::set configured; - size_t n = c.components.size(); - while (configured.size() != n) { - // Keep trying to create components until all have been created. - // This is a poor-man's version of dependency resolution and has O(n^2) - // complexity, which is acceptable given that the expected number of - // components is usually less than 100. - size_t m = configured.size(); - for (const auto& kv : c.components) { - if (configured.count(kv.first)) { - // This component has already been configured. - continue; - } - - auto k = new_component(*x, kv.second); - if (k) { - x->set_component(kv.first, std::move(k)); - configured.insert(kv.first); - } - } - - // Check that we are making progress. - if (configured.size() == m) { - // We have configured.size() != n and has not grown since going - // through all Component configs. This means that we have some unresolved - // dependencies. Find out which and abort. - for (const auto& kv : c.components) { - if (configured.count(kv.first)) { - continue; - } - - // We now have a component that has not been configured, and this - // can only be the case if the dependency is not found. - assert(kv.second.from.size() > 0); - for (const auto& from_comp_name : kv.second.from) { - if (x->has(from_comp_name)) { - continue; - } - throw cloe::ModelError{ - "cannot configure component '{}': cannot resolve dependency '{}'", - kv.first, - from_comp_name, - }; - } - } - } - } - - // Configure vehicle: - auto r = ctx.registrar->with_trigger_prefix(c.name)->with_api_prefix( - std::string("/vehicles/") + c.name); - x->connect(); - x->enroll(*r); - - ctx.now_initializing = nullptr; - return x; - }; - - // Create and configure all vehicles: - size_t n = ctx.config.vehicles.size(); - while (ctx.vehicles.size() != n) { - // Keep trying to create vehicles until all have been created. - // This is a poor-man's version of dependency resolution and has O(n^2) - // complexity, which is acceptable given that the expected number of - // vehicles is almost always less than 10. - size_t m = ctx.vehicles.size(); - for (const auto& c : ctx.config.vehicles) { - if (ctx.vehicles.count(c.name)) { - // This vehicle has already been configured. - continue; - } - - std::shared_ptr v; - try { - v = new_vehicle(c); - } catch (cloe::ModelError& e) { - logger()->critical("Error configuring vehicle {}: {}", c.name, e.what()); - handle_cloe_error(logger(), e); - return ABORT; - } - - if (v) { - ctx.vehicles[c.name] = std::move(v); - } - } - - // Check that we are making progress. - if (ctx.vehicles.size() == m) { - // We have ctx.vehicles.size() != n and has not grown since going - // through all Vehicle configs. This means that we have some unresolved - // dependencies. Find out which and abort. - for (const auto& c : ctx.config.vehicles) { - if (ctx.vehicles.count(c.name)) { - continue; - } - - // We now have a vehicle that has not been configured, and this can - // only be the case if a vehicle dependency is not found. - assert(c.is_from_vehicle()); - throw cloe::ModelError{ - "cannot configure vehicle '{}': cannot resolve dependency '{}'", - c.name, - c.from_veh, - }; - } - } - } - - auto r = ctx.simulation_registrar(); - r->register_api_handler("/vehicles", cloe::HandlerType::STATIC, - cloe::handler::StaticJson(ctx.vehicle_ids())); - } - - { // 7. Initialize controllers - update_progress("controllers"); - - /** - * Return a new Controller given configuration c. - */ - auto new_controller = - [&ctx](const cloe::ControllerConf& c) -> std::unique_ptr { - auto f = c.factory->clone(); - auto name = c.name.value_or(c.binding); - for (auto d : ctx.config.get_controller_defaults(name, f->name())) { - f->from_conf(d.args); - } - auto x = f->make(c.args); - ctx.now_initializing = x.get(); - - // Configure - auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( - std::string("/controllers/") + name); - x->set_vehicle(ctx.vehicles.at(c.vehicle)); - x->connect(); - x->enroll(*r); - - ctx.now_initializing = nullptr; - return x; - }; - - // Create and configure all controllers: - for (const auto& c : ctx.config.controllers) { - auto name = c.name.value_or(c.binding); - assert(ctx.controllers.count(name) == 0); - logger()->info("Configure controller {}", name); - try { - ctx.controllers[name] = new_controller(c); - } catch (cloe::ModelError& e) { - logger()->critical("Error configuring controller {}: {}", name, e.what()); - return ABORT; - } - } - - auto r = ctx.simulation_registrar(); - r->register_api_handler("/controllers", cloe::HandlerType::STATIC, - cloe::handler::StaticJson(ctx.controller_ids())); - } - - { // 8. Initialize Databroker & Lua - auto* dbPtr = ctx.coordinator->data_broker(); - if (!dbPtr) { - throw std::logic_error("Coordinator did not provide a DataBroker instance"); - } - auto& db = *dbPtr; - // Alias signals via lua - { - bool aliasing_failure = false; - // Read cloe.alias_signals - sol::object signal_aliases = cloe::luat_cloe_engine_initial_input(ctx.lua)["signal_aliases"]; - auto type = signal_aliases.get_type(); - switch (type) { - // cloe.alias_signals: expected is a list (i.e. table) of 2-tuple each strings - case sol::type::table: { - sol::table alias_signals = signal_aliases.as(); - auto tbl_size = std::distance(alias_signals.begin(), alias_signals.end()); - //for (auto& kv : alias_signals) - for (int i = 0; i < tbl_size; i++) { - //sol::object value = kv.second; - sol::object value = alias_signals[i + 1]; - sol::type type = value.get_type(); - switch (type) { - // cloe.alias_signals[i]: expected is a 2-tuple (i.e. table) each strings - case sol::type::table: { - sol::table alias_tuple = value.as(); - auto tbl_size = std::distance(alias_tuple.begin(), alias_tuple.end()); - if (tbl_size != 2) { - // clang-format off - logger()->error( - "One or more entries in 'cloe.alias_signals' does not consist out of a 2-tuple. " - "Expected are entries in this format { \"regex\" , \"short-name\" }" - ); - // clang-format on - aliasing_failure = true; - continue; - } - - sol::object value; - sol::type type; - std::string old_name; - std::string alias_name; - value = alias_tuple[1]; - type = value.get_type(); - if (sol::type::string != type) { - // clang-format off - logger()->error( - "One or more parts in a tuple in 'cloe.alias_signals' has an unexpected datatype '{}'. " - "Expected are entries in this format { \"regex\" , \"short-name\" }", - static_cast(type)); - // clang-format on - aliasing_failure = true; - } else { - old_name = value.as(); - } - - value = alias_tuple[2]; - type = value.get_type(); - if (sol::type::string != type) { - // clang-format off - logger()->error( - "One or more parts in a tuple in 'cloe.alias_signals' has an unexpected datatype '{}'. " - "Expected are entries in this format { \"regex\" , \"short-name\" }", - static_cast(type)); - // clang-format on - aliasing_failure = true; - } else { - alias_name = value.as(); - } - try { - db.alias(old_name, alias_name); - // clang-format off - logger()->info( - "Aliasing signal '{}' as '{}'.", - old_name, alias_name); - // clang-format on - } catch (const std::logic_error& ex) { - // clang-format off - logger()->error( - "Aliasing signal specifier '{}' as '{}' failed with this error: {}", - old_name, alias_name, ex.what()); - // clang-format on - aliasing_failure = true; - } catch (...) { - // clang-format off - logger()->error( - "Aliasing signal specifier '{}' as '{}' failed.", - old_name, alias_name); - // clang-format on - aliasing_failure = true; - } - } break; - // cloe.alias_signals[i]: is not a table - default: { - // clang-format off - logger()->error( - "One or more entries in 'cloe.alias_signals' has an unexpected datatype '{}'. " - "Expected are entries in this format { \"regex\" , \"short-name\" }", - static_cast(type)); - // clang-format on - aliasing_failure = true; - } break; - } - } - - } break; - case sol::type::none: - case sol::type::lua_nil: { - // not defined -> nop - } break; - default: { - // clang-format off - logger()->error( - "Expected symbol 'cloe.alias_signals' has unexpected datatype '{}'. " - "Expected is a list of 2-tuples in this format { \"regex\" , \"short-name\" }", - static_cast(type)); - // clang-format on - aliasing_failure = true; - } break; - } - if (aliasing_failure) { - throw cloe::ModelError("Aliasing signals failed with above error. Aborting."); - } - } - - // Inject requested signals into lua - { - auto& signals = db.signals(); - bool binding_failure = false; - // Read cloe.require_signals - sol::object value = cloe::luat_cloe_engine_initial_input(ctx.lua)["signal_requires"]; - auto type = value.get_type(); - switch (type) { - // cloe.require_signals expected is a list (i.e. table) of strings - case sol::type::table: { - sol::table require_signals = value.as(); - auto tbl_size = std::distance(require_signals.begin(), require_signals.end()); - - for (int i = 0; i < tbl_size; i++) { - sol::object value = require_signals[i + 1]; - - sol::type type = value.get_type(); - if (type != sol::type::string) { - logger()->warn( - "One entry of cloe.require_signals has a wrong data type: '{}'. " - "Expected is a list of strings.", - static_cast(type)); - binding_failure = true; - continue; - } - std::string signal_name = value.as(); - - // virtually bind signal 'signal_name' to lua - auto iter = db[signal_name]; - if (iter != signals.end()) { - try { - db.bind_signal(signal_name); - logger()->info("Binding signal '{}' as '{}'.", signal_name, signal_name); - } catch (const std::logic_error& ex) { - logger()->error("Binding signal '{}' failed with error: {}", signal_name, - ex.what()); - } - } else { - logger()->warn("Requested signal '{}' does not exist in DataBroker.", signal_name); - binding_failure = true; - } - } - // actually bind all virtually bound signals to lua - db.bind("signals", cloe::luat_cloe_engine(ctx.lua)); - } break; - case sol::type::none: - case sol::type::lua_nil: { - logger()->warn( - "Expected symbol 'cloe.require_signals' appears to be undefined. " - "Expected is a list of string."); - } break; - default: { - logger()->error( - "Expected symbol 'cloe.require_signals' has unexpected datatype '{}'. " - "Expected is a list of string.", - static_cast(type)); - binding_failure = true; - } break; - } - if (binding_failure) { - throw cloe::ModelError("Binding signals to Lua failed with above error. Aborting."); - } - } - } - ctx.progress.init_end(); - ctx.server->refresh_buffer_start_stream(); - logger()->info("Simulation initialization complete."); - if (ctx.probe_simulation) { - ctx.outcome = SimulationOutcome::Probing; - return DISCONNECT; - } - return START; -} - -// START --------------------------------------------------------------------------------------- // - -size_t insert_triggers_from_config(SimulationContext& ctx) { - auto r = ctx.coordinator->trigger_registrar(cloe::Source::FILESYSTEM); - size_t count = 0; - for (const auto& c : ctx.config.triggers) { - if (!ctx.config.engine.triggers_ignore_source && source_is_transient(c.source)) { - continue; - } - try { - r->insert_trigger(c.conf()); - count++; - } catch (cloe::SchemaError& e) { - ctx.logger()->error("Error inserting trigger: {}", e.what()); - std::stringstream s; - fable::pretty_print(e, s); - ctx.logger()->error("> Message:\n {}", s.str()); - throw cloe::ConcludedError(e); - } catch (cloe::TriggerError& e) { - ctx.logger()->error("Error inserting trigger ({}): {}", e.what(), c.to_json().dump()); - throw cloe::ConcludedError(e); - } - } - return count; -} - -/** - * Pseudo-class which hosts the Cloe-Signals as properties inside of the Lua-VM - */ -class LuaCloeSignal {}; - -StateId SimulationMachine::Start::impl(SimulationContext& ctx) { - logger()->info("Starting simulation..."); - - // Begin execution progress - ctx.progress.exec_begin(); - - - // Process initial trigger list - insert_triggers_from_config(ctx); - ctx.coordinator->process_pending_lua_triggers(ctx.sync); - ctx.coordinator->process(ctx.sync); - ctx.callback_start->trigger(ctx.sync); - - // Process initial context - ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { - logger()->trace("Start {} {}", type, m.name()); - m.start(ctx.sync); - return true; // next model - }); - ctx.sync.increment_step(); - - // We can pause at the start of execution too. - if (ctx.pause_execution) { - return PAUSE; - } - - return STEP_BEGIN; -} - -// STEP_BEGIN ---------------------------------------------------------------------------------- // - -StateId SimulationMachine::StepBegin::impl(SimulationContext& ctx) { - ctx.cycle_duration.reset(); - timer::DurationTimer t([&ctx](cloe::Duration d) { - auto ms = std::chrono::duration_cast(d); - ctx.statistics.engine_time_ms.push_back(ms.count()); - }); - - logger()->trace("Step {:0>9}, Time {} ms", ctx.sync.step(), - std::chrono::duration_cast(ctx.sync.time()).count()); - - // Update execution progress - ctx.progress.exec_update(ctx.sync.time()); - if (ctx.report_progress && ctx.progress.exec_report()) { - logger()->info("Execution progress: {}%", - static_cast(ctx.progress.execution.percent() * 100.0)); - } - - // Refresh the double buffer - // - // Note: this line can easily break your time budget with the current server - // implementation. If you need better performance, disable the server in the - // stack file configuration: - // - // { - // "version": "4", - // "server": { - // "listen": false - // } - // } - // - ctx.server->refresh_buffer(); - - // Run cycle- and time-based triggers - ctx.callback_loop->trigger(ctx.sync); - ctx.callback_time->trigger(ctx.sync); - - // Determine whether to continue simulating or stop - bool all_operational = ctx.foreach_model([this](const cloe::Model& m, const char* type) { - if (!m.is_operational()) { - logger()->info("The {} {} is no longer operational.", type, m.name()); - return false; // abort loop - } - return true; // next model - }); - return (all_operational ? STEP_SIMULATORS : STOP); -} - -// STEP_SIMULATORS ----------------------------------------------------------------------------- // - -StateId SimulationMachine::StepSimulators::impl(SimulationContext& ctx) { - auto guard = ctx.server->lock(); - - timer::DurationTimer t([&ctx](cloe::Duration d) { - auto ms = std::chrono::duration_cast(d); - ctx.statistics.simulator_time_ms.push_back(ms.count()); - }); - - // Call the simulator bindings: - ctx.foreach_simulator([&ctx](cloe::Simulator& simulator) { - try { - cloe::Duration sim_time = simulator.process(ctx.sync); - if (!simulator.is_operational()) { - throw cloe::ModelStop("simulator {} no longer operational", simulator.name()); - } - if (sim_time != ctx.sync.time()) { - throw cloe::ModelError( - "simulator {} did not progress to required time: got {}ms, expected {}ms", - simulator.name(), sim_time.count() / 1'000'000, ctx.sync.time().count() / 1'000'000); - } - } catch (cloe::ModelReset& e) { - throw; - } catch (cloe::ModelStop& e) { - throw; - } catch (cloe::ModelAbort& e) { - throw; - } catch (cloe::ModelError& e) { - throw; - } catch (...) { - throw; - } - return true; - }); - - // Clear vehicle cache - ctx.foreach_vehicle([this, &ctx](cloe::Vehicle& v) { - auto t = v.process(ctx.sync); - if (t < ctx.sync.time()) { - logger()->error("Vehicle ({}, {}) not progressing; simulation compromised!", v.id(), - v.name()); - } - return true; - }); - - return STEP_CONTROLLERS; -} - -// STEP_CONTROLLERS ---------------------------------------------------------------------------- // - -StateId SimulationMachine::StepControllers::impl(SimulationContext& ctx) { - auto guard = ctx.server->lock(); - - timer::DurationTimer t([&ctx](cloe::Duration d) { - auto ms = std::chrono::duration_cast(d); - ctx.statistics.controller_time_ms.push_back(ms.count()); - }); - - // We can only erase from ctx.controllers when we have access to the - // iterator itself, otherwise we get undefined behavior. So we save - // the names of the controllers we want to erase from the list. - std::vector controllers_to_erase; - - // Call each controller and handle any errors that might occur. - ctx.foreach_controller([this, &ctx, &controllers_to_erase](cloe::Controller& ctrl) { - if (!ctrl.has_vehicle()) { - // Skip this controller - return true; - } - - // Keep calling the ctrl until it has caught up the current time. - cloe::Duration ctrl_time; - try { - int64_t retries = 0; - for (;;) { - ctrl_time = ctrl.process(ctx.sync); - - // If we are underneath our target, sleep and try again. - if (ctrl_time < ctx.sync.time()) { - this->logger()->warn("Controller {} not progressing, now at {}", ctrl.name(), - cloe::to_string(ctrl_time)); - - // If a controller is misbehaving, we might get stuck in a loop. - // If this happens more than some random high number, then throw - // an error. - if (retries == ctx.config.simulation.controller_retry_limit) { - throw cloe::ModelError{"controller not progressing to target time {}", - cloe::to_string(ctx.sync.time())}; - } - - // Otherwise, sleep and try again. - std::this_thread::sleep_for(ctx.config.simulation.controller_retry_sleep); - ++retries; - } else { - ctx.statistics.controller_retries.push_back(static_cast(retries)); - break; - } - } - } catch (cloe::ModelReset& e) { - this->logger()->error("Controller {} reset: {}", ctrl.name(), e.what()); - this->state_machine()->reset(); - return false; - } catch (cloe::ModelStop& e) { - this->logger()->error("Controller {} stop: {}", ctrl.name(), e.what()); - this->state_machine()->stop(); - return false; - } catch (cloe::ModelAbort& e) { - this->logger()->error("Controller {} abort: {}", ctrl.name(), e.what()); - this->state_machine()->abort(); - return false; - } catch (cloe::Error& e) { - this->logger()->error("Controller {} died: {}", ctrl.name(), e.what()); - if (e.has_explanation()) { - this->logger()->error("Note:\n{}", e.explanation()); - } - if (ctx.config.simulation.abort_on_controller_failure) { - this->logger()->error("Aborting thanks to controller {}", ctrl.name()); - this->state_machine()->abort(); - return false; - } else { - this->logger()->warn("Continuing without controller {}", ctrl.name()); - ctrl.abort(); - ctrl.disconnect(); - controllers_to_erase.push_back(ctrl.name()); - return true; - } - } catch (...) { - this->logger()->critical("Controller {} encountered a fatal error.", ctrl.name()); - throw; - } - - // Write a notice if the controller is ahead of the simulation time. - cloe::Duration ctrl_ahead = ctrl_time - ctx.sync.time(); - if (ctrl_ahead.count() > 0) { - this->logger()->warn("Controller {} is ahead by {}", ctrl.name(), - cloe::to_string(ctrl_ahead)); - } - - // Continue with next controller. - return true; - }); - - // Remove any controllers that we want to continue without. - for (auto ctrl : controllers_to_erase) { - ctx.controllers.erase(ctrl); - } - - return STEP_END; -} - -// STEP_END ------------------------------------------------------------------------------------ // - -StateId SimulationMachine::StepEnd::impl(SimulationContext& ctx) { - // Adjust sim time to wallclock according to realtime factor. - cloe::Duration padding = cloe::Duration{0}; - cloe::Duration elapsed = ctx.cycle_duration.elapsed(); - { - auto guard = ctx.server->lock(); - ctx.sync.set_cycle_time(elapsed); - } - - if (!ctx.sync.is_realtime_factor_unlimited()) { - auto width = ctx.sync.step_width().count(); - auto target = cloe::Duration(static_cast(width / ctx.sync.realtime_factor())); - padding = target - elapsed; - if (padding.count() > 0) { - std::this_thread::sleep_for(padding); - } else { - logger()->trace("Failing target realtime factor: {:.2f} < {:.2f}", - ctx.sync.achievable_realtime_factor(), ctx.sync.realtime_factor()); - } - } - - auto guard = ctx.server->lock(); - ctx.statistics.cycle_time_ms.push_back( - std::chrono::duration_cast(elapsed).count()); - ctx.statistics.padding_time_ms.push_back( - std::chrono::duration_cast(padding).count()); - ctx.sync.increment_step(); - - // Process all inserted triggers now. - ctx.coordinator->process(ctx.sync); - - // We can pause the simulation between STEP_END and STEP_BEGIN. - if (ctx.pause_execution) { - return PAUSE; - } - - return STEP_BEGIN; -} - -// PAUSE --------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Pause::impl(SimulationContext& ctx) { - if (state_machine()->previous_state() != PAUSE) { - logger()->info("Pausing simulation..."); - logger()->info(R"(Send {"event": "pause", "action": "resume"} trigger to resume.)"); - logger()->debug( - R"(For example: echo '{{"event": "pause", "action": "resume"}}' | curl -d @- http://localhost:{}/api/triggers/input)", - ctx.config.server.listen_port); - // If the server is not enabled, then the user probably won't be able to resume. - if (!ctx.config.server.listen) { - logger()->warn("Start temporary server."); - ctx.server->start(); - } - } - - { - // Process all inserted triggers here, because the main loop is not running - // while we are paused. Ideally, we should only allow triggers that are - // destined for the pause state, although it might be handy to pause, allow - // us to insert triggers, and then resume. Triggers that are inserted via - // the web UI are just as likely to be incorrectly inserted as correctly. - auto guard = ctx.server->lock(); - ctx.coordinator->process(ctx.sync); - } - - // TODO(ben): Process triggers that come in so we can also conclude. - // What kind of triggers do we want to allow? Should we also be processing - // NEXT trigger events? How after pausing do we resume? - ctx.callback_loop->trigger(ctx.sync); - ctx.callback_pause->trigger(ctx.sync); - std::this_thread::sleep_for(ctx.config.engine.polling_interval); - - if (ctx.pause_execution) { - return PAUSE; - } - - return RESUME; -} - -// RESUME -------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Resume::impl(SimulationContext& ctx) { - // TODO(ben): Eliminate the RESUME state and move this functionality into - // the PAUSE state. This more closely matches the way we think about PAUSE - // as a state vs. RESUME as a transition. - logger()->info("Resuming simulation..."); - if (!ctx.config.server.listen) { - logger()->warn("Stop temporary server."); - ctx.server->stop(); - } - ctx.callback_resume->trigger(ctx.sync); - return STEP_BEGIN; -} - -// STOP ---------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Stop::impl(SimulationContext& ctx) { - logger()->info("Stopping simulation..."); - - // If no other outcome has already been defined, then mark as "stopped". - if (!ctx.outcome) { - ctx.outcome = SimulationOutcome::Stopped; - } - - ctx.callback_stop->trigger(ctx.sync); - ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { - try { - if (m.is_operational()) { - logger()->debug("Stop {} {}", type, m.name()); - m.stop(ctx.sync); - } - } catch (std::exception& e) { - logger()->error("Stopping {} {} failed: {}", type, m.name(), e.what()); - } - return true; - }); - ctx.progress.message = "execution complete"; - ctx.progress.execution.end(); - - if (ctx.config.engine.keep_alive) { - return KEEP_ALIVE; - } - return DISCONNECT; -} - -// DISCONNECT ---------------------------------------------------------------------------------- // - -StateId SimulationMachine::Disconnect::impl(SimulationContext& ctx) { - logger()->debug("Disconnecting simulation..."); - ctx.foreach_model([](cloe::Model& m, const char*) { - m.disconnect(); - return true; - }); - logger()->info("Simulation disconnected."); - return nullptr; -} - -// SUCCESS ------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Success::impl(SimulationContext& ctx) { - logger()->info("Simulation successful."); - ctx.outcome = SimulationOutcome::Success; - ctx.callback_success->trigger(ctx.sync); - return STOP; -} - -// FAIL ---------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Fail::impl(SimulationContext& ctx) { - logger()->info("Simulation failed."); - ctx.outcome = SimulationOutcome::Failure; - ctx.callback_failure->trigger(ctx.sync); - return STOP; -} - -// RESET --------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Reset::impl(SimulationContext& ctx) { - logger()->info("Resetting simulation..."); - ctx.callback_reset->trigger(ctx.sync); - auto ok = ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { - try { - logger()->debug("Reset {} {}", type, m.name()); - m.stop(ctx.sync); - m.reset(); - } catch (std::exception& e) { - logger()->error("Resetting {} {} failed: {}", type, m.name(), e.what()); - return false; - } - return true; - }); - if (ok) { - return CONNECT; - } else { - return ABORT; - } -} - -// KEEP_ALIVE ---------------------------------------------------------------------------------- // - -StateId SimulationMachine::KeepAlive::impl(SimulationContext& ctx) { - if (state_machine()->previous_state() != KEEP_ALIVE) { - logger()->info("Keeping simulation alive..."); - logger()->info("Press [Ctrl+C] to disconnect."); - } - ctx.callback_pause->trigger(ctx.sync); - std::this_thread::sleep_for(ctx.config.engine.polling_interval); - return KEEP_ALIVE; -} - -// ABORT --------------------------------------------------------------------------------------- // - -StateId SimulationMachine::Abort::impl(SimulationContext& ctx) { - const auto* previous_state = state_machine()->previous_state(); - if (previous_state == KEEP_ALIVE) { - return DISCONNECT; - } else if (previous_state == CONNECT) { - ctx.outcome = SimulationOutcome::NoStart; - return DISCONNECT; - } - - logger()->info("Aborting simulation..."); - ctx.outcome = SimulationOutcome::Aborted; - ctx.foreach_model([this](cloe::Model& m, const char* type) { - try { - logger()->debug("Abort {} {}", type, m.name()); - m.abort(); - } catch (std::exception& e) { - logger()->error("Aborting {} {} failed: {}", type, m.name(), e.what()); - } - return true; - }); - return DISCONNECT; -} - -// --------------------------------------------------------------------------------------------- // - Simulation::Simulation(cloe::Stack&& config, sol::state_view lua, const std::string& uuid) : config_(std::move(config)) , lua_(std::move(lua)) , logger_(cloe::logger::get("cloe")) - , uuid_(uuid) {} - -struct SignalReport { - std::string name; - std::vector names; - - friend void to_json(cloe::Json& j, const SignalReport& r) { - j = cloe::Json{ - {"name", r.name}, - {"names", r.names}, - }; - } -}; -struct SignalsReport { - std::vector signals; - - friend void to_json(cloe::Json& j, const SignalsReport& r) { - j = cloe::Json{ - {"signals", r.signals}, - }; - } -}; - -cloe::Json dump_signals(cloe::DataBroker& db) { - SignalsReport report; - - const auto& signals = db.signals(); - for (const auto& [key, signal] : signals) { - // create signal - auto& signalreport = report.signals.emplace_back(); - // copy the signal-names - signalreport.name = key; - std::copy(signal->names().begin(), signal->names().end(), - std::back_inserter(signalreport.names)); + , uuid_(uuid) { + set_output_dir(); +} - const auto& metadata = signal->metadatas(); +std::filesystem::path Simulation::get_output_filepath(const std::filesystem::path& filename) const { + std::filesystem::path filepath; + if (filename.is_absolute()) { + filepath = filename; + } else if (output_dir_) { + filepath = *output_dir_ / filename; + } else { + throw cloe::ModelError{"cannot determine output path for '{}'", filename.native()}; } - auto json = cloe::Json{report}; - return json; + return filepath; } -std::vector dump_signals_autocompletion(cloe::DataBroker& db) { - auto result = std::vector{}; - result.emplace_back("--- @meta"); - result.emplace_back("--- @class signals"); - - const auto& signals = db.signals(); - for (const auto& [key, signal] : signals) { - const auto* tag = signal->metadata(); - if (tag) { - const auto lua_type = to_string(tag->datatype); - const auto& lua_helptext = tag->text; - auto line = fmt::format("--- @field {} {} {}", key, lua_type, lua_helptext); - result.emplace_back(std::move(line)); - } else { - auto line = fmt::format("--- @field {}", key); - result.emplace_back(std::move(line)); +void Simulation::set_output_dir() { + if (config_.engine.output_path) { + // For $registry to be of value, output_path (~= $id) here needs to be set. + if (config_.engine.output_path->is_absolute()) { + // If it's absolute, then registry_path doesn't matter. + output_dir_ = *config_.engine.output_path; + } else if (config_.engine.registry_path) { + // Now, since output_dir is relative, we need the registry path. + // We don't care here whether the registry is relative or not. + output_dir_ = *config_.engine.registry_path / *config_.engine.output_path; } } - return result; } -SimulationResult Simulation::run() { - // Input: - SimulationContext ctx{lua_.lua_state()}; - ctx.db = std::make_unique(ctx.lua); - ctx.server = make_server(config_.server); - ctx.coordinator = std::make_unique(ctx.lua, ctx.db.get()); - ctx.registrar = std::make_unique(ctx.server->server_registrar(), ctx.coordinator.get(), - ctx.db.get()); - ctx.commander = std::make_unique(logger()); - ctx.sync = SimulationSync(config_.simulation.model_step_width); - ctx.config = config_; - ctx.uuid = uuid_; - ctx.report_progress = report_progress_; - - // Output: - SimulationResult r; - r.uuid = uuid_; - r.config = ctx.config; - r.set_output_dir(); - r.outcome = SimulationOutcome::NoStart; - - // Abort handler: - SimulationMachine machine; - abort_fn_ = [this, &r, &ctx, &machine]() { +void Simulation::set_abort_handler( + SimulationMachine& machine, SimulationContext& ctx, std::function hook) { + abort_fn_ = [&, this]() { static size_t requests = 0; logger()->info("Signal caught."); - r.errors.emplace_back("user sent abort signal (e.g. with Ctrl+C)"); + if (hook) { + hook(); + } requests += 1; + if (ctx.progress.is_init_ended()) { if (!ctx.progress.is_exec_ended()) { logger()->info("Aborting running simulation."); @@ -1532,16 +173,27 @@ SimulationResult Simulation::run() { return true; }); }; +} + +SimulationResult Simulation::run() { + auto machine = SimulationMachine(); + auto ctx = SimulationContext(config_, lua_.lua_state()); + auto errors = std::vector(); + set_abort_handler(machine, ctx, [&errors]() { + errors.emplace_back("user sent abort signal (e.g. with Ctrl+C)"); + }); - // Execution: try { + ctx.uuid = uuid_; + ctx.report_progress = report_progress_; + // Start the server if enabled if (config_.server.listen) { ctx.server->start(); } // Stream data to the requested file - if (r.config.engine.output_file_data_stream) { - auto filepath = r.get_output_filepath(*r.config.engine.output_file_data_stream); + if (config_.engine.output_file_data_stream) { + auto filepath = get_output_filepath(*config_.engine.output_file_data_stream); if (is_writable(filepath)) { ctx.server->init_stream(filepath.native()); } @@ -1555,11 +207,12 @@ SimulationResult Simulation::run() { // Run the simulation cloe::luat_cloe_engine_state(lua_)["is_running"] = true; machine.run(ctx); + cloe::luat_cloe_engine_state(lua_)["is_running"] = false; } catch (cloe::ConcludedError& e) { - r.errors.emplace_back(e.what()); + errors.emplace_back(e.what()); ctx.outcome = SimulationOutcome::Aborted; } catch (std::exception& e) { - r.errors.emplace_back(e.what()); + errors.emplace_back(e.what()); ctx.outcome = SimulationOutcome::Aborted; } @@ -1569,41 +222,72 @@ SimulationResult Simulation::run() { ctx.commander->run_all(config_.engine.hooks_post_disconnect); } catch (cloe::ConcludedError& e) { // TODO(ben): ensure outcome is correctly saved - r.errors.emplace_back(e.what()); + errors.emplace_back(e.what()); } // Wait for any running children to terminate. // (We could provide an option to time-out; this would involve using wait_for // instead of wait.) ctx.commander->wait_all(); - abort_fn_ = nullptr; + reset_abort_handler(); - // TODO(ben): Preserve NoStart outcome. - if (ctx.outcome) { - r.outcome = *ctx.outcome; - } else { - r.outcome = SimulationOutcome::Aborted; - } + auto result = ctx.result.value_or(SimulationResult{}); + result.outcome = ctx.outcome.value_or(SimulationOutcome::Aborted); + assert(result.errors.empty()); // Not currently used in simulation. + result.errors = errors; + return result; +} - r.sync = ctx.sync; - r.statistics = ctx.statistics; - r.elapsed = ctx.progress.elapsed(); - r.triggers = ctx.coordinator->history(); - r.report = sol::object(cloe::luat_cloe_engine_state(ctx.lua)["report"]); - // Don't create output file data unless the output files are being written - if (ctx.config.engine.output_file_signals || ctx.probe_simulation) { - r.signals = dump_signals(*ctx.db); +SimulationProbe Simulation::probe() { + auto machine = SimulationMachine(); + auto ctx = SimulationContext(config_, lua_.lua_state()); + auto errors = std::vector(); + set_abort_handler(machine, ctx, [&errors]() { + errors.emplace_back("user sent abort signal (e.g. with Ctrl+C)"); + }); + + try { + ctx.uuid = uuid_; + ctx.report_progress = report_progress_; + + // We deviate from run() by only doing the minimal amount of work here. + // In particular: + // - No server + // - No commands / triggers + // - No streaming file output + // - Run pre-connect hooks only + ctx.commander->set_enabled(config_.engine.security_enable_hooks); + ctx.commander->run_all(config_.engine.hooks_pre_connect); + + ctx.probe_simulation = true; + machine.run(ctx); + } catch (cloe::ConcludedError& e) { + errors.emplace_back(e.what()); + ctx.outcome = SimulationOutcome::Aborted; + } catch (std::exception& e) { + errors.emplace_back(e.what()); + ctx.outcome = SimulationOutcome::Aborted; } - if (ctx.config.engine.output_file_signals_autocompletion) { - r.signals_autocompletion = dump_signals_autocompletion(*ctx.db); + + try { + // Run post-disconnect hooks + ctx.commander->set_enabled(config_.engine.security_enable_hooks); + ctx.commander->run_all(config_.engine.hooks_post_disconnect); + } catch (cloe::ConcludedError& e) { + // TODO(ben): ensure outcome is correctly saved + errors.emplace_back(e.what()); } - return r; + auto result = ctx.probe.value_or(SimulationProbe{}); + result.outcome = ctx.outcome.value_or(SimulationOutcome::Aborted); + assert(result.errors.empty()); // Not currently used in simulation. + result.errors = errors; + return result; } size_t Simulation::write_output(const SimulationResult& r) const { - if (r.output_dir) { - logger()->debug("Using output path: {}", r.output_dir->native()); + if (output_dir_) { + logger()->debug("Using output path: {}", output_dir_->native()); } size_t files_written = 0; @@ -1612,17 +296,17 @@ size_t Simulation::write_output(const SimulationResult& r) const { return; } - std::filesystem::path filepath = r.get_output_filepath(*filename); + std::filesystem::path filepath = get_output_filepath(*filename); if (write_output_file(filepath, output)) { files_written++; } }; - write_file(r.config.engine.output_file_result, r); - write_file(r.config.engine.output_file_config, r.config); - write_file(r.config.engine.output_file_triggers, r.triggers); - write_file(r.config.engine.output_file_signals, r.signals); - write_file(r.config.engine.output_file_signals_autocompletion, r.signals_autocompletion); + write_file(config_.engine.output_file_result, r); + write_file(config_.engine.output_file_config, config_); + write_file(config_.engine.output_file_triggers, r.triggers); + // write_file(config_.engine.output_file_signals, .signals); + // write_file(config_.engine.output_file_signals_autocompletion, r.signals_autocompletion); logger()->info("Wrote {} output files.", files_written); return files_written; diff --git a/engine/src/simulation.hpp b/engine/src/simulation.hpp index bc7dcf917..f661eb2b6 100644 --- a/engine/src/simulation.hpp +++ b/engine/src/simulation.hpp @@ -22,96 +22,27 @@ #pragma once +#include // for path #include // for function<> -#include // for unique_ptr<> +#include // for optional<> -#include // for path +#include // for state_view -#include // for ENUM_SERIALIZATION -#include // for state - -#include "simulation_context.hpp" #include "stack.hpp" // for Stack namespace engine { -struct SimulationResult { - cloe::Stack config; - - std::string uuid; - SimulationSync sync; - cloe::Duration elapsed; - SimulationOutcome outcome; - std::vector errors; - SimulationStatistics statistics; - cloe::Json triggers; - cloe::Json report; - cloe::Json signals; // dump of all signals in DataBroker right before the simulation started - std::vector - signals_autocompletion; // pseudo lua file used for vscode autocompletion - std::optional output_dir; - - public: - /** - * The output directory of files is normally built up with: - * - * $registry / $id / $filename - * - * If any of the last variables is absolute, the preceding variables - * shall be ignored; e.g. if $filename is absolute, then neither the - * simulation registry nor the UUID-based path shall be considered. - * - * If not explicitly specified in the configuration file, the registry - * and output path are set automatically. Thus, if they are empty, then - * that is because the user explicitly set them so. - */ - std::filesystem::path get_output_filepath(const std::filesystem::path& filename) const { - std::filesystem::path filepath; - if (filename.is_absolute()) { - filepath = filename; - } else if (output_dir) { - filepath = *output_dir / filename; - } else { - throw cloe::ModelError{"cannot determine output path for '{}'", filename.native()}; - } - - return filepath; - } - - /** - * Determine the output directory from config. - * - * Must be called before output_dir is used. - */ - void set_output_dir() { - if (config.engine.output_path) { - // For $registry to be of value, output_path (~= $id) here needs to be set. - if (config.engine.output_path->is_absolute()) { - // If it's absolute, then registry_path doesn't matter. - output_dir = *config.engine.output_path; - } else if (config.engine.registry_path) { - // Now, since output_dir is relative, we need the registry path. - // We don't care here whether the registry is relative or not. - output_dir = *config.engine.registry_path / *config.engine.output_path; - } - } - } - - friend void to_json(cloe::Json& j, const SimulationResult& r) { - j = cloe::Json{ - {"elapsed", r.elapsed}, - {"errors", r.errors}, - {"outcome", r.outcome}, - {"report", r.report}, - {"simulation", r.sync}, - {"statistics", r.statistics}, - {"uuid", r.uuid}, - }; - } -}; +class SimulationContext; +class SimulationMachine; +class SimulationResult; +class SimulationProbe; class Simulation { public: + Simulation(const Simulation&) = default; + Simulation(Simulation&&) = delete; + Simulation& operator=(const Simulation&) = default; + Simulation& operator=(Simulation&&) = delete; Simulation(cloe::Stack&& config, sol::state_view lua, const std::string& uuid); ~Simulation() = default; @@ -128,6 +59,13 @@ class Simulation { */ SimulationResult run(); + /** + * Probe a simulation. + * + * This connects and enrolls, but does not start the simulation. + */ + SimulationProbe probe(); + /** * Write simulation output into files and return number of files written. */ @@ -148,11 +86,6 @@ class Simulation { */ void set_report_progress(bool value) { report_progress_ = value; } - /** - * Set whether simulation should be probed for information only. - */ - void set_probe_simulation(bool value) { probe_simulation_ = value; } - /** * Abort the simulation from a separate thread. * @@ -160,16 +93,54 @@ class Simulation { */ void signal_abort(); + private: + /** + * Determine the output directory from config. + * + * Must be called before output_dir is used. + */ + void set_output_dir(); + + /** + * The output directory of files is normally built up with: + * + * $registry / $id / $filename + * + * If any of the last variables is absolute, the preceding variables + * shall be ignored; e.g. if $filename is absolute, then neither the + * simulation registry nor the UUID-based path shall be considered. + * + * If not explicitly specified in the configuration file, the registry + * and output path are set automatically. Thus, if they are empty, then + * that is because the user explicitly set them so. + */ + std::filesystem::path get_output_filepath(const std::filesystem::path& filename) const; + + /** + * Create the default abort handler that can be used by signal_abort() on + * this Simulation instance. The return value can be assigned to abort_fn_. + * + * It is important that the lifetime of all passed arguments exceeds that + * of the returned function! Before they are removed, call reset_abort_handler(). + */ + void set_abort_handler(SimulationMachine& machine, SimulationContext& ctx, + std::function hook); + + /** + * Reset the abort handler before it becomes invalid. + */ + void reset_abort_handler() { abort_fn_ = nullptr; } + private: cloe::Stack config_; sol::state_view lua_; cloe::Logger logger_; std::string uuid_; + std::optional output_dir_; std::function abort_fn_; // Options: bool report_progress_{false}; - bool probe_simulation_{false}; }; } // namespace engine diff --git a/engine/src/simulation_actions.hpp b/engine/src/simulation_actions.hpp new file mode 100644 index 000000000..f5e81ef38 --- /dev/null +++ b/engine/src/simulation_actions.hpp @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_actions.hpp + * + * This file defines the simple actions inherent to + * the simulation machine itself. + */ + +#pragma once + +#include // for Trigger, Event, EventFactory, ... +#include // for DEFINE_SET_STATE_ACTION, SetDataActionFactory + +#include "simulation_context.hpp" +#include "simulation_machine.hpp" +#include "simulation_statistics.hpp" +#include "simulation_sync.hpp" + +namespace engine::actions { + +DEFINE_SET_STATE_ACTION(Pause, "pause", "pause simulation", SimulationMachine, { ptr_->pause(); }) + +DEFINE_SET_STATE_ACTION(Resume, "resume", "resume paused simulation", SimulationMachine, + { ptr_->resume(); }) + +DEFINE_SET_STATE_ACTION(Stop, "stop", "stop simulation with neither success nor failure", + SimulationMachine, { ptr_->stop(); }) + +DEFINE_SET_STATE_ACTION(Succeed, "succeed", "stop simulation with success", SimulationMachine, + { ptr_->succeed(); }) + +DEFINE_SET_STATE_ACTION(Fail, "fail", "stop simulation with failure", SimulationMachine, + { ptr_->fail(); }) + +DEFINE_SET_STATE_ACTION(Reset, "reset", "attempt to reset simulation", SimulationMachine, + { ptr_->reset(); }) + +DEFINE_SET_STATE_ACTION(KeepAlive, "keep_alive", "keep simulation alive after termination", + SimulationContext, { ptr_->config.engine.keep_alive = true; }) + +DEFINE_SET_STATE_ACTION(ResetStatistics, "reset_statistics", "reset simulation statistics", + SimulationStatistics, { ptr_->reset(); }) + +DEFINE_SET_DATA_ACTION(RealtimeFactor, "realtime_factor", "modify the simulation speed", + SimulationSync, "factor", double, { + logger()->info("Setting target simulation speed: {}", value_); + ptr_->set_realtime_factor(value_); + }) + +} // namespace engine::actions diff --git a/engine/src/simulation_context.cpp b/engine/src/simulation_context.cpp index 106d7a4aa..5117d0954 100644 --- a/engine/src/simulation_context.cpp +++ b/engine/src/simulation_context.cpp @@ -18,12 +18,33 @@ #include "simulation_context.hpp" -#include -#include -#include +#include // for make_unique<> + +#include // for Controller +#include // for DataBroker +#include // for Simulator +#include // for Vehicle + +#include "coordinator.hpp" // for Coordinator +#include "registrar.hpp" // for Registrar +#include "server.hpp" // for Server +#include "utility/command.hpp" // for CommandExecuter namespace engine { +SimulationContext::SimulationContext(cloe::Stack conf_, sol::state_view lua_) + : config(std::move(conf_)) + , lua(std::move(lua_)) + , db(std::make_unique(lua)) + , server(make_server(config.server)) + , coordinator(std::make_unique(lua, db.get())) + , registrar( + std::make_unique(server->server_registrar(), coordinator.get(), db.get())) + , commander(std::make_unique(logger())) + , sync(SimulationSync(config.simulation.model_step_width)) {} + +cloe::Logger SimulationContext::logger() const { return cloe::logger::get("cloe"); } + std::string SimulationContext::version() const { return CLOE_ENGINE_VERSION; } std::vector SimulationContext::model_ids() const { @@ -73,15 +94,21 @@ std::shared_ptr SimulationContext::simulation_registrar() { bool SimulationContext::foreach_model(std::function f) { for (auto& kv : controllers) { auto ok = f(*kv.second, "controller"); - if (!ok) return false; + if (!ok) { + return false; + } } for (auto& kv : vehicles) { auto ok = f(*kv.second, "vehicle"); - if (!ok) return false; + if (!ok) { + return false; + } } for (auto& kv : simulators) { auto ok = f(*kv.second, "simulator"); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -90,15 +117,21 @@ bool SimulationContext::foreach_model( std::function f) const { for (auto& kv : controllers) { auto ok = f(*kv.second, "controller"); - if (!ok) return false; + if (!ok) { + return false; + } } for (auto& kv : vehicles) { auto ok = f(*kv.second, "vehicle"); - if (!ok) return false; + if (!ok) { + return false; + } } for (auto& kv : simulators) { auto ok = f(*kv.second, "simulator"); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -106,7 +139,9 @@ bool SimulationContext::foreach_model( bool SimulationContext::foreach_simulator(std::function f) { for (auto& kv : simulators) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -114,7 +149,9 @@ bool SimulationContext::foreach_simulator(std::function bool SimulationContext::foreach_simulator(std::function f) const { for (auto& kv : simulators) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -122,7 +159,9 @@ bool SimulationContext::foreach_simulator(std::function f) { for (auto& kv : vehicles) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -130,7 +169,9 @@ bool SimulationContext::foreach_vehicle(std::function f) { bool SimulationContext::foreach_vehicle(std::function f) const { for (auto& kv : vehicles) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -138,7 +179,9 @@ bool SimulationContext::foreach_vehicle(std::function f) { for (auto& kv : controllers) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } @@ -146,7 +189,9 @@ bool SimulationContext::foreach_controller(std::function f) const { for (auto& kv : controllers) { auto ok = f(*kv.second); - if (!ok) return false; + if (!ok) { + return false; + } } return true; } diff --git a/engine/src/simulation_context.hpp b/engine/src/simulation_context.hpp index 7f9bcd335..360cc5feb 100644 --- a/engine/src/simulation_context.hpp +++ b/engine/src/simulation_context.hpp @@ -22,7 +22,6 @@ #pragma once -#include // for uint64_t #include // for function<> #include // for map<> #include // for unique_ptr<>, shared_ptr<> @@ -32,203 +31,121 @@ #include // for state_view -#include // for Simulator, Controller, Registrar, Vehicle, Duration -#include // for DataBroker -#include // for Sync -#include // for DEFINE_NIL_EVENT -#include // for Accumulator -#include // for DurationTimer +#include // for Simulator, Controller, Registrar, Vehicle, Duration +#include // for DurationTimer -#include "coordinator.hpp" // for Coordinator -#include "registrar.hpp" // for Registrar -#include "server.hpp" // for Server -#include "simulation_progress.hpp" // for SimulationProgress -#include "stack.hpp" // for Stack -#include "utility/command.hpp" // for CommandExecuter -#include "utility/time_event.hpp" // for TimeCallback +#include "simulation_events.hpp" // for LoopCallback, ... +#include "simulation_outcome.hpp" // for SimulationOutcome +#include "simulation_probe.hpp" // for SimulationProbe +#include "simulation_progress.hpp" // for SimulationProgress +#include "simulation_result.hpp" // for SimulationResult +#include "simulation_statistics.hpp" // for SimulationStatistics +#include "simulation_sync.hpp" // for SimulationSync +#include "stack.hpp" // for Stack namespace engine { -/** - * SimulationSync is the synchronization context of the simulation. - */ -class SimulationSync : public cloe::Sync { - public: // Overrides - SimulationSync() = default; - explicit SimulationSync(const cloe::Duration& step_width) : step_width_(step_width) {} - - uint64_t step() const override { return step_; } - cloe::Duration step_width() const override { return step_width_; } - cloe::Duration time() const override { return time_; } - cloe::Duration eta() const override { return eta_; } - - /** - * Return the target simulation factor, with 1.0 being realtime. - * - * - If target realtime factor is <= 0.0, then it is interpreted to be unlimited. - * - Currently, the floating INFINITY value is not handled specially. - */ - double realtime_factor() const override { return realtime_factor_; } - - /** - * Return the maximum theorically achievable simulation realtime factor, - * with 1.0 being realtime. - */ - double achievable_realtime_factor() const override { - return static_cast(step_width().count()) / static_cast(cycle_time_.count()); - } - - public: // Modification - /** - * Increase the step number for the simulation. - * - * - It increases the step by one. - * - It moves the simulation time forward by the step width. - * - It stores the real time difference from the last time IncrementStep was called. - */ - void increment_step() { - step_ += 1; - time_ += step_width_; - } - - /** - * Set the target realtime factor, with any value less or equal to zero - * unlimited. - */ - void set_realtime_factor(double s) { realtime_factor_ = s; } - - void set_eta(cloe::Duration d) { eta_ = d; } - - void reset() { - time_ = cloe::Duration(0); - step_ = 0; - } - - void set_cycle_time(cloe::Duration d) { cycle_time_ = d; } - - private: - // Simulation State - uint64_t step_{0}; - cloe::Duration time_{0}; - cloe::Duration eta_{0}; - cloe::Duration cycle_time_; - - // Simulation Configuration - double realtime_factor_{1.0}; // realtime - cloe::Duration step_width_{20'000'000}; // should be 20ms -}; - -struct SimulationStatistics { - cloe::utility::Accumulator engine_time_ms; - cloe::utility::Accumulator cycle_time_ms; - cloe::utility::Accumulator simulator_time_ms; - cloe::utility::Accumulator controller_time_ms; - cloe::utility::Accumulator padding_time_ms; - cloe::utility::Accumulator controller_retries; - - void reset() { - engine_time_ms.reset(); - cycle_time_ms.reset(); - simulator_time_ms.reset(); - controller_time_ms.reset(); - padding_time_ms.reset(); - controller_retries.reset(); - } - - friend void to_json(cloe::Json& j, const SimulationStatistics& s) { - j = cloe::Json{ - {"engine_time_ms", s.engine_time_ms}, {"simulator_time_ms", s.simulator_time_ms}, - {"controller_time_ms", s.controller_time_ms}, {"padding_time_ms", s.padding_time_ms}, - {"cycle_time_ms", s.cycle_time_ms}, {"controller_retries", s.controller_retries}, - }; - } -}; +// Forward-declarations: +class CommandExecuter; +class Registrar; +class Coordinator; +class Server; +class SimulationResult; +class SimulationProbe; /** - * SimulationOutcome describes the possible outcomes a simulation can have. - */ -enum class SimulationOutcome { - NoStart, ///< Simulation unable to start. - Aborted, ///< Simulation aborted due to technical problems or interrupt. - Stopped, ///< Simulation concluded, but without valuation. - Failure, ///< Simulation explicitly concluded with failure. - Success, ///< Simulation explicitly concluded with success. - Probing, ///< Simulation started briefly to gather specific information. -}; - -// If possible, the following exit codes should not be used as they are used -// by the Bash shell, among others: 1-2, 126-165, and 255. That leaves us -// primarily with the range 3-125, which should suffice for our purposes. -// The following exit codes should not be considered stable. -#define EXIT_OUTCOME_SUCCESS EXIT_SUCCESS // normally 0 -#define EXIT_OUTCOME_UNKNOWN EXIT_FAILURE // normally 1 -#define EXIT_OUTCOME_NOSTART 4 // 0b.....1.. -#define EXIT_OUTCOME_STOPPED 8 // 0b....1... -#define EXIT_OUTCOME_FAILURE 9 // 0b....1..1 -#define EXIT_OUTCOME_ABORTED 16 // 0b...1.... - -// clang-format off -ENUM_SERIALIZATION(SimulationOutcome, ({ - {SimulationOutcome::Aborted, "aborted"}, - {SimulationOutcome::NoStart, "no-start"}, - {SimulationOutcome::Failure, "failure"}, - {SimulationOutcome::Success, "success"}, - {SimulationOutcome::Stopped, "stopped"}, - {SimulationOutcome::Probing, "probing"}, -})) -// clang-format on - -namespace events { - -DEFINE_NIL_EVENT(Start, "start", "start of simulation") -DEFINE_NIL_EVENT(Stop, "stop", "stop of simulation") -DEFINE_NIL_EVENT(Success, "success", "simulation success") -DEFINE_NIL_EVENT(Failure, "failure", "simulation failure") -DEFINE_NIL_EVENT(Reset, "reset", "reset of simulation") -DEFINE_NIL_EVENT(Pause, "pause", "pausation of simulation") -DEFINE_NIL_EVENT(Resume, "resume", "resumption of simulation after pause") -DEFINE_NIL_EVENT(Loop, "loop", "begin of inner simulation loop each cycle") - -} // namespace events - -/** - * SimulationContext represents the entire context of a running simulation. + * SimulationContext represents the entire context of a running simulation + * and is used by SimulationMachine class as the data context for the + * state machine. + * + * The simulation states need to store any data they want to access in the + * context here. This does have the caveat that all the data here is + * accessible to all states. * - * This clearly separates data from functionality. There is no constructor - * where extra initialization is performed. Instead any initialization is - * performed in the simulation states in the `simulation.cpp` file. + * All input to and output from the simulation is via this struct. */ struct SimulationContext { - SimulationContext(sol::state_view l) : lua(std::move(l)) {} - + SimulationContext(cloe::Stack conf, sol::state_view l); + + // Configuration ----------------------------------------------------------- + // + // These values are meant to be set before starting the simulation in order + // to affect how the simulation is run. + // + // The other values in this struct should not be directly modified unless + // you really know what you are doing. + // + + cloe::Stack config; ///< Input configuration. + std::string uuid{}; ///< UUID to use for simulation. + + /// Report simulation progress to the console. + bool report_progress{false}; + + /// Setup simulation but only probe for information. + /// The simulation should only go through the CONNECT -> PROBE -> DISCONNECT + /// state. The same errors that can occur for a normal simulation can occur + /// here though, so make sure they are handled. + bool probe_simulation{false}; + + // Setup ------------------------------------------------------------------- + // + // These are functional parts of the simulation framework that mostly come + // from the engine. They are all initialized in the constructor. + // sol::state_view lua; - - // Setup std::unique_ptr db; std::unique_ptr server; std::shared_ptr coordinator; std::shared_ptr registrar; + + /// Configurable system command executer for triggers. std::unique_ptr commander; - // Configuration - cloe::Stack config; ///< Input configuration. - std::string uuid{}; ///< UUID to use for simulation. - bool report_progress{false}; ///< Report simulation progress to console. - bool probe_simulation{false}; ///< Probe for information from enroll methods. + // State ------------------------------------------------------------------- + // + // These are the types that represent the simulation state and have no + // functionality of their own, directly. They may change during the + // simulation. + // - // State + /// Track the simulation timing. SimulationSync sync; + + /// Track the approximate progress of the simulation. SimulationProgress progress; - SimulationStatistics statistics; + + /// Non-owning pointer used in order to keep track which model is being + /// initialized in the CONNECT state in order to allow it to be directly + /// aborted if it is hanging during initialization. If no model is being + /// actively initialized the valid value is nullptr. cloe::Model* now_initializing{nullptr}; + std::map> simulators; std::map> vehicles; std::map> controllers; - std::optional outcome; + timer::DurationTimer cycle_duration; + + /// Tell the simulation that we want to transition into the PAUSE state. + /// + /// We can't do this directly via an interrupt because we can only go + /// into the PAUSE state after STEP_END. bool pause_execution{false}; - // Events + // Output ------------------------------------------------------------------ + SimulationStatistics statistics; + std::optional outcome; + std::optional result; + std::optional probe; + + // Events ------------------------------------------------------------------ + // + // The following callbacks store listeners on the given events. + // In the state where an event occurs, the callback is then triggered. + // There is generally only one place where each of these callbacks is + // triggered. + // std::shared_ptr callback_loop; std::shared_ptr callback_pause; std::shared_ptr callback_resume; @@ -240,8 +157,14 @@ struct SimulationContext { std::shared_ptr callback_time; public: + // Helper Methods ---------------------------------------------------------- + // + // These methods encapsulate methods on the data in this struct that can be + // used by various states. They constitute implementation details and may + // be refactored out of this struct at some point. + // std::string version() const; - cloe::Logger logger() const { return cloe::logger::get("cloe"); } + cloe::Logger logger() const; std::shared_ptr simulation_registrar(); diff --git a/engine/src/utility/time_event.hpp b/engine/src/simulation_events.hpp similarity index 85% rename from engine/src/utility/time_event.hpp rename to engine/src/simulation_events.hpp index c32f2250f..2b01b8161 100644 --- a/engine/src/utility/time_event.hpp +++ b/engine/src/simulation_events.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 Robert Bosch GmbH + * Copyright 2024 Robert Bosch GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ /** - * \file time_event.hpp + * \file simulation_events.hpp * * This file defines the "time" and "next" events. */ @@ -27,12 +27,28 @@ #include // for unique_ptr<>, make_unique<> #include // for priority_queue<> -#include // for Json, Duration, Seconds -#include // for Sync -#include // for Trigger, Event, EventFactory, ... +#include // for Json, Duration, Seconds +#include // for Sync +#include // for Trigger, Event, EventFactory, ... +#include // for DEFINE_NIL_EVENT -namespace engine { -namespace events { +namespace engine::events { + +DEFINE_NIL_EVENT(Start, "start", "start of simulation") + +DEFINE_NIL_EVENT(Stop, "stop", "stop of simulation") + +DEFINE_NIL_EVENT(Success, "success", "simulation success") + +DEFINE_NIL_EVENT(Failure, "failure", "simulation failure") + +DEFINE_NIL_EVENT(Reset, "reset", "reset of simulation") + +DEFINE_NIL_EVENT(Pause, "pause", "pausation of simulation") + +DEFINE_NIL_EVENT(Resume, "resume", "resumption of simulation after pause") + +DEFINE_NIL_EVENT(Loop, "loop", "begin of inner simulation loop each cycle") class NextCallback; @@ -140,7 +156,8 @@ class TimeCallback : public cloe::Callback { // a shared_ptr because you can only get objects by copy out of // a priority_queue. std::priority_queue< - std::shared_ptr, std::vector>, + std::shared_ptr, + std::vector>, std::function&, const std::shared_ptr&)>> storage_{[](const std::shared_ptr& x, const std::shared_ptr& y) -> bool { return x->time > y->time; }}; @@ -193,5 +210,4 @@ class NextCallback : public cloe::AliasCallback { } }; -} // namespace events -} // namespace engine +} // namespace engine::events diff --git a/engine/src/simulation_machine.hpp b/engine/src/simulation_machine.hpp new file mode 100644 index 000000000..5fd1d3572 --- /dev/null +++ b/engine/src/simulation_machine.hpp @@ -0,0 +1,284 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_machine.hpp + * + * This file provides the simulation state machine. + * + * The following flow diagram shows how the states of a simulation are + * traversed in a typical simulation. The nominal flow is rendered in solid + * lines, while irregular situations are rendered in dashed lines. + * + * ┌──────────────────────┐ + * +------------ │ Connect │ + * | └──────────────────────┘ + * | │ + * | ▼ + * | ┌──────────────────────┐ + * +---... │ Start │ <-------------------------+ + * | └──────────────────────┘ | + * | │ | + * | ▼ | + * | ┌──────────────────────┐ +-----------+ | + * +---... │ StepBegin │ ◀──┐<--- | Resume | | + * | └──────────────────────┘ │ +-----------+ | + * | │ │ ^ | + * | ▼ │ | | + * | ┌──────────────────────┐ │ | | + * +---... │ StepSimulators │ │ | | + * | └──────────────────────┘ │ | | + * | │ │ | | + * | ▼ │ | | + * | ┌──────────────────────┐ │ | | + * +---... │ StepControllers │ │ | | + * | └──────────────────────┘ │ | | + * | │ │ | | + * v ▼ │ | | + * +-----------+ ┌──────────────────────┐ │ +-----------+ | + * | Abort | │ StepEnd │ ───┘---> | Pause | | + * +-----------+ └──────────────────────┘ +-----------+ | + * | | │ | ^ | + * | | failure │ success | | | + * | | ▼ +-----+ | + * | | ┌──────────────────────┐ +-----------+ | + * | +--------> │ Stop │ -------> | Reset | ---+ + * | └──────────────────────┘ +-----------+ + * | │ + * | ▼ + * | ┌──────────────────────┐ + * +-------------> │ Disconnect │ + * └──────────────────────┘ + * + * Note that not all possible transitions or states are presented in the above + * diagram; for example, it is possible to go into the Abort state from almost + * any other state. Neither can one see the constraints that apply to the above + * transitions; for example, after Abort, the state machine may go into the + * Stop state, but then will in every case go into the Disconnect state and + * never into the Reset state. + */ + +#pragma once + +#include // for future<>, async + +#include // for AsyncAbort + +#include "simulation_context.hpp" // for SimulationContext +#include "utility/state_machine.hpp" // for State, StateMachine + +namespace engine { + +/** + * The SimulationMachine is statemachine with the given set of states and + * simulation context. + * + * The state transitions are given by the states themselves and are not + * stored in the simulation machine itself. + * + * The entry-point for this simulation machine is the run() method. + * + * If you want to modify the simulation flow, you need to do this with + * the simulation context and by adding a new transition from the desired + * state. You may need to add a new state, which you can do in this file + * by defining it and then registering it in the SimulationMachine constructor. + */ +class SimulationMachine + : private StateMachine, SimulationContext> { + using SimulationState = State; + + public: + SimulationMachine() { + register_states({ + new Connect{this}, + new Start{this}, + new Probe{this}, + new StepBegin{this}, + new StepSimulators{this}, + new StepControllers{this}, + new StepEnd{this}, + new Pause{this}, + new Resume{this}, + new Success{this}, + new Fail{this}, + new Abort{this}, + new Stop{this}, + new Reset{this}, + new KeepAlive{this}, + new Disconnect{this}, + }); + } + + /** + * This is the main entry-point of the simulation. + * + * This should be used even if you have a shortened simulation + * flow, like CONNECT -> PROBING -> DISCONNECT. + */ + void run(SimulationContext& ctx) { run_machine(CONNECT, ctx); } + + /** + * Starting with the initial state, keep running states until the + * sentinel state (nullptr) has been reached. + */ + void run_machine(StateId initial, SimulationContext& ctx) { + StateId id = initial; + std::optional interrupt; + + // Keep processing states as long as they are coming either from + // an interrupt or from normal execution. + while ((interrupt = pop_interrupt()) || id != nullptr) { + try { + // Handle interrupts that have been inserted via push_interrupt. + // Only one interrupt is stored. + // + // If one interrupt follows another, the handler is responsible + // for restoring nominal flow after all is done. + if (interrupt) { + id = handle_interrupt(id, *interrupt, ctx); + continue; + } + + if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Off) { + // Run state in this thread synchronously. + id = run_state(id, ctx); + continue; + } + + id = run_state_async(id, ctx); + } catch (cloe::AsyncAbort&) { + this->push_interrupt(ABORT); + } catch (cloe::ModelReset& e) { + logger()->error("Unhandled reset request in {} state: {}", id, e.what()); + this->push_interrupt(RESET); + } catch (cloe::ModelStop& e) { + logger()->error("Unhandled stop request in {} state: {}", id, e.what()); + this->push_interrupt(STOP); + } catch (cloe::ModelAbort& e) { + logger()->error("Unhandled abort request in {} state: {}", id, e.what()); + this->push_interrupt(ABORT); + } catch (cloe::ModelError& e) { + logger()->error("Unhandled model error in {} state: {}", id, e.what()); + this->push_interrupt(ABORT); + } catch (std::exception& e) { + logger()->critical("Fatal error in {} state: {}", id, e.what()); + throw; + } + } + } + + /** + * Run state in a separate thread asynchronously and abort if + * watchdog_timeout is exceeded. + * + * See configuration: stack.hpp + * See documentation: doc/reference/watchdog.rst + */ + StateId run_state_async(StateId id, SimulationContext& ctx) { + std::chrono::milliseconds timeout = ctx.config.engine.watchdog_default_timeout; + if (ctx.config.engine.watchdog_state_timeouts.count(id)) { + auto maybe = ctx.config.engine.watchdog_state_timeouts[id]; + if (maybe) { + timeout = *maybe; + } + } + auto interval = timeout.count() > 0 ? timeout : ctx.config.engine.polling_interval; + + // Launch state + std::future f = + std::async(std::launch::async, [this, id, &ctx]() { return run_state(id, ctx); }); + + std::future_status status; + for (;;) { + status = f.wait_for(interval); + if (status == std::future_status::ready) { + return f.get(); + } else if (status == std::future_status::deferred) { + if (timeout.count() > 0) { + logger()->warn("Watchdog waiting on deferred execution."); + } + } else if (status == std::future_status::timeout) { + if (timeout.count() > 0) { + logger()->critical("Watchdog timeout of {} ms exceeded for state: {}", timeout.count(), + id); + + if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Abort) { + logger()->critical("Aborting simulation... this might take a while..."); + return ABORT; + } else if (ctx.config.engine.watchdog_mode == cloe::WatchdogMode::Kill) { + logger()->critical("Killing program... this is going to be messy..."); + std::abort(); + } + } + } + } + } + + // Asynchronous Actions: + void pause() { this->push_interrupt(PAUSE); } + void resume() { this->push_interrupt(RESUME); } + void stop() { this->push_interrupt(STOP); } + void succeed() { this->push_interrupt(SUCCESS); } + void fail() { this->push_interrupt(FAIL); } + void reset() { this->push_interrupt(RESET); } + void abort() { this->push_interrupt(ABORT); } + + StateId handle_interrupt(StateId nominal, StateId interrupt, SimulationContext& ctx) override { + logger()->debug("Handle interrupt: {}", interrupt); + // We don't necessarily actually go directly to each desired state. The + // states PAUSE and RESUME are prime examples; they should be entered and + // exited from at pre-defined points. + if (interrupt == PAUSE) { + ctx.pause_execution = true; + } else if (interrupt == RESUME) { + ctx.pause_execution = false; + } else { + // All other interrupts will lead directly to the end of the + // simulation. + return this->run_state(interrupt, ctx); + } + return nominal; + } + + friend void to_json(cloe::Json& j, const SimulationMachine& m) { + j = cloe::Json{ + {"states", m.states()}, + }; + } + +#define DEFINE_STATE(Id, S) DEFINE_STATE_STRUCT(SimulationMachine, SimulationContext, Id, S) + public: + DEFINE_STATE(CONNECT, Connect); + DEFINE_STATE(PROBE, Probe); + DEFINE_STATE(START, Start); + DEFINE_STATE(STEP_BEGIN, StepBegin); + DEFINE_STATE(STEP_SIMULATORS, StepSimulators); + DEFINE_STATE(STEP_CONTROLLERS, StepControllers); + DEFINE_STATE(STEP_END, StepEnd); + DEFINE_STATE(PAUSE, Pause); + DEFINE_STATE(RESUME, Resume); + DEFINE_STATE(SUCCESS, Success); + DEFINE_STATE(FAIL, Fail); + DEFINE_STATE(ABORT, Abort); + DEFINE_STATE(STOP, Stop); + DEFINE_STATE(RESET, Reset); + DEFINE_STATE(KEEP_ALIVE, KeepAlive); + DEFINE_STATE(DISCONNECT, Disconnect); +#undef DEFINE_STATE +}; + +} // namespace engine diff --git a/engine/src/simulation_outcome.hpp b/engine/src/simulation_outcome.hpp new file mode 100644 index 000000000..547bad799 --- /dev/null +++ b/engine/src/simulation_outcome.hpp @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_outcome.hpp + */ + +#pragma once + +#include // for map<> + +#include // for ENUM_SERIALIZATION + +namespace engine { + +/** + * SimulationOutcome describes the possible outcomes a simulation can have. + */ +enum class SimulationOutcome { + NoStart, ///< Simulation unable to start. + Aborted, ///< Simulation aborted due to technical problems or interrupt. + Stopped, ///< Simulation concluded, but without valuation. + Failure, ///< Simulation explicitly concluded with failure. + Success, ///< Simulation explicitly concluded with success. + Probing, ///< Simulation started briefly to gather specific information. +}; + +// If possible, the following exit codes should not be used as they are used +// by the Bash shell, among others: 1-2, 126-165, and 255. That leaves us +// primarily with the range 3-125, which should suffice for our purposes. +// The following exit codes should not be considered stable. +#define EXIT_OUTCOME_SUCCESS EXIT_SUCCESS // normally 0 +#define EXIT_OUTCOME_UNKNOWN EXIT_FAILURE // normally 1 +#define EXIT_OUTCOME_NOSTART 4 // 0b.....1.. +#define EXIT_OUTCOME_STOPPED 8 // 0b....1... +#define EXIT_OUTCOME_FAILURE 9 // 0b....1..1 +#define EXIT_OUTCOME_ABORTED 16 // 0b...1.... + +// clang-format off +ENUM_SERIALIZATION(SimulationOutcome, ({ + {SimulationOutcome::Aborted, "aborted"}, + {SimulationOutcome::NoStart, "no-start"}, + {SimulationOutcome::Failure, "failure"}, + {SimulationOutcome::Success, "success"}, + {SimulationOutcome::Stopped, "stopped"}, + {SimulationOutcome::Probing, "probing"}, +})) +// clang-format on + +inline int as_exit_code(SimulationOutcome outcome, bool require_success = true) { + switch (outcome) { + case SimulationOutcome::Success: + return EXIT_OUTCOME_SUCCESS; + case SimulationOutcome::Stopped: + return (require_success ? EXIT_OUTCOME_STOPPED : EXIT_OUTCOME_SUCCESS); + case SimulationOutcome::Aborted: + return EXIT_OUTCOME_ABORTED; + case SimulationOutcome::NoStart: + return EXIT_OUTCOME_NOSTART; + case SimulationOutcome::Failure: + return EXIT_OUTCOME_FAILURE; + case SimulationOutcome::Probing: + return EXIT_OUTCOME_SUCCESS; + default: + return EXIT_OUTCOME_UNKNOWN; + } +} + +} // namespace engine diff --git a/engine/src/simulation_probe.hpp b/engine/src/simulation_probe.hpp new file mode 100644 index 000000000..bcdeecc61 --- /dev/null +++ b/engine/src/simulation_probe.hpp @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_probe.hpp + */ + +#pragma once + +#include +#include +#include + +#include + +#include "simulation_outcome.hpp" + +namespace engine { + +/** + * SimulationProbe contains the results of a probe of the simulation + * configuration. + * + * These fields are filled in from the PROBE state. + * + * This is primarily presented to the user as a single JSON output. + */ +struct SimulationProbe { + SimulationOutcome outcome; + + /// Collection of errors from running the probe. + std::vector errors; + + /// UUID of the simulation, if any. + std::string uuid; + + /// Map of plugin name -> plugin path. + std::map plugins; + + /// Map of vehicle name -> list of components. + std::map> vehicles; + + /// List of trigger actions enrolled. + std::vector trigger_actions; + + /// List of trigger events enrolled. + std::vector trigger_events; + + /// List of HTTP endpoints that are available. + std::vector http_endpoints; + + /// Mapping from signal name to type. + /// - @field name type help + /// - @field name + /// - @alias name + std::map signal_metadata; + + /// Complex JSON of test metadata, including (but not limited to): + /// - test ID + /// - user-supplied metadata + fable::Json test_metadata; + + friend void to_json(fable::Json& j, const SimulationProbe& r) { + j = fable::Json{ + {"uuid", r.uuid}, + {"plugins", r.plugins}, + {"vehicles", r.vehicles}, + {"trigger_actions", r.trigger_actions}, + {"trigger_events", r.trigger_events}, + {"http_endpoints", r.http_endpoints}, + {"signals", r.signal_metadata}, + {"tests", r.test_metadata}, + }; + } +}; + +} // namespace engine diff --git a/engine/src/simulation_result.hpp b/engine/src/simulation_result.hpp new file mode 100644 index 000000000..51056599b --- /dev/null +++ b/engine/src/simulation_result.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_result.hpp + */ + +#pragma once + +#include + +#include +#include + +#include "simulation_outcome.hpp" +#include "simulation_statistics.hpp" +#include "simulation_sync.hpp" + +namespace engine { + +struct SimulationResult { + SimulationOutcome outcome; + + /// Collection of errors from running the simulation. + std::vector errors; + + /// UUID of the simulation run. + std::string uuid; + + /// Contains data regarding the time synchronization. + SimulationSync sync; + + /// Contains the wall-clock time passed. + cloe::Duration elapsed; + + /// Statistics regarding the simulation performance. + SimulationStatistics statistics; + + /// The list of triggers run (i.e., the history). + fable::Json triggers; + + /// The final report, as constructed from Lua. + fable::Json report; + + friend void to_json(fable::Json& j, const SimulationResult& r) { + j = fable::Json{ + {"elapsed", r.elapsed}, {"errors", r.errors}, {"outcome", r.outcome}, + {"report", r.report}, {"simulation", r.sync}, {"statistics", r.statistics}, + {"uuid", r.uuid}, + }; + } +}; + +} // namespace engine diff --git a/engine/src/simulation_state_abort.cpp b/engine/src/simulation_state_abort.cpp new file mode 100644 index 000000000..4f73f742b --- /dev/null +++ b/engine/src/simulation_state_abort.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_abort.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Abort::impl(SimulationContext& ctx) { + const auto* previous_state = state_machine()->previous_state(); + if (previous_state == KEEP_ALIVE) { + return DISCONNECT; + } else if (previous_state == CONNECT) { + ctx.outcome = SimulationOutcome::NoStart; + return DISCONNECT; + } + + logger()->info("Aborting simulation..."); + ctx.outcome = SimulationOutcome::Aborted; + ctx.foreach_model([this](cloe::Model& m, const char* type) { + try { + logger()->debug("Abort {} {}", type, m.name()); + m.abort(); + } catch (std::exception& e) { + logger()->error("Aborting {} {} failed: {}", type, m.name(), e.what()); + } + return true; + }); + return DISCONNECT; +} + +} // namespace engine diff --git a/engine/src/simulation_state_connect.cpp b/engine/src/simulation_state_connect.cpp new file mode 100644 index 000000000..12e1ddf55 --- /dev/null +++ b/engine/src/simulation_state_connect.cpp @@ -0,0 +1,685 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_connect.cpp + */ + +#include // for Controller +#include // for DataBroker +#include // for DirectCallback +#include // for Simulator +#include // for CommandFactory, BundleFactory, ... +#include // for DEFINE_SET_STATE_ACTION, SetDataActionFactory +#include // for INCLUDE_RESOURCE, RESOURCE_HANDLER +#include // for Vehicle +#include // for indent_string +#include // for sol::object to_json + +#include "coordinator.hpp" // for register_usertype_coordinator +#include "lua_action.hpp" // for LuaAction, ... +#include "lua_api.hpp" // for to_json(json, sol::object) +#include "registrar.hpp" // for Registrar::... +#include "server.hpp" // for Server::... +#include "simulation_actions.hpp" // for StopFactory, ... +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for Connect, ... +#include "simulation_outcome.hpp" // for SimulationOutcome +#include "utility/command.hpp" // for CommandFactory +#include "utility/state_machine.hpp" // for State, StateMachine + +// PROJECT_SOURCE_DIR is normally exported by CMake during build, but it's not +// available for the linters, so we define a dummy value here for that case. +#ifndef PROJECT_SOURCE_DIR +#define PROJECT_SOURCE_DIR "" +#endif + +INCLUDE_RESOURCE(index_html, PROJECT_SOURCE_DIR "/webui/index.html"); +INCLUDE_RESOURCE(favicon, PROJECT_SOURCE_DIR "/webui/cloe_16x16.png"); +INCLUDE_RESOURCE(cloe_logo, PROJECT_SOURCE_DIR "/webui/cloe.svg"); +INCLUDE_RESOURCE(bootstrap_css, PROJECT_SOURCE_DIR "/webui/bootstrap.min.css"); + +namespace engine { + +std::string enumerate_simulator_vehicles(const cloe::Simulator& s) { + std::stringstream buffer; + auto n = s.num_vehicles(); + for (size_t i = 0; i < n; i++) { + auto v = s.get_vehicle(i); + buffer << fmt::format("{}: {}\n", i, v->name()); + } + return buffer.str(); +} + +void handle_cloe_error(cloe::Logger logger, const cloe::Error& e) { + if (e.has_explanation()) { + logger->error("Note:\n{}", fable::indent_string(e.explanation(), " ")); + } +} + +StateId SimulationMachine::Connect::impl(SimulationContext& ctx) { + logger()->info("Initializing simulation..."); + assert(ctx.config.is_valid()); + + ctx.outcome = SimulationOutcome::NoStart; + + // 1. Initialize progress tracking + ctx.progress.init_begin(6); + auto update_progress = [&ctx](const char* str) { + ctx.progress.init(str); + ctx.server->refresh_buffer(); + }; + + { // 2. Initialize loggers + update_progress("logging"); + + for (const auto& c : ctx.config.logging) { + c.apply(); + } + } + + { // 3. Initialize Lua + auto types_tbl = sol::object(cloe::luat_cloe_engine_types(ctx.lua)).as(); + register_usertype_coordinator(types_tbl, ctx.sync); + + cloe::luat_cloe_engine_state(ctx.lua)["scheduler"] = std::ref(*ctx.coordinator); + } + + { // 4. Enroll endpoints and triggers for the server + update_progress("server"); + + auto rp = ctx.simulation_registrar(); + cloe::Registrar& r = *rp; + + // HTML endpoints: + r.register_static_handler("/", RESOURCE_HANDLER(index_html, cloe::ContentType::HTML)); + r.register_static_handler("/index.html", cloe::handler::Redirect("/")); + r.register_static_handler("/cloe_16x16.png", RESOURCE_HANDLER(favicon, cloe::ContentType::PNG)); + r.register_static_handler("/cloe.svg", RESOURCE_HANDLER(cloe_logo, cloe::ContentType::SVG)); + r.register_static_handler("/bootstrap.css", + RESOURCE_HANDLER(bootstrap_css, cloe::ContentType::CSS)); + + // API endpoints: + r.register_api_handler("/uuid", cloe::HandlerType::STATIC, cloe::handler::StaticJson(ctx.uuid)); + r.register_api_handler("/version", cloe::HandlerType::STATIC, + cloe::handler::StaticJson(ctx.version())); + r.register_api_handler("/progress", cloe::HandlerType::BUFFERED, + cloe::handler::ToJson(&ctx.progress)); + r.register_api_handler( + "/configuration", cloe::HandlerType::DYNAMIC, + [&ctx](const cloe::Request& q, cloe::Response& r) { + std::string type = "active"; + auto m = q.query_map(); + if (m.count("type")) { + type = m.at("type"); + } + + if (type == "active") { + r.write(ctx.config.active_config()); + } else if (type == "input") { + r.write(ctx.config.input_config()); + } else { + r.bad_request(cloe::Json{ + {"error", "invalid type value"}, + {"fields", {{"type", "configuration output type, one of: active, input"}}}, + }); + } + }); + r.register_api_handler("/simulation", cloe::HandlerType::BUFFERED, + cloe::handler::ToJson(&ctx.sync)); + r.register_api_handler("/statistics", cloe::HandlerType::BUFFERED, + cloe::handler::ToJson(&ctx.statistics)); + r.register_api_handler("/plugins", cloe::HandlerType::STATIC, + cloe::handler::StaticJson(ctx.plugin_ids())); + + // Coordinator & Server + ctx.server->enroll(r); + ctx.coordinator->enroll(r); + + // Events: + ctx.callback_loop = r.register_event(); + ctx.callback_start = r.register_event(); + ctx.callback_stop = r.register_event(); + ctx.callback_success = r.register_event(); + ctx.callback_failure = r.register_event(); + ctx.callback_reset = r.register_event(); + ctx.callback_pause = r.register_event(); + ctx.callback_resume = r.register_event(); + ctx.callback_time = std::make_shared( + logger(), [this, &ctx](const cloe::Trigger& t, cloe::Duration when) { + static const std::vector eta_names{"stop", "succeed", "fail", "reset"}; + auto name = t.action().name(); + for (std::string x : eta_names) { + // Take possible namespacing of simulation actions into account. + if (ctx.config.simulation.name) { + x = *ctx.config.simulation.name; + x += "/"; + x += x; + } + if (name == x) { + // We are only interested in the earliest stop action. + if (ctx.sync.eta() == cloe::Duration(0) || when < ctx.sync.eta()) { + logger()->info("Set simulation ETA to {}s", cloe::Seconds{when}.count()); + ctx.sync.set_eta(when); + ctx.progress.execution_eta = when; + } + } + } + }); + r.register_event(std::make_unique(), ctx.callback_time); + r.register_event(std::make_unique(), + std::make_shared(ctx.callback_time)); + + // Actions: + r.register_action(this->state_machine()); + r.register_action(this->state_machine()); + r.register_action(this->state_machine()); + r.register_action(this->state_machine()); + r.register_action(this->state_machine()); + r.register_action(this->state_machine()); + r.register_action(&ctx); + r.register_action(&ctx.sync); + r.register_action(&ctx.statistics); + r.register_action(ctx.commander.get()); + r.register_action(ctx.lua); + + // From: cloe/trigger/example_actions.hpp + auto tr = ctx.coordinator->trigger_registrar(cloe::Source::TRIGGER); + r.register_action(tr); + r.register_action(tr); + r.register_action(); + r.register_action(tr); + } + + { // 5. Initialize simulators + update_progress("simulators"); + + /** + * Return a new Simulator given configuration c. + */ + auto new_simulator = [&ctx](const cloe::SimulatorConf& c) -> std::unique_ptr { + auto f = c.factory->clone(); + auto name = c.name.value_or(c.binding); + for (auto d : ctx.config.get_simulator_defaults(name, f->name())) { + f->from_conf(d.args); + } + auto x = f->make(c.args); + ctx.now_initializing = x.get(); + + // Configure simulator: + auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( + std::string("/simulators/") + name); + x->connect(); + x->enroll(*r); + + ctx.now_initializing = nullptr; + return x; + }; + + // Create and configure all simulators: + for (const auto& c : ctx.config.simulators) { + auto name = c.name.value_or(c.binding); + assert(ctx.simulators.count(name) == 0); + logger()->info("Configure simulator {}", name); + + try { + ctx.simulators[name] = new_simulator(c); + } catch (cloe::ModelError& e) { + logger()->critical("Error configuring simulator {}: {}", name, e.what()); + return ABORT; + } + } + + auto r = ctx.simulation_registrar(); + r->register_api_handler("/simulators", cloe::HandlerType::STATIC, + cloe::handler::StaticJson(ctx.simulator_ids())); + } + + { // 6. Initialize vehicles + update_progress("vehicles"); + + /** + * Return a new Component given vehicle v and configuration c. + */ + auto new_component = [&ctx](cloe::Vehicle& v, + const cloe::ComponentConf& c) -> std::shared_ptr { + // Create a copy of the component factory prototype and initialize it with the default stack arguments. + auto f = c.factory->clone(); + auto name = c.name.value_or(c.binding); + for (auto d : ctx.config.get_component_defaults(name, f->name())) { + f->from_conf(d.args); + } + // Get input components, if applicable. + std::vector> from; + for (const auto& from_comp_name : c.from) { + if (!v.has(from_comp_name)) { + return nullptr; + } + from.push_back(v.get(from_comp_name)); + } + // Create the new component. + auto x = f->make(c.args, std::move(from)); + ctx.now_initializing = x.get(); + + // Configure component: + auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( + std::string("/components/") + name); + x->connect(); + x->enroll(*r); + + ctx.now_initializing = nullptr; + return x; + }; + + /** + * Return a new Vehicle given configuration c. + */ + auto new_vehicle = [&](const cloe::VehicleConf& c) -> std::shared_ptr { + static uint64_t gid = 1024; + + // Fetch vehicle prototype. + std::shared_ptr v; + if (c.is_from_simulator()) { + auto& s = ctx.simulators.at(c.from_sim.simulator); + if (c.from_sim.is_by_name()) { + v = s->get_vehicle(c.from_sim.index_str); + if (!v) { + throw cloe::ModelError("simulator {} has no vehicle by name {}", c.from_sim.simulator, + c.from_sim.index_str) + .explanation("Simulator {} has following vehicles:\n{}", c.from_sim.simulator, + enumerate_simulator_vehicles(*s)); + } + } else { + v = s->get_vehicle(c.from_sim.index_num); + if (!v) { + throw cloe::ModelError("simulator {} has no vehicle at index {}", c.from_sim.simulator, + c.from_sim.index_num) + .explanation("Simulator {} has following vehicles:\n{}", c.from_sim.simulator, + enumerate_simulator_vehicles(*s)); + } + } + } else { + if (ctx.vehicles.count(c.from_veh)) { + v = ctx.vehicles.at(c.from_veh); + } else { + // This vehicle depends on another that hasn't been create yet. + return nullptr; + } + } + + // Create vehicle from prototype and configure the components. + logger()->info("Configure vehicle {}", c.name); + auto x = v->clone(++gid, c.name); + ctx.now_initializing = x.get(); + + std::set configured; + size_t n = c.components.size(); + while (configured.size() != n) { + // Keep trying to create components until all have been created. + // This is a poor-man's version of dependency resolution and has O(n^2) + // complexity, which is acceptable given that the expected number of + // components is usually less than 100. + size_t m = configured.size(); + for (const auto& kv : c.components) { + if (configured.count(kv.first)) { + // This component has already been configured. + continue; + } + + auto k = new_component(*x, kv.second); + if (k) { + x->set_component(kv.first, std::move(k)); + configured.insert(kv.first); + } + } + + // Check that we are making progress. + if (configured.size() == m) { + // We have configured.size() != n and has not grown since going + // through all Component configs. This means that we have some unresolved + // dependencies. Find out which and abort. + for (const auto& kv : c.components) { + if (configured.count(kv.first)) { + continue; + } + + // We now have a component that has not been configured, and this + // can only be the case if the dependency is not found. + assert(kv.second.from.size() > 0); + for (const auto& from_comp_name : kv.second.from) { + if (x->has(from_comp_name)) { + continue; + } + throw cloe::ModelError{ + "cannot configure component '{}': cannot resolve dependency '{}'", + kv.first, + from_comp_name, + }; + } + } + } + } + + // Configure vehicle: + auto r = ctx.registrar->with_trigger_prefix(c.name)->with_api_prefix( + std::string("/vehicles/") + c.name); + x->connect(); + x->enroll(*r); + + ctx.now_initializing = nullptr; + return x; + }; + + // Create and configure all vehicles: + size_t n = ctx.config.vehicles.size(); + while (ctx.vehicles.size() != n) { + // Keep trying to create vehicles until all have been created. + // This is a poor-man's version of dependency resolution and has O(n^2) + // complexity, which is acceptable given that the expected number of + // vehicles is almost always less than 10. + size_t m = ctx.vehicles.size(); + for (const auto& c : ctx.config.vehicles) { + if (ctx.vehicles.count(c.name)) { + // This vehicle has already been configured. + continue; + } + + std::shared_ptr v; + try { + v = new_vehicle(c); + } catch (cloe::ModelError& e) { + logger()->critical("Error configuring vehicle {}: {}", c.name, e.what()); + handle_cloe_error(logger(), e); + return ABORT; + } + + if (v) { + ctx.vehicles[c.name] = std::move(v); + } + } + + // Check that we are making progress. + if (ctx.vehicles.size() == m) { + // We have ctx.vehicles.size() != n and has not grown since going + // through all Vehicle configs. This means that we have some unresolved + // dependencies. Find out which and abort. + for (const auto& c : ctx.config.vehicles) { + if (ctx.vehicles.count(c.name)) { + continue; + } + + // We now have a vehicle that has not been configured, and this can + // only be the case if a vehicle dependency is not found. + assert(c.is_from_vehicle()); + throw cloe::ModelError{ + "cannot configure vehicle '{}': cannot resolve dependency '{}'", + c.name, + c.from_veh, + }; + } + } + } + + auto r = ctx.simulation_registrar(); + r->register_api_handler("/vehicles", cloe::HandlerType::STATIC, + cloe::handler::StaticJson(ctx.vehicle_ids())); + } + + { // 7. Initialize controllers + update_progress("controllers"); + + /** + * Return a new Controller given configuration c. + */ + auto new_controller = + [&ctx](const cloe::ControllerConf& c) -> std::unique_ptr { + auto f = c.factory->clone(); + auto name = c.name.value_or(c.binding); + for (auto d : ctx.config.get_controller_defaults(name, f->name())) { + f->from_conf(d.args); + } + auto x = f->make(c.args); + ctx.now_initializing = x.get(); + + // Configure + auto r = ctx.registrar->with_trigger_prefix(name)->with_api_prefix( + std::string("/controllers/") + name); + x->set_vehicle(ctx.vehicles.at(c.vehicle)); + x->connect(); + x->enroll(*r); + + ctx.now_initializing = nullptr; + return x; + }; + + // Create and configure all controllers: + for (const auto& c : ctx.config.controllers) { + auto name = c.name.value_or(c.binding); + assert(ctx.controllers.count(name) == 0); + logger()->info("Configure controller {}", name); + try { + ctx.controllers[name] = new_controller(c); + } catch (cloe::ModelError& e) { + logger()->critical("Error configuring controller {}: {}", name, e.what()); + return ABORT; + } + } + + auto r = ctx.simulation_registrar(); + r->register_api_handler("/controllers", cloe::HandlerType::STATIC, + cloe::handler::StaticJson(ctx.controller_ids())); + } + + { // 8. Initialize Databroker & Lua + auto* dbPtr = ctx.coordinator->data_broker(); + if (dbPtr == nullptr) { + throw std::logic_error("Coordinator did not provide a DataBroker instance"); + } + auto& db = *dbPtr; + // Alias signals via lua + { + bool aliasing_failure = false; + // Read cloe.alias_signals + sol::object signal_aliases = cloe::luat_cloe_engine_initial_input(ctx.lua)["signal_aliases"]; + auto type = signal_aliases.get_type(); + switch (type) { + // cloe.alias_signals: expected is a list (i.e. table) of 2-tuple each strings + case sol::type::table: { + sol::table alias_signals = signal_aliases.as(); + auto tbl_size = std::distance(alias_signals.begin(), alias_signals.end()); + //for (auto& kv : alias_signals) + for (int i = 0; i < tbl_size; i++) { + //sol::object value = kv.second; + sol::object value = alias_signals[i + 1]; + sol::type type = value.get_type(); + switch (type) { + // cloe.alias_signals[i]: expected is a 2-tuple (i.e. table) each strings + case sol::type::table: { + sol::table alias_tuple = value.as(); + auto tbl_size = std::distance(alias_tuple.begin(), alias_tuple.end()); + if (tbl_size != 2) { + // clang-format off + logger()->error( + "One or more entries in 'cloe.alias_signals' does not consist out of a 2-tuple. " + "Expected are entries in this format { \"regex\" , \"short-name\" }" + ); + // clang-format on + aliasing_failure = true; + continue; + } + + sol::object value; + sol::type type; + std::string old_name; + std::string alias_name; + value = alias_tuple[1]; + type = value.get_type(); + if (sol::type::string != type) { + // clang-format off + logger()->error( + "One or more parts in a tuple in 'cloe.alias_signals' has an unexpected datatype '{}'. " + "Expected are entries in this format { \"regex\" , \"short-name\" }", + static_cast(type)); + // clang-format on + aliasing_failure = true; + } else { + old_name = value.as(); + } + + value = alias_tuple[2]; + type = value.get_type(); + if (sol::type::string != type) { + // clang-format off + logger()->error( + "One or more parts in a tuple in 'cloe.alias_signals' has an unexpected datatype '{}'. " + "Expected are entries in this format { \"regex\" , \"short-name\" }", + static_cast(type)); + // clang-format on + aliasing_failure = true; + } else { + alias_name = value.as(); + } + try { + db.alias(old_name, alias_name); + // clang-format off + logger()->info( + "Aliasing signal '{}' as '{}'.", + old_name, alias_name); + // clang-format on + } catch (const std::logic_error& ex) { + // clang-format off + logger()->error( + "Aliasing signal specifier '{}' as '{}' failed with this error: {}", + old_name, alias_name, ex.what()); + // clang-format on + aliasing_failure = true; + } catch (...) { + // clang-format off + logger()->error( + "Aliasing signal specifier '{}' as '{}' failed.", + old_name, alias_name); + // clang-format on + aliasing_failure = true; + } + } break; + // cloe.alias_signals[i]: is not a table + default: { + // clang-format off + logger()->error( + "One or more entries in 'cloe.alias_signals' has an unexpected datatype '{}'. " + "Expected are entries in this format { \"regex\" , \"short-name\" }", + static_cast(type)); + // clang-format on + aliasing_failure = true; + } break; + } + } + + } break; + case sol::type::none: + case sol::type::lua_nil: { + // not defined -> nop + } break; + default: { + // clang-format off + logger()->error( + "Expected symbol 'cloe.alias_signals' has unexpected datatype '{}'. " + "Expected is a list of 2-tuples in this format { \"regex\" , \"short-name\" }", + static_cast(type)); + // clang-format on + aliasing_failure = true; + } break; + } + if (aliasing_failure) { + throw cloe::ModelError("Aliasing signals failed with above error. Aborting."); + } + } + + // Inject requested signals into lua + { + auto& signals = db.signals(); + bool binding_failure = false; + // Read cloe.require_signals + sol::object value = cloe::luat_cloe_engine_initial_input(ctx.lua)["signal_requires"]; + auto type = value.get_type(); + switch (type) { + // cloe.require_signals expected is a list (i.e. table) of strings + case sol::type::table: { + sol::table require_signals = value.as(); + auto tbl_size = std::distance(require_signals.begin(), require_signals.end()); + + for (int i = 0; i < tbl_size; i++) { + sol::object value = require_signals[i + 1]; + + sol::type type = value.get_type(); + if (type != sol::type::string) { + logger()->warn( + "One entry of cloe.require_signals has a wrong data type: '{}'. " + "Expected is a list of strings.", + static_cast(type)); + binding_failure = true; + continue; + } + std::string signal_name = value.as(); + + // virtually bind signal 'signal_name' to lua + auto iter = db[signal_name]; + if (iter != signals.end()) { + try { + db.bind_signal(signal_name); + logger()->info("Binding signal '{}' as '{}'.", signal_name, signal_name); + } catch (const std::logic_error& ex) { + logger()->error("Binding signal '{}' failed with error: {}", signal_name, + ex.what()); + } + } else { + logger()->warn("Requested signal '{}' does not exist in DataBroker.", signal_name); + binding_failure = true; + } + } + // actually bind all virtually bound signals to lua + db.bind("signals", cloe::luat_cloe_engine(ctx.lua)); + } break; + case sol::type::none: + case sol::type::lua_nil: { + logger()->warn( + "Expected symbol 'cloe.require_signals' appears to be undefined. " + "Expected is a list of string."); + } break; + default: { + logger()->error( + "Expected symbol 'cloe.require_signals' has unexpected datatype '{}'. " + "Expected is a list of string.", + static_cast(type)); + binding_failure = true; + } break; + } + if (binding_failure) { + throw cloe::ModelError("Binding signals to Lua failed with above error. Aborting."); + } + } + } + ctx.progress.init_end(); + ctx.server->refresh_buffer_start_stream(); + logger()->info("Simulation initialization complete."); + if (ctx.probe_simulation) { + return PROBE; + } + return START; +} + +} // namespace engine diff --git a/engine/src/simulation_state_disconnect.cpp b/engine/src/simulation_state_disconnect.cpp new file mode 100644 index 000000000..d3dc20b79 --- /dev/null +++ b/engine/src/simulation_state_disconnect.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_disconnect.cpp + */ + +#include // for to_json +#include // for object + +#include "coordinator.hpp" // for Coordinator::history +#include "lua_api.hpp" // for luat_cloe_engine_state +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine +#include "simulation_result.hpp" // for SimulationResult + +namespace engine { + +StateId SimulationMachine::Disconnect::impl(SimulationContext& ctx) { + logger()->debug("Disconnecting simulation..."); + ctx.foreach_model([](cloe::Model& m, const char*) { + m.disconnect(); + return true; + }); + logger()->info("Simulation disconnected."); + + // Gather up the simulation results. + auto result = SimulationResult(); + result.outcome = ctx.outcome.value_or(SimulationOutcome::Aborted); + result.uuid = ctx.uuid; + result.sync = ctx.sync; + result.statistics = ctx.statistics; + result.elapsed = ctx.progress.elapsed(); + result.triggers = ctx.coordinator->history(); + result.report = sol::object(cloe::luat_cloe_engine_state(ctx.lua)["report"]); + ctx.result = result; + + return nullptr; +} + +} // namespace engine diff --git a/engine/src/simulation_state_fail.cpp b/engine/src/simulation_state_fail.cpp new file mode 100644 index 000000000..96e9283b2 --- /dev/null +++ b/engine/src/simulation_state_fail.cpp @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_fail.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Fail::impl(SimulationContext& ctx) { + logger()->info("Simulation failed."); + ctx.outcome = SimulationOutcome::Failure; + ctx.callback_failure->trigger(ctx.sync); + return STOP; +} + +} // namespace engine diff --git a/engine/src/simulation_state_keep_alive.cpp b/engine/src/simulation_state_keep_alive.cpp new file mode 100644 index 000000000..c2abac178 --- /dev/null +++ b/engine/src/simulation_state_keep_alive.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_keep_alive.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::KeepAlive::impl(SimulationContext& ctx) { + if (state_machine()->previous_state() != KEEP_ALIVE) { + logger()->info("Keeping simulation alive..."); + logger()->info("Press [Ctrl+C] to disconnect."); + } + ctx.callback_pause->trigger(ctx.sync); + std::this_thread::sleep_for(ctx.config.engine.polling_interval); + return KEEP_ALIVE; +} + +} // namespace engine diff --git a/engine/src/simulation_state_pause.cpp b/engine/src/simulation_state_pause.cpp new file mode 100644 index 000000000..8ab1dcefb --- /dev/null +++ b/engine/src/simulation_state_pause.cpp @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_pause.cpp + */ + +#include // for this_thread + +#include "coordinator.hpp" // for Coordinator::process +#include "server.hpp" // for Server::... +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Pause::impl(SimulationContext& ctx) { + if (state_machine()->previous_state() != PAUSE) { + logger()->info("Pausing simulation..."); + logger()->info(R"(Send {"event": "pause", "action": "resume"} trigger to resume.)"); + logger()->debug( + R"(For example: echo '{{"event": "pause", "action": "resume"}}' | curl -d @- http://localhost:{}/api/triggers/input)", + ctx.config.server.listen_port); + + // If the server is not enabled, then the user probably won't be able to resume. + if (!ctx.config.server.listen) { + logger()->warn("Start temporary server."); + ctx.server->start(); + } + } + + { + // Process all inserted triggers here, because the main loop is not running + // while we are paused. Ideally, we should only allow triggers that are + // destined for the pause state, although it might be handy to pause, allow + // us to insert triggers, and then resume. Triggers that are inserted via + // the web UI are just as likely to be incorrectly inserted as correctly. + auto guard = ctx.server->lock(); + ctx.coordinator->process(ctx.sync); + } + + // TODO(ben): Process triggers that come in so we can also conclude. + // What kind of triggers do we want to allow? Should we also be processing + // NEXT trigger events? How after pausing do we resume? + ctx.callback_loop->trigger(ctx.sync); + ctx.callback_pause->trigger(ctx.sync); + std::this_thread::sleep_for(ctx.config.engine.polling_interval); + + if (ctx.pause_execution) { + return PAUSE; + } + + return RESUME; +} + +} // namespace engine diff --git a/engine/src/simulation_state_probe.cpp b/engine/src/simulation_state_probe.cpp new file mode 100644 index 000000000..898e6372c --- /dev/null +++ b/engine/src/simulation_state_probe.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_probe.cpp + */ + +#include // for DataBroker +#include // for Vehicle::component_names +#include // for to_json for sol::object + +#include "coordinator.hpp" // for Coordinator::trigger_events, ... +#include "lua_api.hpp" // for luat_cloe_engine_state +#include "server.hpp" // for Server::endpoints +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine +#include "simulation_probe.hpp" // for SimulationProbe + +namespace engine { + +std::map dump_signals(const cloe::DataBroker& db) { + std::map report; + + for (const auto& [key, signal] : db.signals()) { + // Find out if we are dealing with an alias or the actual signal. + assert(!signal->names().empty()); + if (key != signal->names()[0]) { + // We have an alias, because the name is at the first place. + // FIXME: Direct coupling to implementation detail of Signal. + report[key] = fmt::format("@alias {}", signal->names()[0]); + continue; + } + + // Get lua tag if possible + const auto* tag = signal->metadata(); + if (tag == nullptr) { + report[key] = fmt::format("@field {}", key); + continue; + } + report[key] = fmt::format("@field {} {} {}", key, to_string(tag->datatype), tag->text); + } + + return report; +} + +StateId SimulationMachine::Probe::impl(SimulationContext& ctx) { + logger()->info("Probing simulation parameters."); + + ctx.outcome = SimulationOutcome::Probing; + auto data = SimulationProbe(); + data.uuid = ctx.uuid; + for (const auto& [name, plugin] : ctx.config.get_all_plugins()) { + data.plugins[plugin->name()] = plugin->path(); + } + for (const auto& [name, veh] : ctx.vehicles) { + data.vehicles[name] = veh->component_names(); + } + data.trigger_actions = ctx.coordinator->trigger_action_names(); + data.trigger_events = ctx.coordinator->trigger_event_names(); + data.http_endpoints = ctx.server->endpoints(); + data.signal_metadata = dump_signals(*ctx.db); + data.test_metadata = sol::object(cloe::luat_cloe_engine_state(ctx.lua)["report"]["tests"]); + + ctx.probe = data; + return DISCONNECT; +} + +} // namespace engine diff --git a/engine/src/simulation_state_reset.cpp b/engine/src/simulation_state_reset.cpp new file mode 100644 index 000000000..a9b551a15 --- /dev/null +++ b/engine/src/simulation_state_reset.cpp @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_reset.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Reset::impl(SimulationContext& ctx) { + logger()->info("Resetting simulation..."); + ctx.callback_reset->trigger(ctx.sync); + auto ok = ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { + try { + logger()->debug("Reset {} {}", type, m.name()); + m.stop(ctx.sync); + m.reset(); + } catch (std::exception& e) { + logger()->error("Resetting {} {} failed: {}", type, m.name(), e.what()); + return false; + } + return true; + }); + if (ok) { + return CONNECT; + } else { + return ABORT; + } +} + +} // namespace engine diff --git a/engine/src/simulation_state_resume.cpp b/engine/src/simulation_state_resume.cpp new file mode 100644 index 000000000..c846da15b --- /dev/null +++ b/engine/src/simulation_state_resume.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_resume.cpp + */ + +#include "server.hpp" // for Server::stop, ... +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Resume::impl(SimulationContext& ctx) { + // TODO(ben): Eliminate the RESUME state and move this functionality into + // the PAUSE state. This more closely matches the way we think about PAUSE + // as a state vs. RESUME as a transition. + logger()->info("Resuming simulation..."); + if (!ctx.config.server.listen) { + logger()->warn("Stop temporary server."); + ctx.server->stop(); + } + ctx.callback_resume->trigger(ctx.sync); + return STEP_BEGIN; +} + +} // namespace engine diff --git a/engine/src/simulation_state_start.cpp b/engine/src/simulation_state_start.cpp new file mode 100644 index 000000000..f36712505 --- /dev/null +++ b/engine/src/simulation_state_start.cpp @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_start.cpp + */ + +#include // for ConcludedError, TriggerError +#include // for SchemaError +#include // for pretty_print + +#include "coordinator.hpp" // for Coordinator::trigger_registrar +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +size_t insert_triggers_from_config(SimulationContext& ctx) { + auto r = ctx.coordinator->trigger_registrar(cloe::Source::FILESYSTEM); + size_t count = 0; + for (const auto& c : ctx.config.triggers) { + if (!ctx.config.engine.triggers_ignore_source && source_is_transient(c.source)) { + continue; + } + try { + r->insert_trigger(c.conf()); + count++; + } catch (fable::SchemaError& e) { + ctx.logger()->error("Error inserting trigger: {}", e.what()); + std::stringstream s; + fable::pretty_print(e, s); + ctx.logger()->error("> Message:\n {}", s.str()); + throw cloe::ConcludedError(e); + } catch (cloe::TriggerError& e) { + ctx.logger()->error("Error inserting trigger ({}): {}", e.what(), c.to_json().dump()); + throw cloe::ConcludedError(e); + } + } + return count; +} + +StateId SimulationMachine::Start::impl(SimulationContext& ctx) { + logger()->info("Starting simulation..."); + + // Begin execution progress + ctx.progress.exec_begin(); + + // Process initial trigger list + insert_triggers_from_config(ctx); + ctx.coordinator->process_pending_lua_triggers(ctx.sync); + ctx.coordinator->process(ctx.sync); + ctx.callback_start->trigger(ctx.sync); + + // Process initial context + ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { + logger()->trace("Start {} {}", type, m.name()); + m.start(ctx.sync); + return true; // next model + }); + ctx.sync.increment_step(); + + // We can pause at the start of execution too. + if (ctx.pause_execution) { + return PAUSE; + } + + return STEP_BEGIN; +} + +} // namespace engine diff --git a/engine/src/simulation_state_step_begin.cpp b/engine/src/simulation_state_step_begin.cpp new file mode 100644 index 000000000..20d8e8de5 --- /dev/null +++ b/engine/src/simulation_state_step_begin.cpp @@ -0,0 +1,77 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_step_begin.cpp + */ + +#include // for duration_cast + +#include "server.hpp" // for Server::refresh_buffer +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::StepBegin::impl(SimulationContext& ctx) { + ctx.cycle_duration.reset(); + timer::DurationTimer t([&ctx](cloe::Duration d) { + auto ms = std::chrono::duration_cast(d); + ctx.statistics.engine_time_ms.push_back(ms.count()); + }); + + logger()->trace("Step {:0>9}, Time {} ms", ctx.sync.step(), + std::chrono::duration_cast(ctx.sync.time()).count()); + + // Update execution progress + ctx.progress.exec_update(ctx.sync.time()); + if (ctx.report_progress && ctx.progress.exec_report()) { + logger()->info("Execution progress: {}%", + static_cast(ctx.progress.execution.percent() * 100.0)); + } + + // Refresh the double buffer + // + // Note: this line can easily break your time budget with the current server + // implementation. If you need better performance, disable the server in the + // stack file configuration: + // + // { + // "version": "4", + // "server": { + // "listen": false + // } + // } + // + ctx.server->refresh_buffer(); + + // Run cycle- and time-based triggers + ctx.callback_loop->trigger(ctx.sync); + ctx.callback_time->trigger(ctx.sync); + + // Determine whether to continue simulating or stop + bool all_operational = ctx.foreach_model([this](const cloe::Model& m, const char* type) { + if (!m.is_operational()) { + logger()->info("The {} {} is no longer operational.", type, m.name()); + return false; // abort loop + } + return true; // next model + }); + return (all_operational ? STEP_SIMULATORS : STOP); +} + +} // namespace engine diff --git a/engine/src/simulation_state_step_controllers.cpp b/engine/src/simulation_state_step_controllers.cpp new file mode 100644 index 000000000..4fdae84cf --- /dev/null +++ b/engine/src/simulation_state_step_controllers.cpp @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_step_controllers.cpp + */ + +#include "server.hpp" // for Server::lock, ... +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::StepControllers::impl(SimulationContext& ctx) { + auto guard = ctx.server->lock(); + + timer::DurationTimer t([&ctx](cloe::Duration d) { + auto ms = std::chrono::duration_cast(d); + ctx.statistics.controller_time_ms.push_back(ms.count()); + }); + + // We can only erase from ctx.controllers when we have access to the + // iterator itself, otherwise we get undefined behavior. So we save + // the names of the controllers we want to erase from the list. + std::vector controllers_to_erase; + + // Call each controller and handle any errors that might occur. + ctx.foreach_controller([this, &ctx, &controllers_to_erase](cloe::Controller& ctrl) { + if (!ctrl.has_vehicle()) { + // Skip this controller + return true; + } + + // Keep calling the ctrl until it has caught up the current time. + cloe::Duration ctrl_time; + try { + int64_t retries = 0; + for (;;) { + ctrl_time = ctrl.process(ctx.sync); + + // If we are underneath our target, sleep and try again. + if (ctrl_time < ctx.sync.time()) { + this->logger()->warn("Controller {} not progressing, now at {}", ctrl.name(), + cloe::to_string(ctrl_time)); + + // If a controller is misbehaving, we might get stuck in a loop. + // If this happens more than some random high number, then throw + // an error. + if (retries == ctx.config.simulation.controller_retry_limit) { + throw cloe::ModelError{"controller not progressing to target time {}", + cloe::to_string(ctx.sync.time())}; + } + + // Otherwise, sleep and try again. + std::this_thread::sleep_for(ctx.config.simulation.controller_retry_sleep); + ++retries; + } else { + ctx.statistics.controller_retries.push_back(static_cast(retries)); + break; + } + } + } catch (cloe::ModelReset& e) { + this->logger()->error("Controller {} reset: {}", ctrl.name(), e.what()); + this->state_machine()->reset(); + return false; + } catch (cloe::ModelStop& e) { + this->logger()->error("Controller {} stop: {}", ctrl.name(), e.what()); + this->state_machine()->stop(); + return false; + } catch (cloe::ModelAbort& e) { + this->logger()->error("Controller {} abort: {}", ctrl.name(), e.what()); + this->state_machine()->abort(); + return false; + } catch (cloe::Error& e) { + this->logger()->error("Controller {} died: {}", ctrl.name(), e.what()); + if (e.has_explanation()) { + this->logger()->error("Note:\n{}", e.explanation()); + } + if (ctx.config.simulation.abort_on_controller_failure) { + this->logger()->error("Aborting thanks to controller {}", ctrl.name()); + this->state_machine()->abort(); + return false; + } else { + this->logger()->warn("Continuing without controller {}", ctrl.name()); + ctrl.abort(); + ctrl.disconnect(); + controllers_to_erase.push_back(ctrl.name()); + return true; + } + } catch (...) { + this->logger()->critical("Controller {} encountered a fatal error.", ctrl.name()); + throw; + } + + // Write a notice if the controller is ahead of the simulation time. + cloe::Duration ctrl_ahead = ctrl_time - ctx.sync.time(); + if (ctrl_ahead.count() > 0) { + this->logger()->warn("Controller {} is ahead by {}", ctrl.name(), + cloe::to_string(ctrl_ahead)); + } + + // Continue with next controller. + return true; + }); + + // Remove any controllers that we want to continue without. + for (const auto& ctrl : controllers_to_erase) { + ctx.controllers.erase(ctrl); + } + + return STEP_END; +} + +} // namespace engine diff --git a/engine/src/simulation_state_step_end.cpp b/engine/src/simulation_state_step_end.cpp new file mode 100644 index 000000000..1060c7825 --- /dev/null +++ b/engine/src/simulation_state_step_end.cpp @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_step_end.cpp + */ + +#include // for duration_cast +#include // uint64_t +#include // sleep_for + +#include // for Duration + +#include "coordinator.hpp" // for Coordinator::process +#include "server.hpp" // for Server::lock, ... +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::StepEnd::impl(SimulationContext& ctx) { + // Adjust sim time to wallclock according to realtime factor. + cloe::Duration padding = cloe::Duration{0}; + cloe::Duration elapsed = ctx.cycle_duration.elapsed(); + { + auto guard = ctx.server->lock(); + ctx.sync.set_cycle_time(elapsed); + } + + if (!ctx.sync.is_realtime_factor_unlimited()) { + auto width = ctx.sync.step_width().count(); + auto target = cloe::Duration(static_cast(width / ctx.sync.realtime_factor())); + padding = target - elapsed; + if (padding.count() > 0) { + std::this_thread::sleep_for(padding); + } else { + logger()->trace("Failing target realtime factor: {:.2f} < {:.2f}", + ctx.sync.achievable_realtime_factor(), ctx.sync.realtime_factor()); + } + } + + auto guard = ctx.server->lock(); + ctx.statistics.cycle_time_ms.push_back( + std::chrono::duration_cast(elapsed).count()); + ctx.statistics.padding_time_ms.push_back( + std::chrono::duration_cast(padding).count()); + ctx.sync.increment_step(); + + // Process all inserted triggers now. + ctx.coordinator->process(ctx.sync); + + // We can pause the simulation between STEP_END and STEP_BEGIN. + if (ctx.pause_execution) { + return PAUSE; + } + + return STEP_BEGIN; +} + +} // namespace engine diff --git a/engine/src/simulation_state_step_simulators.cpp b/engine/src/simulation_state_step_simulators.cpp new file mode 100644 index 000000000..2b52646b2 --- /dev/null +++ b/engine/src/simulation_state_step_simulators.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_step_simulators.cpp + */ + +#include // for duration_cast<> + +#include // for Duration +#include // for ModelReset, ... +#include // for Simulator +#include // for Vehicle + +#include "server.hpp" // for ctx.server +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::StepSimulators::impl(SimulationContext& ctx) { + auto guard = ctx.server->lock(); + + timer::DurationTimer t([&ctx](cloe::Duration d) { + auto ms = std::chrono::duration_cast(d); + ctx.statistics.simulator_time_ms.push_back(ms.count()); + }); + + // Call the simulator bindings: + ctx.foreach_simulator([&ctx](cloe::Simulator& simulator) { + try { + cloe::Duration sim_time = simulator.process(ctx.sync); + if (!simulator.is_operational()) { + throw cloe::ModelStop("simulator {} no longer operational", simulator.name()); + } + if (sim_time != ctx.sync.time()) { + throw cloe::ModelError( + "simulator {} did not progress to required time: got {}ms, expected {}ms", + simulator.name(), sim_time.count() / 1'000'000, ctx.sync.time().count() / 1'000'000); + } + } catch (cloe::ModelReset& e) { + throw; + } catch (cloe::ModelStop& e) { + throw; + } catch (cloe::ModelAbort& e) { + throw; + } catch (cloe::ModelError& e) { + throw; + } catch (...) { + throw; + } + return true; + }); + + // Clear vehicle cache + ctx.foreach_vehicle([this, &ctx](cloe::Vehicle& v) { + auto t = v.process(ctx.sync); + if (t < ctx.sync.time()) { + logger()->error("Vehicle ({}, {}) not progressing; simulation compromised!", v.id(), + v.name()); + } + return true; + }); + + return STEP_CONTROLLERS; +} + +} // namespace engine diff --git a/engine/src/simulation_state_stop.cpp b/engine/src/simulation_state_stop.cpp new file mode 100644 index 000000000..76644ca63 --- /dev/null +++ b/engine/src/simulation_state_stop.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_stop.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Stop::impl(SimulationContext& ctx) { + logger()->info("Stopping simulation..."); + + // If no other outcome has already been defined, then mark as "stopped". + if (!ctx.outcome) { + ctx.outcome = SimulationOutcome::Stopped; + } + + ctx.callback_stop->trigger(ctx.sync); + ctx.foreach_model([this, &ctx](cloe::Model& m, const char* type) { + try { + if (m.is_operational()) { + logger()->debug("Stop {} {}", type, m.name()); + m.stop(ctx.sync); + } + } catch (std::exception& e) { + logger()->error("Stopping {} {} failed: {}", type, m.name(), e.what()); + } + return true; + }); + ctx.progress.message = "execution complete"; + ctx.progress.execution.end(); + + if (ctx.config.engine.keep_alive) { + return KEEP_ALIVE; + } + return DISCONNECT; +} + +} // namespace engine diff --git a/engine/src/simulation_state_success.cpp b/engine/src/simulation_state_success.cpp new file mode 100644 index 000000000..29d0056c9 --- /dev/null +++ b/engine/src/simulation_state_success.cpp @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_state_success.cpp + */ + +#include "simulation_context.hpp" // for SimulationContext, SimulationOutcome +#include "simulation_machine.hpp" // for SimulationMachine + +namespace engine { + +StateId SimulationMachine::Success::impl(SimulationContext& ctx) { + logger()->info("Simulation successful."); + ctx.outcome = SimulationOutcome::Success; + ctx.callback_success->trigger(ctx.sync); + return STOP; +} + +} // namespace engine diff --git a/engine/src/simulation_statistics.hpp b/engine/src/simulation_statistics.hpp new file mode 100644 index 000000000..3f3448082 --- /dev/null +++ b/engine/src/simulation_statistics.hpp @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_statistics.hpp + */ + +#pragma once + +#include // for Accumulator +#include + +namespace engine { + +struct SimulationStatistics { + cloe::utility::Accumulator engine_time_ms; + cloe::utility::Accumulator cycle_time_ms; + cloe::utility::Accumulator simulator_time_ms; + cloe::utility::Accumulator controller_time_ms; + cloe::utility::Accumulator padding_time_ms; + cloe::utility::Accumulator controller_retries; + + void reset() { + engine_time_ms.reset(); + cycle_time_ms.reset(); + simulator_time_ms.reset(); + controller_time_ms.reset(); + padding_time_ms.reset(); + controller_retries.reset(); + } + + friend void to_json(fable::Json& j, const SimulationStatistics& s) { + j = fable::Json{ + {"engine_time_ms", s.engine_time_ms}, {"simulator_time_ms", s.simulator_time_ms}, + {"controller_time_ms", s.controller_time_ms}, {"padding_time_ms", s.padding_time_ms}, + {"cycle_time_ms", s.cycle_time_ms}, {"controller_retries", s.controller_retries}, + }; + } +}; + +} // namespace engine diff --git a/engine/src/simulation_sync.hpp b/engine/src/simulation_sync.hpp new file mode 100644 index 000000000..a1aa9d582 --- /dev/null +++ b/engine/src/simulation_sync.hpp @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * \file simulation_sync.hpp + */ + +#pragma once + +#include + +#include +#include + +namespace engine { + +/** + * SimulationSync is the synchronization context of the simulation. + */ +class SimulationSync : public cloe::Sync { + public: // Overrides + SimulationSync() = default; + SimulationSync(const SimulationSync &) = default; + SimulationSync(SimulationSync &&) = delete; + SimulationSync &operator=(const SimulationSync &) = default; + SimulationSync &operator=(SimulationSync &&) = delete; + virtual ~SimulationSync() = default; + + explicit SimulationSync(const cloe::Duration &step_width) : step_width_(step_width) {} + + uint64_t step() const override { return step_; } + cloe::Duration step_width() const override { return step_width_; } + cloe::Duration time() const override { return time_; } + cloe::Duration eta() const override { return eta_; } + + /** + * Return the target simulation factor, with 1.0 being realtime. + * + * - If target realtime factor is <= 0.0, then it is interpreted to be unlimited. + * - Currently, the floating INFINITY value is not handled specially. + */ + double realtime_factor() const override { return realtime_factor_; } + + /** + * Return the maximum theorically achievable simulation realtime factor, + * with 1.0 being realtime. + */ + double achievable_realtime_factor() const override { + return static_cast(step_width().count()) / static_cast(cycle_time_.count()); + } + + public: // Modification + /** + * Increase the step number for the simulation. + * + * - It increases the step by one. + * - It moves the simulation time forward by the step width. + * - It stores the real time difference from the last time IncrementStep was called. + */ + void increment_step() { + step_ += 1; + time_ += step_width_; + } + + /** + * Set the target realtime factor, with any value less or equal to zero + * unlimited. + */ + void set_realtime_factor(double s) { realtime_factor_ = s; } + + void set_eta(cloe::Duration d) { eta_ = d; } + + void reset() { + time_ = cloe::Duration(0); + step_ = 0; + } + + void set_cycle_time(cloe::Duration d) { cycle_time_ = d; } + + private: + // Simulation State + uint64_t step_{0}; + cloe::Duration time_{0}; + cloe::Duration eta_{0}; + cloe::Duration cycle_time_{0}; + + // Simulation Configuration + double realtime_factor_{1.0}; // realtime + cloe::Duration step_width_{20'000'000}; // should be 20ms +}; + +} // namespace engine diff --git a/oak/include/oak/server.hpp b/oak/include/oak/server.hpp index 3a4c96738..4e41dc543 100644 --- a/oak/include/oak/server.hpp +++ b/oak/include/oak/server.hpp @@ -71,19 +71,19 @@ class Server { */ void set_address(const std::string& addr) { listen_addr_ = addr; } - const std::string& address() const { return listen_addr_; } + [[nodiscard]] const std::string& address() const { return listen_addr_; } /** * Set the port on which to listen. */ void set_port(int port) { listen_port_ = port; } - int port() const { return listen_port_; } + [[nodiscard]] int port() const { return listen_port_; } /** * Returns whether the server has started and is currently listening. */ - bool is_listening() const { return listening_; } + [[nodiscard]] bool is_listening() const { return listening_; } /** * Start the server. @@ -93,7 +93,7 @@ class Server { /** * Return endpoint data in json format. */ - fable::Json endpoints_to_json(const std::vector& endpoints) const; + [[nodiscard]] fable::Json endpoints_to_json(const std::vector& endpoints) const; /** * Stop the server. @@ -103,7 +103,7 @@ class Server { /** * Return a list of all registered endpoints. */ - std::vector endpoints() const; + [[nodiscard]] std::vector endpoints() const; protected: friend StaticRegistrar; diff --git a/plugins/basic/src/basic.cpp b/plugins/basic/src/basic.cpp index b1fe41504..b8e9fa8d2 100644 --- a/plugins/basic/src/basic.cpp +++ b/plugins/basic/src/basic.cpp @@ -398,35 +398,9 @@ class BasicController : public Controller { void enroll(Registrar& r) override { if (this->veh_) { - auto acc_signal = r.declare_signal("acc"); - acc_signal->set_getter( - [this]() -> const cloe::controller::basic::AccConfiguration& { - return this->acc_.config; - }); - acc_signal->set_setter( - [this](const cloe::controller::basic::AccConfiguration& value) { - this->acc_.config = value; - }); - - auto aeb_signal = r.declare_signal("aeb"); - aeb_signal->set_getter( - [this]() -> const cloe::controller::basic::AebConfiguration& { - return this->aeb_.config; - }); - aeb_signal->set_setter( - [this](const cloe::controller::basic::AebConfiguration& value) { - this->aeb_.config = value; - }); - - auto lka_signal = r.declare_signal("lka"); - lka_signal->set_getter( - [this]() -> const cloe::controller::basic::LkaConfiguration& { - return this->lka_.config; - }); - lka_signal->set_setter( - [this](const cloe::controller::basic::LkaConfiguration& value) { - this->lka_.config = value; - }); + r.declare_signal("acc", &acc_.config); + r.declare_signal("aeb", &aeb_.config); + r.declare_signal("lka", &lka_.config); } auto lua = r.register_lua_table(); diff --git a/runtime/include/cloe/data_broker.hpp b/runtime/include/cloe/data_broker.hpp index 293cdf8d8..7f521c72b 100644 --- a/runtime/include/cloe/data_broker.hpp +++ b/runtime/include/cloe/data_broker.hpp @@ -1574,14 +1574,47 @@ class DataBroker { * \return Pointer to the specified signal */ template - SignalPtr declare(std::string_view new_name) { + SignalPtr declare(std::string_view name) { assert_static_type(); using compatible_type = databroker::compatible_base_t; declare(); SignalPtr signal = Signal::make(); - alias(signal, new_name); + alias(signal, name); + return signal; + } + + /** + * Declare a new signal and auto-implement getter and setter. + * + * \tparam T type of the signal value + * \param name name of the signal + * \param value_ptr pointer to signal value + * \return pointer to specified signal + */ + template + SignalPtr declare(std::string_view name, T* value_ptr) { + assert(value_ptr != nullptr); + auto signal = this->declare(name); + signal->template set_getter([value_ptr]() -> const T& { + return *value_ptr; + }); + signal->template set_setter([value_ptr](const T& value) { + *value_ptr = value; + }); + return signal; + } + + template + SignalPtr declare(std::string_view name, std::function getter, std::function setter) { + auto signal = this->declare(name); + if (getter) { + signal->template set_getter(getter); + } + if (setter) { + signal->template set_setter(setter); + } return signal; } diff --git a/runtime/include/cloe/registrar.hpp b/runtime/include/cloe/registrar.hpp index 4aba20177..2fbadadeb 100644 --- a/runtime/include/cloe/registrar.hpp +++ b/runtime/include/cloe/registrar.hpp @@ -178,6 +178,17 @@ class Registrar { return data_broker().declare(make_signal_name(name)); } + template + SignalPtr declare_signal(std::string_view name, T&& value_ptr) { + return data_broker().declare(make_signal_name(name), std::forward(value_ptr)); + } + + template + SignalPtr declare_signal(std::string_view name, GetterFunc&& getter, SetterFunc&& setter) { + return data_broker().declare(make_signal_name(name), std::forward(getter), + std::forward(setter)); + } + [[nodiscard]] virtual DataBroker& data_broker() const = 0; /** diff --git a/runtime/include/cloe/trigger/nil_event.hpp b/runtime/include/cloe/trigger/nil_event.hpp index 7d4636cce..d97d1f722 100644 --- a/runtime/include/cloe/trigger/nil_event.hpp +++ b/runtime/include/cloe/trigger/nil_event.hpp @@ -25,6 +25,7 @@ #include // for string #include // for Conf, Json +#include // for DirectCallback #include // for Event, EventFactory, Action, ActionFactory #include // for _X_FACTORY, _X_CALLBACK diff --git a/runtime/include/cloe/trigger/set_action.hpp b/runtime/include/cloe/trigger/set_action.hpp index 4f07072e9..5b97e7373 100644 --- a/runtime/include/cloe/trigger/set_action.hpp +++ b/runtime/include/cloe/trigger/set_action.hpp @@ -31,24 +31,23 @@ #include // for Action, ActionFactory #include // for _X_FACTORY, _X_CALLBACK -namespace cloe { -namespace actions { +namespace cloe::actions { template T from_string(const std::string& s); template <> -double from_string(const std::string& s) { +inline double from_string(const std::string& s) { return std::stod(s); } template <> -int from_string(const std::string& s) { +inline int from_string(const std::string& s) { return std::stoi(s); } template <> -bool from_string(const std::string& s) { +inline bool from_string(const std::string& s) { if (s == "true") { return true; } else if (s == "false") { @@ -108,8 +107,7 @@ class SetVariableActionFactory : public ActionFactory { T* data_ptr_; }; -} // namespace actions -} // namespace cloe +} // namespace cloe::actions /** * Macro DEFINE_SET_STATE_ACTION defines an action that has only a single state diff --git a/runtime/include/cloe/vehicle.hpp b/runtime/include/cloe/vehicle.hpp index bf6113617..9e1984a61 100644 --- a/runtime/include/cloe/vehicle.hpp +++ b/runtime/include/cloe/vehicle.hpp @@ -220,6 +220,15 @@ class Vehicle : public Model { this->components_[key] = component; } + std::vector component_names() const { + std::vector results; + results.reserve(components_.size()); + for (const auto& kv : components_) { + results.emplace_back(kv.first); + } + return results; + } + public: // Overrides /** * Process all components. diff --git a/tests/test_lua04_schedule_test.lua b/tests/test_lua04_schedule_test.lua index d75339d1d..639e59bab 100644 --- a/tests/test_lua04_schedule_test.lua +++ b/tests/test_lua04_schedule_test.lua @@ -19,7 +19,7 @@ cloe.schedule({ }) local Sig = { - VehAcc = "vehicles.default.basic.acc" + VehAcc = "basic/acc" } cloe.require_signals_enum(Sig) cloe.record_signals(Sig) diff --git a/tests/test_lua14_speedometer_signals.lua b/tests/test_lua14_speedometer_signals.lua index e3348ab1d..69e8f8dbd 100644 --- a/tests/test_lua14_speedometer_signals.lua +++ b/tests/test_lua14_speedometer_signals.lua @@ -3,5 +3,5 @@ local cloe = require("cloe") cloe.load_stackfile("config_minimator_multi_agent_infinite.json") local Sig = { - Speedometer + Speedometer = "third_speed/kmph" }