diff --git a/engine/src/simulation.cpp b/engine/src/simulation.cpp index b0619d196..272e156d7 100644 --- a/engine/src/simulation.cpp +++ b/engine/src/simulation.cpp @@ -478,19 +478,21 @@ StateId SimulationMachine::Connect::impl(SimulationContext& ctx) { */ 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); } - std::shared_ptr from; - if (c.from) { - if (v.has(*c.from)) { - from = v.get(*c.from); - } else { + // 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(); @@ -578,12 +580,17 @@ StateId SimulationMachine::Connect::impl(SimulationContext& ctx) { // 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); - throw cloe::ModelError{ - "cannot configure component '{}': cannot resolve dependency '{}'", - kv.first, - *kv.second.from, - }; + 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, + }; + } } } } diff --git a/engine/src/stack.cpp b/engine/src/stack.cpp index 5f200b897..7fefd0d14 100644 --- a/engine/src/stack.cpp +++ b/engine/src/stack.cpp @@ -119,7 +119,11 @@ ControllerSchema::ControllerSchema() { ComponentSchema::ComponentSchema() { this->set_transform_schema([](fable::schema::Struct&& s) -> fable::schema::Box { s.set_property("name", id_prototype("globally unique identifier for component")); - s.set_property("from", make_prototype("component input for binding")); + s.set_property("from", + fable::schema::Variant{ + make_prototype("component input for binding"), + make_prototype>("component inputs for binding"), + }); return s; }); } diff --git a/engine/src/stack.hpp b/engine/src/stack.hpp index 836b49646..d214c8c54 100644 --- a/engine/src/stack.hpp +++ b/engine/src/stack.hpp @@ -39,6 +39,7 @@ #include // for SimulatorFactory #include // for Source #include // for Command +#include // for CustomDeserializer #include // for Factory #include "plugin.hpp" // for Plugin @@ -616,7 +617,7 @@ struct FromSimulator : public Confable { struct ComponentConf : public Confable { const std::string binding; boost::optional name; - boost::optional from; + std::vector from; std::shared_ptr factory; Conf args; @@ -626,13 +627,28 @@ struct ComponentConf : public Confable { public: // Confable Overrides CONFABLE_SCHEMA(ComponentConf) { + // clang-format off using namespace schema; // NOLINT(build/namespaces) return Struct{ {"binding", make_const_str(binding, "name of binding").require()}, {"name", make_schema(&name, id_prototype(), "globally unique identifier for component")}, - {"from", make_schema(&from, "component input for binding")}, + {"from", Variant{ + CustomDeserializer( + make_prototype("component input for binding"), + [this](CustomDeserializer*, const Conf& c) { + this->from.push_back(c.get()); + } + ), + CustomDeserializer( + make_prototype>("component inputs for binding"), + [this](CustomDeserializer*, const Conf& c) { + this->from = c.get>(); + } + ), + }}, {"args", make_schema(&args, factory->schema(), "factory-specific args")}, }; + // clang-format on } }; diff --git a/engine/src/stack_test.cpp b/engine/src/stack_test.cpp index 819843735..cb4bc1391 100644 --- a/engine/src/stack_test.cpp +++ b/engine/src/stack_test.cpp @@ -26,6 +26,7 @@ #include #include // for DEFINE_COMPONENT_FACTORY +#include // for EgoSensor #include // for ObjectSensor #include // for Json #include // for assert_from_conf @@ -35,11 +36,9 @@ using namespace cloe; // NOLINT(build/namespaces) TEST(cloe_stack, serialization_of_empty_stack) { Stack s; - fable::assert_from_conf(s, R"( - { - "version": "4" - } - )"); + fable::assert_from_conf(s, R"({ + "version": "4" + })"); Json expect = R"({ "engine": { @@ -120,19 +119,17 @@ TEST(cloe_stack, serialization_of_empty_stack) { TEST(cloe_stack, serialization_with_logging) { Stack s; - assert_from_conf(s, R"( - { - "version": "4", - "defaults": { - "simulators": [ - { "binding": "vtd", "args": { "label_vehicle": "symbol" } } - ] - }, - "logging": [ - { "name": "*", "level": "info" } + assert_from_conf(s, R"({ + "version": "4", + "defaults": { + "simulators": [ + { "binding": "vtd", "args": { "label_vehicle": "symbol" } } ] - } - )"); + }, + "logging": [ + { "name": "*", "level": "info" } + ] + })"); Json expect = R"({ "engine": { @@ -249,25 +246,96 @@ DEFINE_COMPONENT_FACTORY(DummySensorFactory, DummySensorConf, "dummy_object_sens DEFINE_COMPONENT_FACTORY_MAKE(DummySensorFactory, DummySensor, cloe::ObjectSensor) TEST(cloe_stack, deserialization_of_component) { - { - std::shared_ptr cf = std::make_shared(); - ComponentConf cc = ComponentConf("dummy_sensor", cf); - // Create a sensor component from the given configuration. - fable::assert_from_conf(cc, R"( - { - "binding": "dummy_sensor", - "name": "my_dummy_sensor", - "from": "some_obj_sensor", - "args" : { - "freq" : 9 - } - } - )"); - // In production code, "some_obj_sensor" would be fetched from a list of all - // available sensors. Skip this step here. - std::shared_ptr from = std::shared_ptr(); - auto d = std::dynamic_pointer_cast( - std::shared_ptr(std::move(cf->make(cc.args, from)))); - ASSERT_EQ(d->get_freq(), 9); + // Create a sensor component from the given configuration. + std::shared_ptr cf = std::make_shared(); + ComponentConf cc = ComponentConf("dummy_sensor", cf); + fable::assert_from_conf(cc, R"({ + "binding": "dummy_sensor", + "name": "my_dummy_sensor", + "from": "some_obj_sensor", + "args" : { + "freq" : 9 + } + })"); + + // In production code, "some_obj_sensor" would be fetched from a list of all + // available sensors. Skip this step here. + std::vector> from = {std::shared_ptr()}; + auto d = std::dynamic_pointer_cast( + std::shared_ptr(std::move(cf->make(cc.args, from)))); + ASSERT_EQ(d->get_freq(), 9); +} + +class FusionSensor : public NopObjectSensor { + public: + FusionSensor(const std::string& name, const DummySensorConf& conf, + std::vector> obj_sensors, + std::shared_ptr ego_sensor) + : NopObjectSensor(), config_(conf), obj_sensors_(obj_sensors), ego_sensor_(ego_sensor) {} + + virtual ~FusionSensor() noexcept = default; + + uint64_t get_freq() const { return config_.freq; } + + private: + DummySensorConf config_; + std::vector> obj_sensors_; + std::shared_ptr ego_sensor_; +}; + +DEFINE_COMPONENT_FACTORY(FusionSensorFactory, DummySensorConf, "fusion_object_sensor", + "test component config") + +std::unique_ptr<::cloe::Component> FusionSensorFactory::make( + const ::cloe::Conf& c, std::vector> comp_src) const { + decltype(config_) conf{config_}; + if (!c->is_null()) { + conf.from_conf(c); } + std::vector> obj_sensors; + std::vector> ego_sensors; + for (auto& comp : comp_src) { + auto obj_s = std::dynamic_pointer_cast(comp); + if (obj_s != nullptr) { + obj_sensors.push_back(obj_s); + continue; + } + auto ego_s = std::dynamic_pointer_cast(comp); + if (ego_s != nullptr) { + ego_sensors.push_back(ego_s); + continue; + } + throw Error("FusionSensorFactory: Source component type not supported: from {}", comp->name()); + } + if (ego_sensors.size() != 1) { + throw Error("FusionSensorFactory: {}: Require exactly one ego sensor.", this->name()); + } + return std::make_unique(this->name(), conf, obj_sensors, ego_sensors.front()); +} + +TEST(cloe_stack, deserialization_of_fusion_component) { + // Create a sensor component from the given configuration. + std::shared_ptr cf = std::make_shared(); + ComponentConf cc = ComponentConf("fusion_object_sensor", cf); + fable::assert_from_conf(cc, R"({ + "binding": "fusion_object_sensor", + "name": "my_fusion_sensor", + "from": [ + "ego_sensor0", + "obj_sensor1", + "obj_sensor2" + ], + "args" : { + "freq" : 77 + } + })"); + + // In production code, a component list containing "ego_sensor0", ... would + // be generated. Skip this step here. + std::vector> sensor_subset = { + std::make_shared(), std::make_shared(), + std::make_shared()}; + auto f = std::dynamic_pointer_cast( + std::shared_ptr(std::move(cf->make(cc.args, sensor_subset)))); + ASSERT_EQ(f->get_freq(), 77); } diff --git a/runtime/include/cloe/component.hpp b/runtime/include/cloe/component.hpp index 2b872c982..86c5c3700 100644 --- a/runtime/include/cloe/component.hpp +++ b/runtime/include/cloe/component.hpp @@ -53,40 +53,42 @@ * The DEFINE_COMPONENT_FACTORY_MAKE macro can also be used to use the default * implementation. */ -#define DEFINE_COMPONENT_FACTORY(xFactoryType, xConfigType, xName, xDescription) \ - class xFactoryType : public ::cloe::ComponentFactory { \ - public: \ - xFactoryType() : ComponentFactory(xName, xDescription) {} \ - std::unique_ptr<::cloe::ComponentFactory> clone() const override { \ - return std::make_unique::type>(*this); \ - } \ - std::unique_ptr<::cloe::Component> make(const ::cloe::Conf&, \ - std::shared_ptr<::cloe::Component>) const override; \ - \ - protected: \ - ::cloe::Schema schema_impl() override { return config_.schema(); } \ - \ - private: \ - xConfigType config_; \ +#define DEFINE_COMPONENT_FACTORY(xFactoryType, xConfigType, xName, xDescription) \ + class xFactoryType : public ::cloe::ComponentFactory { \ + public: \ + xFactoryType() : ComponentFactory(xName, xDescription) {} \ + std::unique_ptr<::cloe::ComponentFactory> clone() const override { \ + return std::make_unique::type>(*this); \ + } \ + std::unique_ptr<::cloe::Component> make( \ + const ::cloe::Conf&, std::vector>) const override; \ + \ + protected: \ + ::cloe::Schema schema_impl() override { return config_.schema(); } \ + \ + private: \ + xConfigType config_; \ }; /** - * This macro defines the xFactoryType::make method. + * This macro defines the xFactoryType::make method for components with exactly + * one input component. * * For this to work, the xComponentType must have a constructor with the * following signature (see DEFINE_COMPONENT_FACTORY macro): * * xComponentType(const std::string&, const xConfigType&, std::shared_ptr) */ -#define DEFINE_COMPONENT_FACTORY_MAKE(xFactoryType, xComponentType, xInputType) \ - std::unique_ptr<::cloe::Component> xFactoryType::make( \ - const ::cloe::Conf& c, std::shared_ptr<::cloe::Component> comp) const { \ - decltype(config_) conf{config_}; \ - if (!c->is_null()) { \ - conf.from_conf(c); \ - } \ - return std::make_unique( \ - this->name(), conf, std::dynamic_pointer_cast(std::move(comp))); \ +#define DEFINE_COMPONENT_FACTORY_MAKE(xFactoryType, xComponentType, xInputType) \ + std::unique_ptr<::cloe::Component> xFactoryType::make( \ + const ::cloe::Conf& c, std::vector> comp) const { \ + decltype(config_) conf{config_}; \ + assert(comp.size() == 1); \ + if (!c->is_null()) { \ + conf.from_conf(c); \ + } \ + return std::make_unique( \ + this->name(), conf, std::dynamic_pointer_cast(std::move(comp.front()))); \ } namespace cloe { @@ -236,7 +238,8 @@ class ComponentFactory : public ModelFactory { * * - This method may throw Error. */ - virtual std::unique_ptr make(const Conf& c, std::shared_ptr) const = 0; + virtual std::unique_ptr make(const Conf& c, + std::vector>) const = 0; }; } // namespace cloe