From fc3b73144536674d3ec2bebaaa727f02fd8c0bd7 Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 9 Aug 2024 16:08:40 -0400 Subject: [PATCH 01/22] WIP: initial files --- .../include/config_utilities/dynamic_config.h | 83 +++++++++++++++++++ config_utilities/test/CMakeLists.txt | 1 + .../test/tests/dynamic_config.cpp | 50 +++++++++++ 3 files changed, 134 insertions(+) create mode 100644 config_utilities/include/config_utilities/dynamic_config.h create mode 100644 config_utilities/test/tests/dynamic_config.cpp 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..8d11a06 --- /dev/null +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -0,0 +1,83 @@ +/** ----------------------------------------------------------------------------- + * 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 + +namespace config { + +namespace internal { + +/** + * @brief + */ +struct DynamicConfigServer {}; + +/** + * @brief Name-based global registry for dynamic configurations. + */ +struct DynamicConfigRegistry { + using Key = std::string; + + // Singleton access. + static DynamicConfigRegistry& instance() { + static DynamicConfigRegistry instance; + return instance; + } + + /** + * @brief Check whether a dynamic config with the given key exists. + */ + bool hasKey(const Key& key) const { return configs_.count(key); } + + private: + DynamicConfigRegistry() = default; + + std::unordered_map configs_; +}; + +} // namespace internal + +/** + * @brief A wrapper class for for configurations that can be dynamically accessed or changed. + * + * @tparam ConfigT The contained configuration type + */ +template +class DynamicConfig {}; + +} // 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/tests/dynamic_config.cpp b/config_utilities/test/tests/dynamic_config.cpp new file mode 100644 index 0000000..6d65558 --- /dev/null +++ b/config_utilities/test/tests/dynamic_config.cpp @@ -0,0 +1,50 @@ +/** ----------------------------------------------------------------------------- + * 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" + +namespace config::test { + +TEST(DynamicConfig, CharConversionCorrect) { + auto config = DynamicConfig("dynamic_config_1"); + + // +} + +} // namespace config::test From 1539d5c073255dd4694019c273de274ab39449dc Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 9 Aug 2024 23:51:14 -0400 Subject: [PATCH 02/22] add initial dynamic config and server --- config_utilities/CMakeLists.txt | 1 + .../include/config_utilities/dynamic_config.h | 221 +++++++++++++++++- config_utilities/src/dynamic_config.cpp | 159 +++++++++++++ config_utilities/test/src/default_config.cpp | 4 +- config_utilities/test/tests/asl_formatter.cpp | 4 +- .../test/tests/dynamic_config.cpp | 121 +++++++++- 6 files changed, 493 insertions(+), 17 deletions(-) create mode 100644 config_utilities/src/dynamic_config.cpp diff --git a/config_utilities/CMakeLists.txt b/config_utilities/CMakeLists.txt index 8b5faa8..e767656 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/include/config_utilities/dynamic_config.h b/config_utilities/include/config_utilities/dynamic_config.h index 8d11a06..b91b8dc 100644 --- a/config_utilities/include/config_utilities/dynamic_config.h +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -35,23 +35,96 @@ #pragma once +#include +#include +#include #include #include +#include -namespace config { +#include +#include +#include -namespace internal { +namespace config { /** - * @brief + * @brief A server interface to manage all dynamic configs. */ -struct DynamicConfigServer {}; +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 = std::string; + 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() { @@ -60,24 +133,152 @@ struct DynamicConfigRegistry { } /** - * @brief Check whether a dynamic config with the given key exists. + * @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. */ - bool hasKey(const Key& key) const { return configs_.count(key); } + 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 configs_; + std::unordered_map hooks_; + size_t current_hooks_id_ = 0; }; } // namespace internal /** - * @brief A wrapper class for for configurations that can be dynamically accessed or changed. + * @brief A wrapper class for for configs that can be dynamically changed. * * @tparam ConfigT The contained configuration type */ template -class DynamicConfig {}; +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/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/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 index 6d65558..1440964 100644 --- a/config_utilities/test/tests/dynamic_config.cpp +++ b/config_utilities/test/tests/dynamic_config.cpp @@ -38,13 +38,128 @@ #include #include "config_utilities/test/default_config.h" +#include "config_utilities/test/utils.h" namespace config::test { -TEST(DynamicConfig, CharConversionCorrect) { - auto config = DynamicConfig("dynamic_config_1"); +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 From 6fba064737fdf6d4a1275728311787b60d3136ef Mon Sep 17 00:00:00 2001 From: lschmid Date: Mon, 12 Aug 2024 21:57:21 -0400 Subject: [PATCH 03/22] implement move constructors and update tests --- config_utilities/CMakeLists.txt | 6 +- .../include/config_utilities/dynamic_config.h | 103 +++++------ .../internal/dynamic_config_impl.hpp | 169 ++++++++++++++++++ .../include/config_utilities/parsing/ros.h | 39 +++- config_utilities/src/dynamic_config.cpp | 10 +- config_utilities/src/ros.cpp | 82 +++++++++ .../test/tests/dynamic_config.cpp | 51 ++++++ config_utilities/test/tests/factory.cpp | 1 - 8 files changed, 390 insertions(+), 71 deletions(-) create mode 100644 config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp create mode 100644 config_utilities/src/ros.cpp diff --git a/config_utilities/CMakeLists.txt b/config_utilities/CMakeLists.txt index e767656..d152424 100644 --- a/config_utilities/CMakeLists.txt +++ b/config_utilities/CMakeLists.txt @@ -31,8 +31,8 @@ add_library( src/external_registry.cpp src/factory.cpp src/formatter.cpp - src/logger.cpp src/log_to_stdout.cpp + src/logger.cpp src/meta_data.cpp src/namespacing.cpp src/path.cpp @@ -41,7 +41,9 @@ add_library( src/validation.cpp src/visitor.cpp src/yaml_parser.cpp - src/yaml_utils.cpp) + src/yaml_utils.cpp + $<$:src/ros.cpp> + ) target_link_libraries( ${PROJECT_NAME} PUBLIC yaml-cpp diff --git a/config_utilities/include/config_utilities/dynamic_config.h b/config_utilities/include/config_utilities/dynamic_config.h index b91b8dc..37d62df 100644 --- a/config_utilities/include/config_utilities/dynamic_config.h +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -54,6 +54,10 @@ namespace config { struct DynamicConfigServer { using Key = std::string; + /** + * @brief Hooks for the dynamic config server. These functions are called whenever a dynamic config is registered, + * deregistered, or updated. + */ struct Hooks { std::function onRegister; std::function onDeregister; @@ -88,14 +92,15 @@ struct DynamicConfigServer { YAML::Node getValues(const Key& key) const; /** - * @brief Set the values of a dynamic config. + * @brief Set the values of a dynamic config. If the requested values are invalid, no modifications are made. * @param key The unique key of the dynamic config. * @param values The new values to set. + * @return True if the values were updated, false otherwise. */ - void setValues(const Key& key, const YAML::Node& values) const; + bool setValues(const Key& key, const YAML::Node& values) const; /** - * @brief Set the hooks for the dynamic config server. + * @brief Set the hooks for the dynamic config server. Setting empty hooks will deregister the current hooks. */ void setHooks(const Hooks& hooks); @@ -122,7 +127,7 @@ struct DynamicConfigRegistry { */ struct ConfigInterface { std::function getValues; - std::function setValues; + std::function setValues; std::function getInfo; }; @@ -164,6 +169,13 @@ struct DynamicConfigRegistry { */ void deregisterConfig(const Key& key); + /** + * @brief Override an existing registration with a new interface when moving a dynamic config. + * @param key The unique key of the dynamic config. + * @param interface The new interface to the dynamic config. + */ + void overrideRegistration(const Key& key, const ConfigInterface& interface); + /** * @brief Register hooks for a dynamic config server. * @param hooks The hooks to register. @@ -199,86 +211,55 @@ struct DynamicConfigRegistry { */ template struct DynamicConfig { + using Callback = std::function; + /** * @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)}); - } + explicit DynamicConfig(const std::string& name, const ConfigT& config = {}, Callback callback = {}); - ~DynamicConfig() { - if (is_registered_) { - internal::DynamicConfigRegistry::instance().deregisterConfig(name_); - } - } + ~DynamicConfig(); DynamicConfig(const DynamicConfig&) = delete; - DynamicConfig(DynamicConfig&&) = default; DynamicConfig& operator=(const DynamicConfig&) = delete; - DynamicConfig& operator=(DynamicConfig&&) = default; + DynamicConfig(DynamicConfig&&); + DynamicConfig& operator=(DynamicConfig&&); /** * @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_; - } + ConfigT get() const; /** * @brief Set the underlying dynamic config. + * @param config The new config to set. If the config is invalid, no modifications are made. + * @return True if the config was updated, false otherwise. */ - 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); - } + bool set(const ConfigT& config); + + /** + * @brief Set the callback function that is called whenever the config is updated. + * @param callback The callback function to be called. + */ + void setCallback(const Callback& callback); 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 {}; - } + Callback callback_; + const bool is_registered_; + + bool setValues(const YAML::Node& values); + YAML::Node getValues() const; + YAML::Node getInfo() const; + internal::DynamicConfigRegistry::ConfigInterface getInterface(); + void moveMembers(DynamicConfig&& other); }; } // namespace config + +#include diff --git a/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp new file mode 100644 index 0000000..c38ca92 --- /dev/null +++ b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp @@ -0,0 +1,169 @@ +/** ----------------------------------------------------------------------------- + * 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 + +namespace config { + +template +DynamicConfig::DynamicConfig(const std::string& name, + const ConfigT& config, + DynamicConfig::Callback callback) + : name_(name), + config_(config::checkValid(config)), + callback_(callback), + is_registered_(internal::DynamicConfigRegistry::instance().registerConfig(name_, getInterface())) { + static_assert(isConfig(), + "ConfigT must be declared to be a config. Implement 'void declare_config(ConfigT&)'."); + static_assert(std::is_copy_constructible::value, "ConfigT must be copy constructible."); +} + +template +DynamicConfig::~DynamicConfig() { + if (is_registered_) { + internal::DynamicConfigRegistry::instance().deregisterConfig(name_); + } +} + +template +DynamicConfig::DynamicConfig(DynamicConfig&& other) + : name_(other.name_), is_registered_(other.is_registered_) { + moveMembers(std::move(other)); +} + +template +DynamicConfig& DynamicConfig::operator=(DynamicConfig&& other) { + if (this != &other) { + moveMembers(std::move(other)); + } + return *this; +} + +template +ConfigT DynamicConfig::get() const { + std::lock_guard lock(mutex_); + return config_; +} + +template +bool DynamicConfig::set(const ConfigT& config) { + if (!config::isValid(config)) { + return false; + } + if (!is_registered_) { + config_ = config; + // NOTE(lschmid): This returns true even if the config is the same as the old one. Might want to move this further + // down in the future, although I don't think it matters since the user has control over the config here. + return true; + } + + 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 false; + } + config_ = config; + internal::DynamicConfigRegistry::instance().configUpdated(name_, new_yaml); + return true; +} + +template +void DynamicConfig::setCallback(const Callback& callback) { + std::lock_guard lock(mutex_); + callback_ = callback; +} + +template +bool DynamicConfig::setValues(const YAML::Node& values) { + std::lock_guard lock(mutex_); + ConfigT new_config = config_; + internal::Visitor::setValues(new_config, values); + if (!config::isValid(new_config)) { + return false; + } + + // NOTE(lschmid): This is a bit cumbersome, but configs don't have to implement operator==, so we compare + // their YAML representation. Can consider making this optional in the future in the global settings? + const auto old_yaml = internal::Visitor::getValues(config_).data; + const auto new_yaml = internal::Visitor::getValues(new_config).data; + if (internal::isEqual(old_yaml, new_yaml)) { + return false; + } + config_ = new_config; + if (callback_) { + callback_(config_); + } + return true; +} + +template +YAML::Node DynamicConfig::getValues() const { + std::lock_guard lock(mutex_); + return internal::Visitor::getValues(config_).data; +} + +template +YAML::Node DynamicConfig::getInfo() const { + std::lock_guard lock(mutex_); + // TODO(lschmid): Add a visitor function to get the info of a config. + return {}; +} + +template +internal::DynamicConfigRegistry::ConfigInterface DynamicConfig::getInterface() { + internal::DynamicConfigRegistry::ConfigInterface interface; + interface.getValues = [this]() { return getValues(); }; + interface.setValues = [this](const YAML::Node& values) { return setValues(values); }; + interface.getInfo = [this]() { return getInfo(); }; + return interface; +} + +template +void DynamicConfig::moveMembers(DynamicConfig&& other) { + config_ = std::move(other.config_); + callback_ = std::move(other.callback_); + const_cast(is_registered_) = other.is_registered_; + + if (is_registered_) { + if (name_ != other.name_) { + internal::DynamicConfigRegistry::instance().deregisterConfig(name_); + } + internal::DynamicConfigRegistry::instance().overrideRegistration(other.name_, getInterface()); + } + const_cast(name_) = std::move(other.name_); +} + +} // namespace config diff --git a/config_utilities/include/config_utilities/parsing/ros.h b/config_utilities/include/config_utilities/parsing/ros.h index ad9161c..04415ca 100644 --- a/config_utilities/include/config_utilities/parsing/ros.h +++ b/config_utilities/include/config_utilities/parsing/ros.h @@ -36,17 +36,20 @@ #pragma once #include +#include #include #include #include #include +#include +#include "config_utilities/dynamic_config.h" #include "config_utilities/factory.h" #include "config_utilities/internal/string_utils.h" #include "config_utilities/internal/visitor.h" #include "config_utilities/internal/yaml_utils.h" -#include "config_utilities/parsing/yaml.h" // NOTE(lschmid): This pulls in more than needed buyt avoids code duplication. +#include "config_utilities/parsing/yaml.h" // NOTE(lschmid): This pulls in more than needed but avoids code duplication. #include "config_utilities/update.h" namespace config { @@ -183,9 +186,9 @@ std::unique_ptr createFromROSWithNamespace(const ros::NodeHandle& nh, * @brief Update the config with the current parameters in ROS. * @note This function will update the field and check the validity of the config afterwards. If the config is invalid, * the field will be reset to its original value. - * @param config The config to update. - * @param nh The ROS nodehandle to update the config from. - * @param name_space Optionally specify a name space to create the config from. Separate names with slashes '/'. + * @param config The config to update. + * @param nh The ROS nodehandle to update the config from. + * @param name_space Optionally specify a name space to create the config from. Separate names with slashes '/'. */ template bool updateFromRos(ConfigT& config, const ros::NodeHandle& nh, const std::string& name_space = "") { @@ -194,4 +197,32 @@ bool updateFromRos(ConfigT& config, const ros::NodeHandle& nh, const std::string return updateField(config, node, true, name_space); } +/** + * @brief Dynamic config server that allows to set and get configs via ROS topics. + */ +class RosDynamicConfigServer { + public: + explicit RosDynamicConfigServer(const ros::NodeHandle& nh); + + private: + struct ConfigReceiver { + ConfigReceiver(const DynamicConfigServer::Key& key, RosDynamicConfigServer* server, ros::NodeHandle& nh); + const DynamicConfigServer::Key key; + RosDynamicConfigServer* const server; + ros::Subscriber sub; + void callback(const std_msgs::String& msg); + }; + + ros::NodeHandle nh_; + std::map publishers_; + std::map> subscribers_; + ros::Publisher reg_pub_; + ros::Publisher dereg_pub_; + DynamicConfigServer server_; + + void onRegister(const DynamicConfigServer::Key& key); + void onDeregister(const DynamicConfigServer::Key& key); + void onUpdate(const DynamicConfigServer::Key& key, const YAML::Node& new_values); +}; + } // namespace config diff --git a/config_utilities/src/dynamic_config.cpp b/config_utilities/src/dynamic_config.cpp index beead24..770dc0f 100644 --- a/config_utilities/src/dynamic_config.cpp +++ b/config_utilities/src/dynamic_config.cpp @@ -57,12 +57,12 @@ YAML::Node DynamicConfigServer::getValues(const Key& key) const { return config->getValues(); } -void DynamicConfigServer::setValues(const Key& key, const YAML::Node& values) const { +bool DynamicConfigServer::setValues(const Key& key, const YAML::Node& values) const { const auto config = internal::DynamicConfigRegistry::instance().getConfig(key); if (!config) { - return; + return false; } - config->setValues(values); + return config->setValues(values); } YAML::Node DynamicConfigServer::getInfo(const Key& key) const { @@ -131,6 +131,10 @@ void DynamicConfigRegistry::deregisterConfig(const Key& key) { } } +void DynamicConfigRegistry::overrideRegistration(const Key& key, const ConfigInterface& interface) { + configs_[key] = interface; +} + size_t DynamicConfigRegistry::registerHooks(const DynamicConfigServer::Hooks& hooks, size_t hooks_id) { if (hooks.empty()) { return hooks_id; diff --git a/config_utilities/src/ros.cpp b/config_utilities/src/ros.cpp new file mode 100644 index 0000000..8e59a3e --- /dev/null +++ b/config_utilities/src/ros.cpp @@ -0,0 +1,82 @@ +/** ----------------------------------------------------------------------------- + * 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/parsing/ros.h" + +namespace config { + +RosDynamicConfigServer::ConfigReceiver::ConfigReceiver(const DynamicConfigServer::Key& key, + RosDynamicConfigServer* server, + ros::NodeHandle& nh) + : key(key), server(server) { + sub = nh.subscribe(key + "/set", 1, &ConfigReceiver::callback, this); +} + +void RosDynamicConfigServer::ConfigReceiver::callback(const std_msgs::String& msg) { + const auto values = YAML::Load(msg.data); + server->onUpdate(key, values); +} + +RosDynamicConfigServer::RosDynamicConfigServer(const ros::NodeHandle& nh) : nh_(nh) { + reg_pub_ = nh_.advertise("registered", 1); + dereg_pub_ = nh_.advertise("deregistered", 1); + + DynamicConfigServer::Hooks hooks; + hooks.onRegister = [this](const DynamicConfigServer::Key& key) { onRegister(key); }; + hooks.onDeregister = [this](const DynamicConfigServer::Key& key) { onDeregister(key); }; + hooks.onUpdate = [this](const DynamicConfigServer::Key& key, const YAML::Node& values) { onUpdate(key, values); }; + server_.setHooks(hooks); +} + +void RosDynamicConfigServer::onRegister(const DynamicConfigServer::Key& key) { + publishers_[key] = nh_.advertise(key + "/get", 1, true); + subscribers_[key] = std::make_unique(key, this, nh_); + std_msgs::String msg; + msg.data = key; + reg_pub_.publish(msg); +} + +void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { + publishers_.erase(key); + subscribers_.erase(key); + std_msgs::String msg; + msg.data = key; + dereg_pub_.publish(msg); +} + +void RosDynamicConfigServer::onUpdate(const DynamicConfigServer::Key& key, const YAML::Node& values) { + server_.setValues(key, values); +} + +} // namespace config diff --git a/config_utilities/test/tests/dynamic_config.cpp b/config_utilities/test/tests/dynamic_config.cpp index 1440964..a300f77 100644 --- a/config_utilities/test/tests/dynamic_config.cpp +++ b/config_utilities/test/tests/dynamic_config.cpp @@ -162,4 +162,55 @@ TEST(DynamicConfig, Hooks) { EXPECT_EQ(logs, ""); } +TEST(DynamicConfig, Move) { + DynamicConfigServer server; + + // Register a dynamic config. + DefaultConfig config; + config.i = 123; + auto dyn = DynamicConfig("dyn", config); + EXPECT_EQ(server.registeredConfigs().size(), 1); + EXPECT_EQ(server.registeredConfigs()[0], "dyn"); + + // Move constructor. + DynamicConfig dyn2(std::move(dyn)); + EXPECT_EQ(server.registeredConfigs().size(), 1); + EXPECT_EQ(server.registeredConfigs()[0], "dyn"); + EXPECT_EQ(dyn2.get().i, 123); + + // Get/set. + YAML::Node update = YAML::Load("i: 456"); + server.setValues("dyn", update); + EXPECT_EQ(dyn2.get().i, 456); + config.i = 456; + config.f = 2.3f; + dyn2.set(config); + auto values = server.getValues("dyn"); + EXPECT_EQ(values["i"].as(), 456); + EXPECT_EQ(values["f"].as(), 2.3f); + + // Move assignment. + DynamicConfig dyn3("dyn3"); + EXPECT_EQ(server.registeredConfigs().size(), 2); + EXPECT_EQ(server.registeredConfigs()[0], "dyn3"); + EXPECT_EQ(server.registeredConfigs()[1], "dyn"); + + dyn3 = std::move(dyn2); + EXPECT_EQ(server.registeredConfigs().size(), 1); + EXPECT_EQ(server.registeredConfigs()[0], "dyn"); + EXPECT_EQ(dyn3.get().i, 456); + EXPECT_EQ(dyn3.get().f, 2.3f); + + // Get/set. + update = YAML::Load("i: 789"); + server.setValues("dyn", update); + EXPECT_EQ(dyn3.get().i, 789); + config.i = 789; + config.f = 4.5f; + dyn3.set(config); + values = server.getValues("dyn"); + EXPECT_EQ(values["i"].as(), 789); + EXPECT_EQ(values["f"].as(), 4.5f); +} + } // namespace config::test diff --git a/config_utilities/test/tests/factory.cpp b/config_utilities/test/tests/factory.cpp index eab5ae7..dbe5a15 100644 --- a/config_utilities/test/tests/factory.cpp +++ b/config_utilities/test/tests/factory.cpp @@ -308,7 +308,6 @@ Config[config::test::Talker](): Settings().print_width = 40; const std::string modules = internal::ModuleRegistry::getAllRegistered(); - std::cout << modules << std::endl; EXPECT_EQ(modules, expected); Settings().restoreDefaults(); } From 288fd7f7bc38940c4bb92afed3271639f7ce5b4c Mon Sep 17 00:00:00 2001 From: lschmid Date: Wed, 14 Aug 2024 22:37:06 -0400 Subject: [PATCH 04/22] add dynamic reconfigure demo and simple GUI --- config_utilities/demos/CMakeLists.txt | 5 +- .../demos/demo_dynamic_config.launch | 9 + .../demos/demo_dynamic_config_client.py | 378 ++++++++++++++++++ .../demos/demo_dynamic_config_server.cpp | 151 +++++++ .../include/config_utilities/dynamic_config.h | 2 +- .../internal/dynamic_config_impl.hpp | 54 ++- .../include/config_utilities/parsing/ros.h | 1 + config_utilities/src/ros.cpp | 20 +- docs/DynamicConfigs.md | 8 + 9 files changed, 602 insertions(+), 26 deletions(-) create mode 100644 config_utilities/demos/demo_dynamic_config.launch create mode 100755 config_utilities/demos/demo_dynamic_config_client.py create mode 100644 config_utilities/demos/demo_dynamic_config_server.cpp create mode 100644 docs/DynamicConfigs.md diff --git a/config_utilities/demos/CMakeLists.txt b/config_utilities/demos/CMakeLists.txt index 8cc7db0..6cb0845 100644 --- a/config_utilities/demos/CMakeLists.txt +++ b/config_utilities/demos/CMakeLists.txt @@ -13,7 +13,10 @@ if(ENABLE_Eigen3 AND ENABLE_roscpp) add_executable(demo_ros demo_ros.cpp) target_link_libraries(demo_ros ${PROJECT_NAME}) + add_executable(demo_dynamic_config_server demo_dynamic_config_server.cpp) + target_link_libraries(demo_dynamic_config_server ${PROJECT_NAME}) + include(GNUInstallDirs) - install(TARGETS demo_ros + install(TARGETS demo_ros demo_dynamic_config_server RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}) endif() diff --git a/config_utilities/demos/demo_dynamic_config.launch b/config_utilities/demos/demo_dynamic_config.launch new file mode 100644 index 0000000..a7673d2 --- /dev/null +++ b/config_utilities/demos/demo_dynamic_config.launch @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/config_utilities/demos/demo_dynamic_config_client.py b/config_utilities/demos/demo_dynamic_config_client.py new file mode 100755 index 0000000..34f87b9 --- /dev/null +++ b/config_utilities/demos/demo_dynamic_config_client.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +from tkinter import * +import customtkinter as ctk +import rospy +from std_msgs.msg import String +import yaml +from time import sleep + +ctk.set_appearance_mode( + "System") # Modes: "System" (standard), "Dark", "Light" +ctk.set_default_color_theme( + "blue") # Themes: "blue" (standard), "green", "dark-blue" +DEBUG = False # Disable ROS for debugging. +PAD_X = 10 +PAD_Y = 10 +APP_NAME = "[Config Utilities Dynamic Config Client] " + + +class DynamicConfigGUI(ctk.CTk): + + class GUIConfig: + width = 700 + height = 600 + + def __init__(self): + super().__init__() + + # Callbacks from the GUI. Optionally set by the invoker. + self.key_selected_cb = None + self.server_selected_cb = None + self.value_changed_cb = None + + # GUI configuration. + self.gui_config = self.GUIConfig() + + # Data. + self.current_key = None + self.current_server = None + + self.setup_frame() + + def setup_frame(self): + # Master. + self.title("Config Utilities Dynamic Config Client") + self.geometry(f"{self.gui_config.width}x{self.gui_config.height}") + + # Key selection. + self.key_selection = self.SelectionDropDown(self, self._key_selected) + self.key_selection.grid(row=0, column=0, sticky="ew") + + # Config editing. + self.config_frame = self.PlainTextConfigFrame(self, + self.value_changed_cb) + self.config_frame.grid(row=1, column=0, sticky="ns") + + # TODO(lschmid): Consider making this more general, specialized to ROS for now. + self.server_selection = self.RosStatusBar(self, self._server_selected) + self.server_selection.send_cb = self._value_changed_cb + self.server_selection.grid(row=2, column=0, sticky="ew") + + self.rowconfigure([0, 2], + minsize=self.key_selection.winfo_reqheight(), + pad=PAD_Y, + weight=0) + self.rowconfigure(1, weight=1) + self.columnconfigure(0, weight=1, pad=PAD_X) + + # Interfaces for outside interaction with the GUI. + def set_keys(self, keys): + self.key_selection.set_keys(keys) + + def set_servers(self, servers): + self.server_selection.set_keys(servers) + + def set_config(self, new_values): + self.config_frame.set_config(new_values) + + # Functionality. + def _key_selected(self, key): + self.current_key = key + if self.key_selected_cb is not None: + self.key_selected_cb(key) + + def _server_selected(self, server): + self.current_server = server + if self.server_selected_cb is not None: + self.server_selected_cb(server) + + def _value_changed_cb(self): + if self.value_changed_cb is not None: + self.value_changed_cb(self.config_frame.get_config()) + + class SelectionFrame(ctk.CTkFrame): + """ + Interface class for key selection. + """ + + def __init__(self, master, key_selected_cb): + super().__init__(master) + self.key_selected_cb = key_selected_cb + + def set_keys(self, new_keys): + pass + + class SelectionDropDown(SelectionFrame): + + def __init__(self, master, key_selected_cb): + super().__init__(master, key_selected_cb) + self.current_key = None + self.no_options_text = "No Dynamic Configs Registered." + + self.w_label = ctk.CTkLabel(self, text="Config:") + self.w_label.grid(row=0, + column=0, + padx=PAD_X, + pady=PAD_Y, + sticky="nsw") + self.w_dropdown = ctk.CTkOptionMenu(self, + dynamic_resizing=True, + command=self._on_change) + self.w_dropdown.grid(row=0, + column=1, + sticky="nsew", + padx=PAD_X, + pady=PAD_Y) + self.columnconfigure(1, weight=1) + + def set_keys(self, new_keys): + if new_keys == []: + # No keys available. + if self.current_key is not None: + self.key_selected_cb(None) + self.current_key = None + self.w_dropdown.set(self.no_options_text) + self.w_dropdown.configure(state=DISABLED) + return + + self.w_dropdown.configure(values=new_keys, state=NORMAL) + if self.current_key in new_keys: + return + self.current_key = new_keys[0] + self.key_selected_cb(self.current_key) + self.w_dropdown.set(self.current_key) + + def _on_change(self, _): + key = self.w_dropdown.get() + if key == self.current_key: + return + self.current_key = key + self.key_selected_cb(key) + + class RosStatusBar(SelectionDropDown): + + def __init__(self, master, key_selected_cb): + super().__init__(master, key_selected_cb) + # Callbacks hooks. + self.refresh_cb = None + self.send_cb = None + + self.no_options_text = "No RosDynamicConfigServers Registered." + self.w_label.configure(text="Config Server:") + self.w_refresh_button = ctk.CTkButton( + self, text="Refresh", command=self._on_reset_button) + self.w_refresh_button.grid(row=0, + column=3, + padx=PAD_X, + pady=PAD_Y, + sticky="nse") + self.w_send_button = ctk.CTkButton(self, + text="Send", + command=self._on_send_button) + self.w_send_button.grid(row=0, + column=4, + padx=PAD_X, + pady=PAD_Y, + sticky="nse") + self.columnconfigure(2, weight=1) + self.columnconfigure([0, 1, 3, 4], weight=0) + + def _on_reset_button(self): + if self.refresh_cb is not None: + self.refresh_cb() + + def _on_send_button(self): + if self.send_cb is not None: + self.send_cb() + + class ConfigFrame(ctk.CTkFrame): + """ + Interface class for configuration editing. + """ + + def __init__(self, master, send_update_fn=None): + super().__init__(master) + self.send_update_fn = send_update_fn + + def set_config(self, new_config): + pass + + def get_config(self): + return {} + + def set_enabled(self, enabled): + pass + + class PlainTextConfigFrame(ConfigFrame): + + def __init__(self, master, send_update_fn=None): + super().__init__(master, send_update_fn) + self.w_text = Text(self, wrap=CHAR, width=1000, undo=True) + self.w_text.pack(fill=BOTH, expand=True, padx=PAD_X, pady=PAD_Y) + self.w_text.bind("", self._on_key_release) + + def set_config(self, new_config): + self.w_text.delete("0.0", END) + self.w_text.insert("0.0", yaml.dump(new_config)) + + def get_config(self): + try: + return yaml.load(self.w_text.get("1.0", END), + Loader=yaml.FullLoader) + except yaml.YAMLError as e: + print(f"{APP_NAME}Error parsing YAML: {e}") + return None + + def set_enabled(self, enabled): + new_state = NORMAL if enabled else DISABLED + self.w_text.configure(new_state) + + def _on_key_release(self, event): + # Ctrl + Enter. + if event.keysym == "Return" and event.state == 20: + self.send_update_fn() + + +class DynamicConfigRosClient: + + def __init__(self): + # Setup the GUI. + self.gui = DynamicConfigGUI() + self.gui.value_changed_cb = self.send_config + self.gui.key_selected_cb = self.key_selected_cb + self.gui.server_selected_cb = self.server_selected_cb + self.gui.server_selection.refresh_cb = self.refresh_servers + + # Variables. + self.listening_ns = "" + self.last_values_received = "" + + # ROS. + self.config_pub = None + self.config_sub = None + self.reg_sub = None + self.dereg_sub = None + + self.initialize() + + def initialize(self): + servers = self.get_available_servers() + if len(servers) == 0: + print( + f"{APP_NAME}Waiting for ROS Dynamic Config Servers to register..." + ) + while len(servers) == 0: + sleep(0.1) + servers = self.get_available_servers() + + # This will also selected a server and trigger the connection. + self.gui.set_servers(servers) + print(f"{APP_NAME}Connected to server '{self.listening_ns}'.") + + def subscriber_cb(self, msg): + try: + values = yaml.load(msg.data, Loader=yaml.FullLoader) + except yaml.YAMLError as e: + print(f"{APP_NAME}Error parsing incoming message YAML: {e}") + return + self.last_values_received = values + self.gui.set_config(values) + + def reg_cb(self, _): + # Instead of incremental tracking just update the configs. + self.gui.set_keys(self.get_available_keys()) + + def key_selected_cb(self, key): + if key is not None: + self.connect_topic(key) + + def server_selected_cb(self, server): + servers = self.get_available_servers() + if server not in servers: + print(f"{APP_NAME}Server '{server}' not available.") + self.gui.set_servers(servers) + return + self.connect_server(server) + + def send_config(self, new_values): + if self.config_pub is None: + return + msg = String() + msg.data = yaml.dump(new_values, default_flow_style=False) + self.config_pub.publish(msg) + + def refresh_servers(self): + previous_server = self.gui.current_server + previous_key = self.gui.current_key + servers = self.get_available_servers() + self.gui.set_servers(servers) + if (previous_server != self.gui.current_server + or self.gui.current_key != previous_key): + self.last_values_received = "" + else: + self.gui.set_config(self.last_values_received) + + def connect_topic(self, key): + if self.config_pub is not None: + self.config_pub.unregister() + self.config_sub.unregister() + self.config_pub = rospy.Publisher(f"{self.listening_ns}/{key}/set", + String, + queue_size=10) + self.config_sub = rospy.Subscriber(f"{self.listening_ns}/{key}/get", + String, self.subscriber_cb) + + def connect_server(self, server): + self.listening_ns = server + self.reg_sub = rospy.Subscriber(f"{self.listening_ns}/registered", + String, self.reg_cb) + self.dereg_sub = rospy.Subscriber(f"{self.listening_ns}/deregistered", + String, self.reg_cb) + self.gui.set_keys(self.get_available_keys()) + + def get_available_servers(self): + topics = rospy.get_published_topics() + topics = [ + topic[0] for topic in topics if topic[1] == "std_msgs/String" + ] + # We use the queue that all servers advertise these topics. + reg = [t[:-11] for t in topics if t.endswith("/registered")] + dereg = [t[:-13] for t in topics if t.endswith("/deregistered")] + return [t for t in reg if t in dereg] + + def get_available_keys(self): + topics = rospy.get_published_topics() + topics = [t[0] for t in topics if t[1] == "std_msgs/String"] + topics = [ + t[len(self.listening_ns) + 1:] for t in topics + if t.startswith(self.listening_ns) + ] + return [t[:-4] for t in topics if t.endswith("/get")] + + def spin(self): + self.gui.mainloop() + + +def on_shutdown(gui: DynamicConfigGUI): + # Force ctk shutdown. + gui.quit() + + +def debug_main(): + gui = DynamicConfigGUI() + gui.set_keys(["A", "B", "C"]) + gui.mainloop() + + +def main(): + rospy.init_node("dynamic_config_client") + client = DynamicConfigRosClient() + rospy.on_shutdown(lambda: on_shutdown(client.gui)) + client.spin() + + +if __name__ == "__main__": + if DEBUG: + debug_main() + else: + main() diff --git a/config_utilities/demos/demo_dynamic_config_server.cpp b/config_utilities/demos/demo_dynamic_config_server.cpp new file mode 100644 index 0000000..40f472c --- /dev/null +++ b/config_utilities/demos/demo_dynamic_config_server.cpp @@ -0,0 +1,151 @@ +/** ----------------------------------------------------------------------------- + * 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. + * -------------------------------------------------------------------------- */ + +/** + * This demo shows how to use config_utilities with ROS. + */ + +#include +#include + +#include + +#include "config_utilities/config.h" // Enables declare_config(). +#include "config_utilities/dynamic_config.h" // Enables DynamicConfig and DynamicConfigServer. +#include "config_utilities/logging/log_to_stdout.h" // Log config_utilities messages. +#include "config_utilities/parsing/ros.h" // Enable fromRos() and the RosDynamicConfigServer. +#include "config_utilities/printing.h" // Enable toString() +#include "config_utilities/types/eigen_matrix.h" // Enable parsing and printing of Eigen::Matrix types. +#include "config_utilities/types/enum.h" // Enable parsing and printing of enum types. + +namespace demo { + +// A sub-struct for later use. +struct SubConfig { + float f = 1.1f; + std::string s = "test"; +}; + +// A struct that represents what we want to be a config. +// Requirements for a config struct: is default constructable. +struct MyConfig { + int i = 100; + double distance = 42; + bool b = true; + std::vector vec = {1, 2, 3}; + std::map map = {{"a", 1}, {"b", 2}, {"c", 3}}; + Eigen::Matrix mat = Eigen::Matrix::Identity(); + enum class MyEnum { kA, kB, kC } my_enum = MyEnum::kA; + SubConfig sub_config; +}; + +// Defining 'void declare_config(T& config)' function labels a struct as config. +// It **MUST** be declared beforehand if being used in another declare_config +void declare_config(SubConfig&); + +// All config properties are specified within declare_config. +void declare_config(MyConfig& config) { + config::name("MyConfig"); + config::field(config.i, "i"); + config::field(config.distance, "distance", "m"); + config::field(config.b, "b"); + config::field(config.vec, "vec"); + config::field(config.map, "map"); + config::field(config.mat, "mat"); + config::enum_field(config.my_enum, "my_enum", {"A", "B", "C"}); + config::field(config.sub_config, "sub_config"); + + config::check(config.i, config::CheckMode::GT, 0, "i"); + config::checkInRange(config.distance, 0.0, 100.0, "distance"); +} + +// Declaration of the subconfig. +void declare_config(SubConfig& config) { + using namespace config; + name("SubConfig"); + field(config.f, "f"); + field(config.s, "s"); + checkIsOneOf(config.f, {0.0f, 1.1f, 2.2f, 3.3f}, "f"); +} + +// Declare an object with a dynamic config. +template +class DynamicConfigObject { + public: + explicit DynamicConfigObject(const std::string& name, const ConfigT& initial_config) + : name_(name), config_(name, initial_config) { + // The above initialization registers the dynamic config with its global identifier name, which we resolve using the + // nodehandle to get a unique name for every node/object. The config is initialized with the current ROS parameters. + // The callback is called whenever the config is updated. + config_.setCallback(std::bind(&DynamicConfigObject::callback, this)); + } + + private: + const std::string name_; + config::DynamicConfig config_; + + void callback() const { + // Do something with the new config. + std::cout << "Received new config for " << name_ << ":\n" << config::toString(config_.get()) << std::endl; + } +}; + +} // namespace demo + +int main(int argc, char** argv) { + ros::init(argc, argv, "dynamic_config_server"); + ros::NodeHandle nh("~"); + + // Advertize setting and getting dynamic configs via ros topics. + config::RosDynamicConfigServer server(nh); + + // Create dynamic config objects. These will automatically register their config with the server. + demo::DynamicConfigObject obj("dynamic_object_config", demo::MyConfig()); + + // Initialize another config with different name and params. + nh.setParam("i", 42); + nh.setParam("distance", 42.0); + nh.setParam("b", false); + nh.setParam("vec", std::vector()); + nh.setParam("map", std::map({{"ASD", 42}})); + demo::DynamicConfigObject obj2("another_config", config::fromRos(nh)); + + // Initialize a subconfig. + demo::DynamicConfigObject sub_obj("sub_config", demo::SubConfig()); + + // Spin to keep the node alive. + ros::spin(); + return 0; +} diff --git a/config_utilities/include/config_utilities/dynamic_config.h b/config_utilities/include/config_utilities/dynamic_config.h index 37d62df..f57f5f8 100644 --- a/config_utilities/include/config_utilities/dynamic_config.h +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -211,7 +211,7 @@ struct DynamicConfigRegistry { */ template struct DynamicConfig { - using Callback = std::function; + using Callback = std::function; /** * @brief Construct a new Dynamic Config, wrapping a config_uilities config. diff --git a/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp index c38ca92..087d3b0 100644 --- a/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp +++ b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp @@ -79,7 +79,7 @@ ConfigT DynamicConfig::get() const { template bool DynamicConfig::set(const ConfigT& config) { - if (!config::isValid(config)) { + if (!config::isValid(config, true)) { return false; } if (!is_registered_) { @@ -89,13 +89,16 @@ bool DynamicConfig::set(const ConfigT& config) { return true; } - 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 false; - } - config_ = config; + { // critical section + std::lock_guard lock(mutex_); + const auto old_yaml = internal::Visitor::getValues(config_).data; + if (internal::isEqual(old_yaml, new_yaml)) { + return false; + } + config_ = config; + } // end critical section + internal::DynamicConfigRegistry::instance().configUpdated(name_, new_yaml); return true; } @@ -108,24 +111,31 @@ void DynamicConfig::setCallback(const Callback& callback) { template bool DynamicConfig::setValues(const YAML::Node& values) { - std::lock_guard lock(mutex_); - ConfigT new_config = config_; - internal::Visitor::setValues(new_config, values); - if (!config::isValid(new_config)) { - return false; - } + YAML::Node new_yaml; + { // start critical section + std::lock_guard lock(mutex_); + ConfigT new_config = config_; + internal::Visitor::setValues(new_config, values); + if (!config::isValid(new_config, true)) { + return false; + } + + // NOTE(lschmid): This is a bit cumbersome, but configs don't have to implement operator==, so we compare + // their YAML representation. Can consider making this optional in the future in the global settings? + const auto old_yaml = internal::Visitor::getValues(config_).data; + new_yaml = internal::Visitor::getValues(new_config).data; + if (internal::isEqual(old_yaml, new_yaml)) { + return false; + } + config_ = new_config; + } // end critical section - // NOTE(lschmid): This is a bit cumbersome, but configs don't have to implement operator==, so we compare - // their YAML representation. Can consider making this optional in the future in the global settings? - const auto old_yaml = internal::Visitor::getValues(config_).data; - const auto new_yaml = internal::Visitor::getValues(new_config).data; - if (internal::isEqual(old_yaml, new_yaml)) { - return false; - } - config_ = new_config; if (callback_) { - callback_(config_); + callback_(); } + + // Also notify other clients that the config has been updated. + internal::DynamicConfigRegistry::instance().configUpdated(name_, new_yaml); return true; } diff --git a/config_utilities/include/config_utilities/parsing/ros.h b/config_utilities/include/config_utilities/parsing/ros.h index 04415ca..469b603 100644 --- a/config_utilities/include/config_utilities/parsing/ros.h +++ b/config_utilities/include/config_utilities/parsing/ros.h @@ -223,6 +223,7 @@ class RosDynamicConfigServer { void onRegister(const DynamicConfigServer::Key& key); void onDeregister(const DynamicConfigServer::Key& key); void onUpdate(const DynamicConfigServer::Key& key, const YAML::Node& new_values); + void onSet(const DynamicConfigServer::Key& key, const YAML::Node& new_values); }; } // namespace config diff --git a/config_utilities/src/ros.cpp b/config_utilities/src/ros.cpp index 8e59a3e..66cdf9b 100644 --- a/config_utilities/src/ros.cpp +++ b/config_utilities/src/ros.cpp @@ -45,7 +45,7 @@ RosDynamicConfigServer::ConfigReceiver::ConfigReceiver(const DynamicConfigServer void RosDynamicConfigServer::ConfigReceiver::callback(const std_msgs::String& msg) { const auto values = YAML::Load(msg.data); - server->onUpdate(key, values); + server->onSet(key, values); } RosDynamicConfigServer::RosDynamicConfigServer(const ros::NodeHandle& nh) : nh_(nh) { @@ -65,6 +65,9 @@ void RosDynamicConfigServer::onRegister(const DynamicConfigServer::Key& key) { std_msgs::String msg; msg.data = key; reg_pub_.publish(msg); + + // Latch the current state of the config. + onUpdate(key, server_.getValues(key)); } void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { @@ -76,7 +79,20 @@ void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { } void RosDynamicConfigServer::onUpdate(const DynamicConfigServer::Key& key, const YAML::Node& values) { - server_.setValues(key, values); + const auto it = publishers_.find(key); + if (it == publishers_.end()) { + // Shouldn't happen but better to fail gracefully if people extend this. + internal::Logger::logWarning("Tried to publish to dynamic config '" + key + "' without existing publisher."); + return; + } + + std_msgs::String msg; + msg.data = YAML::Dump(values); + it->second.publish(msg); +} + +void RosDynamicConfigServer::onSet(const DynamicConfigServer::Key& key, const YAML::Node& new_values) { + server_.setValues(key, new_values); } } // namespace config diff --git a/docs/DynamicConfigs.md b/docs/DynamicConfigs.md new file mode 100644 index 0000000..a2a4676 --- /dev/null +++ b/docs/DynamicConfigs.md @@ -0,0 +1,8 @@ + + +# Setup +To use the example GUI, install system deps: + +```bash +pip install customtkinter +``` From 0a8ba653a9b1b2aa7af08fe6893f2833fb0045c4 Mon Sep 17 00:00:00 2001 From: lschmid Date: Thu, 15 Aug 2024 19:56:02 -0400 Subject: [PATCH 05/22] initial recover --- .../demos/demo_dynamic_config_client.py | 268 ++-------------- config_utilities/demos/dynamic_config_gui.py | 294 ++++++++++++++++++ .../config_utilities/internal/checks.h | 53 ++++ .../internal/field_input_info.h | 166 ++++++++++ .../config_utilities/internal/meta_data.h | 25 +- .../config_utilities/internal/string_utils.h | 2 +- .../config_utilities/internal/visitor.h | 31 +- .../internal/visitor_impl.hpp | 68 +++- .../config_utilities/internal/yaml_parser.h | 47 +++ .../include/config_utilities/traits.h | 19 +- .../include/config_utilities/types/enum.h | 37 ++- config_utilities/src/asl_formatter.cpp | 4 +- config_utilities/src/field_input_info.cpp | 291 +++++++++++++++++ config_utilities/src/meta_data.cpp | 59 ++++ config_utilities/src/visitor.cpp | 7 +- config_utilities/test/tests/conversions.cpp | 43 +++ .../test/tests/field_input_info.cpp | 228 ++++++++++++++ config_utilities/test/tests/yaml_parsing.cpp | 4 +- docs/Advanced.md | 5 + 19 files changed, 1366 insertions(+), 285 deletions(-) mode change 100755 => 100644 config_utilities/demos/demo_dynamic_config_client.py create mode 100644 config_utilities/demos/dynamic_config_gui.py create mode 100644 config_utilities/include/config_utilities/internal/field_input_info.h create mode 100644 config_utilities/src/field_input_info.cpp create mode 100644 config_utilities/test/tests/field_input_info.cpp diff --git a/config_utilities/demos/demo_dynamic_config_client.py b/config_utilities/demos/demo_dynamic_config_client.py old mode 100755 new mode 100644 index 34f87b9..0dd61de --- a/config_utilities/demos/demo_dynamic_config_client.py +++ b/config_utilities/demos/demo_dynamic_config_client.py @@ -1,239 +1,18 @@ #!/usr/bin/env python3 -from tkinter import * -import customtkinter as ctk import rospy from std_msgs.msg import String import yaml from time import sleep +from dynamic_config_gui import DynamicConfigGUI -ctk.set_appearance_mode( - "System") # Modes: "System" (standard), "Dark", "Light" -ctk.set_default_color_theme( - "blue") # Themes: "blue" (standard), "green", "dark-blue" -DEBUG = False # Disable ROS for debugging. -PAD_X = 10 -PAD_Y = 10 APP_NAME = "[Config Utilities Dynamic Config Client] " -class DynamicConfigGUI(ctk.CTk): - - class GUIConfig: - width = 700 - height = 600 - - def __init__(self): - super().__init__() - - # Callbacks from the GUI. Optionally set by the invoker. - self.key_selected_cb = None - self.server_selected_cb = None - self.value_changed_cb = None - - # GUI configuration. - self.gui_config = self.GUIConfig() - - # Data. - self.current_key = None - self.current_server = None - - self.setup_frame() - - def setup_frame(self): - # Master. - self.title("Config Utilities Dynamic Config Client") - self.geometry(f"{self.gui_config.width}x{self.gui_config.height}") - - # Key selection. - self.key_selection = self.SelectionDropDown(self, self._key_selected) - self.key_selection.grid(row=0, column=0, sticky="ew") - - # Config editing. - self.config_frame = self.PlainTextConfigFrame(self, - self.value_changed_cb) - self.config_frame.grid(row=1, column=0, sticky="ns") - - # TODO(lschmid): Consider making this more general, specialized to ROS for now. - self.server_selection = self.RosStatusBar(self, self._server_selected) - self.server_selection.send_cb = self._value_changed_cb - self.server_selection.grid(row=2, column=0, sticky="ew") - - self.rowconfigure([0, 2], - minsize=self.key_selection.winfo_reqheight(), - pad=PAD_Y, - weight=0) - self.rowconfigure(1, weight=1) - self.columnconfigure(0, weight=1, pad=PAD_X) - - # Interfaces for outside interaction with the GUI. - def set_keys(self, keys): - self.key_selection.set_keys(keys) - - def set_servers(self, servers): - self.server_selection.set_keys(servers) - - def set_config(self, new_values): - self.config_frame.set_config(new_values) - - # Functionality. - def _key_selected(self, key): - self.current_key = key - if self.key_selected_cb is not None: - self.key_selected_cb(key) - - def _server_selected(self, server): - self.current_server = server - if self.server_selected_cb is not None: - self.server_selected_cb(server) - - def _value_changed_cb(self): - if self.value_changed_cb is not None: - self.value_changed_cb(self.config_frame.get_config()) - - class SelectionFrame(ctk.CTkFrame): - """ - Interface class for key selection. - """ - - def __init__(self, master, key_selected_cb): - super().__init__(master) - self.key_selected_cb = key_selected_cb - - def set_keys(self, new_keys): - pass - - class SelectionDropDown(SelectionFrame): - - def __init__(self, master, key_selected_cb): - super().__init__(master, key_selected_cb) - self.current_key = None - self.no_options_text = "No Dynamic Configs Registered." - - self.w_label = ctk.CTkLabel(self, text="Config:") - self.w_label.grid(row=0, - column=0, - padx=PAD_X, - pady=PAD_Y, - sticky="nsw") - self.w_dropdown = ctk.CTkOptionMenu(self, - dynamic_resizing=True, - command=self._on_change) - self.w_dropdown.grid(row=0, - column=1, - sticky="nsew", - padx=PAD_X, - pady=PAD_Y) - self.columnconfigure(1, weight=1) - - def set_keys(self, new_keys): - if new_keys == []: - # No keys available. - if self.current_key is not None: - self.key_selected_cb(None) - self.current_key = None - self.w_dropdown.set(self.no_options_text) - self.w_dropdown.configure(state=DISABLED) - return - - self.w_dropdown.configure(values=new_keys, state=NORMAL) - if self.current_key in new_keys: - return - self.current_key = new_keys[0] - self.key_selected_cb(self.current_key) - self.w_dropdown.set(self.current_key) - - def _on_change(self, _): - key = self.w_dropdown.get() - if key == self.current_key: - return - self.current_key = key - self.key_selected_cb(key) - - class RosStatusBar(SelectionDropDown): - - def __init__(self, master, key_selected_cb): - super().__init__(master, key_selected_cb) - # Callbacks hooks. - self.refresh_cb = None - self.send_cb = None - - self.no_options_text = "No RosDynamicConfigServers Registered." - self.w_label.configure(text="Config Server:") - self.w_refresh_button = ctk.CTkButton( - self, text="Refresh", command=self._on_reset_button) - self.w_refresh_button.grid(row=0, - column=3, - padx=PAD_X, - pady=PAD_Y, - sticky="nse") - self.w_send_button = ctk.CTkButton(self, - text="Send", - command=self._on_send_button) - self.w_send_button.grid(row=0, - column=4, - padx=PAD_X, - pady=PAD_Y, - sticky="nse") - self.columnconfigure(2, weight=1) - self.columnconfigure([0, 1, 3, 4], weight=0) - - def _on_reset_button(self): - if self.refresh_cb is not None: - self.refresh_cb() - - def _on_send_button(self): - if self.send_cb is not None: - self.send_cb() - - class ConfigFrame(ctk.CTkFrame): - """ - Interface class for configuration editing. - """ - - def __init__(self, master, send_update_fn=None): - super().__init__(master) - self.send_update_fn = send_update_fn - - def set_config(self, new_config): - pass - - def get_config(self): - return {} - - def set_enabled(self, enabled): - pass - - class PlainTextConfigFrame(ConfigFrame): - - def __init__(self, master, send_update_fn=None): - super().__init__(master, send_update_fn) - self.w_text = Text(self, wrap=CHAR, width=1000, undo=True) - self.w_text.pack(fill=BOTH, expand=True, padx=PAD_X, pady=PAD_Y) - self.w_text.bind("", self._on_key_release) - - def set_config(self, new_config): - self.w_text.delete("0.0", END) - self.w_text.insert("0.0", yaml.dump(new_config)) - - def get_config(self): - try: - return yaml.load(self.w_text.get("1.0", END), - Loader=yaml.FullLoader) - except yaml.YAMLError as e: - print(f"{APP_NAME}Error parsing YAML: {e}") - return None - - def set_enabled(self, enabled): - new_state = NORMAL if enabled else DISABLED - self.w_text.configure(new_state) - - def _on_key_release(self, event): - # Ctrl + Enter. - if event.keysym == "Return" and event.state == 20: - self.send_update_fn() - - class DynamicConfigRosClient: + """ + A ROS client for the dynamic config GUI. This class connects to a dynamic config server + and allows the user to interact with the server's configuration through YAML. + """ def __init__(self): # Setup the GUI. @@ -246,10 +25,12 @@ def __init__(self): # Variables. self.listening_ns = "" self.last_values_received = "" + self.last_info_received = "" # ROS. self.config_pub = None self.config_sub = None + self.config_info_sub = None self.reg_sub = None self.dereg_sub = None @@ -278,6 +59,15 @@ def subscriber_cb(self, msg): self.last_values_received = values self.gui.set_config(values) + def info_sub_cb(self, msg): + try: + values = yaml.load(msg.data, Loader=yaml.FullLoader) + except yaml.YAMLError as e: + print(f"{APP_NAME}Error parsing incoming message YAML: {e}") + return + self.last_info_received = values + self.gui.set_config_info(values) + def reg_cb(self, _): # Instead of incremental tracking just update the configs. self.gui.set_keys(self.get_available_keys()) @@ -309,18 +99,25 @@ def refresh_servers(self): if (previous_server != self.gui.current_server or self.gui.current_key != previous_key): self.last_values_received = "" - else: + self.last_info_received = "" + return + if self.last_info_received != "": + self.gui.set_config_info(self.last_info_received) + elif self.last_values_received != "": self.gui.set_config(self.last_values_received) def connect_topic(self, key): if self.config_pub is not None: self.config_pub.unregister() self.config_sub.unregister() + self.config_info_sub.unregister() self.config_pub = rospy.Publisher(f"{self.listening_ns}/{key}/set", String, queue_size=10) self.config_sub = rospy.Subscriber(f"{self.listening_ns}/{key}/get", String, self.subscriber_cb) + self.config_info_sub = rospy.Subscriber( + f"{self.listening_ns}/{key}/info", String, self.info_sub_cb) def connect_server(self, server): self.listening_ns = server @@ -353,26 +150,17 @@ def spin(self): self.gui.mainloop() -def on_shutdown(gui: DynamicConfigGUI): +def on_shutdown(client: DynamicConfigRosClient): # Force ctk shutdown. - gui.quit() - - -def debug_main(): - gui = DynamicConfigGUI() - gui.set_keys(["A", "B", "C"]) - gui.mainloop() + client.gui.quit() def main(): rospy.init_node("dynamic_config_client") client = DynamicConfigRosClient() - rospy.on_shutdown(lambda: on_shutdown(client.gui)) + rospy.on_shutdown(lambda: on_shutdown(client)) client.spin() if __name__ == "__main__": - if DEBUG: - debug_main() - else: - main() + main() diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py new file mode 100644 index 0000000..0734682 --- /dev/null +++ b/config_utilities/demos/dynamic_config_gui.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +from tkinter import * +from typing import Any +import customtkinter as ctk +import yaml + +ctk.set_appearance_mode( + "System") # Modes: "System" (standard), "Dark", "Light" +ctk.set_default_color_theme( + "blue") # Themes: "blue" (standard), "green", "dark-blue" +PAD_X = 10 +PAD_Y = 10 +GUI_NAME = "[Config Utilities Dynamic Config GUI] " + + +class Settings: + """ + A class to store settings for the DynamicConfigGUI. This can also be opened as a top-level window. + """ + + def __init__(self) -> None: + # Settings. + self.width = 800 + self.height = 600 + self.appearance_mode = "System" + self.color_theme = "blue" + self.scale_factor = 1.0 + + # GUI. + self._gui = None + + def gui(self) -> None: + """ + Open the GUI for changing the settings. + """ + if self._gui is not None: + return + self._gui = ctk.CTkToplevel() + self._gui.title("Config Utilities Dynamic Config Client Settings") + self._gui.geometry(f"{400}x{400}") + + +class DynamicConfigGUI(ctk.CTk): + """ + A GUI for interacting with dynamic configurations. This GUI allows the user to select a server, a key, and + edit the configuration for that key. The GUI is designed to be used with a dynamic configuration server + that can hook into the *_cb functions to send and receive new configurations. + """ + + def __init__(self, settings: Settings = Settings()) -> None: + super().__init__() + + # Callbacks from the GUI. Optionally set by the invoker. + self.key_selected_cb = None + self.server_selected_cb = None + self.value_changed_cb = None + + # GUI configuration. + self.settings = settings + + # Data. + self.current_key = None + self.current_server = None + + # Initialization. + self.setup_frame() + + def setup_frame(self): + # Master. + self.title("Config Utilities Dynamic Config Client") + self.geometry(f"{self.settings.width}x{self.settings.height}") + + # Key selection. + self.key_selection = SelectionDropDown(self, self._key_selected) + self.key_selection.grid(row=0, column=0, sticky="ew") + + # Settings Button. + # TODO(lschmid): For now baked into the key selection. + self.settings_button = ctk.CTkButton(self.key_selection, + text="Settings", + command=self.settings.gui) + self.settings_button.grid(row=0, + column=3, + sticky="ew", + padx=PAD_X, + pady=PAD_Y) + self.key_selection.columnconfigure( + 3, weight=0, minsize=self.settings_button.winfo_reqwidth()) + + # Config editing. + self.config_frame = PlainTextConfigFrame(self, self.value_changed_cb) + self.config_frame.grid(row=1, column=0, sticky="ns", columnspan=2) + + # TODO(lschmid): Consider making this more general, specialized to ROS for now. + self.server_selection = RosStatusBar(self, self._server_selected) + self.server_selection.send_cb = self._value_changed_cb + self.server_selection.grid(row=2, column=0, sticky="ew", columnspan=2) + + self.rowconfigure([0, 2], + minsize=self.key_selection.winfo_reqheight(), + pad=PAD_Y, + weight=0) + self.rowconfigure(1, weight=1) + self.columnconfigure(0, weight=1, pad=PAD_X) + + # Interfaces for outside interaction with the GUI. + def set_keys(self, keys): + self.key_selection.set_keys(keys) + + def set_servers(self, servers): + self.server_selection.set_keys(servers) + + def set_config(self, new_values): + self.config_frame.set_config(new_values) + + def set_config_info(self, new_info): + """ + Update the GUI with new information about the configuration. + new_info: A dictionary with information about the configuration. + """ + print(f"Got info: {new_info}") + pass + + # Functionality. + def _key_selected(self, key): + self.current_key = key + if self.key_selected_cb is not None: + self.key_selected_cb(key) + + def _server_selected(self, server): + self.current_server = server + if self.server_selected_cb is not None: + self.server_selected_cb(server) + + def _value_changed_cb(self): + if self.value_changed_cb is not None: + self.value_changed_cb(self.config_frame.get_config()) + + +class SelectionFrame(ctk.CTkFrame): + """ + Interface class for key selection. + """ + + def __init__(self, master, key_selected_cb): + super().__init__(master) + self.key_selected_cb = key_selected_cb + + def set_keys(self, new_keys): + pass + + +class SelectionDropDown(SelectionFrame): + + def __init__(self, master, key_selected_cb): + super().__init__(master, key_selected_cb) + self.current_key = None + self.no_options_text = "No Dynamic Configs Registered." + + self.w_label = ctk.CTkLabel(self, text="Config:") + self.w_label.grid(row=0, + column=0, + padx=PAD_X, + pady=PAD_Y, + sticky="nsw") + self.w_dropdown = ctk.CTkOptionMenu(self, + dynamic_resizing=True, + command=self._on_change) + self.w_dropdown.grid(row=0, + column=1, + sticky="nsew", + padx=PAD_X, + pady=PAD_Y) + self.columnconfigure(1, weight=1) + + def set_keys(self, new_keys): + if new_keys == []: + # No keys available. + if self.current_key is not None: + self.key_selected_cb(None) + self.current_key = None + self.w_dropdown.set(self.no_options_text) + self.w_dropdown.configure(state=DISABLED) + return + + self.w_dropdown.configure(values=new_keys, state=NORMAL) + if self.current_key in new_keys: + return + self.current_key = new_keys[0] + self.key_selected_cb(self.current_key) + self.w_dropdown.set(self.current_key) + + def _on_change(self, _): + key = self.w_dropdown.get() + if key == self.current_key: + return + self.current_key = key + self.key_selected_cb(key) + + +class RosStatusBar(SelectionDropDown): + + def __init__(self, master, key_selected_cb): + super().__init__(master, key_selected_cb) + # Callbacks hooks. + self.refresh_cb = None + self.send_cb = None + + self.no_options_text = "No RosDynamicConfigServers Registered." + self.w_label.configure(text="Config Server:") + self.w_refresh_button = ctk.CTkButton(self, + text="Refresh", + command=self._on_reset_button) + self.w_refresh_button.grid(row=0, + column=3, + padx=PAD_X, + pady=PAD_Y, + sticky="nse") + self.w_send_button = ctk.CTkButton(self, + text="Send", + command=self._on_send_button) + self.w_send_button.grid(row=0, + column=4, + padx=PAD_X, + pady=PAD_Y, + sticky="nse") + self.columnconfigure(2, weight=1) + self.columnconfigure([0, 1, 3, 4], weight=0) + + def _on_reset_button(self): + if self.refresh_cb is not None: + self.refresh_cb() + + def _on_send_button(self): + if self.send_cb is not None: + self.send_cb() + + +class ConfigFrame(ctk.CTkFrame): + """ + Interface class for configuration editing. + """ + + def __init__(self, master, send_update_fn=None): + super().__init__(master) + self.send_update_fn = send_update_fn + + def set_config(self, new_config): + pass + + def get_config(self): + return {} + + def set_enabled(self, enabled): + pass + + +class PlainTextConfigFrame(ConfigFrame): + + def __init__(self, master, send_update_fn=None): + super().__init__(master, send_update_fn) + self.w_text = ctk.CTkTextbox(self, wrap=CHAR, width=1000, undo=True) + self.w_text.pack(fill=BOTH, expand=True, padx=PAD_X, pady=PAD_Y) + self.w_text.bind("", self._on_key_release) + + def set_config(self, new_config): + self.w_text.delete("0.0", END) + self.w_text.insert("0.0", yaml.dump(new_config)) + + def get_config(self): + try: + return yaml.load(self.w_text.get("1.0", END), + Loader=yaml.FullLoader) + except yaml.YAMLError as e: + print(f"{GUI_NAME}Error parsing YAML: {e}") + return None + + def set_enabled(self, enabled): + new_state = NORMAL if enabled else DISABLED + self.w_text.configure(new_state) + + def _on_key_release(self, event): + # Ctrl + Enter. + if event.keysym == "Return" and event.state == 20: + self.send_update_fn() + + +def main(): + app = DynamicConfigGUI() + app.mainloop() + + +if __name__ == "__main__": + main() diff --git a/config_utilities/include/config_utilities/internal/checks.h b/config_utilities/include/config_utilities/internal/checks.h index 846aad0..7bb85dd 100644 --- a/config_utilities/include/config_utilities/internal/checks.h +++ b/config_utilities/include/config_utilities/internal/checks.h @@ -41,6 +41,9 @@ #include #include +#include "config_utilities/internal/field_input_info.h" +#include "config_utilities/internal/yaml_parser.h" + namespace config::internal { struct CheckBase { @@ -50,6 +53,7 @@ struct CheckBase { virtual std::string message() const = 0; virtual std::string name() const { return ""; } virtual std::unique_ptr clone() const = 0; + virtual IntFieldInputInfo::Ptr fieldInputInfo() const { return nullptr; } inline operator bool() const { return valid(); } }; @@ -99,6 +103,35 @@ class BinaryCheck : public CheckBase { return std::make_unique>(param_, value_, name_); } + IntFieldInputInfo::Ptr fieldInputInfo() const override { + auto info = createFieldInputInfo(); + if (!info || (info->type != FieldInputInfo::Type::kInt && info->type != FieldInputInfo::Type::kFloat)) { + return nullptr; + } + YAML::Node value = YamlParser::toYaml(value_); + if (!value) { + return nullptr; + } + // This is a bit stupid but we avoid re-defining another template trait. + const std::string sym = CompareMessageTrait::message(); + if (sym == ">") { + info->setMin(value, false); + } else if (sym == ">=") { + info->setMin(value, true); + } else if (sym == "<") { + info->setMax(value, false); + } else if (sym == "<=") { + info->setMax(value, true); + } else if (sym == "==") { + // Will have interesting behavior, consider replacing with option. + info->setMin(value, true); + info->setMax(value, true); + } + // Not equal does not have a clear representation for input infos and will be handled like all other irregular + // checks upon parsing. + return info; + } + protected: T param_; T value_; @@ -170,6 +203,16 @@ class CheckRange : public CheckBase { return std::make_unique>(param_, lower_, upper_, name_, lower_inclusive_, upper_inclusive_); } + IntFieldInputInfo::Ptr fieldInputInfo() const override { + auto info = createFieldInputInfo(); + if (!info || (info->type != FieldInputInfo::Type::kInt && info->type != FieldInputInfo::Type::kFloat)) { + return nullptr; + } + info->setMin(YamlParser::toYaml(lower_), lower_inclusive_); + info->setMax(YamlParser::toYaml(lower_), upper_inclusive_); + return info; + } + protected: const T param_; const T lower_; @@ -215,6 +258,16 @@ class CheckIsOneOf : public CheckBase { return std::make_unique>(param_, candidates_, name_); } + IntFieldInputInfo::Ptr fieldInputInfo() const override { + auto info = std::make_shared(); + for (const T& candidate : candidates_) { + std::stringstream ss; + ss << candidate; + info->options.push_back(ss.str()); + } + return info; + } + private: const T param_; const std::vector candidates_; diff --git a/config_utilities/include/config_utilities/internal/field_input_info.h b/config_utilities/include/config_utilities/internal/field_input_info.h new file mode 100644 index 0000000..12749c5 --- /dev/null +++ b/config_utilities/include/config_utilities/internal/field_input_info.h @@ -0,0 +1,166 @@ +/** ----------------------------------------------------------------------------- + * 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 + +namespace config::internal { + +/** + * @brief Information about the input type and constraints of a field. + */ +struct FieldInputInfo { + using Ptr = std::shared_ptr; + //! Type of the field input. Anything that is not specialized can be parsed as kYAML. + enum class Type { kBool, kInt, kFloat, kString, kOptions, kYAML } type; + static std::string typeToString(Type type); + + explicit FieldInputInfo(Type type) : type(type) {} + virtual ~FieldInputInfo() = default; + + //! Convert the input info to yaml format for serialization. + virtual YAML::Node toYaml() const; + + //! Merge the input info with another one. This is used to combine constraints from different sources. + static Ptr merge(const FieldInputInfo::Ptr& from, const FieldInputInfo::Ptr& to); + + // Utility interface to set int/float constraints. + virtual void setMin(YAML::Node min, bool lower_inclusive = true) {} + virtual void setMax(YAML::Node max, bool upper_inclusive = true) {} + + private: + // Implementation of merging for the same type. + virtual void mergeSame(const FieldInputInfo& other) {} +}; + +struct IntFieldInputInfo : public FieldInputInfo { + IntFieldInputInfo() : FieldInputInfo(Type::kInt) {} + + // Constraints for the field. + // NOTE(lschmid): We do not consider data larger than 64 bit integers. + int64_t min = std::numeric_limits::lowest(); + uint64_t max = std::numeric_limits::max(); + bool lower_inclusive = true; + bool upper_inclusive = true; + + YAML::Node toYaml() const override; + void mergeSame(const FieldInputInfo& other) override; + void setMin(YAML::Node min, bool lower_inclusive = true) override; + void setMax(YAML::Node max, bool upper_inclusive = true) override; +}; + +struct FloatFieldInputInfo : public FieldInputInfo { + FloatFieldInputInfo() : FieldInputInfo(Type::kFloat) {} + + // Constraints for the field. + double min = std::numeric_limits::lowest(); + double max = std::numeric_limits::max(); + bool lower_inclusive = true; + bool upper_inclusive = true; + + YAML::Node toYaml() const override; + void mergeSame(const FieldInputInfo& other) override; + void setMin(YAML::Node min, bool lower_inclusive = true) override; + void setMax(YAML::Node max, bool upper_inclusive = true) override; +}; + +struct OptionsFieldInputInfo : public FieldInputInfo { + OptionsFieldInputInfo() : FieldInputInfo(Type::kOptions) {} + + // The possible options for the field. Note that since these will anyways be stringified, all types can be stored as + // strings. + std::vector options; + + YAML::Node toYaml() const override; + void mergeSame(const FieldInputInfo& other) override; +}; + +/** + * @brief Create field info based on common types. + */ +template +FieldInputInfo::Ptr createFieldInputInfo() { + // Default anything not specialized to YAML. + return std::make_shared(FieldInputInfo::Type::kYAML); +} + +// Specializations for common types. +// Bool. +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +// Ints. +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +// Floats. +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +// Strings. +template <> +FieldInputInfo::Ptr createFieldInputInfo(); + +} // namespace config::internal diff --git a/config_utilities/include/config_utilities/internal/meta_data.h b/config_utilities/include/config_utilities/internal/meta_data.h index 1a0be26..eb00160 100644 --- a/config_utilities/include/config_utilities/internal/meta_data.h +++ b/config_utilities/include/config_utilities/internal/meta_data.h @@ -45,6 +45,7 @@ #include #include "config_utilities/internal/checks.h" +#include "config_utilities/internal/field_input_info.h" namespace config::internal { @@ -52,20 +53,29 @@ namespace config::internal { * @brief Struct that holds additional information about fields for printing. */ struct FieldInfo { - // Name of the field. This is always given. + //! Name of the field. This is always given. std::string name; - // Optional: Unit of the field. + //! Optional: Unit of the field. std::string unit; - // The value of the field if the field is not a config. + //! The value of the field if the field is not a config. YAML::Node value; - // Whether the field corresponds to its default value. Only queried if Settings().indicate_default_values is true. - bool is_default = false; + //! The default value of the field if the field is not a config. + YAML::Node default_value; - // Whether or not the field was parsed + //! Whether the field corresponds to its default value. Only queried if Settings().indicate_default_values is true. + bool isDefault() const; + + //! Whether or not the field was parsed bool was_parsed = false; + + //! Additional information about the input type and constraints of the field. Only queried when using getInfo. + std::shared_ptr input_info; + + //! Serialize the field info to yaml. + YAML::Node serializeFieldInfos() const; }; // Struct to issue warnings. Currently used for parsing errors but can be extended to other warnings in the future. @@ -137,6 +147,9 @@ struct MetaData { void performOnAll(const std::function& func); void performOnAll(const std::function& func) const; + // Utility function to get field info. + YAML::Node serializeFieldInfos() const; + private: void copyValues(const MetaData& other) { name = other.name; diff --git a/config_utilities/include/config_utilities/internal/string_utils.h b/config_utilities/include/config_utilities/internal/string_utils.h index b0ffa87..7e25191 100644 --- a/config_utilities/include/config_utilities/internal/string_utils.h +++ b/config_utilities/include/config_utilities/internal/string_utils.h @@ -107,7 +107,7 @@ std::string dataToString(const YAML::Node& data, bool reformat_float = false); std::vector findAllSubstrings(const std::string& text, const std::string& substring); /** - * @brief Get a human readable type name of a type if cmopiled with GCC, otherwise default to the mangled typename. + * @brief Get a human readable type name of a type if compiled with GCC, otherwise default to the mangled typename. * @tparam T The type to get the name of. */ template diff --git a/config_utilities/include/config_utilities/internal/visitor.h b/config_utilities/include/config_utilities/internal/visitor.h index 37717b9..4073c92 100644 --- a/config_utilities/include/config_utilities/internal/visitor.h +++ b/config_utilities/include/config_utilities/internal/visitor.h @@ -40,11 +40,13 @@ #include #include #include +#include #include #include "config_utilities/internal/meta_data.h" #include "config_utilities/internal/namespacing.h" #include "config_utilities/internal/yaml_parser.h" +#include "config_utilities/traits.h" namespace config { @@ -64,7 +66,11 @@ struct Visitor { static bool hasInstance(); // Interfaces for all internal tools interact with configs through the visitor. - // Set the data in the config from the node. + /** + * @brief Set the values of a config from a YAML node. + * @param config The config to set the values for. + * @param node The data to set the values from. + */ template static MetaData setValues(ConfigT& config, const YAML::Node& node, @@ -80,6 +86,13 @@ struct Visitor { const std::string& name_space = "", const std::string& field_name = ""); + // Get the data and field info stored in the config. + template + static MetaData getInfo(const ConfigT& config, + const bool print_warnings = true, + const std::string& name_space = "", + const std::string& field_name = ""); + // Execute all checks specified in the config. template static MetaData getChecks(const ConfigT& config, const std::string& field_name = ""); @@ -132,7 +145,7 @@ struct Visitor { friend std::string config::current_namespace(); // Which operations to perform on the data. - enum class Mode { kGet, kGetDefaults, kSet, kCheck }; + enum class Mode { kGet, kGetDefaults, kSet, kCheck, kGetInfo }; const Mode mode; // Create and access the meta data for the current thread. Lifetime of the meta data is managed internally by the @@ -141,18 +154,26 @@ struct Visitor { // by calling 'declare_config()'. explicit Visitor(Mode _mode, const std::string& _name_space = "", const std::string& _field_name = ""); + // Singleton access. static Visitor& instance(); /* Utility function to manipulate data. */ - // Helper function to get the default values of a config. + // Dispatch getting a default meta data for a config. template ::value, bool>::type = true> static MetaData getDefaults(const ConfigT& config); template ::value, bool>::type = true> static MetaData getDefaults(const ConfigT& config); - // Labels all fields in the data as default if they match the default values of the config. + // Dispatch getting field input info from conversions. + template (), bool>::type = true> + static FieldInputInfo::Ptr getFieldInputInfo(); + template (), bool>::type = true> + static FieldInputInfo::Ptr getFieldInputInfo(); + + // Computes the default values for all fields in the meta data. This assumes that the meta data is already created, + // and the meta data was created from ConfigT. template - static void flagDefaultValues(const ConfigT& config, MetaData& data); + static void getDefaultValues(const ConfigT& config, MetaData& data); // Extend the current visitor with a sub-visitor, replicating the previous specification. template diff --git a/config_utilities/include/config_utilities/internal/visitor_impl.hpp b/config_utilities/include/config_utilities/internal/visitor_impl.hpp index 2e5fe9d..c32d031 100644 --- a/config_utilities/include/config_utilities/internal/visitor_impl.hpp +++ b/config_utilities/include/config_utilities/internal/visitor_impl.hpp @@ -81,11 +81,11 @@ MetaData Visitor::getValues(const ConfigT& config, const std::string& name_space, const std::string& field_name) { Visitor visitor(Mode::kGet, name_space, field_name); - // NOTE: We know that in mode kGet, the config is not modified. + // NOTE(lschmid): We know that in mode kGet, the config is not modified. ::config::declare_config(const_cast(config)); if (Settings::instance().indicate_default_values) { - flagDefaultValues(config, visitor.data); + Visitor::getDefaultValues(config, visitor.data); } if (print_warnings && visitor.data.hasErrors()) { Logger::logWarning(Formatter::formatErrors(visitor.data, "Errors parsing config", Severity::kWarning)); @@ -94,10 +94,34 @@ MetaData Visitor::getValues(const ConfigT& config, return visitor.data; } +template +MetaData Visitor::getInfo(const ConfigT& config, + const bool print_warnings, + const std::string& name_space, + const std::string& field_name) { + Visitor visitor(Mode::kGetInfo, name_space, field_name); + // NOTE(lschmid): We know that in mode kGetInfo, the config is not modified. + ::config::declare_config(const_cast(config)); + Visitor::getDefaultValues(config, visitor.data); + + // Try to associate check data with the fieds by name. + visitor.data.performOnAll([](MetaData& data) { + for (const auto& check : data.checks) { + for (auto& field_info : data.field_infos) { + if (field_info.name == check->name()) { + field_info.input_info = FieldInputInfo::merge(check->fieldInputInfo(), field_info.input_info); + break; + } + } + } + }); + return visitor.data; +} + template MetaData Visitor::getChecks(const ConfigT& config, const std::string& field_name) { Visitor visitor(Mode::kCheck, "", field_name); - // NOTE: We know that in mode kCheck, the config is not modified. + // NOTE(lschmid): We know that in mode kCheck, the config is not modified. ::config::declare_config(const_cast(config)); return visitor.data; } @@ -120,6 +144,8 @@ MetaData Visitor::subVisit(ConfigT& config, case Visitor::Mode::kCheck: data = getChecks(config, field_name); break; + case Visitor::Mode::kGetInfo: + data = getInfo(config, print_warnings, name_space, field_name); default: break; } @@ -145,7 +171,8 @@ void Visitor::visitField(T& field, const std::string& field_name, const std::str } } - if (visitor.mode == Visitor::Mode::kGet || visitor.mode == Visitor::Mode::kGetDefaults) { + if (visitor.mode == Visitor::Mode::kGet || visitor.mode == Visitor::Mode::kGetDefaults || + visitor.mode == Visitor::Mode::kGetInfo) { std::string error; YAML::Node node = YamlParser::toYaml(field_name, field, visitor.name_space, error); mergeYamlNodes(visitor.data.data, node); @@ -154,6 +181,12 @@ void Visitor::visitField(T& field, const std::string& field_name, const std::str if (!error.empty()) { visitor.data.errors.emplace_back(new Warning(field_name, error)); } + + // Get type information if requested. + if (visitor.mode == Visitor::Mode::kGetInfo) { + auto input_info = createFieldInputInfo(); + info.input_info = FieldInputInfo::merge(input_info, info.input_info); + } } } @@ -185,7 +218,8 @@ void Visitor::visitField(T& field, const std::string& field_name, const std::str } } - if (visitor.mode == Visitor::Mode::kGet || visitor.mode == Visitor::Mode::kGetDefaults) { + if (visitor.mode == Visitor::Mode::kGet || visitor.mode == Visitor::Mode::kGetDefaults || + visitor.mode == Visitor::Mode::kGetInfo) { std::string error; const auto intermediate = Conversion::toIntermediate(field, error); if (!error.empty()) { @@ -199,6 +233,12 @@ void Visitor::visitField(T& field, const std::string& field_name, const std::str if (!error.empty()) { visitor.data.errors.emplace_back(new Warning(field_name, error)); } + + // Get type information if requested. + if (visitor.mode == Visitor::Mode::kGetInfo) { + auto input_info = Visitor::getFieldInputInfo(); + info.input_info = FieldInputInfo::merge(input_info, info.input_info); + } } } @@ -389,10 +429,10 @@ MetaData Visitor::getDefaults(const ConfigT& config) { } template -void Visitor::flagDefaultValues(const ConfigT& config, MetaData& data) { +void Visitor::getDefaultValues(const ConfigT& config, MetaData& data) { // Get defaults from a default constructed ConfigT. Extract the default values of all non-config fields. Subconfigs // are managed separately. - const MetaData default_data = getDefaults(config); + const MetaData default_data = Visitor::getDefaults(config); // Compare all fields. These should always be in the same order if they are from the same config and exclude // subconfigs. @@ -414,10 +454,18 @@ void Visitor::flagDefaultValues(const ConfigT& config, MetaData& data) { // NOTE(lschmid): Operator YAML::Node== checks for identity, not equality. Since these are all scalars, comparing // the formatted strings should be identical. const auto& default_info = default_data.field_infos.at(default_idx); - if (internal::dataToString(info.value) == internal::dataToString(default_info.value)) { - info.is_default = true; - } + info.default_value = default_info.value; } } +template (), bool>::type> +FieldInputInfo::Ptr Visitor::getFieldInputInfo() { + return nullptr; +} + +template (), bool>::type> +FieldInputInfo::Ptr Visitor::getFieldInputInfo() { + return Conversion::getFieldInputInfo(); +} + } // namespace config::internal diff --git a/config_utilities/include/config_utilities/internal/yaml_parser.h b/config_utilities/include/config_utilities/internal/yaml_parser.h index 43cf2b8..ada5373 100644 --- a/config_utilities/include/config_utilities/internal/yaml_parser.h +++ b/config_utilities/include/config_utilities/internal/yaml_parser.h @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,29 @@ class YamlParser { YamlParser() = default; ~YamlParser() = default; + /** + * @brief Parse a single node to a value. If the conversion fails, a warning is issued and the value is not modified. + * @param node The yaml node to parse the value from. + * @param error Optional: Where to store the error message if conversion fails. + */ + template + static std::optional fromYaml(const YAML::Node& node, std::string* error = nullptr) { + auto value = T(); + std::string err; + try { + fromYamlImpl(value, node, err); + } catch (const std::exception& e) { + err = std::string(e.what()); + } + if (!err.empty()) { + if (error) { + *error = err; + } + return std::nullopt; + } + return value; + } + /** * @brief Parse a value from the yaml node. If the value is not found, the value is not modified, and thus should * remain the default value. If the value is found, but the conversion fails, a warning is issued and the value is @@ -91,6 +115,29 @@ class YamlParser { return error.empty(); } + /** + * @brief Parse a single value to the yaml node. + * @param value The value to parse. + * @param error Optional: Where to store the error message if conversion fails. + */ + template + static YAML::Node toYaml(const T& value, std::string* error = nullptr) { + YAML::Node node; + std::string err; + try { + node = toYamlImpl("", value, err); + } catch (const std::exception& e) { + err = std::string(e.what()); + } + if (!err.empty()) { + if (error) { + *error = err; + } + return YAML::Node(YAML::NodeType::Null); + } + return node; + } + /** * @brief Parse a C++ value to the yaml node. If the conversion fails, a warning is issued and the node is not * modified. diff --git a/config_utilities/include/config_utilities/traits.h b/config_utilities/include/config_utilities/traits.h index 90e55e3..ec3a676 100644 --- a/config_utilities/include/config_utilities/traits.h +++ b/config_utilities/include/config_utilities/traits.h @@ -38,6 +38,8 @@ #include #include +#include "config_utilities/internal/field_input_info.h" + namespace config { namespace internal { @@ -52,6 +54,13 @@ struct is_config_impl()) template struct is_virtual_config : std::false_type {}; +// Check whether conversions implement input info. +template +struct conversion_has_input_info_impl : std::false_type {}; + +template +struct conversion_has_input_info_impl> : std::true_type {}; + // ODR workaround template constexpr T static_const{}; @@ -72,8 +81,16 @@ constexpr bool isConfig() { } template -constexpr bool isConfig(const T& config) { +constexpr bool isConfig(const T& /* config */) { return internal::is_config_impl::value; } +/** + * @brief Check whether a conversion implements input information. + */ +template +constexpr bool hasFieldInputInfo() { + return internal::conversion_has_input_info_impl::value; +} + } // namespace config diff --git a/config_utilities/include/config_utilities/types/enum.h b/config_utilities/include/config_utilities/types/enum.h index 97f8b5f..2a8c465 100644 --- a/config_utilities/include/config_utilities/types/enum.h +++ b/config_utilities/include/config_utilities/types/enum.h @@ -36,6 +36,7 @@ #pragma once #include +#include #include #include #include @@ -64,7 +65,6 @@ std::map createEnumMap(const std::vector& enum_ return enum_map; } - /** * @brief A struct that provides conversion between an ennum type and its string representation. The enum definition can * provides interfaces for user-side conversion, and is an automatic type converter for config field parsing. @@ -128,19 +128,6 @@ struct Enum { explicit Initializer(const std::map& enum_names) { setNames(enum_names); } }; - private: - friend internal::Visitor; - template - friend void enum_field(T&, const std::string&, const std::map&); - - // Singleton implementation as initialization order of static variables is not guaranteed. - Enum() = default; - - static Enum& instance() { - static Enum instance; - return instance; - } - // Interfaces to work as a type converter for config field parsing. static std::string toIntermediate(EnumT value, std::string& error) { std::string result; @@ -159,6 +146,28 @@ struct Enum { } } + static internal::FieldInputInfo::Ptr getFieldInputInfo() { + std::lock_guard lock(instance().mutex_); + auto info = std::make_shared(); + for (const auto& [value, name] : instance().enum_names_) { + info->options.push_back(name); + } + return info; + } + + private: + friend internal::Visitor; + template + friend void enum_field(T&, const std::string&, const std::map&); + + // Singleton implementation as initialization order of static variables is not guaranteed. + Enum() = default; + + static Enum& instance() { + static Enum instance; + return instance; + } + // Tools to print the enum names and values for error messages. These are therefore not locked. static std::string printNameList() { std::string result; diff --git a/config_utilities/src/asl_formatter.cpp b/config_utilities/src/asl_formatter.cpp index 5f7f2a9..1de111c 100644 --- a/config_utilities/src/asl_formatter.cpp +++ b/config_utilities/src/asl_formatter.cpp @@ -225,7 +225,7 @@ std::string AslFormatter::formatSubconfig(const MetaData& data, size_t indent) c if (Settings::instance().indicate_default_values && indicate_subconfig_default_ && !data.is_virtual_config) { bool is_default = true; for (const FieldInfo& info : data.field_infos) { - if (!info.is_default) { + if (!info.isDefault()) { is_default = false; break; } @@ -249,7 +249,7 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons // field is the stringified value, The header is the field name. std::string field = dataToString(info.value, reformat_floats); - if (info.is_default && Settings::instance().indicate_default_values) { + if (info.isDefault() && Settings::instance().indicate_default_values) { field += " (default)"; } std::string header = std::string(indent, ' ') + info.name; diff --git a/config_utilities/src/field_input_info.cpp b/config_utilities/src/field_input_info.cpp new file mode 100644 index 0000000..afdc844 --- /dev/null +++ b/config_utilities/src/field_input_info.cpp @@ -0,0 +1,291 @@ +/** ----------------------------------------------------------------------------- + * 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/internal/field_input_info.h" + +#include +#include // TMP +#include + +#include "config_utilities/internal/yaml_parser.h" + +namespace config::internal { + +std::string FieldInputInfo::typeToString(Type type) { + switch (type) { + case Type::kBool: + return "bool"; + case Type::kInt: + return "int"; + case Type::kFloat: + return "float"; + case Type::kString: + return "string"; + case Type::kOptions: + return "options"; + case Type::kYAML: + return "yaml"; + } + return "unknown"; +} + +YAML::Node FieldInputInfo::toYaml() const { + YAML::Node node; + node["type"] = typeToString(type); + return node; +} + +FieldInputInfo::Ptr FieldInputInfo::merge(const FieldInputInfo::Ptr& from, const FieldInputInfo::Ptr& to) { + if (!from) { + return to; + } + if (!to) { + return from; + } + + if (from->type == to->type) { + to->mergeSame(*from); + return to; + } + + if (to->type == FieldInputInfo::Type::kOptions) { + // Options will always overwrite other types. + return to; + } + if (from->type == FieldInputInfo::Type::kOptions) { + return from; + } + + // For general conflicts resort to YAML and let the configs sort it out. + return std::make_shared(FieldInputInfo::Type::kYAML); +} + +YAML::Node IntFieldInputInfo::toYaml() const { + YAML::Node node; + node["type"] = "int"; + node["min"] = min; + node["max"] = max; + // Only store the rarer cases. + if (!lower_inclusive) { + node["lower_exclusive"] = true; + } + if (!upper_inclusive) { + node["upper_exclusive"] = true; + } + return node; +} + +void IntFieldInputInfo::mergeSame(const FieldInputInfo& other) { + const auto& other_info = dynamic_cast(other); + if (min > other_info.min) { + min = other_info.min; + lower_inclusive = other_info.lower_inclusive; + } else if (min == other_info.min) { + lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } + if (max < other_info.max) { + max = other_info.max; + upper_inclusive = other_info.upper_inclusive; + } else if (max == other_info.max) { + upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } +} + +void IntFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { + auto val = YamlParser::fromYaml(min); + if (!val) { + std::cout << "Failed to parse min int value: " << min << std::endl; + return; + } + this->min = *val; + this->lower_inclusive = lower_inclusive; + std::cout << "Set int min: " << this->min << std::endl; +} + +void IntFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { + auto val = YamlParser::fromYaml(max); + if (!val) { + std::cout << "Failed to parse max int value: " << min << std::endl; + return; + } + this->max = *val; + this->upper_inclusive = upper_inclusive; + std::cout << "Set int max: " << this->max << std::endl; +} + +YAML::Node FloatFieldInputInfo::toYaml() const { + YAML::Node node; + node["type"] = "float"; + node["min"] = min; + node["max"] = max; + if (!lower_inclusive) { + node["lower_exclusive"] = true; + } + if (!upper_inclusive) { + node["upper_exclusive"] = true; + } + return node; +} + +void FloatFieldInputInfo::mergeSame(const FieldInputInfo& other) { + const auto& other_info = dynamic_cast(other); + if (min > other_info.min) { + min = other_info.min; + lower_inclusive = other_info.lower_inclusive; + } else if (min == other_info.min) { + lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } + if (max < other_info.max) { + max = other_info.max; + upper_inclusive = other_info.upper_inclusive; + } else if (max == other_info.max) { + upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } +} + +void FloatFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { + auto val = YamlParser::fromYaml(min); + if (!val) { + std::cout << "Failed to parse min float value: " << min << std::endl; + return; + } + this->min = *val; + this->lower_inclusive = lower_inclusive; + std::cout << "Set float min: " << this->min << std::endl; +} + +void FloatFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { + auto val = YamlParser::fromYaml(max); + if (!val) { + std::cout << "Failed to parse max float value: " << min << std::endl; + return; + } + this->max = *val; + this->upper_inclusive = upper_inclusive; + std::cout << "Set float max: " << this->max << std::endl; +} + +YAML::Node OptionsFieldInputInfo::toYaml() const { + YAML::Node node; + node["type"] = "options"; + node["options"] = options; + return node; +} + +void OptionsFieldInputInfo::mergeSame(const FieldInputInfo& other) { + const auto& other_info = dynamic_cast(other); + // Intersection of options. + std::unordered_set prev_options(options.begin(), options.end()); + options.clear(); + for (const auto& option : other_info.options) { + if (prev_options.count(option)) { + options.push_back(option); + } + } +} + +// Helper function for int types. +template +FieldInputInfo::Ptr createNumericInfo() { + auto info = std::make_shared(); + info->min = std::numeric_limits::lowest(); + info->max = std::numeric_limits::max(); + return info; +} + +// Bool. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared(FieldInputInfo::Type::kBool); +} + +// Ints. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return createNumericInfo(); +} + +// Floats. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared(); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared(); +} + +// Strings. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared(FieldInputInfo::Type::kString); +} + +} // namespace config::internal diff --git a/config_utilities/src/meta_data.cpp b/config_utilities/src/meta_data.cpp index 59b5eef..911941d 100644 --- a/config_utilities/src/meta_data.cpp +++ b/config_utilities/src/meta_data.cpp @@ -35,8 +35,16 @@ #include "config_utilities/internal/meta_data.h" +#include "config_utilities/internal/string_utils.h" + namespace config::internal { +bool FieldInfo::isDefault() const { + // NOTE(lschmid): Operator YAML::Node== checks for identity, not equality. Since these are all scalars, comparing + // the formatted strings should be identical. + return internal::dataToString(value) == internal::dataToString(default_value); +} + bool MetaData::hasErrors() const { if (!errors.empty()) { return true; @@ -81,4 +89,55 @@ void MetaData::performOnAll(const std::function& func) co } } +YAML::Node FieldInfo::serializeFieldInfos() const { + YAML::Node result; + result["type"] = "field"; + result["name"] = name; + if (!unit.empty()) { + result["unit"] = unit; + } + result["value"] = YAML::Clone(value); + result["default"] = YAML::Clone(default_value); + if (was_parsed) { + result["was_parsed"] = true; + } + if (input_info) { + result["input_info"] = input_info->toYaml(); + } + return result; +} + +YAML::Node MetaData::serializeFieldInfos() const { + YAML::Node result; + // Log the config. + result["type"] = "config"; + result["name"] = name; + if (!field_name.empty()) { + result["field_name"] = field_name; + } + if (is_virtual_config) { + result["is_virtual"] = true; + } + if (array_config_index >= 0) { + result["array_index"] = array_config_index; + } + if (map_config_key) { + result["map_config_key"] = map_config_key.value(); + } + YAML::Node fields; + + // Parse the direct fields. + for (const FieldInfo& info : field_infos) { + fields.push_back(info.serializeFieldInfos()); + } + + // Parse the sub-configs. + for (const MetaData& sub_data : sub_configs) { + fields.push_back(sub_data.serializeFieldInfos()); + } + + result["fields"] = fields; + return result; +} + } // namespace config::internal diff --git a/config_utilities/src/visitor.cpp b/config_utilities/src/visitor.cpp index 49aaadc..3ae9399 100644 --- a/config_utilities/src/visitor.cpp +++ b/config_utilities/src/visitor.cpp @@ -75,10 +75,9 @@ void Visitor::visitName(const std::string& name) { void Visitor::visitCheck(const CheckBase& check) { Visitor& visitor = Visitor::instance(); - if (visitor.mode != Visitor::Mode::kCheck) { - return; + if (visitor.mode == Visitor::Mode::kCheck || visitor.mode == Visitor::Mode::kGetInfo) { + visitor.data.checks.emplace_back(check.clone()); } - visitor.data.checks.emplace_back(check.clone()); } std::optional Visitor::visitVirtualConfig(bool is_set, bool is_optional, const std::string& type) { @@ -96,7 +95,7 @@ std::optional Visitor::visitVirtualConfig(bool is_set, bool is_optio } } - if (visitor.mode == Visitor::Mode::kGet) { + if (visitor.mode == Visitor::Mode::kGet || visitor.mode == Visitor::Mode::kGetInfo) { if (is_set) { // Also write the type param back to file. std::string error; diff --git a/config_utilities/test/tests/conversions.cpp b/config_utilities/test/tests/conversions.cpp index a2fa649..f72a4d6 100644 --- a/config_utilities/test/tests/conversions.cpp +++ b/config_utilities/test/tests/conversions.cpp @@ -40,11 +40,26 @@ #include #include "config_utilities/config.h" +#include "config_utilities/internal/visitor.h" #include "config_utilities/parsing/yaml.h" #include "config_utilities/printing.h" namespace config::test { +struct TestConversion { + static std::string toIntermediate(int value, std::string& error) { return std::to_string(value); } + static void fromIntermediate(const std::string& intermediate, int& value, std::string& error) { + value = std::stoi(intermediate); + } + + // Optional: Define this to provide a field input info. + static internal::FieldInputInfo::Ptr getFieldInputInfo() { + auto info = std::make_shared(); + info->options = {"OptionFromTestConversion"}; + return info; + } +}; + template std::string toYamlString(const T& conf) { const auto data = internal::Visitor::getValues(conf); @@ -64,6 +79,10 @@ struct NoConversionStruct { uint8_t some_character = 'a'; }; +struct TestConversionStruct { + int test = 0; +}; + void declare_config(ConversionStruct& conf) { field(conf.num_threads, "num_threads"); field(conf.some_character, "some_character"); @@ -74,6 +93,8 @@ void declare_config(NoConversionStruct& conf) { field(conf.some_character, "some_character"); } +void declare_config(TestConversionStruct& conf) { field(conf.test, "test"); } + // tests that we pull the right character from a string TEST(Conversions, CharConversionCorrect) { std::string normal = "h"; @@ -163,4 +184,26 @@ some_character: 5 EXPECT_EQ(toYamlString(no_conv), yaml_string); } +TEST(Conversions, FieldInputInfo) { + // Test SFINAE traits. + EXPECT_FALSE(hasFieldInputInfo()); + EXPECT_TRUE(hasFieldInputInfo()); + + // Get info from the conversion. + TestConversionStruct with_info; + auto data = internal::Visitor::getInfo(with_info); + EXPECT_EQ(data.field_infos.size(), 1); + EXPECT_TRUE(data.field_infos[0].input_info); + EXPECT_EQ(data.field_infos[0].input_info->type, internal::FieldInputInfo::Type::kOptions); + auto options = std::dynamic_pointer_cast(data.field_infos[0].input_info)->options; + EXPECT_EQ(options.size(), 1); + EXPECT_EQ(options[0], "OptionFromTestConversion"); + + ConversionStruct without_info; + data = internal::Visitor::getInfo(without_info); + EXPECT_EQ(data.field_infos.size(), 2); + EXPECT_FALSE(data.field_infos[0].input_info); + EXPECT_FALSE(data.field_infos[1].input_info); +} + } // namespace config::test diff --git a/config_utilities/test/tests/field_input_info.cpp b/config_utilities/test/tests/field_input_info.cpp new file mode 100644 index 0000000..913add3 --- /dev/null +++ b/config_utilities/test/tests/field_input_info.cpp @@ -0,0 +1,228 @@ +/** ----------------------------------------------------------------------------- + * 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 +#include + +#include "config_utilities/test/default_config.h" +#include "config_utilities/test/utils.h" + +namespace config::test { + +TEST(FieldInputInfo, GetInfo) { + DefaultConfig config; + const internal::MetaData data = internal::Visitor::getInfo(config); + auto info = data.serializeFieldInfos(); + const std::string expected = R"( +type: config +name: DefaultConfig +fields: + - type: field + name: i + unit: m + value: 1 + default: 1 + input_info: + type: int + min: -2147483648 + max: 2147483647 + - type: field + name: f + unit: s + value: 2.1 + default: 2.1 + input_info: + type: float + min: -inf + max: inf + - type: field + name: d + unit: m/s + value: 3.2 + default: 3.2 + input_info: + type: float + min: -inf + max: inf + - type: field + name: b + value: true + default: true + input_info: + type: bool + - type: field + name: u8 + value: 4 + default: 4 + input_info: + type: int + min: 0 + max: 255 + - type: field + name: s + value: test string + default: test string + input_info: + type: string + - type: field + name: vec + unit: frames + value: + - 1 + - 2 + - 3 + default: + - 1 + - 2 + - 3 + input_info: + type: yaml + - type: field + name: map + value: + a: 1 + b: 2 + c: 3 + default: + a: 1 + b: 2 + c: 3 + input_info: + type: yaml + - type: field + name: set + value: + - 1.1 + - 2.2 + - 3.3 + default: + - 1.1 + - 2.2 + - 3.3 + input_info: + type: yaml + - type: field + name: mat + value: + - + - 1 + - 0 + - 0 + - + - 0 + - 1 + - 0 + - + - 0 + - 0 + - 1 + default: + - + - 1 + - 0 + - 0 + - + - 0 + - 1 + - 0 + - + - 0 + - 0 + - 1 + input_info: + type: yaml + - type: field + name: my_enum + value: A + default: A + input_info: + type: options + options: + - A + - B + - C + - type: field + name: my_strange_enum + value: X + default: X + input_info: + type: options + options: + - Z + - X + - Y + - type: config + name: SubConfig + field_name: sub_config + fields: + - type: field + name: i + value: 1 + default: 1 + input_info: + type: int + min: -2147483648 + max: 2147483647 + - type: config + name: SubSubConfig + field_name: sub_sub_config + fields: + - type: field + name: i + value: 1 + default: 1 + input_info: + type: int + min: -2147483648 + max: 2147483647 + - type: config + name: SubSubConfig + field_name: sub_sub_config + fields: + - type: field + name: i + value: 1 + default: 1 + input_info: + type: int + min: -2147483648 + max: 2147483647 +)"; + // expectEqual(info, YAML::Load(expected)); + + // std::cout << info << std::endl; +} + +} // namespace config::test diff --git a/config_utilities/test/tests/yaml_parsing.cpp b/config_utilities/test/tests/yaml_parsing.cpp index c28657e..1837456 100644 --- a/config_utilities/test/tests/yaml_parsing.cpp +++ b/config_utilities/test/tests/yaml_parsing.cpp @@ -220,7 +220,7 @@ TEST(YamlParsing, getValues) { EXPECT_EQ(meta_data.errors.size(), 0ul); meta_data.performOnAll([](const internal::MetaData& d) { for (const auto& field : d.field_infos) { - EXPECT_TRUE(field.is_default); + EXPECT_TRUE(field.isDefault()); } }); EXPECT_EQ(meta_data.name, "DefaultConfig"); @@ -233,7 +233,7 @@ TEST(YamlParsing, getValues) { EXPECT_EQ(meta_data.errors.size(), 0ul); meta_data.performOnAll([](const internal::MetaData& d) { for (const auto& field : d.field_infos) { - EXPECT_FALSE(field.is_default); + EXPECT_FALSE(field.isDefault()); } }); } diff --git a/docs/Advanced.md b/docs/Advanced.md index a8fbf9e..8cf97c7 100644 --- a/docs/Advanced.md +++ b/docs/Advanced.md @@ -26,11 +26,16 @@ struct convert { ## Adding custom conversions To implement custom field conversions, you can create a conversion struct. The struct must implement two static conversion functions `toIntermediate` and `fromIntermediate`, where intermediate is a yaml-castable type. Examples of this are given in `types/conversions.h`. +Conversions can also optionally implement a `getFieldInputInfo()` function to return additional field info constraints when getting config infos. If this function is not implemented, no additional field info will be issued. + ```c++ struct MyConversion { // If conversion fails, set 'error' to the failure message. static IntermediateType toIntermediate(MyType value, std::string& error); static void fromIntermediate(const IntermediateType& intermediate, MyType& value, std::string& error); + + // Optionally provide more field info. Must have exaxctly this signature. + static config::internal::FieldInputInfo::Ptr getFieldInputInfo(); }; ``` From aef9f0818cf4d13c9c18f134434267d9a3d516af Mon Sep 17 00:00:00 2001 From: lschmid Date: Thu, 15 Aug 2024 20:25:14 -0400 Subject: [PATCH 06/22] fix info extraction --- config_utilities/CMakeLists.txt | 1 + .../config_utilities/internal/checks.h | 4 +-- .../config_utilities/internal/string_utils.h | 2 +- .../config_utilities/internal/yaml_parser.h | 6 +++-- .../include/config_utilities/traits.h | 2 -- config_utilities/src/field_input_info.cpp | 17 +++--------- config_utilities/test/CMakeLists.txt | 1 + .../test/tests/field_input_info.cpp | 27 ++++++++++--------- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/config_utilities/CMakeLists.txt b/config_utilities/CMakeLists.txt index d152424..e1f77b3 100644 --- a/config_utilities/CMakeLists.txt +++ b/config_utilities/CMakeLists.txt @@ -31,6 +31,7 @@ add_library( src/external_registry.cpp src/factory.cpp src/formatter.cpp + src/field_input_info.cpp src/log_to_stdout.cpp src/logger.cpp src/meta_data.cpp diff --git a/config_utilities/include/config_utilities/internal/checks.h b/config_utilities/include/config_utilities/internal/checks.h index 7bb85dd..6c4f43f 100644 --- a/config_utilities/include/config_utilities/internal/checks.h +++ b/config_utilities/include/config_utilities/internal/checks.h @@ -108,7 +108,7 @@ class BinaryCheck : public CheckBase { if (!info || (info->type != FieldInputInfo::Type::kInt && info->type != FieldInputInfo::Type::kFloat)) { return nullptr; } - YAML::Node value = YamlParser::toYaml(value_); + YAML::Node value = YamlParser::toYaml(value_); if (!value) { return nullptr; } @@ -209,7 +209,7 @@ class CheckRange : public CheckBase { return nullptr; } info->setMin(YamlParser::toYaml(lower_), lower_inclusive_); - info->setMax(YamlParser::toYaml(lower_), upper_inclusive_); + info->setMax(YamlParser::toYaml(upper_), upper_inclusive_); return info; } diff --git a/config_utilities/include/config_utilities/internal/string_utils.h b/config_utilities/include/config_utilities/internal/string_utils.h index 7e25191..2681282 100644 --- a/config_utilities/include/config_utilities/internal/string_utils.h +++ b/config_utilities/include/config_utilities/internal/string_utils.h @@ -40,7 +40,7 @@ #include #include -#include "config_utilities/internal/meta_data.h" +#include // clang-format off #ifdef __GNUG__ diff --git a/config_utilities/include/config_utilities/internal/yaml_parser.h b/config_utilities/include/config_utilities/internal/yaml_parser.h index ada5373..9703832 100644 --- a/config_utilities/include/config_utilities/internal/yaml_parser.h +++ b/config_utilities/include/config_utilities/internal/yaml_parser.h @@ -48,6 +48,7 @@ #include "config_utilities/internal/yaml_utils.h" #include "config_utilities/traits.h" + namespace config::internal { /** @@ -125,7 +126,7 @@ class YamlParser { YAML::Node node; std::string err; try { - node = toYamlImpl("", value, err); + node = toYamlImpl("tmp", value, err); } catch (const std::exception& e) { err = std::string(e.what()); } @@ -135,7 +136,7 @@ class YamlParser { } return YAML::Node(YAML::NodeType::Null); } - return node; + return node["tmp"]; } /** @@ -206,6 +207,7 @@ class YamlParser { return node; } + // Set. template static void fromYamlImpl(std::set& value, const YAML::Node& node, std::string& error) { diff --git a/config_utilities/include/config_utilities/traits.h b/config_utilities/include/config_utilities/traits.h index ec3a676..4b4c7a3 100644 --- a/config_utilities/include/config_utilities/traits.h +++ b/config_utilities/include/config_utilities/traits.h @@ -38,8 +38,6 @@ #include #include -#include "config_utilities/internal/field_input_info.h" - namespace config { namespace internal { diff --git a/config_utilities/src/field_input_info.cpp b/config_utilities/src/field_input_info.cpp index afdc844..c877b9b 100644 --- a/config_utilities/src/field_input_info.cpp +++ b/config_utilities/src/field_input_info.cpp @@ -35,7 +35,6 @@ #include "config_utilities/internal/field_input_info.h" #include -#include // TMP #include #include "config_utilities/internal/yaml_parser.h" @@ -108,13 +107,13 @@ YAML::Node IntFieldInputInfo::toYaml() const { void IntFieldInputInfo::mergeSame(const FieldInputInfo& other) { const auto& other_info = dynamic_cast(other); - if (min > other_info.min) { + if (min < other_info.min) { min = other_info.min; lower_inclusive = other_info.lower_inclusive; } else if (min == other_info.min) { lower_inclusive = lower_inclusive && other_info.lower_inclusive; } - if (max < other_info.max) { + if (max > other_info.max) { max = other_info.max; upper_inclusive = other_info.upper_inclusive; } else if (max == other_info.max) { @@ -125,23 +124,19 @@ void IntFieldInputInfo::mergeSame(const FieldInputInfo& other) { void IntFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { auto val = YamlParser::fromYaml(min); if (!val) { - std::cout << "Failed to parse min int value: " << min << std::endl; return; } this->min = *val; this->lower_inclusive = lower_inclusive; - std::cout << "Set int min: " << this->min << std::endl; } void IntFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { auto val = YamlParser::fromYaml(max); if (!val) { - std::cout << "Failed to parse max int value: " << min << std::endl; return; } this->max = *val; this->upper_inclusive = upper_inclusive; - std::cout << "Set int max: " << this->max << std::endl; } YAML::Node FloatFieldInputInfo::toYaml() const { @@ -160,13 +155,13 @@ YAML::Node FloatFieldInputInfo::toYaml() const { void FloatFieldInputInfo::mergeSame(const FieldInputInfo& other) { const auto& other_info = dynamic_cast(other); - if (min > other_info.min) { + if (min < other_info.min) { min = other_info.min; lower_inclusive = other_info.lower_inclusive; } else if (min == other_info.min) { lower_inclusive = lower_inclusive && other_info.lower_inclusive; } - if (max < other_info.max) { + if (max > other_info.max) { max = other_info.max; upper_inclusive = other_info.upper_inclusive; } else if (max == other_info.max) { @@ -177,23 +172,19 @@ void FloatFieldInputInfo::mergeSame(const FieldInputInfo& other) { void FloatFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { auto val = YamlParser::fromYaml(min); if (!val) { - std::cout << "Failed to parse min float value: " << min << std::endl; return; } this->min = *val; this->lower_inclusive = lower_inclusive; - std::cout << "Set float min: " << this->min << std::endl; } void FloatFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { auto val = YamlParser::fromYaml(max); if (!val) { - std::cout << "Failed to parse max float value: " << min << std::endl; return; } this->max = *val; this->upper_inclusive = upper_inclusive; - std::cout << "Set float max: " << this->max << std::endl; } YAML::Node OptionsFieldInputInfo::toYaml() const { diff --git a/config_utilities/test/CMakeLists.txt b/config_utilities/test/CMakeLists.txt index d1ed8a3..97e7ec8 100644 --- a/config_utilities/test/CMakeLists.txt +++ b/config_utilities/test/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable( tests/enums.cpp tests/external_registry.cpp tests/factory.cpp + tests/field_input_info.cpp tests/inheritance.cpp tests/missing_fields.cpp tests/namespacing.cpp diff --git a/config_utilities/test/tests/field_input_info.cpp b/config_utilities/test/tests/field_input_info.cpp index 913add3..8544117 100644 --- a/config_utilities/test/tests/field_input_info.cpp +++ b/config_utilities/test/tests/field_input_info.cpp @@ -56,8 +56,9 @@ name: DefaultConfig default: 1 input_info: type: int - min: -2147483648 + min: 0 max: 2147483647 + lower_exclusive: true - type: field name: f unit: s @@ -65,8 +66,8 @@ name: DefaultConfig default: 2.1 input_info: type: float - min: -inf - max: inf + min: 0 + max: 1.844674407370955e+19 - type: field name: d unit: m/s @@ -74,8 +75,9 @@ name: DefaultConfig default: 3.2 input_info: type: float - min: -inf - max: inf + min: 0 + max: 4 + upper_exclusive: true - type: field name: b value: true @@ -89,7 +91,7 @@ name: DefaultConfig input_info: type: int min: 0 - max: 255 + max: 5 - type: field name: s value: test string @@ -193,8 +195,9 @@ name: DefaultConfig default: 1 input_info: type: int - min: -2147483648 + min: 0 max: 2147483647 + lower_exclusive: true - type: config name: SubSubConfig field_name: sub_sub_config @@ -205,8 +208,9 @@ name: DefaultConfig default: 1 input_info: type: int - min: -2147483648 + min: 0 max: 2147483647 + lower_exclusive: true - type: config name: SubSubConfig field_name: sub_sub_config @@ -217,12 +221,11 @@ name: DefaultConfig default: 1 input_info: type: int - min: -2147483648 + min: 0 max: 2147483647 + lower_exclusive: true )"; - // expectEqual(info, YAML::Load(expected)); - - // std::cout << info << std::endl; + expectEqual(info, YAML::Load(expected)); } } // namespace config::test From 52d11dfd1d5e4ff2deef911c7a0110c639848752 Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 16 Aug 2024 10:03:04 -0400 Subject: [PATCH 07/22] refactor settings --- config_utilities/demos/demo_config.cpp | 2 +- config_utilities/demos/demo_factory.cpp | 2 +- config_utilities/demos/demo_inheritance.cpp | 2 +- .../config_utilities/internal/meta_data.h | 2 +- .../internal/visitor_impl.hpp | 4 +- .../include/config_utilities/settings.h | 84 ++++++++++++------- config_utilities/src/asl_formatter.cpp | 80 +++++++++--------- config_utilities/src/external_registry.cpp | 8 +- config_utilities/src/factory.cpp | 2 +- config_utilities/src/settings.cpp | 35 ++++++++ config_utilities/src/visitor.cpp | 2 +- config_utilities/test/tests/asl_formatter.cpp | 38 ++++----- config_utilities/test/tests/config_arrays.cpp | 2 +- config_utilities/test/tests/config_maps.cpp | 2 +- config_utilities/test/tests/factory.cpp | 4 +- docs/External.md | 6 +- docs/Varia.md | 17 +--- 17 files changed, 172 insertions(+), 120 deletions(-) diff --git a/config_utilities/demos/demo_config.cpp b/config_utilities/demos/demo_config.cpp index 22b3437..68f2406 100644 --- a/config_utilities/demos/demo_config.cpp +++ b/config_utilities/demos/demo_config.cpp @@ -173,7 +173,7 @@ int main(int argc, char** argv) { 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; + config::Settings().printing.inline_subconfigs = true; // ===================================== Checking whether a struct is a config ===================================== std::cout << "\n\n----- Checking whether a struct is a config -----\n\n" << std::endl; diff --git a/config_utilities/demos/demo_factory.cpp b/config_utilities/demos/demo_factory.cpp index 9d60243..507d76c 100644 --- a/config_utilities/demos/demo_factory.cpp +++ b/config_utilities/demos/demo_factory.cpp @@ -232,7 +232,7 @@ int main(int argc, char** argv) { std::cout << "\n\n----- Creating objects from file -----\n\n" << std::endl; // Optionally specify the name of the type-identifying param. Default is 'type'. - config::Settings().factory_type_param_name = "type"; + config::Settings().factory.type_param_name = "type"; // Create an object of type and with config as specified in a file. object = config::createFromYamlFile(my_root_path + "factory.yaml", 123); diff --git a/config_utilities/demos/demo_inheritance.cpp b/config_utilities/demos/demo_inheritance.cpp index 85dc451..8a97952 100644 --- a/config_utilities/demos/demo_inheritance.cpp +++ b/config_utilities/demos/demo_inheritance.cpp @@ -144,7 +144,7 @@ int main(int argc, char** argv) { const std::string my_root_path = std::string(argv[1]) + "/"; - config::Settings().inline_subconfig_field_names = true; + config::Settings().printing.inline_subconfigs = true; // ===================================== Checking whether a struct is a config ===================================== diff --git a/config_utilities/include/config_utilities/internal/meta_data.h b/config_utilities/include/config_utilities/internal/meta_data.h index eb00160..0d96688 100644 --- a/config_utilities/include/config_utilities/internal/meta_data.h +++ b/config_utilities/include/config_utilities/internal/meta_data.h @@ -65,7 +65,7 @@ struct FieldInfo { //! The default value of the field if the field is not a config. YAML::Node default_value; - //! Whether the field corresponds to its default value. Only queried if Settings().indicate_default_values is true. + //! Whether the field corresponds to its default value. Only queried if Settings().printing.show_defaults is true. bool isDefault() const; //! Whether or not the field was parsed diff --git a/config_utilities/include/config_utilities/internal/visitor_impl.hpp b/config_utilities/include/config_utilities/internal/visitor_impl.hpp index c32d031..46756d1 100644 --- a/config_utilities/include/config_utilities/internal/visitor_impl.hpp +++ b/config_utilities/include/config_utilities/internal/visitor_impl.hpp @@ -68,7 +68,7 @@ MetaData Visitor::setValues(ConfigT& config, Logger::logWarning(Formatter::formatErrors(visitor.data, "Errors parsing config", Severity::kWarning)); } - if (print_missing && Settings::instance().print_missing && visitor.data.hasMissing()) { + if (print_missing && Settings::instance().printing.show_missing && visitor.data.hasMissing()) { Logger::logWarning(Formatter::formatMissing(visitor.data, "Missing fields from config", Severity::kWarning)); } @@ -84,7 +84,7 @@ MetaData Visitor::getValues(const ConfigT& config, // NOTE(lschmid): We know that in mode kGet, the config is not modified. ::config::declare_config(const_cast(config)); - if (Settings::instance().indicate_default_values) { + if (Settings::instance().printing.show_defaults) { Visitor::getDefaultValues(config, visitor.data); } if (print_warnings && visitor.data.hasErrors()) { diff --git a/config_utilities/include/config_utilities/settings.h b/config_utilities/include/config_utilities/settings.h index 4371843..cda7e6f 100644 --- a/config_utilities/include/config_utilities/settings.h +++ b/config_utilities/include/config_utilities/settings.h @@ -49,47 +49,67 @@ struct Settings { // Singleton access to the global settings. static Settings& instance(); - /* Printing Settings. */ - // TODO(lschmid): These should probably be moved into a config or so for different formatters. - // @brief Width of the 'toString()' output of configs. - unsigned int print_width = 80u; + /** + * @brief Settings for how and what of configs is printed. + */ + struct Printing { + //! @brief Width of the 'toString()' output of configs. + unsigned int width = 80u; - // @brief Indent after which values are printed. - unsigned int print_indent = 30u; + //! @brief Indent after which values are printed aftert the field name. + unsigned indent = 30u; - // @brief Indent for nested configs. - unsigned int subconfig_indent = 3u; + //! @brief Indent for nested configs. + unsigned int subconfig_indent = 3u; - // @brief If true, indicate which values are identical to the default. - bool indicate_default_values = true; + //! @brief If true, indicate which values are identical to the default. + bool show_defaults = true; - // @brief If true, also display the unit of each parameter where provided. - bool indicate_units = true; + //! @brief If true, also display the unit of each parameter where provided. + bool show_units = true; - // @brief If true integrate subconfig fields into the main config, if false print them separately. - bool inline_subconfig_field_names = true; + //! @brief If true integrate subconfig fields into the main config, if false print them as individual configs. + bool inline_subconfigs = true; - // @brief If true, store all validated configs for global printing. - bool store_valid_configs = true; + //! @brief If true, attempts to print floats and float-like fields with default stream precision. + bool reformat_floats = true; - // @brief If true, attempts to print floats and float-like fields with default stream precision - bool reformat_floats = true; + //! @brief If true, prints fields that had no value present when being parsed. + // TODO(lschmid): I think the implementation of was_parsed is actually also empty if parsing fails in some cases, + // could double check if that's inportant. + bool show_missing = false; - // @brief If true, prints fields that had no value present when being parsed - bool print_missing = false; + //! @brief If true, print the type of subconfigs in the output. + bool show_subconfig_types = true; - /* Factory settings */ - // @brief The factory will look for this param to deduce the type of the object to be created. - std::string factory_type_param_name = "type"; + //! @brief If true, indicate that a field is a virtual field in the output. + bool show_virtual_configs = true; - //! @brief Whether or not loading external libraries are enabled - bool allow_external_libraries = true; + //! @brief If true show the enumeration info of failed checks in the output. + bool show_num_checks = true; + } printing; - //! @brief Whether or not loading and unloading libraries should be verbose - bool verbose_external_load = true; + /** + * @brief Settings for factory type registration and object creation + */ + struct Factory { + //! @brief The factory will look for this param to deduce the type of the object to be created. + std::string type_param_name = "type"; + } factory; - //! @brief Log any factory creation from an external library (for debugging purposes) - bool print_external_allocations = false; + /** + * @brief Settings to load external libraries and their modules into the factories. + */ + struct ExternalLibraries { + //! @brief Whether or not loading external libraries are enabled + bool enabled = true; + + //! @brief Whether or not loading and unloading libraries should be verbose + bool verbose_load = true; + + //! @brief Log any factory creation from an external library (for debugging purposes) + bool log_allocation = false; + } external_libraries; /* Options to specify the logger and formatter at run time. */ // Specify the default logger to be used for printing. Loggers register themselves if included. @@ -108,6 +128,12 @@ struct Settings { static Settings instance_; }; +// Define global settings as configs so they can be set/get with any interface. +void declare_config(Settings& config); +void declare_config(Settings::Printing& config); +void declare_config(Settings::Factory& config); +void declare_config(Settings::ExternalLibraries& config); + } // namespace internal // Access function in regular namespace. diff --git a/config_utilities/src/asl_formatter.cpp b/config_utilities/src/asl_formatter.cpp index 1de111c..e501a7e 100644 --- a/config_utilities/src/asl_formatter.cpp +++ b/config_utilities/src/asl_formatter.cpp @@ -41,27 +41,28 @@ namespace config::internal { std::string AslFormatter::formatErrorsImpl(const MetaData& data, const std::string& what, const Severity severity) { const std::string sev = severityToString(severity) + ": "; - const size_t print_width = Settings::instance().print_width; + const auto& settings = Settings::instance().printing; is_first_divider_ = true; name_prefix_ = ""; current_check_ = 0; - if (indicate_num_checks_ && Settings::instance().inline_subconfig_field_names) { + if (settings.show_num_checks && settings.inline_subconfigs) { total_num_checks_ = 0; data.performOnAll([this](const MetaData& data) { total_num_checks_ += data.checks.size(); }); } // Header line. std::string result = what + " '" + resolveConfigName(data) + "':\n" + - internal::printCenter(resolveConfigName(data), print_width, '=') + "\n"; + internal::printCenter(resolveConfigName(data), settings.width, '=') + "\n"; // Format all checks and errors. - result += formatErrorsRecursive(data, sev, print_width); - return result + std::string(print_width, '='); + result += formatErrorsRecursive(data, sev, settings.width); + return result + std::string(settings.width, '='); } std::string AslFormatter::formatErrorsRecursive(const MetaData& data, const std::string& sev, const size_t length) { const std::string name_prefix_before = name_prefix_; - if (Settings::instance().inline_subconfig_field_names) { + const auto& settings = Settings::instance().printing; + if (settings.inline_subconfigs) { if (!data.field_name.empty()) { // TOOD(nathan) refactor to put in metadata name_prefix_ += data.field_name; @@ -79,11 +80,11 @@ std::string AslFormatter::formatErrorsRecursive(const MetaData& data, const std: std::string result = formatChecksInternal(data, sev, length) + formatErrorsInternal(data, sev, length); // Add more dividers if necessary. - if (!Settings::instance().inline_subconfig_field_names && !result.empty()) { + if (!settings.inline_subconfigs && !result.empty()) { if (is_first_divider_) { is_first_divider_ = false; } else { - result = internal::printCenter(resolveConfigName(data), Settings::instance().print_width, '-') + "\n" + result; + result = internal::printCenter(resolveConfigName(data), settings.width, '-') + "\n" + result; } } @@ -96,22 +97,23 @@ std::string AslFormatter::formatErrorsRecursive(const MetaData& data, const std: std::string AslFormatter::formatMissingImpl(const MetaData& data, const std::string& what, const Severity severity) { const std::string sev = severityToString(severity) + ": "; - const size_t print_width = Settings::instance().print_width; + const auto& settings = Settings::instance().printing; is_first_divider_ = true; name_prefix_ = ""; // Header line. std::string result = what + " '" + resolveConfigName(data) + "':\n" + - internal::printCenter(resolveConfigName(data), print_width, '=') + "\n"; + internal::printCenter(resolveConfigName(data), settings.width, '=') + "\n"; // Format all checks and errors. - result += formatMissingRecursive(data, sev, print_width); - return result + std::string(print_width, '='); + result += formatMissingRecursive(data, sev, settings.width); + return result + std::string(settings.width, '='); } std::string AslFormatter::formatMissingRecursive(const MetaData& data, const std::string& sev, const size_t length) { const std::string name_prefix_before = name_prefix_; - if (Settings::instance().inline_subconfig_field_names) { + const auto& settings = Settings::instance().printing; + if (settings.inline_subconfigs) { if (!data.field_name.empty()) { // TOOD(nathan) refactor to put in metadata name_prefix_ += data.field_name; @@ -136,11 +138,11 @@ std::string AslFormatter::formatMissingRecursive(const MetaData& data, const std } // Add more dividers if necessary. - if (!Settings::instance().inline_subconfig_field_names && !result.empty()) { + if (!settings.inline_subconfigs && !result.empty()) { if (is_first_divider_) { is_first_divider_ = false; } else { - result = internal::printCenter(resolveConfigName(data), Settings::instance().print_width, '-') + "\n" + result; + result = internal::printCenter(resolveConfigName(data), settings.width, '-') + "\n" + result; } } @@ -181,8 +183,8 @@ std::string AslFormatter::formatErrorsInternal(const MetaData& data, const std:: } std::string AslFormatter::formatConfigImpl(const MetaData& data) { - return internal::printCenter(resolveConfigName(data), Settings::instance().print_width, '=') + "\n" + - toStringInternal(data, 0) + std::string(Settings::instance().print_width, '='); + return internal::printCenter(resolveConfigName(data), Settings::instance().printing.width, '=') + "\n" + + toStringInternal(data, 0) + std::string(Settings::instance().printing.width, '='); } std::string AslFormatter::formatConfigsImpl(const std::vector& data) { @@ -195,7 +197,7 @@ std::string AslFormatter::formatConfigsImpl(const std::vector& data) { entry.erase(entry.find_last_of("\n")); result += entry + "\n"; } - result += std::string(Settings::instance().print_width, '='); + result += std::string(Settings::instance().printing.width, '='); return result; } @@ -211,6 +213,7 @@ std::string AslFormatter::toStringInternal(const MetaData& data, size_t indent) } std::string AslFormatter::formatSubconfig(const MetaData& data, size_t indent) const { + const auto& settings = Settings::instance().printing; // Header. std::string header = std::string(indent, ' ') + data.field_name; // TODO(nathan) refactor into metadata @@ -219,10 +222,10 @@ std::string AslFormatter::formatSubconfig(const MetaData& data, size_t indent) c } else if (data.map_config_key) { header += "[" + *data.map_config_key + "]"; } - if (indicate_subconfig_types_) { + if (settings.show_subconfig_types) { header += " [" + resolveConfigName(data) + "]"; } - if (Settings::instance().indicate_default_values && indicate_subconfig_default_ && !data.is_virtual_config) { + if (settings.show_defaults && !data.is_virtual_config) { bool is_default = true; for (const FieldInfo& info : data.field_infos) { if (!info.isDefault()) { @@ -238,22 +241,20 @@ std::string AslFormatter::formatSubconfig(const MetaData& data, size_t indent) c header += ":"; } header += "\n"; - return header + toStringInternal(data, indent + Settings::instance().subconfig_indent); + return header + toStringInternal(data, indent + settings.subconfig_indent); } std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) const { std::string result; - const size_t print_width = Settings::instance().print_width; - const size_t global_indent = Settings::instance().print_indent; - const auto reformat_floats = Settings::instance().reformat_floats; + const auto& settings = Settings::instance().printing; // field is the stringified value, The header is the field name. - std::string field = dataToString(info.value, reformat_floats); - if (info.isDefault() && Settings::instance().indicate_default_values) { + std::string field = dataToString(info.value, settings.reformat_floats); + if (info.isDefault() && Settings::instance().printing.show_defaults) { field += " (default)"; } std::string header = std::string(indent, ' ') + info.name; - if (Settings::instance().indicate_units && !info.unit.empty()) { + if (settings.show_units && !info.unit.empty()) { header += " [" + info.unit + "]"; } header += ":"; @@ -279,17 +280,17 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons } // Format the header to width. - result += wrapString(header, print_width, indent, false); + result += wrapString(header, settings.width, indent, false); const size_t last_header_line = result.find_last_of('\n'); size_t header_size = result.substr(last_header_line != std::string::npos ? last_header_line + 1 : 0).size(); - if (header_size < global_indent) { - result += std::string(global_indent - header_size, ' '); - header_size = global_indent; - } else if (print_width - header_size - 1 < field.length() || is_multiline) { + if (header_size < settings.indent) { + result += std::string(settings.indent - header_size, ' '); + header_size = settings.indent; + } else if (settings.width - header_size - 1 < field.length() || is_multiline) { // If the field does not fit entirely or is multi-line anyways just start a new line. result = pruneTrailingWhitespace(result); - result += "\n" + std::string(global_indent, ' '); - header_size = global_indent; + result += "\n" + std::string(settings.indent, ' '); + header_size = settings.indent; } else { // If the field fits partly on the same line, add a space after the header. result += " "; @@ -297,7 +298,7 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons } // First line of field could be shorter due to header over extension. - const size_t available_length = print_width - header_size; + const size_t available_length = settings.width - header_size; if (is_multiline) { // Multiline fields need formatting but start at new lines anyways. size_t prev_break = 0; @@ -308,9 +309,9 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons std::count_if(closed_brackets.begin(), closed_brackets.end(), isBefore); std::string line = field.substr(prev_break, linebreak - prev_break + 2); line = std::string(num_open, ' ') + line; - line = wrapString(line, print_width, global_indent) + "\n"; + line = wrapString(line, settings.width, settings.indent) + "\n"; if (prev_break == 0) { - line = line.substr(global_indent); + line = line.substr(settings.indent); } result += pruneTrailingWhitespace(line); prev_break = linebreak + 3; @@ -321,7 +322,8 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons } else { // Add as much as fits on the first line and fill the rest. result += pruneTrailingWhitespace(field.substr(0, available_length)) + "\n"; - result += wrapString(pruneLeadingWhitespace(field.substr(available_length)), print_width, global_indent) + "\n"; + result += + wrapString(pruneLeadingWhitespace(field.substr(available_length)), settings.width, settings.indent) + "\n"; } return result; } @@ -334,7 +336,7 @@ std::string AslFormatter::resolveConfigName(const MetaData& data) const { return "Unnamed Config"; } } else { - if (data.is_virtual_config && indicate_virtual_configs_) { + if (data.is_virtual_config && Settings::instance().printing.show_virtual_configs) { return "Virtual Config: " + data.name; } else { return data.name; diff --git a/config_utilities/src/external_registry.cpp b/config_utilities/src/external_registry.cpp index 583bce2..98f66b6 100644 --- a/config_utilities/src/external_registry.cpp +++ b/config_utilities/src/external_registry.cpp @@ -107,7 +107,7 @@ ExternalRegistry::~ExternalRegistry() { } void ExternalRegistry::unload(const std::filesystem::path& library_path) { - if (Settings::instance().verbose_external_load) { + if (Settings::instance().external_libraries.verbose_load) { // NOTE(nathan) this is separate from the logger becuase there is no guarantee that it will be visible to the user // if it is through the logger std::cerr << "[WARNING] Unloading external library: " << library_path << std::endl; @@ -145,12 +145,12 @@ struct RegistryLock { }; LibraryGuard ExternalRegistry::load(const std::filesystem::path& library_path) { - if (!Settings::instance().allow_external_libraries) { + if (!Settings::instance().external_libraries.enabled) { Logger::logError("External library loading is disallowed! Not loading " + library_path.string()); return {}; } - if (Settings::instance().verbose_external_load) { + if (Settings::instance().external_libraries.verbose_load) { Logger::logInfo("Loading external library '" + library_path.string() + "'."); } @@ -213,7 +213,7 @@ void ExternalRegistry::logAllocation(const RegistryEntry& entry, void* pointer) ExternalRegistry& ExternalRegistry::instance() { if (!s_instance_) { s_instance_.reset(new ExternalRegistry()); - if (Settings::instance().print_external_allocations) { + if (Settings::instance().external_libraries.log_allocation) { ModuleRegistry::setCreationCallback([](const auto& info, const auto& type, void* pointer) { ExternalRegistry::logAllocation({info, type}, pointer); }); diff --git a/config_utilities/src/factory.cpp b/config_utilities/src/factory.cpp index 44a97d2..097a861 100644 --- a/config_utilities/src/factory.cpp +++ b/config_utilities/src/factory.cpp @@ -128,7 +128,7 @@ bool operator<(const ConfigPair& lhs, const ConfigPair& rhs) { } std::string ModuleRegistry::getAllRegistered() { - const auto width = Settings::instance().print_width; + const auto width = Settings::instance().printing.width; const auto& registry = instance().type_registry; std::stringstream ss; ss << banner("Registered Objects", width) << showWithFilter(registry, &isPlainObject) << "\n"; diff --git a/config_utilities/src/settings.cpp b/config_utilities/src/settings.cpp index 011b0e4..d25731e 100644 --- a/config_utilities/src/settings.cpp +++ b/config_utilities/src/settings.cpp @@ -37,6 +37,7 @@ #include +#include "config_utilities/config.h" #include "config_utilities/factory.h" #include "config_utilities/internal/formatter.h" #include "config_utilities/internal/logger.h" @@ -69,4 +70,38 @@ void Settings::setFormatter(const std::string& name) { } } +void declare_config(Settings& config) { + name("Settings"); + field(config.printing, "printing"); + field(config.factory, "factory"); + field(config.external_libraries, "external_libraries"); +} + +void declare_config(Settings::Printing& config) { + name("Printing"); + field(config.width, "width"); + field(config.indent, "indent"); + field(config.subconfig_indent, "subconfig_indent"); + field(config.show_defaults, "show_defaults"); + field(config.show_units, "show_units"); + field(config.inline_subconfigs, "inline_subconfigs"); + field(config.reformat_floats, "reformat_floats"); + field(config.show_missing, "show_missing"); + field(config.show_subconfig_types, "show_subconfig_types"); + field(config.show_virtual_configs, "show_virtual_configs"); + field(config.show_num_checks, "show_num_checks"); +} + +void declare_config(Settings::Factory& config) { + name("Factory"); + field(config.type_param_name, "type_param_name"); +} + +void declare_config(Settings::ExternalLibraries& config) { + name("ExternalLibraries"); + field(config.enabled, "enabled"); + field(config.verbose_load, "verbose_load"); + field(config.log_allocation, "log_allocation"); +} + } // namespace config::internal diff --git a/config_utilities/src/visitor.cpp b/config_utilities/src/visitor.cpp index 3ae9399..915781b 100644 --- a/config_utilities/src/visitor.cpp +++ b/config_utilities/src/visitor.cpp @@ -100,7 +100,7 @@ std::optional Visitor::visitVirtualConfig(bool is_set, bool is_optio // Also write the type param back to file. std::string error; YAML::Node type_node = - YamlParser::toYaml(Settings::instance().factory_type_param_name, type, visitor.name_space, error); + YamlParser::toYaml(Settings::instance().factory.type_param_name, type, visitor.name_space, error); mergeYamlNodes(visitor.data.data, type_node); } } diff --git a/config_utilities/test/tests/asl_formatter.cpp b/config_utilities/test/tests/asl_formatter.cpp index c54567e..fce3666 100644 --- a/config_utilities/test/tests/asl_formatter.cpp +++ b/config_utilities/test/tests/asl_formatter.cpp @@ -142,7 +142,7 @@ Warning: Failed to parse param 'Field 6': Error 6. ================================================================================)"""; EXPECT_EQ(formatted, expected); - Settings().inline_subconfig_field_names = false; + Settings().printing.inline_subconfigs = false; formatted = internal::Formatter::formatErrors(data); EXPECT_EQ(countLines(formatted), 12); @@ -177,7 +177,7 @@ TEST(AslFormatter, FormatChecks) { config.sub_config.sub_sub_config.i = -1; Settings().restoreDefaults(); - Settings().inline_subconfig_field_names = false; + Settings().printing.inline_subconfigs = false; internal::MetaData data = internal::Visitor::getChecks(config); std::string formatted = internal::Formatter::formatErrors(data); std::string expected = R"""( 'DefaultConfig': @@ -198,7 +198,7 @@ Warning: Check [1/1] failed for 'i': param > 0 (is: '-1'). ================================================================================ )"""; - Settings().inline_subconfig_field_names = true; + Settings().printing.inline_subconfigs = true; data = internal::Visitor::getChecks(config); formatted = internal::Formatter::formatErrors(data); expected = R"""( 'DefaultConfig': @@ -221,10 +221,10 @@ Warning: Check [11/11] failed for 'sub_sub_config.i': param > 0 (is: '-1'). TEST(AslFormatter, FormatConfig) { internal::MetaData data = internal::Visitor::getValues(TestConfig()); - Settings().indicate_default_values = false; - Settings().indicate_units = false; - Settings().inline_subconfig_field_names = true; - Settings().reformat_floats = true; + Settings().printing.show_defaults = false; + Settings().printing.show_units = false; + Settings().printing.inline_subconfigs = true; + Settings().printing.reformat_floats = true; std::string formatted = internal::Formatter::formatConfig(data); std::string expected = R"""(================================= Test Config ================================== @@ -259,7 +259,7 @@ sub_sub_config [SubSubConfig]: EXPECT_EQ(formatted.size(), expected.size()); EXPECT_EQ(formatted, expected); - Settings().print_width = 50; + Settings().printing.width = 50; formatted = internal::Formatter::formatConfig(data); expected = R"""(================== Test Config =================== @@ -300,8 +300,8 @@ sub_sub_config [SubSubConfig]: EXPECT_EQ(formatted.size(), expected.size()); EXPECT_EQ(formatted, expected); - Settings().print_width = 80; - Settings().print_indent = 20; + Settings().printing.width = 80; + Settings().printing.indent = 20; formatted = internal::Formatter::formatConfig(data); expected = R"""(================================= Test Config ================================== @@ -338,11 +338,11 @@ sub_sub_config [SubSubConfig]: } TEST(AslFormatter, FormatUnits) { - Settings().indicate_default_values = false; - Settings().indicate_units = true; - Settings().inline_subconfig_field_names = true; - Settings().print_width = 80; // force print width to be consistent for tests - Settings().print_indent = 20; + Settings().printing.show_defaults = false; + Settings().printing.show_units = true; + Settings().printing.inline_subconfigs = true; + Settings().printing.width = 80; // force print width to be consistent for tests + Settings().printing.indent = 20; internal::MetaData data = internal::Visitor::getValues(TestConfig()); const std::string formatted = internal::Formatter::formatConfig(data); @@ -382,10 +382,10 @@ sub_sub_config [SubSubConfig]: } TEST(AslFormatter, FormatDefaultValues) { - Settings().indicate_default_values = true; - Settings().indicate_units = false; - Settings().inline_subconfig_field_names = true; - Settings().print_indent = 20; + Settings().printing.show_defaults = true; + Settings().printing.show_units = false; + Settings().printing.inline_subconfigs = true; + Settings().printing.indent = 20; const internal::MetaData default_data = internal::Visitor::getValues(TestConfig()); std::string formatted = internal::Formatter::formatConfig(default_data); diff --git a/config_utilities/test/tests/config_arrays.cpp b/config_utilities/test/tests/config_arrays.cpp index 55fca0e..d3fd855 100644 --- a/config_utilities/test/tests/config_arrays.cpp +++ b/config_utilities/test/tests/config_arrays.cpp @@ -405,7 +405,7 @@ TEST(ConfigArrays, PrintArrayConfigs) { configs.emplace_back("a", 1.0f); configs.emplace_back("b", 2.0f); configs.emplace_back("c", 3.0f); - Settings().print_indent = 20; + Settings().printing.indent = 20; internal::Formatter::setFormatter(std::make_unique()); diff --git a/config_utilities/test/tests/config_maps.cpp b/config_utilities/test/tests/config_maps.cpp index eeedd7c..1cf33cc 100644 --- a/config_utilities/test/tests/config_maps.cpp +++ b/config_utilities/test/tests/config_maps.cpp @@ -272,7 +272,7 @@ TEST(ConfigMaps, NestedSubConfig) { TEST(ConfigMaps, PrintMapConfigs) { std::map configs{{2, {"a", 1}}, {3, {"b", 2}}, {4, {"c", 3}}}; - Settings().print_indent = 20; + Settings().printing.indent = 20; internal::Formatter::setFormatter(std::make_unique()); diff --git a/config_utilities/test/tests/factory.cpp b/config_utilities/test/tests/factory.cpp index dbe5a15..ce92a4a 100644 --- a/config_utilities/test/tests/factory.cpp +++ b/config_utilities/test/tests/factory.cpp @@ -180,7 +180,7 @@ TEST(Factory, createWithConfig) { std::string msg = logger->messages().back().second; EXPECT_EQ(msg.find("No module of type 'NotRegistered' registered to the factory"), 0); - Settings().factory_type_param_name = "test_type"; + Settings().factory.type_param_name = "test_type"; base = createFromYaml(data, 12); EXPECT_FALSE(base); EXPECT_EQ(logger->numMessages(), 2); @@ -306,7 +306,7 @@ Config[config::test::Talker](): )"""; - Settings().print_width = 40; + Settings().printing.width = 40; const std::string modules = internal::ModuleRegistry::getAllRegistered(); EXPECT_EQ(modules, expected); Settings().restoreDefaults(); diff --git a/docs/External.md b/docs/External.md index b0ae231..bd00d3b 100644 --- a/docs/External.md +++ b/docs/External.md @@ -104,16 +104,16 @@ You may find it helpful to turn on allocation logging by doing the following: #include // or your preferred logger #include -config::Settings::instance().print_external_allocations = true; +config::Settings().external_libraries.log_allocation = true; ``` You can also disable loading external libraries by doing the following: ```c++ -config::Settings::instance().allow_external_libraries = false; +config::Settings().external_libraries.enabled = false; ``` Finally, we intentionally print to stderr when a library is being unloaded. You can turn this behavior off by default by doing ```c++ -config::Settings::instance().verbose_external_load = false; +config::Settings().external_libraries.verbose_load = false; ``` diff --git a/docs/Varia.md b/docs/Varia.md index a9079bf..566aebd 100644 --- a/docs/Varia.md +++ b/docs/Varia.md @@ -4,29 +4,18 @@ This tutorial explains various additional `config_utilities` functionalities. **Contents:** - [Settings](#settings) -- [Globals](#globals) ## Settings `config_utilities` provides some configuration options that can be set at runtime using the `Settings` struct: ```c++ +#include // Example settings for formatting and printing. -Settings().print_width = 80; +Settings().printing.width = 80; // Example settings for factory creation. -Settings().factory_type_param_name = "type"; +Settings().factory.type_param_name = "type"; // You can set the formatting or logging at runtime. Settings().setLogger("stdout"); Settings().setFormatter("asl"); ``` - -## Globals -`config_utilities` also provides some preliminary functionalities for global processing. For example, it can keep track of all configs that have been checked for validity using `checkValid()`. This can be used as a proxy for all configs used in a system and can also be disabled in the settings. - -```c++ -{ /* build a complicated architecture using configs */ } -std::ofstream config_log(log_dest); - -// Write the realized configuration of the system, clearing the memory used to store this information. -config_log << Globals().printAllValidConfigs(true); -``` From 25cdcbdd0f64e4ec8340e7ee5b10bc74a9b23283 Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 16 Aug 2024 11:02:28 -0400 Subject: [PATCH 08/22] update GUI --- .../demos/demo_dynamic_config_client.py | 0 config_utilities/demos/dynamic_config_gui.py | 257 ++++++++++++++---- 2 files changed, 200 insertions(+), 57 deletions(-) mode change 100644 => 100755 config_utilities/demos/demo_dynamic_config_client.py diff --git a/config_utilities/demos/demo_dynamic_config_client.py b/config_utilities/demos/demo_dynamic_config_client.py old mode 100644 new mode 100755 diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index 0734682..8f317e4 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 from tkinter import * -from typing import Any import customtkinter as ctk import yaml +import copy -ctk.set_appearance_mode( - "System") # Modes: "System" (standard), "Dark", "Light" -ctk.set_default_color_theme( - "blue") # Themes: "blue" (standard), "green", "dark-blue" +ctk.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" +ctk.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" PAD_X = 10 PAD_Y = 10 GUI_NAME = "[Config Utilities Dynamic Config GUI] " @@ -18,27 +16,152 @@ class Settings: A class to store settings for the DynamicConfigGUI. This can also be opened as a top-level window. """ + METHOD_OPTIONS = ["Type Info", "Plain YAML"] + APPEARANCE_OPTIONS = ["System", "Light", "Dark"] + COLOR_THEME_OPTIONS = ["blue", "green", "dark-blue"] + SACLE_MIN = 0.5 + SACLE_MAX = 2.0 + def __init__(self) -> None: # Settings. self.width = 800 self.height = 600 - self.appearance_mode = "System" - self.color_theme = "blue" + self.appearance_mode = self.APPEARANCE_OPTIONS[0] + self.color_theme = self.COLOR_THEME_OPTIONS[0] + self.method = self.METHOD_OPTIONS[0] self.scale_factor = 1.0 # GUI. self._gui = None + self._master = None + + def validate(self) -> None: + """ + Validate the settings. + """ + if not self.appearance_mode in self.APPEARANCE_OPTIONS: + self.appearance_mode = self.APPEARANCE_OPTIONS[0] + if not self.color_theme in self.COLOR_THEME_OPTIONS: + self.color_theme = self.COLOR_THEME_OPTIONS[0] + if not self.method in self.METHOD_OPTIONS: + self.method = self.METHOD_OPTIONS[0] + if self.scale_factor < self.SACLE_MIN: + self.scale_factor = self.SACLE_MIN + if self.scale_factor > self.SACLE_MAX: + self.scale_factor = self.SACLE_MAX + + def apply(self) -> None: + """ + Apply the settings. + """ + self.validate() + ctk.set_appearance_mode(self.appearance_mode) + ctk.set_default_color_theme(self.color_theme) + ctk.set_widget_scaling(self.scale_factor) def gui(self) -> None: """ Open the GUI for changing the settings. """ - if self._gui is not None: - return self._gui = ctk.CTkToplevel() self._gui.title("Config Utilities Dynamic Config Client Settings") self._gui.geometry(f"{400}x{400}") + # Add all settings. + current_row = 0 + + # Entry tool. + self.w_method_label = ctk.CTkLabel(self._gui, text="UI Method:", anchor="w") + self.w_method_label.grid( + row=current_row, column=0, padx=PAD_X, pady=PAD_Y, sticky="nsw" + ) + self.w_method = ctk.CTkOptionMenu( + self._gui, values=self.METHOD_OPTIONS, command=self._method_cb + ) + self.w_method.set(self.method) + self.w_method.grid(row=current_row, column=1, padx=PAD_X, pady=PAD_Y) + current_row += 1 + + # Appearance. + self.w_appearance_label = ctk.CTkLabel( + self._gui, text="Appearance Mode:", anchor="w" + ) + self.w_appearance = ctk.CTkOptionMenu( + self._gui, values=self.APPEARANCE_OPTIONS, command=self._appearance_cb + ) + self.w_appearance.set(self.appearance_mode) + self.w_appearance_label.grid( + row=current_row, column=0, padx=PAD_X, pady=PAD_Y, sticky="nsw" + ) + self.w_appearance.grid(row=current_row, column=1, padx=PAD_X, pady=PAD_Y) + current_row += 1 + + # Color Theme. + self.w_color_theme_label = ctk.CTkLabel( + self._gui, text="Color Theme:", anchor="w" + ) + self.w_color_theme = ctk.CTkOptionMenu( + self._gui, + values=self.COLOR_THEME_OPTIONS, + command=self._color_theme_cb, + ) + self.w_color_theme.set(self.color_theme) + self.w_color_theme_label.grid( + row=current_row, column=0, padx=PAD_X, pady=PAD_Y, sticky="nsw" + ) + self.w_color_theme.grid(row=current_row, column=1, padx=PAD_X, pady=PAD_Y) + current_row += 1 + + # Scaling. + self.w_scaling_label = ctk.CTkLabel(self._gui, text="UI Scaling:", anchor="w") + self.w_scaling_label.grid( + row=current_row, column=0, padx=PAD_X, pady=PAD_Y, sticky="nsw" + ) + self.w_scaling = ctk.CTkComboBox( + self._gui, + values=["50", "75%", "90%", "100%", "110%", "125%", "150%", "200%"], + command=self._scaling_cb, + ) + self.w_scaling.bind("", self._scaling_key_cb) + self.w_scaling.grid(row=current_row, column=1, padx=PAD_X, pady=PAD_Y) + self.w_scaling.set(f"{int(self.scale_factor * 100)}%") + current_row += 1 + + # Apply and cancel Button. + self.w_apply_button = ctk.CTkButton( + self._gui, text="Apply", command=self._gui.destroy, anchor="c" + ) + self.w_apply_button.grid( + row=current_row, column=0, columnspan=2, padx=PAD_X, pady=PAD_Y, sticky="se" + ) + + # Formatting, + self._gui.rowconfigure(current_row, weight=1) + self._gui.columnconfigure(1, weight=1) + + def _appearance_cb(self, new_appearance_mode): + self.appearance_mode = new_appearance_mode + ctk.set_appearance_mode(new_appearance_mode) + + def _scaling_cb(self, new_scaling): + self.scale_factor = float(new_scaling.replace("%", "")) / 100 + self.scale_factor = min(self.SACLE_MAX, max(self.SACLE_MIN, self.scale_factor)) + self.w_scaling.set(f"{int(self.scale_factor * 100)}%") + ctk.set_widget_scaling(self.scale_factor) + + def _scaling_key_cb(self, event): + if event.keysym == "Return": + self._scaling_cb(self.w_scaling.get()) + + def _color_theme_cb(self, new_color_theme): + self.color_theme = new_color_theme + ctk.set_default_color_theme(new_color_theme) + + def _method_cb(self, new_method): + self.method = new_method + if self._master is not None: + self._master.setup_config_frame() + class DynamicConfigGUI(ctk.CTk): """ @@ -57,10 +180,13 @@ def __init__(self, settings: Settings = Settings()) -> None: # GUI configuration. self.settings = settings + self.settings._master = self # Data. self.current_key = None self.current_server = None + self.current_values = None + self.current_info = None # Initialization. self.setup_frame() @@ -76,33 +202,41 @@ def setup_frame(self): # Settings Button. # TODO(lschmid): For now baked into the key selection. - self.settings_button = ctk.CTkButton(self.key_selection, - text="Settings", - command=self.settings.gui) - self.settings_button.grid(row=0, - column=3, - sticky="ew", - padx=PAD_X, - pady=PAD_Y) + self.settings_button = ctk.CTkButton( + self.key_selection, text="Settings", command=self.settings.gui + ) + self.settings_button.grid(row=0, column=3, sticky="ew", padx=PAD_X, pady=PAD_Y) self.key_selection.columnconfigure( - 3, weight=0, minsize=self.settings_button.winfo_reqwidth()) + 3, weight=0, minsize=self.settings_button.winfo_reqwidth() + ) # Config editing. - self.config_frame = PlainTextConfigFrame(self, self.value_changed_cb) - self.config_frame.grid(row=1, column=0, sticky="ns", columnspan=2) + self.setup_config_frame() # TODO(lschmid): Consider making this more general, specialized to ROS for now. self.server_selection = RosStatusBar(self, self._server_selected) self.server_selection.send_cb = self._value_changed_cb self.server_selection.grid(row=2, column=0, sticky="ew", columnspan=2) - self.rowconfigure([0, 2], - minsize=self.key_selection.winfo_reqheight(), - pad=PAD_Y, - weight=0) + self.rowconfigure( + [0, 2], minsize=self.key_selection.winfo_reqheight(), pad=PAD_Y, weight=0 + ) self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1, pad=PAD_X) + def setup_config_frame(self): + if self.settings.method == "Type Info": + self.config_frame = TypeInfoConfigFrame(self, self.value_changed_cb) + else: + # Default to plain text if unsupported. + self.config_frame = PlainTextConfigFrame(self, self.value_changed_cb) + self.config_frame.grid(row=1, column=0, sticky="nsew", columnspan=2) + + if self.current_values is not None: + self.config_frame.set_config(self.current_values) + if self.current_info is not None: + self.config_frame.set_config_info(self.current_info) + # Interfaces for outside interaction with the GUI. def set_keys(self, keys): self.key_selection.set_keys(keys) @@ -111,6 +245,7 @@ def set_servers(self, servers): self.server_selection.set_keys(servers) def set_config(self, new_values): + self.current_values = new_values self.config_frame.set_config(new_values) def set_config_info(self, new_info): @@ -118,8 +253,8 @@ def set_config_info(self, new_info): Update the GUI with new information about the configuration. new_info: A dictionary with information about the configuration. """ - print(f"Got info: {new_info}") - pass + self.current_info = new_info + self.config_frame.set_config_info(new_info) # Functionality. def _key_selected(self, key): @@ -158,19 +293,11 @@ def __init__(self, master, key_selected_cb): self.no_options_text = "No Dynamic Configs Registered." self.w_label = ctk.CTkLabel(self, text="Config:") - self.w_label.grid(row=0, - column=0, - padx=PAD_X, - pady=PAD_Y, - sticky="nsw") - self.w_dropdown = ctk.CTkOptionMenu(self, - dynamic_resizing=True, - command=self._on_change) - self.w_dropdown.grid(row=0, - column=1, - sticky="nsew", - padx=PAD_X, - pady=PAD_Y) + self.w_label.grid(row=0, column=0, padx=PAD_X, pady=PAD_Y, sticky="nsw") + self.w_dropdown = ctk.CTkOptionMenu( + self, dynamic_resizing=True, command=self._on_change + ) + self.w_dropdown.grid(row=0, column=1, sticky="nsew", padx=PAD_X, pady=PAD_Y) self.columnconfigure(1, weight=1) def set_keys(self, new_keys): @@ -208,22 +335,16 @@ def __init__(self, master, key_selected_cb): self.no_options_text = "No RosDynamicConfigServers Registered." self.w_label.configure(text="Config Server:") - self.w_refresh_button = ctk.CTkButton(self, - text="Refresh", - command=self._on_reset_button) - self.w_refresh_button.grid(row=0, - column=3, - padx=PAD_X, - pady=PAD_Y, - sticky="nse") - self.w_send_button = ctk.CTkButton(self, - text="Send", - command=self._on_send_button) - self.w_send_button.grid(row=0, - column=4, - padx=PAD_X, - pady=PAD_Y, - sticky="nse") + self.w_refresh_button = ctk.CTkButton( + self, text="Refresh", command=self._on_reset_button + ) + self.w_refresh_button.grid( + row=0, column=3, padx=PAD_X, pady=PAD_Y, sticky="nse" + ) + self.w_send_button = ctk.CTkButton( + self, text="Send", command=self._on_send_button + ) + self.w_send_button.grid(row=0, column=4, padx=PAD_X, pady=PAD_Y, sticky="nse") self.columnconfigure(2, weight=1) self.columnconfigure([0, 1, 3, 4], weight=0) @@ -248,6 +369,9 @@ def __init__(self, master, send_update_fn=None): def set_config(self, new_config): pass + def set_config_info(self, new_info): + pass + def get_config(self): return {} @@ -256,6 +380,9 @@ def set_enabled(self, enabled): class PlainTextConfigFrame(ConfigFrame): + """ + A frame to enable editing a config in plain YAML. + """ def __init__(self, master, send_update_fn=None): super().__init__(master, send_update_fn) @@ -269,8 +396,7 @@ def set_config(self, new_config): def get_config(self): try: - return yaml.load(self.w_text.get("1.0", END), - Loader=yaml.FullLoader) + return yaml.load(self.w_text.get("1.0", END), Loader=yaml.FullLoader) except yaml.YAMLError as e: print(f"{GUI_NAME}Error parsing YAML: {e}") return None @@ -285,6 +411,23 @@ def _on_key_release(self, event): self.send_update_fn() +class TypeInfoConfigFrame(ConfigFrame): + """ + A frame to restrict editing a config based on type information. + """ + + def __init__(self, master, send_update_fn=None): + super().__init__(master, send_update_fn) + + # TODO Build frame from info. + + def set_config_info(self, new_info): + print(f"Got info: {new_info}") + + def get_config(self): + return {} + + def main(): app = DynamicConfigGUI() app.mainloop() From 41fb53a96e212aa3f2e95a2b1e09775fa7e6483c Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 16 Aug 2024 11:20:32 -0400 Subject: [PATCH 09/22] fix info publishing --- config_utilities/demos/dynamic_config_gui.py | 12 ++++-------- .../internal/dynamic_config_impl.hpp | 3 +-- .../include/config_utilities/parsing/ros.h | 3 ++- config_utilities/src/ros.cpp | 19 +++++++++++++++---- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index 8f317e4..5c4ad2b 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -231,7 +231,7 @@ def setup_config_frame(self): # Default to plain text if unsupported. self.config_frame = PlainTextConfigFrame(self, self.value_changed_cb) self.config_frame.grid(row=1, column=0, sticky="nsew", columnspan=2) - + if self.current_values is not None: self.config_frame.set_config(self.current_values) if self.current_info is not None: @@ -249,10 +249,6 @@ def set_config(self, new_values): self.config_frame.set_config(new_values) def set_config_info(self, new_info): - """ - Update the GUI with new information about the configuration. - new_info: A dictionary with information about the configuration. - """ self.current_info = new_info self.config_frame.set_config_info(new_info) @@ -336,7 +332,7 @@ def __init__(self, master, key_selected_cb): self.no_options_text = "No RosDynamicConfigServers Registered." self.w_label.configure(text="Config Server:") self.w_refresh_button = ctk.CTkButton( - self, text="Refresh", command=self._on_reset_button + self, text="Refresh", command=self._on_refresh_button ) self.w_refresh_button.grid( row=0, column=3, padx=PAD_X, pady=PAD_Y, sticky="nse" @@ -348,7 +344,7 @@ def __init__(self, master, key_selected_cb): self.columnconfigure(2, weight=1) self.columnconfigure([0, 1, 3, 4], weight=0) - def _on_reset_button(self): + def _on_refresh_button(self): if self.refresh_cb is not None: self.refresh_cb() @@ -386,7 +382,7 @@ class PlainTextConfigFrame(ConfigFrame): def __init__(self, master, send_update_fn=None): super().__init__(master, send_update_fn) - self.w_text = ctk.CTkTextbox(self, wrap=CHAR, width=1000, undo=True) + self.w_text = ctk.CTkTextbox(self, wrap=CHAR, width=1000, undo=True, font=ctk.CTkFont(size=14)) self.w_text.pack(fill=BOTH, expand=True, padx=PAD_X, pady=PAD_Y) self.w_text.bind("", self._on_key_release) diff --git a/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp index 087d3b0..227645b 100644 --- a/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp +++ b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp @@ -148,8 +148,7 @@ YAML::Node DynamicConfig::getValues() const { template YAML::Node DynamicConfig::getInfo() const { std::lock_guard lock(mutex_); - // TODO(lschmid): Add a visitor function to get the info of a config. - return {}; + return internal::Visitor::getInfo(config_).serializeFieldInfos(); } template diff --git a/config_utilities/include/config_utilities/parsing/ros.h b/config_utilities/include/config_utilities/parsing/ros.h index 469b603..d096c3c 100644 --- a/config_utilities/include/config_utilities/parsing/ros.h +++ b/config_utilities/include/config_utilities/parsing/ros.h @@ -214,7 +214,8 @@ class RosDynamicConfigServer { }; ros::NodeHandle nh_; - std::map publishers_; + std::map value_publishers_; + std::map info_publishers_; std::map> subscribers_; ros::Publisher reg_pub_; ros::Publisher dereg_pub_; diff --git a/config_utilities/src/ros.cpp b/config_utilities/src/ros.cpp index 66cdf9b..ef3c602 100644 --- a/config_utilities/src/ros.cpp +++ b/config_utilities/src/ros.cpp @@ -60,7 +60,8 @@ RosDynamicConfigServer::RosDynamicConfigServer(const ros::NodeHandle& nh) : nh_( } void RosDynamicConfigServer::onRegister(const DynamicConfigServer::Key& key) { - publishers_[key] = nh_.advertise(key + "/get", 1, true); + value_publishers_[key] = nh_.advertise(key + "/get", 1, true); + info_publishers_[key] = nh_.advertise(key + "/info", 1, true); subscribers_[key] = std::make_unique(key, this, nh_); std_msgs::String msg; msg.data = key; @@ -71,7 +72,8 @@ void RosDynamicConfigServer::onRegister(const DynamicConfigServer::Key& key) { } void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { - publishers_.erase(key); + value_publishers_.erase(key); + info_publishers_.erase(key); subscribers_.erase(key); std_msgs::String msg; msg.data = key; @@ -79,8 +81,8 @@ void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { } void RosDynamicConfigServer::onUpdate(const DynamicConfigServer::Key& key, const YAML::Node& values) { - const auto it = publishers_.find(key); - if (it == publishers_.end()) { + const auto it = value_publishers_.find(key); + if (it == value_publishers_.end()) { // Shouldn't happen but better to fail gracefully if people extend this. internal::Logger::logWarning("Tried to publish to dynamic config '" + key + "' without existing publisher."); return; @@ -89,6 +91,15 @@ void RosDynamicConfigServer::onUpdate(const DynamicConfigServer::Key& key, const std_msgs::String msg; msg.data = YAML::Dump(values); it->second.publish(msg); + + // For now also always publish the info. Can consider being smarter about this if this ever is a limitation. + const auto info_it = info_publishers_.find(key); + if (info_it == info_publishers_.end()) { + return; + } + const auto info = server_.getInfo(key); + msg.data = YAML::Dump(info); + info_it->second.publish(msg); } void RosDynamicConfigServer::onSet(const DynamicConfigServer::Key& key, const YAML::Node& new_values) { From f718928973ff52d358166e47c6dfd1b232f14e84 Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 16 Aug 2024 17:19:44 -0400 Subject: [PATCH 10/22] play with GUI --- config_utilities/demos/dynamic_config_gui.py | 318 ++++++++++++++++++- 1 file changed, 308 insertions(+), 10 deletions(-) diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index 5c4ad2b..a10f376 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -226,10 +226,10 @@ def setup_frame(self): def setup_config_frame(self): if self.settings.method == "Type Info": - self.config_frame = TypeInfoConfigFrame(self, self.value_changed_cb) + self.config_frame = TypeInfoConfigFrame(self, self._value_changed_cb) else: # Default to plain text if unsupported. - self.config_frame = PlainTextConfigFrame(self, self.value_changed_cb) + self.config_frame = PlainTextConfigFrame(self, self._value_changed_cb) self.config_frame.grid(row=1, column=0, sticky="nsew", columnspan=2) if self.current_values is not None: @@ -264,6 +264,9 @@ def _server_selected(self, server): self.server_selected_cb(server) def _value_changed_cb(self): + # TMP + print("Sending Values: ") + print(self.config_frame.get_config()) if self.value_changed_cb is not None: self.value_changed_cb(self.config_frame.get_config()) @@ -353,7 +356,7 @@ def _on_send_button(self): self.send_cb() -class ConfigFrame(ctk.CTkFrame): +class ConfigFrame: """ Interface class for configuration editing. """ @@ -375,14 +378,16 @@ def set_enabled(self, enabled): pass -class PlainTextConfigFrame(ConfigFrame): +class PlainTextConfigFrame(ConfigFrame, ctk.CTkFrame): """ A frame to enable editing a config in plain YAML. """ def __init__(self, master, send_update_fn=None): super().__init__(master, send_update_fn) - self.w_text = ctk.CTkTextbox(self, wrap=CHAR, width=1000, undo=True, font=ctk.CTkFont(size=14)) + self.w_text = ctk.CTkTextbox( + self, wrap=CHAR, width=1000, undo=True, font=ctk.CTkFont(size=14) + ) self.w_text.pack(fill=BOTH, expand=True, padx=PAD_X, pady=PAD_Y) self.w_text.bind("", self._on_key_release) @@ -407,25 +412,318 @@ def _on_key_release(self, event): self.send_update_fn() -class TypeInfoConfigFrame(ConfigFrame): +class TypeInfoConfigFrame(ConfigFrame, ctk.CTkScrollableFrame): """ A frame to restrict editing a config based on type information. """ + INDENT = 4 # Number of spaces per indent level. + ROW_HEIGHT = 18 + CORNER_RADIUS = 2 + def __init__(self, master, send_update_fn=None): super().__init__(master, send_update_fn) - - # TODO Build frame from info. + + self.current_row = 0 + self.current_indent = 0 + self.current_ns = [] + self.get_value_fns = {} + self.set_value_fns = {} + self.widgets = [] def set_config_info(self, new_info): - print(f"Got info: {new_info}") + # TODO(lschmid): Check if only values need to be updated. + self.clear_config_ui() + self.build_config_ui(new_info) def get_config(self): - return {} + yaml_data = {} + for key, fn in self.get_value_fns.items(): + ns = key.split("/") + curr_node = yaml_data + for n in ns[:-1]: + if not n: + continue + if not n in curr_node: + curr_node[n] = {} + curr_node = curr_node[n] + curr_node[ns[-1]] = fn() + return yaml_data + + # Build the UI for a config info. + def clear_config_ui(self): + for widget in self.widgets: + widget.destroy() + self.current_row = 0 + self.current_indent = 0 + self.current_ns = [] + self.widgets.clear() + self.get_value_fns.clear() + + def build_config_ui(self, config_info): + # Header row. + name = config_info["name"] if "name" in config_info else "Unknown Config" + self.w_header_name = ctk.CTkLabel(self, text=f"{name}:", anchor="w") + self.w_header_name.grid(row=self.current_row, column=0, sticky="nsw", padx=PAD_X) + self.w_header_value = ctk.CTkLabel( + self, + text="Value:", + anchor="w", + ) + self.w_header_value.grid(row=self.current_row, column=1, sticky="nsw", padx=PAD_X) + self.w_header_default = ctk.CTkLabel( + self, + text="Default:", + anchor="w", + ) + self.w_header_default.grid(row=self.current_row, column=2, sticky="nsw", padx=PAD_X) + self.current_row += 1 + + # Build the config. + if "fields" in config_info: + for info in config_info["fields"]: + self.build(info) + + # Configure columns. + self.rowconfigure([i for i in range(self.current_row + 1)], weight=0, pad=0) + self.columnconfigure(0, weight=0, pad=0) + self.columnconfigure(1, weight=1, pad=0) + self.columnconfigure(2, weight=0, pad=0) + + def build(self, info): + # Build configs or fields. + if not "type" in info: + return + if info["type"] == "config": + self.build_config(info) + elif info["type"] == "field": + self.build_field(info) + + def build_config(self, info): + name = info["field_name"] if "field_name" in info else "Unknown Field" + type = f" [{info['name']}]" if "name" in info else "" + self.add_label(f"{name}{type}:") + self.current_row += 1 + if not "fields" in info: + return + + self.current_indent += 1 + self.current_ns.append(name) + for field in info["fields"]: + self.build(field) + self.current_indent -= 1 + self.current_ns.pop() + + def build_field(self, info): + name = info["name"] if "name" in info else "Unknown Field" + unit = f" [{info['unit']}]" if "unit" in info else "" + default = info["default"] if "default" in info else "Unknown Default" + self.add_label(f"{name}{unit}:") + self.add_value_entry(info) + self.add_default(str(default)) + self.current_row += 1 + + def add_label(self, text): + label = ctk.CTkLabel( + self, + text=" " * self.current_indent * self.INDENT + text, + height=self.ROW_HEIGHT, + anchor="w", + ) + label.grid(row=self.current_row, column=0, sticky="nsw", pady=0) + self.widgets.append(label) + + def add_default(self, text): + label = ctk.CTkTextbox( + self, + fg_color="light gray", + text_color="gray", + border_spacing=0, + corner_radius=self.CORNER_RADIUS, + height=self.ROW_HEIGHT, + ) + label.insert("1.0", text) + label.configure(state=DISABLED) + label.grid(row=self.current_row, column=2, sticky="nsw", pady=0) + self.widgets.append(label) + + def add_value_entry(self, info): + input_type = "yaml" + param_name = "/".join(self.current_ns) + "/" + info["name"] + if "input_info" in info and "type" in info["input_info"]: + input_type = info["input_info"]["type"] + if input_type == "bool": + self.add_bool_value_entry(info, param_name) + elif input_type == "int": + self.add_numeric_value_entry(info, param_name, True) + elif input_type == "float": + self.add_numeric_value_entry(info, param_name, False) + elif input_type == "string": + self.add_string_value_entry(info, param_name) + elif input_type == "options": + self.add_options_value_entry(info, param_name) + else: + self.add_yaml_value_entry(info, param_name) + + def add_yaml_value_entry(self, info, param_name): + widget = ctk.CTkTextbox( + self, + wrap=CHAR, + undo=True, + height=self.ROW_HEIGHT, + border_spacing=0, + corner_radius=self.CORNER_RADIUS, + ) + widget.insert("0.0", yaml.dump(info["value"], default_flow_style=True).rstrip()) + widget.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) + self.widgets.append(widget) + self.get_value_fns[param_name] = lambda: self.get_yaml_value(widget) + + def add_bool_value_entry(self, info, param_name): + widget = ctk.CTkCheckBox( + self, height=self.ROW_HEIGHT, corner_radius=self.CORNER_RADIUS, text="" + ) + widget.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) + if info["value"]: + widget.select() + else: + widget.deselect() + self.widgets.append(widget) + self.get_value_fns[param_name] = lambda: "true" if widget.get() else "false" + + def add_numeric_value_entry(self, info, param_name, is_int = True): + frame = ctk.CTkFrame(self, height=self.ROW_HEIGHT, border_width=0) + frame.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) + # Value. + w2 = ctk.CTkTextbox( + frame, + height=self.ROW_HEIGHT, + border_spacing=0, + corner_radius=self.CORNER_RADIUS, + width=50, + ) + w2.insert("0.0", info["value"]) + w2.grid(row=0, column=0, sticky="nsew", pady=0) + self.widgets.append(w2) + self.get_value_fns[param_name] = lambda: w2.get("1.0", END) + frame.columnconfigure(1, weight=0) + # Constraints. + min_val = None + max_val = None + if "min" in info["input_info"]: + min_val = info["input_info"]["min"] + if "max" in info["input_info"]: + max_val = info["input_info"]["max"] + if min_val is not None: + w3 = ctk.CTkLabel( + frame, + text=f"{'(' if 'lower_exclusive' in info['input_info'] else '['}{min_val}", + anchor="w", + height=self.ROW_HEIGHT, + ) + w3.grid(row=0, column=1, sticky="nsw", pady=0) + frame.columnconfigure(1, weight=0) + self.widgets.append(w3) + + if min_val is not None and max_val is not None: + slid_min = min_val + if is_int and 'lower_exclusive' in info['input_info']: + slid_min += 1 + slid_max = max_val + if is_int and 'upper_exclusive' in info['input_info']: + slid_max -= 1 + w4 = ctk.CTkSlider( + frame, + from_=slid_min, + to=slid_max, + orientation=HORIZONTAL, + corner_radius=self.CORNER_RADIUS, + command=lambda _: self.sync_text_to_slider(w2,w4, is_int) + ) + w2.bind("", lambda _: self.sync_slider_to_text(w4, w2)) + if is_int and max_val - slid_min < 100: + w4.configure(number_of_steps=slid_max - min_val + 1) + w4.set(info["value"]) + w4.grid(row=0, column=2, sticky="nsew", pady=0) + self.widgets.append(w4) + frame.columnconfigure(2, weight=1) + + if max_val is not None: + w5 = ctk.CTkLabel( + frame, + text=f"{', ' if min_val is None else ''}{max_val}{')' if 'upper_exclusive' in info['input_info'] else ']'}", + anchor="w", + height=self.ROW_HEIGHT, + ) + col = 3 if min_val is not None else 1 + w5.grid(row=0, column=col, sticky="nsw", pady=0) + frame.columnconfigure(col, weight=0) + self.widgets.append(w5) + self.widgets.append(frame) + + def sync_slider_to_text(self, slider, text): + try: + value = float(text.get("1.0", END)) + slider.set(value) + except ValueError: + pass + + def sync_text_to_slider(self, text, slider, is_int): + text.delete("1.0", END) + new_text = f"{slider.get():.0f}" if is_int else f"{slider.get():.2f}" + text.insert("1.0", new_text) + + def add_float_value_entry(self, info, param_name): + self.add_int_value_entry(info, param_name) + + def add_string_value_entry(self, info, param_name): + widget = ctk.CTkTextbox( + self, + wrap=CHAR, + undo=True, + height=self.ROW_HEIGHT, + border_spacing=0, + corner_radius=self.CORNER_RADIUS, + ) + widget.insert("0.0", info["value"]) + widget.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) + self.widgets.append(widget) + self.get_value_fns[param_name] = lambda: widget.get("1.0") + + def add_options_value_entry(self, info, param_name): + options = [] + if "options" in info["input_info"]: + for o in info["input_info"]["options"]: + options.append(str(o)) + if str(info["value"]) not in options: + options.append(str(info["value"])) + widget = ctk.CTkOptionMenu( + self, + values=options, + corner_radius=self.CORNER_RADIUS, + height=self.ROW_HEIGHT, + ) + widget.set(info["value"]) + widget.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) + self.widgets.append(widget) + self.get_value_fns[param_name] = lambda: widget.get() + + def get_yaml_value(self, widget): + try: + return yaml.load(widget.get("1.0", END), Loader=yaml.FullLoader) + except yaml.YAMLError as e: + print(f"{GUI_NAME}Error parsing YAML: {e}") + return None def main(): app = DynamicConfigGUI() + # TEST + data = yaml.load( + "{'type': 'config', 'name': 'MyConfig', 'fields': [{'type': 'field', 'name': 'i', 'value': 100, 'default': 100, 'input_info': {'type': 'int', 'min': 0, 'max': 2147483647, 'lower_exclusive': True}}, {'type': 'field', 'name': 'distance', 'unit': 'm', 'value': 42, 'default': 42, 'input_info': {'type': 'float', 'min': 0, 'max': 100}}, {'type': 'field', 'name': 'b', 'value': True, 'default': True, 'input_info': {'type': 'bool'}}, {'type': 'field', 'name': 'vec', 'value': [1, 2, 3], 'default': [1, 2, 3], 'input_info': {'type': 'yaml'}}, {'type': 'field', 'name': 'map', 'value': {'a': 1, 'b': 2, 'c': 3}, 'default': {'a': 1, 'b': 2, 'c': 3}, 'input_info': {'type': 'yaml'}}, {'type': 'field', 'name': 'mat', 'value': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'default': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'input_info': {'type': 'yaml'}}, {'type': 'field', 'name': 'my_enum', 'value': 'A', 'default': 'A', 'input_info': {'type': 'options', 'options': ['A', 'B', 'C']}}, {'type': 'config', 'name': 'SubConfig', 'field_name': 'sub_config', 'fields': [{'type': 'field', 'name': 'f', 'value': 1.1, 'default': 1.1, 'input_info': {'type': 'options', 'options': [0, 1.1, 2.2, 3.3]}}, {'type': 'field', 'name': 's', 'value': 'test', 'default': 'test', 'input_info': {'type': 'string'}}]}]}", + Loader=yaml.FullLoader, + ) + app.set_config_info(data) app.mainloop() From f8b62d5ef93140ba8cc0714e949d862ed0982d02 Mon Sep 17 00:00:00 2001 From: lschmid Date: Sun, 18 Aug 2024 16:12:21 -0400 Subject: [PATCH 11/22] update docs --- config_utilities/demos/dynamic_config_gui.py | 12 +- docs/Configs.md | 4 +- docs/DynamicConfigs.md | 8 -- docs/Dynamic_Configs.md | 122 +++++++++++++++++++ docs/README.md | 24 +++- 5 files changed, 152 insertions(+), 18 deletions(-) delete mode 100644 docs/DynamicConfigs.md create mode 100644 docs/Dynamic_Configs.md diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index a10f376..b542b85 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -2,7 +2,6 @@ from tkinter import * import customtkinter as ctk import yaml -import copy ctk.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" ctk.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" @@ -10,13 +9,22 @@ PAD_Y = 10 GUI_NAME = "[Config Utilities Dynamic Config GUI] " +""" +TODO(lschmid): Type Info known limitations: +- Make sure namespaces are handled correctly. +- Enable updating from values without re-building the entire GUI +- Add registered type information for virtual configs. +- Add support for config maps and vectors. +- Find a good interface and visualization for int/float types (consider exteded type information). +""" + class Settings: """ A class to store settings for the DynamicConfigGUI. This can also be opened as a top-level window. """ - METHOD_OPTIONS = ["Type Info", "Plain YAML"] + METHOD_OPTIONS = [ "Plain YAML", "Type Info (Experimental)"] APPEARANCE_OPTIONS = ["System", "Light", "Dark"] COLOR_THEME_OPTIONS = ["blue", "green", "dark-blue"] SACLE_MIN = 0.5 diff --git a/docs/Configs.md b/docs/Configs.md index 55a19b4..fb140b0 100644 --- a/docs/Configs.md +++ b/docs/Configs.md @@ -19,13 +19,13 @@ void declare_config(MyConfig& config) { ... } // Works! > The declaration of `declare_config` *must* be in the same namespace as the object type being declared. ```c++ -#include external/other_object.h +#include namespace external { void declare_config(OtherObject& config) { ... } // Also works! } // namespace external ``` ```c++ -#include external/other_object.h +#include void declare_config(external::OtherObject& config) { ... } // Will not work! ``` diff --git a/docs/DynamicConfigs.md b/docs/DynamicConfigs.md deleted file mode 100644 index a2a4676..0000000 --- a/docs/DynamicConfigs.md +++ /dev/null @@ -1,8 +0,0 @@ - - -# Setup -To use the example GUI, install system deps: - -```bash -pip install customtkinter -``` diff --git a/docs/Dynamic_Configs.md b/docs/Dynamic_Configs.md new file mode 100644 index 0000000..d912b6b --- /dev/null +++ b/docs/Dynamic_Configs.md @@ -0,0 +1,122 @@ +# Dynamic Configs + +This tutorial demonstrates how to register configs as dynamic, i.e., they can receive get/set requests at runtime to update their values, and how to use the dynamic config server to access these configs. + + +**Contents:** +- [Declaring a dynamic config](#declaring-a-dynamic-config) +- [Dynamic Config Callbacks](#dynamic-config-callbacks) +- [Setting Dynamic Configs](#setting-dynamic-configs) +- [Custom Dynamic Config Servers](#custom-dynamic-config-servers) + + +## Declaring a dynamic config +Any `config_utilities` config can be declared as a dynamic config. For this simply use the `DynamicConfig` struct. +Note that all dynamic configs must have a unique key that used to get/set their values: + +```c++ +#include + +// Define your configs as usual in some header: +struct MyConfig { ... }; +void declare_config(MyConfig& config) { ... } + +// Instantiate a dynamic config: +config::DynamicConfig dynamic_config("my_config"); // key: my_config +``` + +Dynamic configs are thread-safe, you can get and set their values as follows: + +```c++ +// Getting single values: +if (dynamic_config.get().some_param >= some_value) { + doMagic(); +} +``` + +> **ℹ️ Note**
+> Dynamic config `get()` returns a copy for thread safety, to get multiple values, this is more efficient: + +```c++ +// Getting multiple values: +const auto current_config = dynamic_config.get(); +float z = current_config.x * current_config.y; +``` + +You can also set dynamic configs using the `set()` function. Note that this takes an entire config as input. To only update specific values, get the current config first: + +```c++ +// Set the dynamic config to a config: +MyConfig new_values; +dynamic_config.set(new_values); // Works! Sets all values. + +// To update single params, get the config first: +auto values = dynamic_config.get(); +values.x = new_x; +dynamic_config.set(values); // Works! Will only update MyConfig.x. + +// Recall that config.get() returns a copy: +dynamic_config.get().x = new_x; // Won't work! Only modifies the copy. +``` + +## Dynamic Config Callbacks + +All dynamic configs allow registering callback functions that are triggered whenever the dynamic config is updated: + +```c++ +config::DynamicConfig dynamic_config("my_config"); + +// Register a callback: +dynamic_config.setCallback([&dynamic_config](){ + std::cout << "Got new config values: " << config::toString(dynamic_config) << std::endl; +};) +``` + +## Setting Dynamic Configs +We provide a base interface to set dynamic configs via the `DynamicConfigServer`. The server can get configs by key and uses `YAML` as an interface to interact with the config: + +```c++ +#include + +config::DynamicConfigServer server; + +// Get the values of a dynamic config: +YAML::Node values = server.getValues("my_config"); + +// Set the values of a dynamic config: +YAML::Node new_values = ...; +server.setValues("my_config", new_values); // Works! + +// Note that the new values can also contain only a subset of params: +server.setValues("my_config", YAML::Load("x: 123")); // Works! Only sets the x param. +``` + +Similarly to the dynamic configs, also the server allows registering hooks to keep track of which configs are registered and updated: + +```c++ +DynamicConfigServer::Hooks hooks; +hooks.onRegister = { ... }; +hooks.onDeregister = { ... }; +hooks.onUpdate = { ... }; + +server.setHooks(hooks); // All done! +``` + +To see further functionalities and use cases see the demo and source code. + +## Custom Dynamic Config Servers +Custom servers or client can easily be implemented by building on top of the provided `DynamicConfigServer`. +An example of this is given in the `RosDynamicConfigServer` in `config_utilities/parsing/ros.h`, which advertizes all config get/set interfaces via ROS topics. +This can be used to, for example, modify the C++ configs using a python GUI, as demonstrated in our `demo_dynamic_config`. Give it a try: + +```bash + # Required for the GUI: +pip install customtkinter + +# Run the demo: +roslaunch config_utilities demo_dynamic_config.launch +``` + +> **ℹ️ Note**
+We further provide an initial implementation of a field-based GUI. However, this is still experimental and we recommend using the `Plain YAML` editor. +To try it and get some inspiration how the config info from the server can be used, click `Settings` in te GUI and pick the `Type Info (Experimental)` UI Method in the demo. diff --git a/docs/README.md b/docs/README.md index daa6297..d3a81bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,14 +40,19 @@ The following tutorials will guide you through functionalities of `config_utilit - [Managed instances](External.md#managed-instances) - [Debugging](External.md#debugging) -8. [**Varia**](Varia.md) +8. [**Dynamic Configs**](Dynamic_Configs.md) +- [Declaring a dynamic config](Dynamix_Configs.md#declaring-a-dynamic-config) +- [Dynamic Config Callbacks](Dynamix_Configs.md#dynamic-config-callbacks) +- [Setting Dynamic Configs](Dynamix_Configs.md#setting-dynamic-configs) +- [Custom Dynamic Config Servers](Dynamix_Configs.md#custom-dynamic-config-servers) + +9. [**Varia**](Varia.md) - [Settings](Varia.md#settings) - - [Globals](Varia.md#globals) ## Demos The (non-ros) demos can be run via the `run_demo.py` utility in the scripts directory. If you are building this library via catkin, you can run one of the following to see the results of one of the corresponding demo files: -``` +```bash python3 scripts/run_demo.py config python3 scripts/run_demo.py inheritance python3 scripts/run_demo.py factory @@ -56,9 +61,16 @@ python3 scripts/run_demo.py factory > **ℹ️ Note**
> If you're building via cmake, you can point `run_demo.py` to the build directory with `-b/--build_path`. -The ros demo can be run via: -``` +The ros demos can be run via: +```bash roslaunch config_utilities demo_ros.launch +roslaunch config_utilities demo_dynamic_config.launch ``` -If you are looking for a specific use case that is not in the tutorials or demos, chances are you can find a good example in the `tests/` directory! +Note that for the `dynamic config demo` customtkinter is required to run the GUI: +```bash +pip install customtkinter +``` + +> **ℹ️ Note**
+If you are looking for a specific use case that is not in the tutorials or demos, chances are you can find a good example in the `tests/` directory! Try and give it a look! From bbedb132cdbf5d0c574aa8046f4d138406e3e3bf Mon Sep 17 00:00:00 2001 From: lschmid Date: Sun, 18 Aug 2024 16:14:33 -0400 Subject: [PATCH 12/22] fix docs inlining --- docs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index d3a81bb..5e6c698 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,10 +41,10 @@ The following tutorials will guide you through functionalities of `config_utilit - [Debugging](External.md#debugging) 8. [**Dynamic Configs**](Dynamic_Configs.md) -- [Declaring a dynamic config](Dynamix_Configs.md#declaring-a-dynamic-config) -- [Dynamic Config Callbacks](Dynamix_Configs.md#dynamic-config-callbacks) -- [Setting Dynamic Configs](Dynamix_Configs.md#setting-dynamic-configs) -- [Custom Dynamic Config Servers](Dynamix_Configs.md#custom-dynamic-config-servers) + - [Declaring a dynamic config](Dynamix_Configs.md#declaring-a-dynamic-config) + - [Dynamic Config Callbacks](Dynamix_Configs.md#dynamic-config-callbacks) + - [Setting Dynamic Configs](Dynamix_Configs.md#setting-dynamic-configs) + - [Custom Dynamic Config Servers](Dynamix_Configs.md#custom-dynamic-config-servers) 9. [**Varia**](Varia.md) - [Settings](Varia.md#settings) From aa01a1ae808c9fe557c6fddacd3b138693cab2b4 Mon Sep 17 00:00:00 2001 From: lschmid Date: Sun, 18 Aug 2024 16:25:02 -0400 Subject: [PATCH 13/22] minor cleanup --- config_utilities/demos/demo_dynamic_config.launch | 2 -- 1 file changed, 2 deletions(-) diff --git a/config_utilities/demos/demo_dynamic_config.launch b/config_utilities/demos/demo_dynamic_config.launch index a7673d2..7892d50 100644 --- a/config_utilities/demos/demo_dynamic_config.launch +++ b/config_utilities/demos/demo_dynamic_config.launch @@ -1,9 +1,7 @@ - - From 40b03a144ccf0e81e64cde086eb5c2cf6280e54a Mon Sep 17 00:00:00 2001 From: lschmid Date: Tue, 3 Sep 2024 21:18:45 +0200 Subject: [PATCH 14/22] fix experimental python GUI --- config_utilities/demos/dynamic_config_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index b542b85..405f637 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -233,7 +233,7 @@ def setup_frame(self): self.columnconfigure(0, weight=1, pad=PAD_X) def setup_config_frame(self): - if self.settings.method == "Type Info": + if self.settings.method == "Type Info (Experimental)": self.config_frame = TypeInfoConfigFrame(self, self._value_changed_cb) else: # Default to plain text if unsupported. From 14c84d35e79c94adb75775c0952e13969807422b Mon Sep 17 00:00:00 2001 From: lschmid Date: Tue, 3 Sep 2024 22:16:57 +0200 Subject: [PATCH 15/22] update config field types --- config_utilities/demos/dynamic_config_gui.py | 4 +- .../include/config_utilities/dynamic_config.h | 4 +- .../internal/field_input_info.h | 17 +-- config_utilities/src/field_input_info.cpp | 114 +++++++++++------- .../test/tests/field_input_info.cpp | 20 ++- 5 files changed, 89 insertions(+), 70 deletions(-) diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index 405f637..6d2e81c 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -562,9 +562,9 @@ def add_value_entry(self, info): input_type = info["input_info"]["type"] if input_type == "bool": self.add_bool_value_entry(info, param_name) - elif input_type == "int": + elif "int" in input_type: self.add_numeric_value_entry(info, param_name, True) - elif input_type == "float": + elif "float" in input_type: self.add_numeric_value_entry(info, param_name, False) elif input_type == "string": self.add_string_value_entry(info, param_name) diff --git a/config_utilities/include/config_utilities/dynamic_config.h b/config_utilities/include/config_utilities/dynamic_config.h index f57f5f8..db0c9c9 100644 --- a/config_utilities/include/config_utilities/dynamic_config.h +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -207,7 +207,7 @@ struct DynamicConfigRegistry { /** * @brief A wrapper class for for configs that can be dynamically changed. * - * @tparam ConfigT The contained configuration type + * @tparam ConfigT The contained configuration type. */ template struct DynamicConfig { @@ -215,7 +215,7 @@ 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 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 = {}, Callback callback = {}); diff --git a/config_utilities/include/config_utilities/internal/field_input_info.h b/config_utilities/include/config_utilities/internal/field_input_info.h index 12749c5..fca0de9 100644 --- a/config_utilities/include/config_utilities/internal/field_input_info.h +++ b/config_utilities/include/config_utilities/internal/field_input_info.h @@ -72,12 +72,13 @@ struct FieldInputInfo { }; struct IntFieldInputInfo : public FieldInputInfo { - IntFieldInputInfo() : FieldInputInfo(Type::kInt) {} + IntFieldInputInfo(const std::string& type_str) : FieldInputInfo(Type::kInt), type_str(type_str) {} // Constraints for the field. - // NOTE(lschmid): We do not consider data larger than 64 bit integers. - int64_t min = std::numeric_limits::lowest(); - uint64_t max = std::numeric_limits::max(); + // NOTE(lschmid): We currently do not consider data larger than 64 bit integers. + std::string type_str; // "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64" + std::optional min; + std::optional max; bool lower_inclusive = true; bool upper_inclusive = true; @@ -88,11 +89,13 @@ struct IntFieldInputInfo : public FieldInputInfo { }; struct FloatFieldInputInfo : public FieldInputInfo { - FloatFieldInputInfo() : FieldInputInfo(Type::kFloat) {} + FloatFieldInputInfo(const std::string& type_str) : FieldInputInfo(Type::kFloat), type_str(type_str) {} // Constraints for the field. - double min = std::numeric_limits::lowest(); - double max = std::numeric_limits::max(); + // NOTE(lschmid): We currently do not consider data larger than 64 bit floats. + std::string type_str; // "float32", "float64" + std::optional min; + std::optional max; bool lower_inclusive = true; bool upper_inclusive = true; diff --git a/config_utilities/src/field_input_info.cpp b/config_utilities/src/field_input_info.cpp index c877b9b..fd3be3a 100644 --- a/config_utilities/src/field_input_info.cpp +++ b/config_utilities/src/field_input_info.cpp @@ -92,32 +92,47 @@ FieldInputInfo::Ptr FieldInputInfo::merge(const FieldInputInfo::Ptr& from, const YAML::Node IntFieldInputInfo::toYaml() const { YAML::Node node; - node["type"] = "int"; - node["min"] = min; - node["max"] = max; - // Only store the rarer cases. - if (!lower_inclusive) { - node["lower_exclusive"] = true; + node["type"] = type_str; + if (min) { + node["min"] = *min; + // Only store the rarer cases. + if (!lower_inclusive) { + node["lower_exclusive"] = true; + } } - if (!upper_inclusive) { - node["upper_exclusive"] = true; + if (max) { + node["max"] = *max; + if (!upper_inclusive) { + node["upper_exclusive"] = true; + } } return node; } void IntFieldInputInfo::mergeSame(const FieldInputInfo& other) { const auto& other_info = dynamic_cast(other); - if (min < other_info.min) { + if (!min && other_info.min) { min = other_info.min; lower_inclusive = other_info.lower_inclusive; - } else if (min == other_info.min) { - lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } else if (min && other_info.min) { + if (*min < *other_info.min) { + min = *other_info.min; + lower_inclusive = other_info.lower_inclusive; + } else if (*min == *other_info.min) { + lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } } - if (max > other_info.max) { + + if (!max && other_info.max) { max = other_info.max; upper_inclusive = other_info.upper_inclusive; - } else if (max == other_info.max) { - upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } else if (max && other_info.max) { + if (*max > *other_info.max) { + max = other_info.max; + upper_inclusive = other_info.upper_inclusive; + } else if (*max == *other_info.max) { + upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } } } @@ -141,31 +156,47 @@ void IntFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { YAML::Node FloatFieldInputInfo::toYaml() const { YAML::Node node; - node["type"] = "float"; - node["min"] = min; - node["max"] = max; - if (!lower_inclusive) { - node["lower_exclusive"] = true; + node["type"] = type_str; + if (min) { + node["min"] = *min; + // Only store the rarer cases. + if (!lower_inclusive) { + node["lower_exclusive"] = true; + } } - if (!upper_inclusive) { - node["upper_exclusive"] = true; + if (max) { + node["max"] = *max; + if (!upper_inclusive) { + node["upper_exclusive"] = true; + } } return node; } void FloatFieldInputInfo::mergeSame(const FieldInputInfo& other) { const auto& other_info = dynamic_cast(other); - if (min < other_info.min) { + if (!min && other_info.min) { min = other_info.min; lower_inclusive = other_info.lower_inclusive; - } else if (min == other_info.min) { - lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } else if (min && other_info.min) { + if (*min < *other_info.min) { + min = *other_info.min; + lower_inclusive = other_info.lower_inclusive; + } else if (*min == *other_info.min) { + lower_inclusive = lower_inclusive && other_info.lower_inclusive; + } } - if (max > other_info.max) { + + if (!max && other_info.max) { max = other_info.max; upper_inclusive = other_info.upper_inclusive; - } else if (max == other_info.max) { - upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } else if (max && other_info.max) { + if (*max > *other_info.max) { + max = other_info.max; + upper_inclusive = other_info.upper_inclusive; + } else if (*max == *other_info.max) { + upper_inclusive = upper_inclusive && other_info.upper_inclusive; + } } } @@ -206,15 +237,6 @@ void OptionsFieldInputInfo::mergeSame(const FieldInputInfo& other) { } } -// Helper function for int types. -template -FieldInputInfo::Ptr createNumericInfo() { - auto info = std::make_shared(); - info->min = std::numeric_limits::lowest(); - info->max = std::numeric_limits::max(); - return info; -} - // Bool. template <> FieldInputInfo::Ptr createFieldInputInfo() { @@ -224,53 +246,53 @@ FieldInputInfo::Ptr createFieldInputInfo() { // Ints. template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("int8"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("int16"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("int32"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("int64"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("uint8"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("uint16"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("uint32"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return createNumericInfo(); + return std::make_shared("uint64"); } // Floats. template <> FieldInputInfo::Ptr createFieldInputInfo() { - return std::make_shared(); + return std::make_shared("float32"); } template <> FieldInputInfo::Ptr createFieldInputInfo() { - return std::make_shared(); + return std::make_shared("float64"); } // Strings. diff --git a/config_utilities/test/tests/field_input_info.cpp b/config_utilities/test/tests/field_input_info.cpp index 8544117..5c0bc90 100644 --- a/config_utilities/test/tests/field_input_info.cpp +++ b/config_utilities/test/tests/field_input_info.cpp @@ -55,9 +55,8 @@ name: DefaultConfig value: 1 default: 1 input_info: - type: int + type: int32 min: 0 - max: 2147483647 lower_exclusive: true - type: field name: f @@ -65,16 +64,15 @@ name: DefaultConfig value: 2.1 default: 2.1 input_info: - type: float + type: float32 min: 0 - max: 1.844674407370955e+19 - type: field name: d unit: m/s value: 3.2 default: 3.2 input_info: - type: float + type: float64 min: 0 max: 4 upper_exclusive: true @@ -89,8 +87,7 @@ name: DefaultConfig value: 4 default: 4 input_info: - type: int - min: 0 + type: uint8 max: 5 - type: field name: s @@ -194,9 +191,8 @@ name: DefaultConfig value: 1 default: 1 input_info: - type: int + type: int32 min: 0 - max: 2147483647 lower_exclusive: true - type: config name: SubSubConfig @@ -207,9 +203,8 @@ name: DefaultConfig value: 1 default: 1 input_info: - type: int + type: int32 min: 0 - max: 2147483647 lower_exclusive: true - type: config name: SubSubConfig @@ -220,9 +215,8 @@ name: DefaultConfig value: 1 default: 1 input_info: - type: int + type: int32 min: 0 - max: 2147483647 lower_exclusive: true )"; expectEqual(info, YAML::Load(expected)); From 228f2027da59054192119a622b4fdca1ae6ef09a Mon Sep 17 00:00:00 2001 From: lschmid Date: Tue, 3 Sep 2024 22:29:51 +0200 Subject: [PATCH 16/22] update GUI for parameter display --- config_utilities/demos/dynamic_config_gui.py | 50 ++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/config_utilities/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py index 6d2e81c..4255df2 100644 --- a/config_utilities/demos/dynamic_config_gui.py +++ b/config_utilities/demos/dynamic_config_gui.py @@ -24,7 +24,7 @@ class Settings: A class to store settings for the DynamicConfigGUI. This can also be opened as a top-level window. """ - METHOD_OPTIONS = [ "Plain YAML", "Type Info (Experimental)"] + METHOD_OPTIONS = ["Plain YAML", "Type Info (Experimental)"] APPEARANCE_OPTIONS = ["System", "Light", "Dark"] COLOR_THEME_OPTIONS = ["blue", "green", "dark-blue"] SACLE_MIN = 0.5 @@ -457,7 +457,7 @@ def get_config(self): curr_node = curr_node[n] curr_node[ns[-1]] = fn() return yaml_data - + # Build the UI for a config info. def clear_config_ui(self): for widget in self.widgets: @@ -472,19 +472,25 @@ def build_config_ui(self, config_info): # Header row. name = config_info["name"] if "name" in config_info else "Unknown Config" self.w_header_name = ctk.CTkLabel(self, text=f"{name}:", anchor="w") - self.w_header_name.grid(row=self.current_row, column=0, sticky="nsw", padx=PAD_X) + self.w_header_name.grid( + row=self.current_row, column=0, sticky="nsw", padx=PAD_X + ) self.w_header_value = ctk.CTkLabel( self, text="Value:", anchor="w", ) - self.w_header_value.grid(row=self.current_row, column=1, sticky="nsw", padx=PAD_X) + self.w_header_value.grid( + row=self.current_row, column=1, sticky="nsw", padx=PAD_X + ) self.w_header_default = ctk.CTkLabel( self, text="Default:", anchor="w", ) - self.w_header_default.grid(row=self.current_row, column=2, sticky="nsw", padx=PAD_X) + self.w_header_default.grid( + row=self.current_row, column=2, sticky="nsw", padx=PAD_X + ) self.current_row += 1 # Build the config. @@ -599,7 +605,7 @@ def add_bool_value_entry(self, info, param_name): self.widgets.append(widget) self.get_value_fns[param_name] = lambda: "true" if widget.get() else "false" - def add_numeric_value_entry(self, info, param_name, is_int = True): + def add_numeric_value_entry(self, info, param_name, is_int=True): frame = ctk.CTkFrame(self, height=self.ROW_HEIGHT, border_width=0) frame.grid(row=self.current_row, column=1, sticky="nsew", pady=0, padx=PAD_X) # Value. @@ -622,23 +628,24 @@ def add_numeric_value_entry(self, info, param_name, is_int = True): min_val = info["input_info"]["min"] if "max" in info["input_info"]: max_val = info["input_info"]["max"] - if min_val is not None: + + if min_val is not None and max_val is not None: + # Clamped values: Use slider. w3 = ctk.CTkLabel( frame, text=f"{'(' if 'lower_exclusive' in info['input_info'] else '['}{min_val}", anchor="w", height=self.ROW_HEIGHT, ) - w3.grid(row=0, column=1, sticky="nsw", pady=0) + w3.grid(row=0, column=1, sticky="nsw", pady=0, padx=PAD_X) frame.columnconfigure(1, weight=0) self.widgets.append(w3) - if min_val is not None and max_val is not None: slid_min = min_val - if is_int and 'lower_exclusive' in info['input_info']: + if is_int and "lower_exclusive" in info["input_info"]: slid_min += 1 slid_max = max_val - if is_int and 'upper_exclusive' in info['input_info']: + if is_int and "upper_exclusive" in info["input_info"]: slid_max -= 1 w4 = ctk.CTkSlider( frame, @@ -646,7 +653,7 @@ def add_numeric_value_entry(self, info, param_name, is_int = True): to=slid_max, orientation=HORIZONTAL, corner_radius=self.CORNER_RADIUS, - command=lambda _: self.sync_text_to_slider(w2,w4, is_int) + command=lambda _: self.sync_text_to_slider(w2, w4, is_int), ) w2.bind("", lambda _: self.sync_slider_to_text(w4, w2)) if is_int and max_val - slid_min < 100: @@ -655,8 +662,6 @@ def add_numeric_value_entry(self, info, param_name, is_int = True): w4.grid(row=0, column=2, sticky="nsew", pady=0) self.widgets.append(w4) frame.columnconfigure(2, weight=1) - - if max_val is not None: w5 = ctk.CTkLabel( frame, text=f"{', ' if min_val is None else ''}{max_val}{')' if 'upper_exclusive' in info['input_info'] else ']'}", @@ -667,6 +672,23 @@ def add_numeric_value_entry(self, info, param_name, is_int = True): w5.grid(row=0, column=col, sticky="nsw", pady=0) frame.columnconfigure(col, weight=0) self.widgets.append(w5) + elif min_val is not None or max_val is not None: + # Constraints: Use label. + w3 = ctk.CTkLabel( + frame, + text=( + f"(>{'' if 'lower_exclusive' in info['input_info'] else '='}{min_val})" + if min_val is not None + else f"(<{'' if 'upper_exclusive' in info['input_info'] else '='}{max_val})" + ), + anchor="w", + height=self.ROW_HEIGHT, + ) + w3.grid(row=0, column=1, sticky="nsw", pady=0, padx=PAD_X) + frame.columnconfigure(1, weight=0) + frame.columnconfigure(0, weight=1) + self.widgets.append(w3) + self.widgets.append(frame) def sync_slider_to_text(self, slider, text): From 8bddd64ffcaeba02ac7e7e5d57e29a7c9d32e389 Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 6 Sep 2024 17:23:09 +0200 Subject: [PATCH 17/22] minor cleanup --- .../include/config_utilities/formatting/asl.h | 11 ----------- .../include/config_utilities/internal/string_utils.h | 2 +- config_utilities/src/asl_formatter.cpp | 5 +++-- config_utilities/test/src/utils.cpp | 3 ++- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/config_utilities/include/config_utilities/formatting/asl.h b/config_utilities/include/config_utilities/formatting/asl.h index 6b8451b..e137fe2 100644 --- a/config_utilities/include/config_utilities/formatting/asl.h +++ b/config_utilities/include/config_utilities/formatting/asl.h @@ -79,17 +79,6 @@ class AslFormatter : public Formatter { std::string formatSubconfig(const MetaData& data, size_t indent) const; std::string resolveConfigName(const MetaData& data) const; - // Formatting options, currently not exposed in global settings but work if want changed. - // TODO(lschmid): Global formatting options should probably be a config of the formatter. - // If true add subconfig types after the fieldname. - constexpr static bool indicate_subconfig_types_ = true; - // If true label subconfigs as default if all their values are default. - constexpr static bool indicate_subconfig_default_ = true; - // If true indicate that a config is a virtual config in the config name. - constexpr static bool indicate_virtual_configs_ = true; - // If true indicate the number of a check and total number of checks in failed checks. - constexpr static bool indicate_num_checks_ = true; - // Variables. std::string name_prefix_; size_t total_num_checks_; diff --git a/config_utilities/include/config_utilities/internal/string_utils.h b/config_utilities/include/config_utilities/internal/string_utils.h index 2681282..03b7089 100644 --- a/config_utilities/include/config_utilities/internal/string_utils.h +++ b/config_utilities/include/config_utilities/internal/string_utils.h @@ -91,7 +91,7 @@ std::string joinNamespace(const std::string& namespace_1, /** * @brief Formatting of YAML nodes to strings. Most config types can be neatly represented as low-depth yaml nodes, or - * should otherwise probably be wrapped in a separate confi struct. + * should otherwise probably be wrapped in a separate config struct. * @param data The data to be formatted. * @param reformat_float Whether to try and print floats with default stream precision * @returns The formatted string. diff --git a/config_utilities/src/asl_formatter.cpp b/config_utilities/src/asl_formatter.cpp index e501a7e..488d8eb 100644 --- a/config_utilities/src/asl_formatter.cpp +++ b/config_utilities/src/asl_formatter.cpp @@ -164,8 +164,9 @@ std::string AslFormatter::formatChecksInternal(const MetaData& data, const std:: } const std::string rendered_name = check->name().empty() ? "" : " for '" + name_prefix_ + check->name() + "'"; const std::string rendered_num = - indicate_num_checks_ ? "[" + std::to_string(current_check_) + "/" + std::to_string(total_num_checks_) + "] " - : ""; + Settings::instance().printing.show_num_checks + ? "[" + std::to_string(current_check_) + "/" + std::to_string(total_num_checks_) + "] " + : ""; const std::string msg = sev + "Check " + rendered_num + "failed" + rendered_name + ": " + check->message() + "."; result.append(wrapString(msg, length, sev.length(), false) + "\n"); } diff --git a/config_utilities/test/src/utils.cpp b/config_utilities/test/src/utils.cpp index 6eae451..0ef362b 100644 --- a/config_utilities/test/src/utils.cpp +++ b/config_utilities/test/src/utils.cpp @@ -68,7 +68,7 @@ bool expectEqual(const YAML::Node& a, const YAML::Node& b) { for (const auto& kv_pair : a) { const std::string key = kv_pair.first.Scalar(); if (!b[key]) { - ADD_FAILURE() << "Key " << key << " not found in b."; + ADD_FAILURE() << "Key '" << key << "' not found in b."; return false; } EXPECT_TRUE(expectEqual(kv_pair.second, b[key])); @@ -100,4 +100,5 @@ void TestLogger::print() const { std::cout << internal::severityToString(message.first) << ": " << message.second << std::endl; } } + } // namespace config::test From a95749007914a9d17a1b0b2957f29f991883367c Mon Sep 17 00:00:00 2001 From: lschmid Date: Fri, 6 Sep 2024 17:41:19 +0200 Subject: [PATCH 18/22] include optional --- .../include/config_utilities/internal/field_input_info.h | 1 + 1 file changed, 1 insertion(+) diff --git a/config_utilities/include/config_utilities/internal/field_input_info.h b/config_utilities/include/config_utilities/internal/field_input_info.h index fca0de9..53d7777 100644 --- a/config_utilities/include/config_utilities/internal/field_input_info.h +++ b/config_utilities/include/config_utilities/internal/field_input_info.h @@ -37,6 +37,7 @@ #include #include +#include #include #include From 83c43796c2660b2661224e58a6ebb60a76bfe1b1 Mon Sep 17 00:00:00 2001 From: lschmid Date: Mon, 9 Sep 2024 16:59:05 -0400 Subject: [PATCH 19/22] add float conversion tolerance for tests --- .../test/include/config_utilities/test/utils.h | 8 +++++++- config_utilities/test/src/utils.cpp | 10 +++++++++- config_utilities/test/tests/field_input_info.cpp | 3 ++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/config_utilities/test/include/config_utilities/test/utils.h b/config_utilities/test/include/config_utilities/test/utils.h index 5ae1927..2dbac96 100644 --- a/config_utilities/test/include/config_utilities/test/utils.h +++ b/config_utilities/test/include/config_utilities/test/utils.h @@ -47,7 +47,13 @@ namespace config::test { -bool expectEqual(const YAML::Node& a, const YAML::Node& b); +/** + * @brief Compare two YAML nodes for equality. + * @param a The first node. + * @param b The second node. + * @param epsilon The tolerance for floating point comparisons. + */ +bool expectEqual(const YAML::Node& a, const YAML::Node& b, double epsilon = 0); class TestLogger : public internal::Logger { public: diff --git a/config_utilities/test/src/utils.cpp b/config_utilities/test/src/utils.cpp index 0ef362b..e0bef4c 100644 --- a/config_utilities/test/src/utils.cpp +++ b/config_utilities/test/src/utils.cpp @@ -39,13 +39,21 @@ namespace config::test { -bool expectEqual(const YAML::Node& a, const YAML::Node& b) { +bool expectEqual(const YAML::Node& a, const YAML::Node& b, double epsilon) { EXPECT_EQ(a.Type(), b.Type()); if (a.Type() != b.Type()) { return false; } switch (a.Type()) { case YAML::NodeType::Scalar: + if (epsilon > 0.0) { + // Attempt double conversion and comparison. + double a_val, b_val; + if (YAML::convert::decode(a, a_val) && YAML::convert::decode(b, b_val)) { + EXPECT_NEAR(a_val, b_val, epsilon); + return std::abs(a_val - b_val) <= epsilon; + } + } EXPECT_EQ(a.Scalar(), b.Scalar()); return a.Scalar() == b.Scalar(); case YAML::NodeType::Sequence: diff --git a/config_utilities/test/tests/field_input_info.cpp b/config_utilities/test/tests/field_input_info.cpp index 5c0bc90..5a7b020 100644 --- a/config_utilities/test/tests/field_input_info.cpp +++ b/config_utilities/test/tests/field_input_info.cpp @@ -219,7 +219,8 @@ name: DefaultConfig min: 0 lower_exclusive: true )"; - expectEqual(info, YAML::Load(expected)); + // Epect near equal for floating point values. + expectEqual(info, YAML::Load(expected), 1e-4); } } // namespace config::test From 222a82a2b5dc19197be930b38a08e8de88550607 Mon Sep 17 00:00:00 2001 From: lschmid Date: Mon, 9 Sep 2024 17:05:04 -0400 Subject: [PATCH 20/22] properly propagate epsilon values --- config_utilities/test/src/utils.cpp | 8 ++++---- config_utilities/test/tests/field_input_info.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config_utilities/test/src/utils.cpp b/config_utilities/test/src/utils.cpp index e0bef4c..7e8697a 100644 --- a/config_utilities/test/src/utils.cpp +++ b/config_utilities/test/src/utils.cpp @@ -62,8 +62,8 @@ bool expectEqual(const YAML::Node& a, const YAML::Node& b, double epsilon) { return false; } for (size_t i = 0; i < a.size(); ++i) { - EXPECT_TRUE(expectEqual(a[i], b[i])); - if (!expectEqual(a[i], b[i])) { + EXPECT_TRUE(expectEqual(a[i], b[i], epsilon)); + if (!expectEqual(a[i], b[i], epsilon)) { return false; } } @@ -79,8 +79,8 @@ bool expectEqual(const YAML::Node& a, const YAML::Node& b, double epsilon) { ADD_FAILURE() << "Key '" << key << "' not found in b."; return false; } - EXPECT_TRUE(expectEqual(kv_pair.second, b[key])); - if (!expectEqual(kv_pair.second, b[key])) { + EXPECT_TRUE(expectEqual(kv_pair.second, b[key], epsilon)); + if (!expectEqual(kv_pair.second, b[key], epsilon)) { return false; } } diff --git a/config_utilities/test/tests/field_input_info.cpp b/config_utilities/test/tests/field_input_info.cpp index 5a7b020..1187e28 100644 --- a/config_utilities/test/tests/field_input_info.cpp +++ b/config_utilities/test/tests/field_input_info.cpp @@ -220,7 +220,7 @@ name: DefaultConfig lower_exclusive: true )"; // Epect near equal for floating point values. - expectEqual(info, YAML::Load(expected), 1e-4); + expectEqual(info, YAML::Load(expected), 1e-6); } } // namespace config::test From 72b01949e6b7d8242d5b306bf756a2245586602a Mon Sep 17 00:00:00 2001 From: lschmid Date: Sun, 17 Nov 2024 18:13:17 -0500 Subject: [PATCH 21/22] refactor yaml parsing --- .../include/config_utilities/factory.h | 2 +- .../config_utilities/internal/yaml_parser.h | 47 ++++++++++--------- config_utilities/src/factory.cpp | 2 +- config_utilities/src/yaml_parser.cpp | 4 +- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/config_utilities/include/config_utilities/factory.h b/config_utilities/include/config_utilities/factory.h index 7c03122..5dbb6f2 100644 --- a/config_utilities/include/config_utilities/factory.h +++ b/config_utilities/include/config_utilities/factory.h @@ -65,7 +65,7 @@ std::vector convertArguments() { * @param data YAML node to read type from * @param type Type value to filll * @param required Whether or not the type field is required - * @param param_name Field in YAML node to read (empty string defaults to Settings().factory_type_param_name) + * @param param_name Field in YAML node to read (empty string defaults to Settings().factory.type_param_name) */ bool getType(const YAML::Node& data, std::string& type, bool required = true, const std::string& param_name = ""); diff --git a/config_utilities/include/config_utilities/internal/yaml_parser.h b/config_utilities/include/config_utilities/internal/yaml_parser.h index 9703832..c9c13e0 100644 --- a/config_utilities/include/config_utilities/internal/yaml_parser.h +++ b/config_utilities/include/config_utilities/internal/yaml_parser.h @@ -48,7 +48,6 @@ #include "config_utilities/internal/yaml_utils.h" #include "config_utilities/traits.h" - namespace config::internal { /** @@ -94,7 +93,7 @@ class YamlParser { * @param name Name of the param to look up. * @param value Value to parse. * @param sub_namespace Sub-namespace of the param to look up in the node. - * @param error Where to store the error message if conversion fails. + * @param error Where to store the error message if conversion fails. If successful, error will be empty. * @return true If the value was found and successfully parsed. */ template @@ -108,6 +107,7 @@ class YamlParser { // The param is not defined. This is not an error. return false; } + error.clear(); try { fromYamlImpl(value, child_node, error); } catch (const std::exception& e) { @@ -126,7 +126,7 @@ class YamlParser { YAML::Node node; std::string err; try { - node = toYamlImpl("tmp", value, err); + node = toYamlImpl(value, err); } catch (const std::exception& e) { err = std::string(e.what()); } @@ -136,7 +136,7 @@ class YamlParser { } return YAML::Node(YAML::NodeType::Null); } - return node["tmp"]; + return node; } /** @@ -147,7 +147,7 @@ class YamlParser { * @param name Name of the param to store. * @param value Value to parse. * @param sub_namespace Sub-namespace of the param when adding it to the root node. - * @param error Where to store the error message if conversion fails. + * @param error Where to store the error message if conversion fails. If successful, error will be empty. * @return The yaml node the value was successfully parsed. Null-node if conversion failed. */ template @@ -156,8 +156,9 @@ class YamlParser { const std::string& sub_namespace, std::string& error) { YAML::Node node; + error.clear(); try { - node = toYamlImpl(name, value, error); + node = toYamlImpl(value, error); } catch (const std::exception& e) { error = std::string(e.what()); } @@ -166,12 +167,17 @@ class YamlParser { return YAML::Node(YAML::NodeType::Null); } - moveDownNamespace(node, sub_namespace); - return node; + // Fix the namespacing and param name. + YAML::Node root_node; + root_node[name] = node; + moveDownNamespace(root_node, sub_namespace); + return root_node; } private: // Generic types. + // NOTE(lschmid): fromYamlImpl may throw if an conversion error occurs, which will be caught in the public facing + // implementations. template , bool>::type = true, typename std::enable_if::value, bool>::type = true> @@ -180,9 +186,9 @@ class YamlParser { } template - static YAML::Node toYamlImpl(const std::string& name, const T& value, std::string& error) { + static YAML::Node toYamlImpl(const T& value, std::string& error) { YAML::Node node; - node[name] = value; + node = value; return node; } @@ -198,16 +204,15 @@ class YamlParser { } template - static YAML::Node toYamlImpl(const std::string& name, const std::vector& value, std::string& error) { + static YAML::Node toYamlImpl(const std::vector& value, std::string& error) { YAML::Node node; - node[name] = YAML::Node(YAML::NodeType::Sequence); + node = YAML::Node(YAML::NodeType::Sequence); for (const T& element : value) { - node[name].push_back(element); + node.push_back(element); } return node; } - // Set. template static void fromYamlImpl(std::set& value, const YAML::Node& node, std::string& error) { @@ -236,11 +241,11 @@ class YamlParser { } template - static YAML::Node toYamlImpl(const std::string& name, const std::set& value, std::string& error) { + static YAML::Node toYamlImpl(const std::set& value, std::string& error) { YAML::Node node; - node[name] = YAML::Node(YAML::NodeType::Sequence); + node = YAML::Node(YAML::NodeType::Sequence); for (const T& element : value) { - node[name].push_back(element); + node.push_back(element); } return node; @@ -257,11 +262,11 @@ class YamlParser { } template - static YAML::Node toYamlImpl(const std::string& name, const std::map& value, std::string& error) { + static YAML::Node toYamlImpl(const std::map& value, std::string& error) { YAML::Node node; - node[name] = YAML::Node(YAML::NodeType::Map); + node = YAML::Node(YAML::NodeType::Map); for (const auto& kv_pair : value) { - node[name][kv_pair.first] = kv_pair.second; + node[kv_pair.first] = kv_pair.second; } return node; } @@ -332,7 +337,7 @@ class YamlParser { // Specialization for uint8 to not represent it as char but as number. static void fromYamlImpl(uint8_t& value, const YAML::Node& node, std::string& error); - static YAML::Node toYamlImpl(const std::string& name, const uint8_t& value, std::string& error); + static YAML::Node toYamlImpl(const uint8_t& value, std::string& error); }; } // namespace config::internal diff --git a/config_utilities/src/factory.cpp b/config_utilities/src/factory.cpp index 097a861..418ec6a 100644 --- a/config_utilities/src/factory.cpp +++ b/config_utilities/src/factory.cpp @@ -233,7 +233,7 @@ bool getTypeImpl(const YAML::Node& data, std::string& type, const std::string& k } bool getType(const YAML::Node& data, std::string& type, bool required, const std::string& param_name) { - const std::string key = param_name.empty() ? Settings::instance().factory_type_param_name : param_name; + const std::string key = param_name.empty() ? Settings::instance().factory.type_param_name : param_name; const auto success = getTypeImpl(data, type, key); if (!success && required) { Logger::logError("Could not read the param '" + key + "' to deduce the type of the module to create."); diff --git a/config_utilities/src/yaml_parser.cpp b/config_utilities/src/yaml_parser.cpp index 02265ab..e8fc84d 100644 --- a/config_utilities/src/yaml_parser.cpp +++ b/config_utilities/src/yaml_parser.cpp @@ -56,9 +56,9 @@ void YamlParser::fromYamlImpl(uint8_t& value, const YAML::Node& node, std::strin value = node.as(); } -YAML::Node YamlParser::toYamlImpl(const std::string& name, const uint8_t& value, std::string& error) { +YAML::Node YamlParser::toYamlImpl(const uint8_t& value, std::string& error) { YAML::Node node; - node[name] = static_cast(value); + node = static_cast(value); return node; } From d149cc610c5cbcac8c951be4076958a2fff4f22b Mon Sep 17 00:00:00 2001 From: lschmid Date: Sun, 17 Nov 2024 18:28:01 -0500 Subject: [PATCH 22/22] clean up utils includes --- .../config_utilities/internal/string_utils.h | 11 ---- .../config_utilities/internal/yaml_parser.h | 2 +- .../config_utilities/internal/yaml_utils.h | 9 +++ config_utilities/src/asl_formatter.cpp | 2 +- config_utilities/src/meta_data.cpp | 2 +- config_utilities/src/string_utils.cpp | 62 ------------------ config_utilities/src/yaml_utils.cpp | 63 +++++++++++++++++++ config_utilities/test/tests/asl_formatter.cpp | 24 +++---- 8 files changed, 87 insertions(+), 88 deletions(-) diff --git a/config_utilities/include/config_utilities/internal/string_utils.h b/config_utilities/include/config_utilities/internal/string_utils.h index 03b7089..3775bff 100644 --- a/config_utilities/include/config_utilities/internal/string_utils.h +++ b/config_utilities/include/config_utilities/internal/string_utils.h @@ -40,8 +40,6 @@ #include #include -#include - // clang-format off #ifdef __GNUG__ #include @@ -89,15 +87,6 @@ std::string joinNamespace(const std::string& namespace_1, const std::string& namespace_2, const std::string& delimiter = "/"); -/** - * @brief Formatting of YAML nodes to strings. Most config types can be neatly represented as low-depth yaml nodes, or - * should otherwise probably be wrapped in a separate config struct. - * @param data The data to be formatted. - * @param reformat_float Whether to try and print floats with default stream precision - * @returns The formatted string. - */ -std::string dataToString(const YAML::Node& data, bool reformat_float = false); - /** * @brief Find all occurences of a substring in a string. * @param text The text to be searched. diff --git a/config_utilities/include/config_utilities/internal/yaml_parser.h b/config_utilities/include/config_utilities/internal/yaml_parser.h index c9c13e0..394726b 100644 --- a/config_utilities/include/config_utilities/internal/yaml_parser.h +++ b/config_utilities/include/config_utilities/internal/yaml_parser.h @@ -225,7 +225,7 @@ class YamlParser { for (const auto& element : node) { const T& element_value = element.as(); if (value.find(element_value) != value.end()) { - repeated_entries.insert(dataToString(element)); + repeated_entries.insert(yamlToString(element)); } else { value.insert(element_value); } diff --git a/config_utilities/include/config_utilities/internal/yaml_utils.h b/config_utilities/include/config_utilities/internal/yaml_utils.h index 2d0fe70..a2abeab 100644 --- a/config_utilities/include/config_utilities/internal/yaml_utils.h +++ b/config_utilities/include/config_utilities/internal/yaml_utils.h @@ -80,4 +80,13 @@ std::vector getNodeArray(const YAML::Node& node); */ std::vector> getNodeMap(const YAML::Node& node); +/** + * @brief Formatting of YAML nodes to strings. Most config types can be neatly represented as low-depth yaml nodes, or + * should otherwise probably be wrapped in a separate config struct. + * @param data The data to be formatted. + * @param reformat_float Whether to try and print floats with default stream precision + * @returns The formatted string. + */ +std::string yamlToString(const YAML::Node& data, bool reformat_float = false); + } // namespace config::internal diff --git a/config_utilities/src/asl_formatter.cpp b/config_utilities/src/asl_formatter.cpp index 488d8eb..4e8b0e5 100644 --- a/config_utilities/src/asl_formatter.cpp +++ b/config_utilities/src/asl_formatter.cpp @@ -250,7 +250,7 @@ std::string AslFormatter::formatField(const FieldInfo& info, size_t indent) cons const auto& settings = Settings::instance().printing; // field is the stringified value, The header is the field name. - std::string field = dataToString(info.value, settings.reformat_floats); + std::string field = yamlToString(info.value, settings.reformat_floats); if (info.isDefault() && Settings::instance().printing.show_defaults) { field += " (default)"; } diff --git a/config_utilities/src/meta_data.cpp b/config_utilities/src/meta_data.cpp index 911941d..bf14432 100644 --- a/config_utilities/src/meta_data.cpp +++ b/config_utilities/src/meta_data.cpp @@ -42,7 +42,7 @@ namespace config::internal { bool FieldInfo::isDefault() const { // NOTE(lschmid): Operator YAML::Node== checks for identity, not equality. Since these are all scalars, comparing // the formatted strings should be identical. - return internal::dataToString(value) == internal::dataToString(default_value); + return internal::yamlToString(value) == internal::yamlToString(default_value); } bool MetaData::hasErrors() const { diff --git a/config_utilities/src/string_utils.cpp b/config_utilities/src/string_utils.cpp index 50105ed..6d8e632 100644 --- a/config_utilities/src/string_utils.cpp +++ b/config_utilities/src/string_utils.cpp @@ -36,7 +36,6 @@ #include "config_utilities/internal/string_utils.h" #include -#include #include namespace config::internal { @@ -89,67 +88,6 @@ std::string joinNamespace(const std::string& namespace_1, return joinNamespace(ns_1, delimiter); } -std::string scalarToString(const YAML::Node& data, bool reformat_float) { - std::stringstream orig; - orig << data; - if (!reformat_float) { - return orig.str(); - } - - const std::regex float_detector("[+-]?[0-9]*[.][0-9]+"); - if (!std::regex_search(orig.str(), float_detector)) { - return orig.str(); // no reason to reformat if no decimal points - } - - double value; - try { - value = data.as(); - } catch (const std::exception&) { - return orig.str(); // value is some sort of string that can't be parsed as a float - } - - // this should have default ostream precision for formatting float - std::stringstream ss; - ss << value; - return ss.str(); -} - -std::string dataToString(const YAML::Node& data, bool reformat_float) { - switch (data.Type()) { - case YAML::NodeType::Scalar: { - // scalars require special handling for float precision - return scalarToString(data, reformat_float); - } - case YAML::NodeType::Sequence: { - std::string result = "["; - for (size_t i = 0; i < data.size(); ++i) { - result += dataToString(data[i], reformat_float); - if (i < data.size() - 1) { - result += ", "; - } - } - result += "]"; - return result; - } - case YAML::NodeType::Map: { - std::string result = "{"; - bool has_data = false; - for (const auto& kv_pair : data) { - has_data = true; - result += - dataToString(kv_pair.first, reformat_float) + ": " + dataToString(kv_pair.second, reformat_float) + ", "; - } - if (has_data) { - result = result.substr(0, result.length() - 2); - } - result += "}"; - return result; - } - default: - return kInvalidField; - } -} - std::vector findAllSubstrings(const std::string& text, const std::string& substring) { std::vector result; size_t pos = text.find(substring, 0); diff --git a/config_utilities/src/yaml_utils.cpp b/config_utilities/src/yaml_utils.cpp index 6eb5ae9..2402e03 100644 --- a/config_utilities/src/yaml_utils.cpp +++ b/config_utilities/src/yaml_utils.cpp @@ -35,6 +35,8 @@ #include "config_utilities/internal/yaml_utils.h" +#include + #include "config_utilities/internal/string_utils.h" namespace config::internal { @@ -166,4 +168,65 @@ std::vector> getNodeMap(const YAML::Node& node return result; } +std::string scalarToString(const YAML::Node& data, bool reformat_float) { + std::stringstream orig; + orig << data; + if (!reformat_float) { + return orig.str(); + } + + const std::regex float_detector("[+-]?[0-9]*[.][0-9]+"); + if (!std::regex_search(orig.str(), float_detector)) { + return orig.str(); // no reason to reformat if no decimal points + } + + double value; + try { + value = data.as(); + } catch (const std::exception&) { + return orig.str(); // value is some sort of string that can't be parsed as a float + } + + // this should have default ostream precision for formatting float + std::stringstream ss; + ss << value; + return ss.str(); +} + +std::string yamlToString(const YAML::Node& data, bool reformat_float) { + switch (data.Type()) { + case YAML::NodeType::Scalar: { + // scalars require special handling for float precision + return scalarToString(data, reformat_float); + } + case YAML::NodeType::Sequence: { + std::string result = "["; + for (size_t i = 0; i < data.size(); ++i) { + result += yamlToString(data[i], reformat_float); + if (i < data.size() - 1) { + result += ", "; + } + } + result += "]"; + return result; + } + case YAML::NodeType::Map: { + std::string result = "{"; + bool has_data = false; + for (const auto& kv_pair : data) { + has_data = true; + result += + yamlToString(kv_pair.first, reformat_float) + ": " + yamlToString(kv_pair.second, reformat_float) + ", "; + } + if (has_data) { + result = result.substr(0, result.length() - 2); + } + result += "}"; + return result; + } + default: + return kInvalidField; + } +} + } // namespace config::internal diff --git a/config_utilities/test/tests/asl_formatter.cpp b/config_utilities/test/tests/asl_formatter.cpp index fce3666..54e546a 100644 --- a/config_utilities/test/tests/asl_formatter.cpp +++ b/config_utilities/test/tests/asl_formatter.cpp @@ -89,26 +89,26 @@ void declare_config(ConfigUsingArrays& config) { field(config.arr, "arr"); } -TEST(AslFormatter, DataToString) { +TEST(AslFormatter, yamlToString) { YAML::Node data = internal::Visitor::getValues(TestConfig()).data; // note: float reformatting should have no effect on other fields (and fixes full precision formatting for internal // yaml representation) - EXPECT_EQ(internal::dataToString(data["i"], true), "1"); - EXPECT_EQ(internal::dataToString(data["f"], true), "2.1"); - EXPECT_EQ(internal::dataToString(data["d"], true), "3.2"); - EXPECT_EQ(internal::dataToString(data["b"], true), "true"); - EXPECT_EQ(internal::dataToString(data["u8"], true), "4"); - EXPECT_EQ(internal::dataToString(data["s"], true), "test string"); - EXPECT_EQ(internal::dataToString(data["vec"], true), "[1, 2, 3]"); - EXPECT_EQ(internal::dataToString(data["map"], true), "{a: 1, b: 2, c: 3}"); - EXPECT_EQ(internal::dataToString(data["set"], true), "[1.1, 2.2, 3.3]"); - EXPECT_EQ(internal::dataToString(data["mat"], true), "[[1, 0, 0], [0, 1, 0], [0, 0, 1]]"); + EXPECT_EQ(internal::yamlToString(data["i"], true), "1"); + EXPECT_EQ(internal::yamlToString(data["f"], true), "2.1"); + EXPECT_EQ(internal::yamlToString(data["d"], true), "3.2"); + EXPECT_EQ(internal::yamlToString(data["b"], true), "true"); + EXPECT_EQ(internal::yamlToString(data["u8"], true), "4"); + EXPECT_EQ(internal::yamlToString(data["s"], true), "test string"); + EXPECT_EQ(internal::yamlToString(data["vec"], true), "[1, 2, 3]"); + EXPECT_EQ(internal::yamlToString(data["map"], true), "{a: 1, b: 2, c: 3}"); + EXPECT_EQ(internal::yamlToString(data["set"], true), "[1.1, 2.2, 3.3]"); + EXPECT_EQ(internal::yamlToString(data["mat"], true), "[[1, 0, 0], [0, 1, 0], [0, 0, 1]]"); YAML::Node nested_set; nested_set["a"]["x"] = 1; nested_set["a"]["y"] = 2; nested_set["b"]["x"] = 3; nested_set["b"]["y"] = 4; - EXPECT_EQ(internal::dataToString(nested_set, true), "{a: {x: 1, y: 2}, b: {x: 3, y: 4}}"); + EXPECT_EQ(internal::yamlToString(nested_set, true), "{a: {x: 1, y: 2}, b: {x: 3, y: 4}}"); } TEST(AslFormatter, FormatErrors) {