diff --git a/config_utilities/CMakeLists.txt b/config_utilities/CMakeLists.txt index eac9a93..fd262a4 100644 --- a/config_utilities/CMakeLists.txt +++ b/config_utilities/CMakeLists.txt @@ -27,6 +27,7 @@ add_library( ${PROJECT_NAME} src/asl_formatter.cpp src/conversions.cpp + src/dynamic_config.cpp src/external_registry.cpp src/factory.cpp src/formatter.cpp diff --git a/config_utilities/demos/demo_config.cpp b/config_utilities/demos/demo_config.cpp index 22b3437..4c85ad2 100644 --- a/config_utilities/demos/demo_config.cpp +++ b/config_utilities/demos/demo_config.cpp @@ -79,6 +79,7 @@ struct MyConfig { std::string test3 = "A really really really ridiculously long string that will also be wrapped."; std::vector vec = {1, 2, 3}; std::map map = {{"a", 1}, {"b", 2}, {"c", 3}}; + // std::map map; Eigen::Matrix mat = Eigen::Matrix::Identity(); enum class MyEnum { kA, kB, kC } my_enum = MyEnum::kA; enum MyStrangeEnum : int { kX = 0, kY = 42, kZ = -7 } my_strange_enum = MyStrangeEnum::kX; @@ -128,7 +129,7 @@ void declare_config(MyConfig& config) { // Specify all checks to denote a valid configuration. Checks are specified as param, value, and param name to be // displayed. Implemented checks are GT (>), GE (>=), LT (<), LE (<=), EQ (==), NE (!=). config::check(config.i, config::GT, 0, "i"); - config::check(config.i, config::LT, -2, "i"); + // config::check(config.i, config::LT, -2, "i"); // Double sided checks can be invoked as in range. const bool lower_inclusive = true; @@ -160,78 +161,362 @@ void declare_config(SubSubConfig& config) { field(config.color, "color"); field(config.size, "size"); check(config.size, CheckMode::EQ, size_t(5), "size"); + + // std::cout << "Namespace in declare_config " << config::current_namespace(); } } // namespace demo -int main(int argc, char** argv) { - if (argc < 2) { - std::cerr << "invalid usage! expected resource directory as argument" << std::endl; - return 1; + +struct SimpleConfig { + float f = 0.123; + std::string s = "test"; + +}; + +void declare_config(SimpleConfig& config) { + using namespace config; + name("SimpleConfig"); + field(config.f, "f"); + field(config.s, "s"); +} + + +#include "config_utilities/internal/visitor.h" +#include +#include +#include + +#include +#include +#include + + + +//register this to the interface +class ParameterEvenHandler { + + public: + template + ParameterEvenHandler(ConfigT* config, const std::string& key) + : key_(key), + meta_data_(config::internal::Visitor::getValues(*config)), //get meta data from config - we want this for field information etc... TODO: need to do checks + expected_type_index_(typeid(ConfigT)), + expected_type_(config::internal::typeName()) { + + std::cout << "Registereing handler with key " << key << " and type " << expected_type_ << std::endl; + + update_ = [=](const YAML::Node& new_yaml) -> bool { + if(config) { + const auto old_yaml = config::internal::Visitor::getValues(*config).data; + + std::cout << "old yaml " << old_yaml << std::endl; + + if (config::internal::isEqual(old_yaml, new_yaml)) { + std::cout << "Is equal" << std::endl; + return false; + } + + //TODO: alert which field has changed!? + + // std::lock_guard lock(mutex_); + // TODO(lschmid): We should check if the values are valid before setting them. Ideally field by field... + ConfigT new_config; + // std::cout << "new config " << new_config << std::endl; + config::internal::Visitor::setValues(new_config, new_yaml); + //update config + *config = new_config; + //update hooks!! + return true; + } + else { + return false; + } + }; + } + + template + bool attemptUpdate(const ConfigT& config) { + //TODO: check incoming config matches expected type (i.e they are the same!) + if (expected_type_index_ != typeid(ConfigT)) { + //TODO: throw exception + std::cout << "Different types " << expected_type_index_.name() << " is expected vs " << typeid(ConfigT).name() << std::endl; + return false; + } + + if (!config::isValid(config, config::Settings().print_warnings)) { + std::cout << "incoming config with key " << key_ << " is not valid" << std::endl; + return false; + } + + //TODO: (recursive lock)? + const auto new_yaml = config::internal::Visitor::getValues(config).data; + std::cout << "new yaml " << new_yaml << std::endl; + return this->update_(new_yaml); + } + + //TODO: getters + std::string getKey() const { return key_; } + + private: + const std::string key_; //top level name + const config::internal::MetaData meta_data_; //information about the structure of the config + const std::type_index expected_type_index_; + const std::string expected_type_; + //function that does the update + std::function update_; +}; + + + +//base class to external interface to monitor for updates to configs +//may be tcp/ros2 etc... +//what to override...? +//I think its easiest to just let the user implement this: e.g in ROS there is no external interface to call +//as the ParameterEventHandler's get triggered by the executor +//TCP or other connections can be added at (derived) construction and the user can start a thread here (or we can have a derived Interface) +//that handles the thread creation. A more defined class interface can be implemeneted at this point. +class ReconfigureInterface { + + public: + ReconfigureInterface() = default; + virtual ~ReconfigureInterface() = default; + + + void monitorParam(std::unique_ptr param_handler) { + //TODO: check that key in param handler is unique + const auto key = param_handler->getKey(); + parameters_.insert({key, std::move(param_handler)}); + } + + protected: + //TODO: should not return bool but a more sophsticated 'MetaData' style structure informing the user + //what went wrong (or right!!) + template + bool set(const ConfigT& config, const std::string& key) { + if(parameters_.find(key) != parameters_.end()) { + std::unique_ptr& handler = parameters_.at(key); + return handler->attemptUpdate(config); + } + std::cout << key << " not found"; + return false; + } + + private: + std::map> parameters_; //! All the params being monitored and ones that an be updated + +}; + +// template +class ManualInterface : public ReconfigureInterface { + +public: + // dummy setter for demo + template + bool set(const ConfigT& config, const std::string& key) { + return ReconfigureInterface::set(config, key); } - const std::string my_root_path = std::string(argv[1]) + "/"; - // GLobal settings can be set at runtime to change the behavior and presentation of configs. - config::Settings().inline_subconfig_field_names = true; +}; + +//this is the client? +class DynamicReconfigureServer { + +public: + static DynamicReconfigureServer& instance() { + static DynamicReconfigureServer instance; + return instance; + } - // ===================================== Checking whether a struct is a config ===================================== - std::cout << "\n\n----- Checking whether a struct is a config -----\n\n" << std::endl; - - // Use isConfig to check whether an object has been declared a config. - std::cout << "MyConfig is a config: " << std::boolalpha << config::isConfig() << std::endl; - std::cout << "NotAConfig is a config: " << config::isConfig() << std::endl; - - // ====================================== Checking whether a config is valid ====================================== - std::cout << "\n\n----- Checking whether a config is valid \n\n" << std::endl; - - // Create a valid and an invalid config. - demo::MyConfig config, invalid_config; - invalid_config.i = -1; - invalid_config.distance = 123; - invalid_config.s.clear(); - invalid_config.sub_config.f = -1.f; - invalid_config.sub_config.sub_sub_config.size = 0; - - // Print whether they are valid. Since we invalidated all fields of 'invalid_config' a comprehensive summary of all - // issues is printed. - constexpr bool print_warnings = true; - const bool config_is_valid = config::isValid(config, print_warnings); - std::cout << "'config' is valid: " << config_is_valid << std::endl; - - const bool invalid_config_is_valid = config::isValid(invalid_config, print_warnings); - std::cout << "'invalid_config' is valid: " << invalid_config_is_valid << std::endl; - - // Check valid will enforce that the config is valid, throwing an error and always printing the warnings if not. - try { - config::checkValid(invalid_config); - } catch (std::runtime_error& e) { - std::cout << "Exception thrown: " << e.what() << std::endl; +public: + //interfacce must be derived from ReconfigureInterface + template + static void registerInterface(std::shared_ptr interface) { + //TODO: static assert to enforce type + DynamicReconfigureServer& server = DynamicReconfigureServer::instance(); + + const std::type_index derived_type_index(typeid(Interface)); + std::cout << "Registered interface " << derived_type_index.name() << std::endl; + //TODO: check that interface is unique? Do we want multiple intferfaces of the same type + //what about ROS2 nodes? we could have many nodes in the same program (maybe?) and each one would be its own interface of the same type!! + //if this is the case then we cannot use the Interface type as the key. In this case, we would need to pass an instnace of the interface + //along with the config in registerConfig, which would totally be okay too! + + //Jesse but later: the more I think about this more I think a Singleton Interface is okay, but the user may need do things like add nodes + //to the interface externally + server.registered_interface_.insert({derived_type_index, interface}); + + + } + + template + //key here? or when its registered with declare_config? + static void registerConfig(ConfigT* config, const std::string& key) { + //get interface + DynamicReconfigureServer& server = DynamicReconfigureServer::instance(); + //TODO: check registered + std::shared_ptr interface = server.registered_interface_.at(typeid(Interface)); + assert(interface != nullptr); + + std::cout << "Loaded inteface " << typeid(Interface).name() << std::endl; + //make new paramter interface to update this config + auto handler = std::make_unique(config, key); + //add to interface + interface->monitorParam(std::move(handler)); } - // ======================================== Read the config from file ======================================== - std::cout << "\n\n----- Reading the config from file -----\n\n" << std::endl; - // Read the config from file. - config = config::fromYamlFile(my_root_path + "params.yaml"); - std::cout << "Read values i='" << config.i << "', s='" << config.s << "', distance='" << config.distance - << "' from file." << std::endl; - std::cout << "Enum 'config.my_enum' is now B: " << (config.my_enum == demo::MyConfig::MyEnum::kB) << std::endl; +private: + DynamicReconfigureServer() = default; + //map holding the interfaces and their derived type index so that when a config + //is registered we can get the right interface + //this relies on the type index being the same, maybe worth doing some actual casting to check + std::map> registered_interface_; - // Any errors parsing configs will print verbose warnings if desired and use the default values. - invalid_config = config::fromYamlFile(my_root_path + "invalid_params.yaml"); +}; - // ======================================== Printing configs to string ======================================== - std::cout << "\n\n----- Printing configs to string -----\n\n" << std::endl; +int main(int argc, char** argv) { + if (argc < 2) { + std::cerr << "invalid usage! expected resource directory as argument" << std::endl; + return 1; + } - // Easier automatic printing of all configs with unit and additional information can be done using the toString(): - const std::string config_as_string = config::toString(config); - std::cout << config_as_string << std::endl; + const std::string my_root_path = std::string(argv[1]) + "/"; - // Inclunding "printing.h" also implements the ostream operator for decared config types. The above is thus equivalent - // to: - std::cout << config << std::endl; + // GLobal settings can be set at runtime to change the behavior and presentation of configs. + config::Settings().inline_subconfig_field_names = true; + config::Settings().print_warnings = true; + + SimpleConfig config; + std::cout << config::toString(config) << std::endl; + auto interface = std::make_shared(); + DynamicReconfigureServer::registerInterface(interface); + + //I am unclear about this key. Does it need to be namespaced according to anything else in the system? + //Ideally not as its just some key that refers to an INSTANCE of a config and not the Config type (like config::name()) + //some bookeeping could be done here. + DynamicReconfigureServer::registerConfig(&config, "my_config"); + + SimpleConfig new_config; + interface->set(new_config, "my_config"); + + new_config.f = 10.0; + interface->set(new_config, "my_config"); + + std::cout << config::toString(config) << std::endl; + + interface->set(new_config, "non_existant_config"); + + //should not work as different config types (between what is registered as "my_config" and the incoming config) + demo::SubConfig sub_config; + interface->set(sub_config, "my_config"); + + + /** + * @brief Ideas + * sub config modification via namespacing + * a config is registered with a name, e.g "my_config" and (using the MetaData) we make namespacing for each of the fields + * e.g my_config/f, my_config/s + * + * The interface gets a new function that allows sub-updating e.g. Interface::setField(T, namespace) + * e.g interface->setField(10.1, "my_config/f") + * + * since we have access to my_config and we know how to access the YAML field 'f' associated with that config we can just update the field in the yaml. + * In this way we dont need to check the type of T against the type of SimpleConfig::f, we just need to check it against the type in the YAML. + * Then we can update the new yaml, construt a new SimpleConfig out of it with just the new value for f and update the config as usual. + * + * Of course you can update the whole config by using the "base"/(root?) namespace, which in this example would be "my_config". This is also type checked as we store + * the type of SimpleConfig (as is currently implemented)/ + * + * This would get around the client side needing to know how to build the WHOLE Config type, you just need to know the namespace and the type of the field (which I guess you + * need to do anyway?). The client side wouldn't need to parse around the enture YAML - there might be some complecxity in nested data types however. + * I think the first version would to work on types that are directly convertable to YAML types (ie. primitives, vectors, maps) or other Config types, + * since we know how to convert these. + * + * + */ + + //update intermediate value + //TODO: (maybe? this does not work as config.f is not a config) + //we could make this work if we wanted this type of interface + // DynamicReconfigureServer::registerConfig>(&config.f, "my_config_f"); + + + + + // config::internal::MetaData md = config::internal::Visitor::getValues(config); + // std::cout << "Name: " << md.name << "\n"; + // std::cout << "field_name: " << md.field_name << "\n"; + // if (md.map_config_key) std::cout << "map_config_key: " << *md.map_config_key; + // std::cout << "Data: " << md.data << "\n"; + + // std::cout << "Field info: \n"; + // for(const auto fi : md.field_infos) { + // std::cout << "\t name: " << fi.name << "\n"; + // std::cout << "\t unit: " << fi.unit << "\n"; + // std::cout << "\t value: " << fi.value << "\n"; + // } + // std::cout << std::endl; + + // // ===================================== Checking whether a struct is a config ===================================== + // std::cout << "\n\n----- Checking whether a struct is a config -----\n\n" << std::endl; + + // // Use isConfig to check whether an object has been declared a config. + // std::cout << "MyConfig is a config: " << std::boolalpha << config::isConfig() << std::endl; + // std::cout << "NotAConfig is a config: " << config::isConfig() << std::endl; + + // // ====================================== Checking whether a config is valid ====================================== + // std::cout << "\n\n----- Checking whether a config is valid \n\n" << std::endl; + + // // Create a valid and an invalid config. + // demo::MyConfig config, invalid_config; + // invalid_config.i = -1; + // invalid_config.distance = 123; + // invalid_config.s.clear(); + // invalid_config.sub_config.f = -1.f; + // invalid_config.sub_config.sub_sub_config.size = 0; + + // // Print whether they are valid. Since we invalidated all fields of 'invalid_config' a comprehensive summary of all + // // issues is printed. + // constexpr bool print_warnings = true; + // const bool config_is_valid = config::isValid(config, print_warnings); + // std::cout << "'config' is valid: " << config_is_valid << std::endl; + + // const bool invalid_config_is_valid = config::isValid(invalid_config, print_warnings); + // std::cout << "'invalid_config' is valid: " << invalid_config_is_valid << std::endl; + + // // Check valid will enforce that the config is valid, throwing an error and always printing the warnings if not. + // try { + // config::checkValid(invalid_config); + // } catch (std::runtime_error& e) { + // std::cout << "Exception thrown: " << e.what() << std::endl; + // } + + // // ======================================== Read the config from file ======================================== + // std::cout << "\n\n----- Reading the config from file -----\n\n" << std::endl; + + // // Read the config from file. + // config = config::fromYamlFile(my_root_path + "params.yaml"); + + // std::cout << "Read values i='" << config.i << "', s='" << config.s << "', distance='" << config.distance + // << "' from file." << std::endl; + // std::cout << "Enum 'config.my_enum' is now B: " << (config.my_enum == demo::MyConfig::MyEnum::kB) << std::endl; + + // // Any errors parsing configs will print verbose warnings if desired and use the default values. + // invalid_config = config::fromYamlFile(my_root_path + "invalid_params.yaml"); + + // // ======================================== Printing configs to string ======================================== + // std::cout << "\n\n----- Printing configs to string -----\n\n" << std::endl; + + // // Easier automatic printing of all configs with unit and additional information can be done using the toString(): + // const std::string config_as_string = config::toString(config); + // std::cout << config_as_string << std::endl; + + // // Inclunding "printing.h" also implements the ostream operator for decared config types. The above is thus equivalent + // // to: + // std::cout << config << std::endl; return 0; } diff --git a/config_utilities/include/config_utilities/dynamic_config.h b/config_utilities/include/config_utilities/dynamic_config.h new file mode 100644 index 0000000..b91b8dc --- /dev/null +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -0,0 +1,284 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace config { + +/** + * @brief A server interface to manage all dynamic configs. + */ +struct DynamicConfigServer { + using Key = std::string; + + struct Hooks { + std::function onRegister; + std::function onDeregister; + std::function onUpdate; + + bool empty() const; + }; + + DynamicConfigServer() = default; + virtual ~DynamicConfigServer(); + DynamicConfigServer(const DynamicConfigServer&) = delete; + DynamicConfigServer(DynamicConfigServer&&) = default; + DynamicConfigServer& operator=(const DynamicConfigServer&) = delete; + DynamicConfigServer& operator=(DynamicConfigServer&&) = default; + + /** + * @brief Check if a dynamic config with the given key exists. + * @param key The unique key of the dynamic config. + * @return True if the dynamic config exists, false otherwise. + */ + bool hasConfig(const Key& key) const; + + /** + * @brief Get the keys of all registered dynamic configs. + */ + std::vector registeredConfigs() const; + + /** + * @brief Get the values of a dynamic config. + * @param key The unique key of the dynamic config. + */ + YAML::Node getValues(const Key& key) const; + + /** + * @brief Set the values of a dynamic config. + * @param key The unique key of the dynamic config. + * @param values The new values to set. + */ + void setValues(const Key& key, const YAML::Node& values) const; + + /** + * @brief Set the hooks for the dynamic config server. + */ + void setHooks(const Hooks& hooks); + + /** + * @brief Get the info of a dynamic config. + * @param key The unique key of the dynamic config. + */ + YAML::Node getInfo(const Key& key) const; + + private: + size_t hooks_id_ = 0; +}; + +namespace internal { + +/** + * @brief Name-based global registry for dynamic configurations. + */ +struct DynamicConfigRegistry { + using Key = DynamicConfigServer::Key; + + /** + * @brief Server-side interface to dynamic configs. + */ + struct ConfigInterface { + std::function getValues; + std::function setValues; + std::function getInfo; + }; + + // Singleton access. + static DynamicConfigRegistry& instance() { + static DynamicConfigRegistry instance; + return instance; + } + + /** + * @brief Check if a dynamic config with the given key is registered. + */ + bool hasKey(const Key& key) const; + + /** + * @brief Get the interface to a dynamic config with the given key. + * @param key The unique key of the dynamic config. + * @return The interface to the dynamic config, if it exists. + */ + std::optional getConfig(const Key& key) const; + + /** + * @brief Get all keys of the registered dynamic configs. + */ + std::vector keys() const; + + // Dynamic config registration and de-registration. + /** + * @brief Register a dynamic config with the given key. + * @param key The unique key of the dynamic config. + * @param interface The interface to the dynamic config. + * @return True if the registration was successful, false otherwise. + */ + bool registerConfig(const Key& key, const ConfigInterface& interface); + + /** + * @brief De-register a dynamic config with the given key. + * @param key The unique key of the dynamic config. + */ + void deregisterConfig(const Key& key); + + /** + * @brief Register hooks for a dynamic config server. + * @param hooks The hooks to register. + * @param hooks_id The id of the server adding the hooks. + * @return The new_id of the server registered hooks. + */ + size_t registerHooks(const DynamicConfigServer::Hooks& hooks, size_t hooks_id); + + /** + * @brief Deregister hooks for a dynamic config server. + */ + void deregisterHooks(size_t hooks_id); + + /** + * @brief Notify all hooks that a config was updated. + */ + void configUpdated(const Key& key, const YAML::Node& new_values); + + private: + DynamicConfigRegistry() = default; + + std::unordered_map configs_; + std::unordered_map hooks_; + size_t current_hooks_id_ = 0; +}; + +} // namespace internal + +/** + * @brief A wrapper class for for configs that can be dynamically changed. + * + * @tparam ConfigT The contained configuration type + */ +template +struct DynamicConfig { + /** + * @brief Construct a new Dynamic Config, wrapping a config_uilities config. + * @param name Unique name of the dynamic config. This identifier is used to access the config on the client side + * @param config The config to wrap. + */ + explicit DynamicConfig(const std::string& name, const ConfigT& config = {}) + : name_(name), config_(config::checkValid(config)) { + static_assert(isConfig(), + "ConfigT must be declared to be a config. Implement 'void declare_config(ConfigT&)'."); + is_registered_ = internal::DynamicConfigRegistry::instance().registerConfig( + name_, + {std::bind(&DynamicConfig::getValues, this), + std::bind(&DynamicConfig::setValues, this, std::placeholders::_1), + std::bind(&DynamicConfig::getInfo, this)}); + } + + ~DynamicConfig() { + if (is_registered_) { + internal::DynamicConfigRegistry::instance().deregisterConfig(name_); + } + } + + DynamicConfig(const DynamicConfig&) = delete; + DynamicConfig(DynamicConfig&&) = default; + DynamicConfig& operator=(const DynamicConfig&) = delete; + DynamicConfig& operator=(DynamicConfig&&) = default; + + /** + * @brief Get the underlying dynamic config. + * @note This returns a copy of the config, so changes to the returned config will not affect the dynamic config. + */ + ConfigT get() const { + std::lock_guard lock(mutex_); + return config_; + } + + /** + * @brief Set the underlying dynamic config. + */ + void set(const ConfigT& config) { + if (!config::isValid(config)) { + return; + } + if (!is_registered_) { + config_ = config; + return; + } + + std::lock_guard lock(mutex_); + const auto old_yaml = internal::Visitor::getValues(config_).data; + const auto new_yaml = internal::Visitor::getValues(config).data; + if (internal::isEqual(old_yaml, new_yaml)) { + return; + } + config_ = config; + internal::DynamicConfigRegistry::instance().configUpdated(name_, new_yaml); + } + + private: + const std::string name_; + ConfigT config_; + mutable std::mutex mutex_; + bool is_registered_; + + void setValues(const YAML::Node& values) { + std::lock_guard lock(mutex_); + // TODO(lschmid): We should check if the values are valid before setting them. Ideally field by field... + internal::Visitor::setValues(config_, values); + } + + YAML::Node getValues() const { + std::lock_guard lock(mutex_); + return internal::Visitor::getValues(config_).data; + } + + YAML::Node getInfo() const { + std::lock_guard lock(mutex_); + // TODO(lschmid): Add a visitor function to get the info of a config. + return {}; + } +}; + +} // namespace config diff --git a/config_utilities/include/config_utilities/settings.h b/config_utilities/include/config_utilities/settings.h index 4371843..afea8ed 100644 --- a/config_utilities/include/config_utilities/settings.h +++ b/config_utilities/include/config_utilities/settings.h @@ -69,6 +69,8 @@ struct Settings { // @brief If true integrate subconfig fields into the main config, if false print them separately. bool inline_subconfig_field_names = true; + bool print_warnings = false; + // @brief If true, store all validated configs for global printing. bool store_valid_configs = true; diff --git a/config_utilities/src/dynamic_config.cpp b/config_utilities/src/dynamic_config.cpp new file mode 100644 index 0000000..beead24 --- /dev/null +++ b/config_utilities/src/dynamic_config.cpp @@ -0,0 +1,159 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#include "config_utilities/dynamic_config.h" + +#include "config_utilities/internal/logger.h" + +namespace config { + +bool DynamicConfigServer::Hooks::empty() const { return !onRegister && !onDeregister; } + +bool DynamicConfigServer::hasConfig(const Key& key) const { + return internal::DynamicConfigRegistry::instance().hasKey(key); +} + +std::vector DynamicConfigServer::registeredConfigs() const { + return internal::DynamicConfigRegistry::instance().keys(); +} + +YAML::Node DynamicConfigServer::getValues(const Key& key) const { + const auto config = internal::DynamicConfigRegistry::instance().getConfig(key); + if (!config) { + return {}; + } + return config->getValues(); +} + +void DynamicConfigServer::setValues(const Key& key, const YAML::Node& values) const { + const auto config = internal::DynamicConfigRegistry::instance().getConfig(key); + if (!config) { + return; + } + config->setValues(values); +} + +YAML::Node DynamicConfigServer::getInfo(const Key& key) const { + const auto config = internal::DynamicConfigRegistry::instance().getConfig(key); + if (!config) { + return {}; + } + return config->getInfo(); +} + +void DynamicConfigServer::setHooks(const Hooks& hooks) { + if (hooks.empty()) { + internal::DynamicConfigRegistry::instance().deregisterHooks(hooks_id_); + return; + } + hooks_id_ = internal::DynamicConfigRegistry::instance().registerHooks(hooks, hooks_id_); +} + +DynamicConfigServer::~DynamicConfigServer() { internal::DynamicConfigRegistry::instance().deregisterHooks(hooks_id_); } + +namespace internal { + +bool DynamicConfigRegistry::hasKey(const Key& key) const { return configs_.count(key); } + +std::optional DynamicConfigRegistry::getConfig(const Key& key) const { + const auto it = configs_.find(key); + if (it == configs_.end()) { + return std::nullopt; + } + return it->second; +} + +std::vector DynamicConfigRegistry::keys() const { + std::vector keys; + keys.reserve(configs_.size()); + for (const auto& [key, _] : configs_) { + keys.push_back(key); + } + return keys; +} + +bool DynamicConfigRegistry::registerConfig(const Key& key, const ConfigInterface& interface) { + if (configs_.count(key)) { + Logger::logWarning("Cannot register dynamic config: key '" + key + "' already exists."); + return false; + } + configs_[key] = interface; + for (const auto& [hooks_id, hooks] : hooks_) { + if (hooks.onRegister) { + hooks.onRegister(key); + } + } + return true; +} + +void DynamicConfigRegistry::deregisterConfig(const Key& key) { + auto it = configs_.find(key); + if (it == configs_.end()) { + return; + } + configs_.erase(key); + for (const auto& [hooks_id, hooks] : hooks_) { + if (hooks.onDeregister) { + hooks.onDeregister(key); + } + } +} + +size_t DynamicConfigRegistry::registerHooks(const DynamicConfigServer::Hooks& hooks, size_t hooks_id) { + if (hooks.empty()) { + return hooks_id; + } + + if (hooks_id == 0) { + ++current_hooks_id_; + } + + hooks_[hooks_id] = hooks; + return hooks_id; +} + +void DynamicConfigRegistry::deregisterHooks(size_t hooks_id) { hooks_.erase(hooks_id); } + +void DynamicConfigRegistry::configUpdated(const Key& key, const YAML::Node& new_values) { + for (const auto& [hooks_id, hooks] : hooks_) { + if (hooks.onUpdate) { + hooks.onUpdate(key, new_values); + } + } +} + +} // namespace internal + +} // namespace config diff --git a/config_utilities/test/CMakeLists.txt b/config_utilities/test/CMakeLists.txt index 2f0061e..d1ed8a3 100644 --- a/config_utilities/test/CMakeLists.txt +++ b/config_utilities/test/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable( tests/config_arrays.cpp tests/config_maps.cpp tests/conversions.cpp + tests/dynamic_config.cpp tests/enums.cpp tests/external_registry.cpp tests/factory.cpp diff --git a/config_utilities/test/src/default_config.cpp b/config_utilities/test/src/default_config.cpp index 6639974..e95c335 100644 --- a/config_utilities/test/src/default_config.cpp +++ b/config_utilities/test/src/default_config.cpp @@ -85,7 +85,7 @@ void declare_config(DefaultConfig& config) { check(config.u8, CheckMode::LE, uint8_t(5), "u8"); check(config.s, CheckMode::EQ, std::string("test string"), "s"); check(config.b, CheckMode::NE, false, "b"); - checkCondition(config.vec.size() == 3, "param 'vec' must b of size '3'"); + checkCondition(config.vec.size() == 3, "param 'vec' must be of size '3'"); checkInRange(config.d, 0.0, 500.0, "d"); } @@ -116,7 +116,7 @@ YAML::Node DefaultConfig::modifiedValues() { YAML::Node data; data["i"] = 2; data["f"] = -1.f; - data["d"] = 3.14159; // intentionally avoid precision issues + data["d"] = 3.14159; // intentionally avoid precision issues data["b"] = false; data["u8"] = 255; data["s"] = "a different test string"; diff --git a/config_utilities/test/tests/asl_formatter.cpp b/config_utilities/test/tests/asl_formatter.cpp index 00b20f6..c54567e 100644 --- a/config_utilities/test/tests/asl_formatter.cpp +++ b/config_utilities/test/tests/asl_formatter.cpp @@ -187,7 +187,7 @@ Warning: Check [2/8] failed for 'f': param >= 0 (is: '-1'). Warning: Check [3/8] failed for 'd': param < 4 (is: '1000'). Warning: Check [5/8] failed for 's': param == test string (is: ''). Warning: Check [6/8] failed for 'b': param != 0 (is: '0'). -Warning: Check [7/8] failed: param 'vec' must b of size '3'. +Warning: Check [7/8] failed: param 'vec' must be of size '3'. Warning: Check [8/8] failed for 'd': param within [0, 500] (is: '1000'). ---------------------------------- SubConfig ----------------------------------- Warning: Check [1/1] failed for 'i': param > 0 (is: '-1'). @@ -208,7 +208,7 @@ Warning: Check [2/11] failed for 'f': param >= 0 (is: '-1'). Warning: Check [3/11] failed for 'd': param < 4 (is: '1000'). Warning: Check [5/11] failed for 's': param == test string (is: ''). Warning: Check [6/11] failed for 'b': param != 0 (is: '0'). -Warning: Check [7/11] failed: param 'vec' must b of size '3'. +Warning: Check [7/11] failed: param 'vec' must be of size '3'. Warning: Check [8/11] failed for 'd': param within [0, 500] (is: '1000'). Warning: Check [9/11] failed for 'sub_config.i': param > 0 (is: '-1'). Warning: Check [10/11] failed for 'sub_config.sub_sub_config.i': param > 0 (is: diff --git a/config_utilities/test/tests/dynamic_config.cpp b/config_utilities/test/tests/dynamic_config.cpp new file mode 100644 index 0000000..1440964 --- /dev/null +++ b/config_utilities/test/tests/dynamic_config.cpp @@ -0,0 +1,165 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#include "config_utilities/dynamic_config.h" + +#include + +#include "config_utilities/test/default_config.h" +#include "config_utilities/test/utils.h" + +namespace config::test { + +DefaultConfig modified_config() { + DefaultConfig config; + config.i = 2; + config.f = 3.2f; + config.vec = {7, 8, 9}; + config.sub_config.i = 3; + return config; +} + +TEST(DynamicConfig, CheckRegistered) { + DynamicConfigServer server; + + // No dynamic configs registered. + EXPECT_EQ(server.registeredConfigs().empty(), true); + + // Register a dynamic config. + { + auto dyn1 = DynamicConfig("dynamic_config_1"); + auto dyn2 = DynamicConfig("dynamic_config_2", modified_config()); + const auto registered = server.registeredConfigs(); + EXPECT_EQ(registered.size(), 2); + EXPECT_TRUE(std::find(registered.begin(), registered.end(), "dynamic_config_1") != registered.end()); + EXPECT_TRUE(std::find(registered.begin(), registered.end(), "dynamic_config_2") != registered.end()); + + // Check names unique. + auto logger = TestLogger::create(); + auto dyn3 = DynamicConfig("dynamic_config_1"); + EXPECT_EQ(logger->numMessages(), 1); + EXPECT_EQ(logger->lastMessage(), "Cannot register dynamic config: key 'dynamic_config_1' already exists."); + } + + // Dynamic configs should deregister automatically. + EXPECT_EQ(server.registeredConfigs().empty(), true); +} + +TEST(DynamicConfig, SetGet) { + DynamicConfig dyn("dyn"); + + DynamicConfigServer server; + + // Get values. + auto values = server.getValues("dyn"); + EXPECT_TRUE(expectEqual(values, DefaultConfig::defaultValues())); + + // Set values. + std::string yaml_str = R"( + i: 7 + f: 7.7 + vec: [7, 7, 7] + sub_ns: + i: 7 + )"; + auto yaml = YAML::Load(yaml_str); + server.setValues("dyn", yaml); + + // Check actual values. + auto config = dyn.get(); + EXPECT_EQ(config.i, 7); + EXPECT_EQ(config.f, 7.7f); + EXPECT_EQ(config.vec, std::vector({7, 7, 7})); + EXPECT_EQ(config.sub_config.i, 7); + + // Check serialized values. + values = server.getValues("dyn"); + EXPECT_EQ(values["i"].as(), 7); + EXPECT_EQ(values["f"].as(), 7.7f); + EXPECT_EQ(values["vec"].as>(), std::vector({7, 7, 7})); + EXPECT_EQ(values["sub_ns"]["i"].as(), 7); + EXPECT_EQ(values["u8"].as(), 4); // Default value. + + // Check invalid key. + values = server.getValues("invalid"); + EXPECT_TRUE(values.IsNull()); + server.setValues("invalid", DefaultConfig::defaultValues()); + EXPECT_EQ(dyn.get().i, 7); +} + +TEST(DynamicConfig, Hooks) { + auto server = std::make_unique(); + std::string logs; + + // Register hooks. + DynamicConfigServer::Hooks hooks; + hooks.onRegister = [&logs](const std::string& key) { logs += "register " + key + "; "; }; + hooks.onDeregister = [&logs](const std::string& key) { logs += "deregister " + key + "; "; }; + hooks.onUpdate = [&logs](const std::string& key, const YAML::Node& new_values) { logs += "update " + key + "; "; }; + server->setHooks(hooks); + + // Register a dynamic config. + auto a = std::make_unique>("A"); + auto b = std::make_unique>("B"); + DefaultConfig config; + a->set(config); // Should be identical, so not trigger update. + config.i = 123; + b->set(config); // Should trigger update. + b.reset(); + a.reset(); + EXPECT_EQ(logs, "register A; register B; update B; deregister B; deregister A; "); + + // Update hooks. + hooks.onRegister = [&logs](const std::string& key) { logs += "register " + key + " again; "; }; + hooks.onDeregister = nullptr; + server->setHooks(hooks); + logs.clear(); + + // Register a dynamic config. + auto c = std::make_unique>("C"); + c.reset(); + EXPECT_EQ(logs, "register C again; "); + + // Deregister hooks. + server.reset(); + logs.clear(); + + // Register a dynamic config. + auto d = std::make_unique>("D"); + d.reset(); + EXPECT_EQ(logs, ""); +} + +} // namespace config::test