diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 305c21b9f..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 @@ -170,6 +193,11 @@ if(BUILD_TESTING) message(STATUS "Building test-enginelib executable.") add_executable(test-enginelib 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 @@ -192,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/lua/cloe-engine/init.lua b/engine/lua/cloe-engine/init.lua index ce16073b5..2ed7c28e0 100644 --- a/engine/lua/cloe-engine/init.lua +++ b/engine/lua/cloe-engine/init.lua @@ -44,9 +44,6 @@ local engine = { --- Contains engine state for a simulation. state = { - --- @type StackConf The current active stack configuration (volatile). - config = {}, - --- @type table A table of feature flags. features = { ["cloe-0.18.0"] = true, @@ -74,6 +71,9 @@ local engine = { --- @type Stack Reference to simulation stack type. stack = nil, + --- @type boolean True if simulation has started. + is_running = false, + --- @type string|nil Path to currently executing Lua script file. current_script_file = nil, @@ -144,6 +144,13 @@ function engine.is_available() return false end +--- Return whether the simulation has started. +--- +--- @return boolean +function engine.is_simulation_running() + return engine.state.is_running +end + --- Return path to Lua file that the engine is currently merging, --- or nil if no file is being loaded. --- @@ -167,6 +174,14 @@ function engine.get_stack() return unavailable("get_stack") end +--- Return the current simulation configuration. +--- +--- This is essential a dump of the stack. +--- @return StackConf +function engine.get_config() + return unavailable("get_config") +end + --- Return the simulation scheduler (aka. Coordinator) global instance. --- --- @return Coordinator diff --git a/engine/lua/cloe/engine.lua b/engine/lua/cloe/engine.lua index 6e1d954ea..f20a95cd8 100644 --- a/engine/lua/cloe/engine.lua +++ b/engine/lua/cloe/engine.lua @@ -39,7 +39,7 @@ end --- @nodiscard function engine.has_feature(id) validate("cloe.has_feature(string)", id) - return api.state.features[id] and true or false + return api.get_features()[id] and true or false end --- Throw an exception if Cloe does not have feature as defined by string. @@ -60,7 +60,7 @@ end --- --- @return StackConf function engine.config() - return api.state.config + return api.get_config() end --- Try to load (merge) stackfile. @@ -69,12 +69,12 @@ end --- @return Stack function engine.load_stackfile(file) validate("cloe.load_stackfile(string)", file) - local cwd = api.state.current_script_dir or "." + local cwd = api.get_script_dir() or "." if fs.is_relative(file) then file = cwd .. "/" .. file end - api.state.stack:merge_stackfile(file) - return api.state.stack + api.get_stack():merge_stackfile(file) + return api.get_stack() end --- Read JSON file into Lua types (most likely as Lua table). @@ -99,11 +99,11 @@ end --- @return nil function engine.apply_stack(stack) validate("cloe.apply_stack(string|table)", stack) - local file = api.state.current_script_file or "" + local file = api.get_script_file() or "" if type(stack) == "table" then - api.state.stack:merge_stacktable(stack --[[ @as table ]], file) + api.get_stack():merge_stacktable(stack --[[ @as table ]], file) else - api.state.stack:merge_stackjson(stack --[[ @as string ]], file) + api.get_stack():merge_stackjson(stack --[[ @as string ]], file) end end @@ -126,10 +126,18 @@ end --- Alias a set of signals in the Cloe data broker. --- +--- TODO: Does this mean that the signals are also required? +--- { +--- ["^regular expression$"] = +--- } +--- --- @param list table # regular expression to alias key --- @return table # current signal aliases table function engine.alias_signals(list) - -- TODO: Throw an error if simulation already started. + if api.is_simulation_running() then + error("can only alias signals before simulation start") + end + api.initial_input.signal_aliases = luax.tbl_extend("force", api.initial_input.signal_aliases, list) return api.initial_input.signal_aliases end @@ -139,7 +147,10 @@ end --- @param list string[] signals to merge into main list of required signals --- @return string[] # merged list of signals function engine.require_signals(list) - -- TODO: Throw an error if simulation already started. + if api.is_simulation_running() then + error("can only require signals before simulation start") + end + api.initial_input.signal_requires = luax.tbl_extend("force", api.initial_input.signal_requires, list) return api.initial_input.signal_requires end @@ -151,33 +162,50 @@ end --- --- ---@enum Sig --- local Sig = { ---- DriverDoorLatch = "vehicle::framework::chassis::.*driver_door::latch", ---- VehicleMps = "vehicle::sensors::chassis::velocity", +--- DriverDoorLatch = "^vehicle::framework::chassis::.*driver_door::latch$", +--- VehicleMps = "^vehicle::sensors::chassis::velocity$", --- } ---- cloe.require_signals_enum(Sig, true) +--- cloe.require_signals_enum(Sig) --- --- Later, you can use the enum with `cloe.signal()`: --- --- cloe.signal(Sig.DriverDoorLatch) +--- cloe.signal("...") --- --- @param enum table input mappging from enum name to signal name ---- @param alias boolean whether to treat signal names as alias regular expressions --- @return nil -function engine.require_signals_enum(enum, alias) - -- TODO: Throw an error if simulation already started. - local signals = {} - if alias then - local aliases = {} - for key, sigregex in pairs(enum) do - table.insert(aliases, { sigregex, key }) - table.insert(signals, key) +function engine.require_signals_enum(enum) + if api.is_simulation_running() then + error("can only require/alias signals before simulation start") + end + + -- Return true if signal should be treated as a regular expression. + local function is_regex(s) + return string.match(s, "^%^.*%$$") ~= nil + end + + -- We have to handle the following three variants: + -- + -- { + -- A = "A", -- do not alias + -- B = "B.b", -- alias B + -- C = "^C$", -- alias C and ^C$ + -- ["^D$"] = "^D$", -- alias ^D$ + -- } + local signals, aliases = {}, {} + for key, signal in pairs(enum) do + -- Case A + table.insert(signals, signal) + if signal ~= key then + -- Case B and C + table.insert(aliases, { signal, key }) end - engine.alias_signals(aliases) - else - for _, signame in pairs(enum) do - table.insert(signals, signame) + if is_regex(signal) then + -- Case C + table.insert(aliases, { signal, signal }) end end + engine.alias_signals(aliases) engine.require_signals(signals) end @@ -215,23 +243,73 @@ function engine.set_signal(name, value) api.signals[name] = value end ---- Record the given list of signals into the report. +--- Record the given list of signals each cycle and write the results into the +--- report. --- ---- This can be called multiple times, but if the signal is already ---- being recorded, then an error will be raised. +--- This (currently) works by scheduling a Lua function to run every +--- cycle and write the current signal value. If a signal changes +--- value multiple times during a cycle, it's currently *undefined* +--- which of these values will be recorded. --- ---- This should be called before simulation starts, ---- so not from a scheduled callback. +--- This setup function can be called multiple times, but if the output signal +--- name is already being recorded, then an error will be raised. +--- +--- This should be called before simulation starts, so not from a scheduled +--- callback. --- --- You can pass it a list of signals to record, or a mapping ---- from name to +--- from output name or signal name or function to produce value. +--- +--- When just specifying signal names to be recorded without a function +--- defining how they are to be recorded, a default implementation of: +--- +--- function() return cloe.signal(signal_name) end +--- +--- is used. This means that the signal needs to be made available through a +--- call to `cloe.require_signals()` or equivalent. +--- +--- Example 1: plain signal list +--- +--- local want = {"A", "B-signal", "C"} +--- cloe.require_signals(want) +--- cloe.record_signals(want) +--- +--- Example 2: mapping from recorded value to +--- +--- math.randomseed(os.time()) +--- cloe.record_signals({ +--- ["random_number"] = math.random, +--- ["A_subvalue"] = function() return cloe.signal("A").subvalue end, +--- }) +--- +--- Example 3: +--- +--- cloe.record_signals({ +--- "A", +--- ["B"] = "B-signal", +--- ["C"] = function() return cloe.signal("C").subvalue end, +--- }) +--- +--- Example 4: +--- +--- local Sig = { +--- Brake = "^vehicles.default.controllerA.brake_position$", +--- Accel = "^vehicles.default.controllerA.accel_position$", +--- } +--- cloe.require_signal_enum(Sig) +--- cloe.record_signals(Sig) --- --- @param mapping table mapping from signal names --- @return nil function engine.record_signals(mapping) validate("cloe.record_signals(table)", mapping) - api.state.report.signals = api.state.report.signals or {} - local signals = api.state.report.signals + if api.is_simulation_running() then + error("cloe.record_signals() cannot be called after simulation start") + end + + local report = api.get_report() + report.signals = report.signals or {} + local signals = report.signals signals.time = signals.time or {} for sig, getter in pairs(mapping) do if type(sig) == "number" then @@ -259,6 +337,7 @@ function engine.record_signals(mapping) for name, getter in pairs(mapping) do local value if type(name) == "number" then + assert(type(getter) == "string") name = getter end if type(getter) == "string" then @@ -267,8 +346,7 @@ function engine.record_signals(mapping) value = getter() end if value == nil then - -- TODO: Improve error message! - error("nil value received as signal value") + error(string.format("cannot record nil as value for signal %s at %d ms", name, cur_time)) end table.insert(signals[name], value) end @@ -291,8 +369,8 @@ function engine.insert_trigger(trigger) -- events are put in a queue and picked up by the engine at simulation -- start. After this, cloe.state.scheduler exists and we can use its -- methods. - if api.state.scheduler then - api.state.scheduler:insert_trigger(trigger) + if api.get_scheduler() then + api.get_scheduler():insert_trigger(trigger) else table.insert(api.initial_input.triggers, trigger) end @@ -307,8 +385,8 @@ end --- @return nil function engine.execute_action(action) validate("cloe.execute_action(string|table)", action) - if api.state.scheduler then - api.state.scheduler:execute_action(action) + if api.get_scheduler() then + api.get_scheduler():execute_action(action) else error("can only execute actions within scheduled events") end @@ -330,7 +408,7 @@ end local Task do local types = require("tableshape").types - Task = types.shape { + Task = types.shape({ on = types.string + types.table + types.func, run = types.string + types.table + types.func, desc = types.string:is_optional(), @@ -339,7 +417,7 @@ do pin = types.boolean:is_optional(), priority = types.integer:is_optional(), source = types.string:is_optional(), - } + }) end --- @class PartialTask @@ -373,12 +451,9 @@ end local Tasks do local types = require("tableshape").types - Tasks = types.shape( - PartialTaskSpec, - { - extra_fields = types.array_of(PartialTask) - } - ) + Tasks = types.shape(PartialTaskSpec, { + extra_fields = types.array_of(PartialTask), + }) end --- Expand a list of partial tasks to a list of complete tasks. @@ -501,7 +576,7 @@ end local Test do local types = require("tableshape").types - Test = types.shape { + Test = types.shape({ id = types.string, on = types.string + types.table + types.func, run = types.string + types.table + types.func, @@ -509,7 +584,7 @@ do info = types.table:is_optional(), enable = types.boolean:is_optional(), terminate = types.boolean:is_optional(), - } + }) end --- Schedule a test as a coroutine that can yield to Cloe. diff --git a/engine/lua/cloe/events.lua b/engine/lua/cloe/events.lua index baf94a8fa..668ed0630 100644 --- a/engine/lua/cloe/events.lua +++ b/engine/lua/cloe/events.lua @@ -53,12 +53,12 @@ function events.after_tests(...) if #names == 1 then local name = names[1] return function() - return api.state.report.tests[name].complete + return api.get_report().tests[name].complete end else return function() for _, k in ipairs(names) do - if not api.state.report.tests[k].complete then + if not api.get_report().tests[k].complete then return false end end @@ -78,7 +78,7 @@ function events.every(duration) if type(duration) == "string" then duration = types.Duration.new(duration) end - if duration:ns() % api.state.config.simulation.model_step_width ~= 0 then + if duration:ns() % api.get_config().simulation.model_step_width ~= 0 then error("interval duration is not a multiple of nominal step width") end return function(sync) diff --git a/engine/lua/cloe/init.lua b/engine/lua/cloe/init.lua index 96ca5968d..0b52ec41e 100644 --- a/engine/lua/cloe/init.lua +++ b/engine/lua/cloe/init.lua @@ -81,12 +81,12 @@ end --- Require a module, prioritizing modules relative to the script --- launched by cloe-engine. --- ---- If api.state.current_script_dir is nil, this is equivalent to require(). +--- If api.get_script_dir() is nil, this is equivalent to require(). --- --- @param module string module identifier, such as "project" function cloe.require(module) cloe.validate("cloe.require(string)", module) - local script_dir = api.state.current_script_dir + local script_dir = api.get_script_dir() if script_dir then local old_package_path = package.path package.path = string.format("%s/?.lua;%s/?/init.lua;%s", script_dir, script_dir, package.path) @@ -106,7 +106,7 @@ end function cloe.init_report(header) cloe.validate("cloe.init_report(?table)", header) local system = require("cloe.system") - local report = api.state.report + local report = api.get_report() report.metadata = { hostname = system.get_hostname(), username = system.get_username(), diff --git a/engine/lua/cloe/testing.lua b/engine/lua/cloe/testing.lua index 5b61c08da..49660a879 100644 --- a/engine/lua/cloe/testing.lua +++ b/engine/lua/cloe/testing.lua @@ -75,7 +75,7 @@ function TestFixture.new(test, scheduler) local debinfo = debug.getinfo(test.run) local source = string.format("%s:%s-%s", debinfo.short_src, debinfo.linedefined, debinfo.lastlinedefined) - local report = api.state.report + local report = api.get_report() if report["tests"] == nil then report["tests"] = {} end @@ -217,7 +217,7 @@ end --- @private function TestFixture:_terminate() - local report = api.state.report + local report = api.get_report() local tests = 0 local tests_failed = 0 for _, test in pairs(report["tests"]) do 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 6fa025e2c..ea0d5bbdb 100644 --- a/engine/src/coordinator.hpp +++ b/engine/src/coordinator.hpp @@ -117,6 +117,16 @@ class Coordinator { [[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 * events. diff --git a/engine/src/lua_api.cpp b/engine/src/lua_api.cpp index 6043341ab..4a8131b22 100644 --- a/engine/src/lua_api.cpp +++ b/engine/src/lua_api.cpp @@ -25,7 +25,7 @@ namespace cloe { -sol::protected_function_result lua_safe_script_file(sol::state_view& lua, +sol::protected_function_result lua_safe_script_file(sol::state_view lua, const std::filesystem::path& filepath) { auto file = std::filesystem::path(filepath); auto dir = file.parent_path().generic_string(); @@ -33,16 +33,17 @@ sol::protected_function_result lua_safe_script_file(sol::state_view& lua, dir = "."; } - auto state = luat_cloe_engine_state(lua); - auto old_file = state["current_script_file"]; - auto old_dir = state["current_script_dir"]; - state["scripts_loaded"].get().add(file.generic_string()); - state["current_script_file"] = file.generic_string(); - state["current_script_dir"] = dir; + sol::object old_file = luat_cloe_engine_state(lua)["current_script_file"]; + sol::object old_dir = luat_cloe_engine_state(lua)["current_script_dir"]; + sol::table scripts_loaded = luat_cloe_engine_state(lua)["scripts_loaded"]; + scripts_loaded[scripts_loaded.size() + 1] = file.generic_string(); + luat_cloe_engine_state(lua)["scripts_loaded"] = scripts_loaded; + luat_cloe_engine_state(lua)["current_script_file"] = file.generic_string(); + luat_cloe_engine_state(lua)["current_script_dir"] = dir; logger::get("cloe")->info("Loading {}", file.generic_string()); auto result = lua.safe_script_file(file.generic_string(), sol::script_pass_on_error); - state["current_script_file"] = old_file; - state["current_script_dir"] = old_dir; + luat_cloe_engine_state(lua)["current_script_file"] = old_file; + luat_cloe_engine_state(lua)["current_script_dir"] = old_dir; return result; } diff --git a/engine/src/lua_api.hpp b/engine/src/lua_api.hpp index 1af36e676..83763f012 100644 --- a/engine/src/lua_api.hpp +++ b/engine/src/lua_api.hpp @@ -36,7 +36,7 @@ namespace cloe { * Safely load and run a user Lua script. */ [[nodiscard]] sol::protected_function_result lua_safe_script_file( - sol::state_view& lua, const std::filesystem::path& filepath); + sol::state_view lua, const std::filesystem::path& filepath); /** * Return the cloe-engine table as it is exported into Lua. @@ -46,28 +46,28 @@ namespace cloe { * engine/lua/cloe-engine/init.lua * */ -[[nodiscard]] inline auto luat_cloe_engine(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]; +[[nodiscard]] inline auto luat_cloe_engine(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]; } -[[nodiscard]] inline auto luat_cloe_engine_fs(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine.fs"]; +[[nodiscard]] inline auto luat_cloe_engine_fs(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine.fs"]; } -[[nodiscard]] inline auto luat_cloe_engine_types(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine.types"]; +[[nodiscard]] inline auto luat_cloe_engine_types(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine.types"]; } -[[nodiscard]] inline auto luat_cloe_engine_initial_input(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["initial_input"]; +[[nodiscard]] inline auto luat_cloe_engine_initial_input(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["initial_input"]; } -[[nodiscard]] inline auto luat_cloe_engine_state(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["state"]; +[[nodiscard]] inline auto luat_cloe_engine_state(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["state"]; } -[[nodiscard]] inline auto luat_cloe_engine_plugins(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["plugins"]; +[[nodiscard]] inline auto luat_cloe_engine_plugins(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["plugins"]; } } // namespace cloe diff --git a/engine/src/lua_debugger.cpp b/engine/src/lua_debugger.cpp index 8f68f8fe2..13f4e6f6c 100644 --- a/engine/src/lua_debugger.cpp +++ b/engine/src/lua_debugger.cpp @@ -26,7 +26,7 @@ namespace cloe { -void start_lua_debugger(sol::state& lua, int listen_port) { +void start_lua_debugger(sol::state_view lua, int listen_port) { static lrdb::server debug_server(listen_port); debug_server.reset(lua.lua_state()); } diff --git a/engine/src/lua_setup.cpp b/engine/src/lua_setup.cpp index b6c21a434..e1cdf1146 100644 --- a/engine/src/lua_setup.cpp +++ b/engine/src/lua_setup.cpp @@ -22,6 +22,7 @@ #include "lua_setup.hpp" #include // for path +#include #include // for state_view @@ -107,7 +108,7 @@ int lua_exception_handler(lua_State* L, sol::optional may * * \see lua_setup_builtin.cpp */ -void configure_package_path(sol::state_view& lua, const std::vector& paths) { +void configure_package_path(sol::state_view lua, const std::vector& paths) { std::string package_path = lua["package"]["path"]; for (const std::string& p : paths) { package_path += ";" + p + "/?.lua"; @@ -119,7 +120,7 @@ void configure_package_path(sol::state_view& lua, const std::vector /** * Add Lua package paths so that bundled Lua libaries can be found. */ -void register_package_path(sol::state_view& lua, const LuaOptions& opt) { +void register_package_path(sol::state_view lua, const LuaOptions& opt) { // Setup lua path: std::vector lua_path{}; if (!opt.no_system_lua) { @@ -157,7 +158,7 @@ void register_package_path(sol::state_view& lua, const LuaOptions& opt) { * * engine/lua/cloe-engine/init.lua */ -void register_cloe_engine(sol::state_view& lua, Stack& stack) { +void register_cloe_engine(sol::state_view lua, Stack& stack) { sol::table tbl = lua.create_table(); // Initial input will be processed at simulation start. @@ -175,10 +176,10 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { tbl["state"] = lua.create_table(); tbl["state"]["report"] = lua.create_table(); tbl["state"]["stack"] = std::ref(stack); - tbl["state"]["config"] = fable::into_sol_object(lua, stack.active_config()); tbl["state"]["scheduler"] = sol::lua_nil; tbl["state"]["current_script_file"] = sol::lua_nil; tbl["state"]["current_script_dir"] = sol::lua_nil; + tbl["state"]["is_running"] = false; tbl["state"]["scripts_loaded"] = lua.create_table(); tbl["state"]["features"] = lua.create_table_with( // Version compatibility: @@ -188,8 +189,16 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { "cloe-0.19", true, "cloe-0.20.0", true, "cloe-0.20", true, - "cloe-0.21.0", true, // nightly - "cloe-0.21", true, // nightly + "cloe-0.21.0", true, + "cloe-0.21", true, + "cloe-0.22.0", true, + "cloe-0.22", true, + "cloe-0.23.0", true, + "cloe-0.23", true, + "cloe-0.24.0", true, + "cloe-0.24", true, + "cloe-0.25.0", true, + "cloe-0.25", true, // Stackfile versions support: "cloe-stackfile", true, @@ -203,8 +212,11 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { ); // clang-format on -#if 0 tbl.set_function("is_available", []() { return true; }); + + tbl.set_function("is_simulation_running", + [](sol::this_state lua) { return luat_cloe_engine_state(lua)["is_running"]; }); + tbl.set_function("get_script_file", [](sol::this_state lua) { return luat_cloe_engine_state(lua)["current_script_file"]; }); @@ -217,16 +229,18 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { [](sol::this_state lua) { return luat_cloe_engine_state(lua)["scheduler"]; }); tbl.set_function("get_features", [](sol::this_state lua) { return luat_cloe_engine_state(lua)["features"]; }); - tbl.set_function("get_stack", - [](sol::this_state lua) { return luat_cloe_engine_state(lua)["stack"]; }); -#endif + tbl.set_function("get_stack", [&stack]() { return std::ref(stack); }); + tbl.set_function("get_config", [&stack](sol::this_state lua) { + return fable::into_sol_object(lua, stack.active_config()); + }); + tbl.set_function("log", cloe_api_log); tbl.set_function("exec", cloe_api_exec); luat_cloe_engine(lua) = tbl; } -void register_enum_loglevel(sol::state_view& lua, sol::table& tbl) { +void register_enum_loglevel(sol::state_view lua, sol::table& tbl) { // clang-format off tbl["LogLevel"] = lua.create_table_with( "TRACE", "trace", @@ -250,7 +264,7 @@ void register_enum_loglevel(sol::state_view& lua, sol::table& tbl) { * * engine/lua/cloe-engine/types.lua */ -void register_cloe_engine_types(sol::state_view& lua) { +void register_cloe_engine_types(sol::state_view lua) { sol::table tbl = lua.create_table(); register_usertype_duration(tbl); register_usertype_sync(tbl); @@ -270,7 +284,7 @@ void register_cloe_engine_types(sol::state_view& lua) { * * engine/lua/cloe-engine/fs.lua */ -void register_cloe_engine_fs(sol::state_view& lua) { +void register_cloe_engine_fs(sol::state_view lua) { sol::table tbl = lua.create_table(); register_lib_fs(tbl); luat_cloe_engine_fs(lua) = tbl; @@ -282,7 +296,7 @@ void register_cloe_engine_fs(sol::state_view& lua) { * You can just use `cloe`, and it will auto-require the cloe module. * If you don't use it, then it won't be loaded. */ -void register_cloe(sol::state_view& lua) { +void register_cloe(sol::state_view lua) { // This takes advantage of the `__index` function for metatables, which is called // when a key can't be found in the original table, here an empty table // assigned to cloe. It then loads the cloe module, and returns the key @@ -305,9 +319,8 @@ void register_cloe(sol::state_view& lua) { } // anonymous namespace -sol::state new_lua(const LuaOptions& opt, Stack& stack) { +void setup_lua(sol::state_view lua, const LuaOptions& opt, Stack& stack) { // clang-format off - sol::state lua; lua.open_libraries( sol::lib::base, sol::lib::coroutine, @@ -329,10 +342,9 @@ sol::state new_lua(const LuaOptions& opt, Stack& stack) { if (opt.auto_require_cloe) { register_cloe(lua); } - return lua; } -void merge_lua(sol::state_view& lua, const std::string& filepath) { +void merge_lua(sol::state_view lua, const std::string& filepath) { logger::get("cloe")->debug("Load script {}", filepath); auto result = lua_safe_script_file(lua, std::filesystem::path(filepath)); if (!result.valid()) { diff --git a/engine/src/lua_setup.hpp b/engine/src/lua_setup.hpp index 00e473675..fc183b45c 100644 --- a/engine/src/lua_setup.hpp +++ b/engine/src/lua_setup.hpp @@ -56,7 +56,7 @@ struct LuaOptions { * \see stack_factory.hpp * \see lua_setup.cpp */ -sol::state new_lua(const LuaOptions& opt, Stack& s); +void setup_lua(sol::state_view lua, const LuaOptions& opt, Stack& s); #if CLOE_ENGINE_WITH_LRDB /** @@ -65,7 +65,7 @@ sol::state new_lua(const LuaOptions& opt, Stack& s); * \param lua * \param listen_port */ -void start_lua_debugger(sol::state& lua, int listen_port); +void start_lua_debugger(sol::state_view lua, int listen_port); #endif /** @@ -73,7 +73,7 @@ void start_lua_debugger(sol::state& lua, int listen_port); * * \see lua_setup.cpp */ -void merge_lua(sol::state_view& lua, const std::string& filepath); +void merge_lua(sol::state_view lua, const std::string& filepath); /** * Define the filesystem library functions in the given table. diff --git a/engine/src/lua_setup_test.cpp b/engine/src/lua_setup_test.cpp new file mode 100644 index 000000000..2d5fd8372 --- /dev/null +++ b/engine/src/lua_setup_test.cpp @@ -0,0 +1,132 @@ +/* + * 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 + */ + +#include + +#include // for tmpnam +#include +#include +#include + +#include + +#include "lua_api.hpp" // for lua_safe_script_file +#include "lua_setup.hpp" // for setup_lua +#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; + + 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) { + // NOTE: Regarding the danger of std::tmpname. + // It is required to have a real file in the filesystem that can be + // then laoded by lua.safe_script_file. Because tests are run in parallel, + // this filename needs to be reasonably random. Since this program is not + // running at high privileges, the potential attack vector should not result + // in an escalation of privileges. + auto temp_file = std::filesystem::path(std::string(std::tmpnam(nullptr)) + ".lua"); // NOLINT + std::ofstream ofs(temp_file); + if (!ofs.is_open()) { + throw std::ios_base::failure("Failed to create temporary file"); + } + ofs << content; + ofs.close(); + + defer_deletion_.push_back(temp_file); + return temp_file; + } + + void TearDown() override { + for (const auto& f : defer_deletion_) { + std::filesystem::remove(f); + } + } +}; + +TEST_F(cloe_lua_setup, cloe_engine_is_available) { + setup_lua(lua, opt, stack); + lua.script(R"( + local api = require("cloe-engine") + assert(api.is_available()) + assert(not api.is_simulation_running()) + )"); +} + +TEST_F(cloe_lua_setup, describe_cloe) { + setup_lua(lua, opt, stack); + ASSERT_EQ(lua.script("local cloe = require('cloe'); return cloe.inspect(cloe.LogLevel.CRITICAL)").get(), + std::string("\"critical\"")); +} + +TEST_F(cloe_lua_setup, describe_cloe_without_require) { + opt.auto_require_cloe = true; + setup_lua(lua, opt, stack); + ASSERT_EQ(lua.script("return cloe.inspect(cloe.LogLevel.CRITICAL)").get(), + std::string("\"critical\"")); +} + +TEST_F(cloe_lua_setup, read_engine_state) { + setup_lua(lua, opt, stack); + ASSERT_TRUE(lua.script("return require('cloe-engine').is_available()").get()); + ASSERT_FALSE(luat_cloe_engine_state(lua)["is_running"].get()); +} + +TEST_F(cloe_lua_setup, write_engine_state) { + setup_lua(lua, opt, stack); + luat_cloe_engine_state(lua)["extra"] = "hello world!"; + ASSERT_EQ(lua.script("return require('cloe-engine').state.extra").get(), + std::string("hello world!")); +} + +TEST_F(cloe_lua_setup, write_engine_state_table) { + setup_lua(lua, opt, stack); + sol::table state = luat_cloe_engine_state(lua); + sol::table scripts_loaded = state["scripts_loaded"]; + ASSERT_TRUE(scripts_loaded.valid()); + scripts_loaded[scripts_loaded.size() + 1] = "hello_world.lua"; + ASSERT_EQ(lua.script("return require('cloe-engine').state.scripts_loaded[1]").get(), + std::string("hello_world.lua")); +} + +TEST_F(cloe_lua_setup, lua_safe_script_file) { + auto file = WriteTempLuaFile(R"( + local cloe = require("cloe") + local api = require("cloe-engine") + return api.get_script_file() + )"); + setup_lua(lua, opt, stack); + ASSERT_EQ(lua_safe_script_file(lua, file).get(), file.generic_string()); +} diff --git a/engine/src/main.cpp b/engine/src/main.cpp index 081972759..918eef30a 100644 --- a/engine/src/main.cpp +++ b/engine/src/main.cpp @@ -63,6 +63,12 @@ int main(int argc, char** argv) { check->add_option("-J,--json-indent", check_options.json_indent, "JSON indentation level"); check->add_option("files", check_files, "Files to check"); + engine::ProbeOptions probe_options{}; + std::vector probe_files{}; + auto* probe = app.add_subcommand("probe", "Probe a simulation with (merged) stack files."); + probe->add_option("-J,--json-indent", probe_options.json_indent, "JSON indentation level"); + probe->add_option("files", probe_files, "Files to merge into a single stackfile")->required(); + // Run Command: engine::RunOptions run_options{}; std::vector run_files{}; @@ -195,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 17345987a..910c1a53c 100644 --- a/engine/src/main_commands.hpp +++ b/engine/src/main_commands.hpp @@ -59,6 +59,25 @@ struct DumpOptions { int dump(const DumpOptions& opt, const std::vector& filepaths); +struct ProbeOptions { + cloe::StackOptions stack_options; + cloe::LuaOptions lua_options; + + 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); + struct RunOptions { cloe::StackOptions stack_options; cloe::LuaOptions lua_options; @@ -126,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 new file mode 100644 index 000000000..03ee86477 --- /dev/null +++ b/engine/src/main_probe.cpp @@ -0,0 +1,52 @@ +/* + * 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 + */ + +#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) { + 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 ce7068e3d..0097f6a4c 100644 --- a/engine/src/main_run.cpp +++ b/engine/src/main_run.cpp @@ -16,156 +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: - cloe::Stack stack = cloe::new_stack(opt.stack_options); - sol::state lua = cloe::new_lua(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, 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, 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), std::move(lua), 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) << std::endl; - - 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; - 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 << std::endl; // 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/main_shell.cpp b/engine/src/main_shell.cpp index 732f2cbe4..09776d397 100644 --- a/engine/src/main_shell.cpp +++ b/engine/src/main_shell.cpp @@ -39,7 +39,7 @@ void print_error(std::ostream& os, const S& chunk) { os << sol::to_string(chunk.status()) << " error: " << err.what() << std::endl; } -bool evaluate(sol::state& lua, std::ostream& os, const char* buf) { +bool evaluate(sol::state_view lua, std::ostream& os, const char* buf) { try { auto result = lua.safe_script(buf, sol::script_pass_on_error); if (!result.valid()) { @@ -53,7 +53,7 @@ bool evaluate(sol::state& lua, std::ostream& os, const char* buf) { return true; } -int noninteractive_shell(sol::state& lua, std::ostream& os, const std::vector& actions, +int noninteractive_shell(sol::state_view lua, std::ostream& os, const std::vector& actions, bool ignore_errors) { int errors = 0; for (const auto& action : actions) { @@ -68,7 +68,7 @@ int noninteractive_shell(sol::state& lua, std::ostream& os, const std::vector& actions, +void interactive_shell(sol::state_view lua, std::ostream& os, const std::vector& actions, bool ignore_errors) { constexpr auto PROMPT = "> "; constexpr auto PROMPT_CONTINUE = ">> "; @@ -165,7 +165,10 @@ int shell(const ShellOptions& opt, const std::vector& filepaths) { cloe::Stack stack = cloe::new_stack(stack_opt); auto lopt = opt.lua_options; lopt.auto_require_cloe = true; - sol::state lua = cloe::new_lua(lopt, stack); + + sol::state lua_state; + sol::state_view lua_view(lua_state.lua_state()); + cloe::setup_lua(lua_view, lopt, stack); // Collect input files and strings to execute std::vector actions{}; @@ -178,12 +181,12 @@ int shell(const ShellOptions& opt, const std::vector& filepaths) { // Determine whether we should be interactive or not bool interactive = opt.interactive ? *opt.interactive : opt.commands.empty() && filepaths.empty(); if (!interactive) { - auto errors = noninteractive_shell(lua, *opt.error, actions, opt.ignore_errors); + auto errors = noninteractive_shell(lua_view, *opt.error, actions, opt.ignore_errors); if (errors != 0) { return EXIT_FAILURE; } } else { - interactive_shell(lua, *opt.output, actions, opt.ignore_errors); + interactive_shell(lua_view, *opt.output, actions, opt.ignore_errors); } return EXIT_SUCCESS; } diff --git a/engine/src/server.cpp b/engine/src/server.cpp index 2699cafe6..48b1ec8f7 100644 --- a/engine/src/server.cpp +++ b/engine/src/server.cpp @@ -173,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 9dc0ed76d..3cfc0cccf 100644 --- a/engine/src/server.hpp +++ b/engine/src/server.hpp @@ -120,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. * 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 4ad51082b..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,1428 +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())); - } - - ctx.progress.init_end(); - ctx.server->refresh_buffer_start_stream(); - logger()->info("Simulation initialization complete."); - 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(); - - { - // Bind lua state_view to databroker - 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."); - } - } - } - - // 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&& lua, const std::string& uuid) +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."); @@ -1529,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()); } @@ -1550,12 +205,14 @@ SimulationResult Simulation::run() { ctx.commander->set_enabled(config_.engine.security_enable_commands); // 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; } @@ -1565,40 +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(); + reset_abort_handler(); - // TODO(ben): Preserve NoStart outcome. - if (ctx.outcome) { - r.outcome = *ctx.outcome; - } else { - r.outcome = SimulationOutcome::Aborted; - } - 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) { - r.signals = dump_signals(*ctx.db); + 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; +} + +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()); } - abort_fn_ = nullptr; - 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; @@ -1607,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; @@ -1636,7 +325,7 @@ bool Simulation::write_output_file(const std::filesystem::path& filepath, return false; } logger()->debug("Writing file: {}", native); - ofs << j.dump(2) << std::endl; + ofs << j.dump(2) << "\n"; return true; } diff --git a/engine/src/simulation.hpp b/engine/src/simulation.hpp index c83abcada..f661eb2b6 100644 --- a/engine/src/simulation.hpp +++ b/engine/src/simulation.hpp @@ -22,97 +22,28 @@ #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(cloe::Stack&& config, sol::state&& lua, const std::string& uuid); + 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. */ @@ -155,11 +93,50 @@ 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 lua_; + sol::state_view lua_; cloe::Logger logger_; std::string uuid_; + std::optional output_dir_; std::function abort_fn_; // Options: 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 cb2fd73b2..360cc5feb 100644 --- a/engine/src/simulation_context.hpp +++ b/engine/src/simulation_context.hpp @@ -22,210 +22,130 @@ #pragma once -#include // for uint64_t -#include // for function<> -#include // for map<> -#include // for unique_ptr<>, shared_ptr<> -#include // for optional<> -#include // for string -#include // for vector<> - -#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 "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 // for function<> +#include // for map<> +#include // for unique_ptr<>, shared_ptr<> +#include // for optional<> +#include // for string +#include // for vector<> + +#include // for state_view + +#include // for Simulator, Controller, Registrar, Vehicle, Duration +#include // for DurationTimer + +#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. + * 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. + * + * All input to and output from the simulation is via this struct. */ -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. -}; - -// 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"}, -})) -// clang-format on - -namespace events { +struct SimulationContext { + SimulationContext(cloe::Stack conf, sol::state_view l); -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") + // 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. + // -} // namespace events + cloe::Stack config; ///< Input configuration. + std::string uuid{}; ///< UUID to use for simulation. -/** - * SimulationContext represents the entire context of a running simulation. - * - * 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. - */ -struct SimulationContext { - SimulationContext(sol::state_view&& l) : lua(l) {} + /// 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; - std::string uuid{}; - bool report_progress{false}; + // 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; @@ -237,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/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/project.lua b/tests/project.lua index bdd01f352..ce930e833 100644 --- a/tests/project.lua +++ b/tests/project.lua @@ -29,7 +29,7 @@ local m = {} --- @return nil function m.init_report(...) local results = {} - local file = api.state.current_script_file + local file = api.get_script_file() if file then results["source"] = cloe.fs.realpath(file) end @@ -38,7 +38,7 @@ function m.init_report(...) results = luax.tbl_extend("force", results, tbl) end - api.state.report.metadata = results + api.get_report().metadata = results end --- Apply a stackfile, setting version to "4". diff --git a/tests/test_lua14_speedometer_signals.lua b/tests/test_lua14_speedometer_signals.lua new file mode 100644 index 000000000..69e8f8dbd --- /dev/null +++ b/tests/test_lua14_speedometer_signals.lua @@ -0,0 +1,7 @@ +local cloe = require("cloe") + +cloe.load_stackfile("config_minimator_multi_agent_infinite.json") + +local Sig = { + Speedometer = "third_speed/kmph" +} diff --git a/tests/test_lua15_acc_record.lua b/tests/test_lua15_acc_record.lua new file mode 100644 index 000000000..a7452f5b1 --- /dev/null +++ b/tests/test_lua15_acc_record.lua @@ -0,0 +1,42 @@ +local cloe = require("cloe") +local events, actions = cloe.events, cloe.actions + +cloe.load_stackfile("config_nop_smoketest.json") + +local Sig = { + VehAcc = "vehicles.default.basic.acc" +} +cloe.require_signals_enum(Sig) +cloe.record_signals(Sig) +cloe.record_signals( { + ["acc_config.limit_acceleration"] = function() + return cloe.signal(Sig.VehAcc).limit_acceleration + end, + ["acc_config.limit_deceleration"] = function() + return cloe.signal(Sig.VehAcc).limit_deceleration + end, +}) + +-- Run a simple test. +cloe.schedule_test({ + id = "20b741ee-ef82-4638-bd61-87a3fb4221d2", + on = events.start(), + terminate = false, + run = function(z, sync) + z:wait_duration("1s") + z:stop() + end, +}) + +-- Check recording. +cloe.schedule_test { + id = "a0065f68-2e1f-436c-8b17-fa19a630509c", + on = events.stop(), + run = function(z, sync) + -- Inspect the recording in the report: + local api = require("cloe-engine") + local report = api.get_report() + + z:assert(report.signals ~= nil, "report.signals should not be nil") + end +}