diff --git a/config_utilities/CMakeLists.txt b/config_utilities/CMakeLists.txt index 8b5faa8..e1f77b3 100644 --- a/config_utilities/CMakeLists.txt +++ b/config_utilities/CMakeLists.txt @@ -27,11 +27,13 @@ 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 - src/logger.cpp + src/field_input_info.cpp src/log_to_stdout.cpp + src/logger.cpp src/meta_data.cpp src/namespacing.cpp src/path.cpp @@ -40,7 +42,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/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_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_dynamic_config.launch b/config_utilities/demos/demo_dynamic_config.launch new file mode 100644 index 0000000..7892d50 --- /dev/null +++ b/config_utilities/demos/demo_dynamic_config.launch @@ -0,0 +1,7 @@ + + + + + + + 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..0dd61de --- /dev/null +++ b/config_utilities/demos/demo_dynamic_config_client.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +import rospy +from std_msgs.msg import String +import yaml +from time import sleep +from dynamic_config_gui import DynamicConfigGUI + +APP_NAME = "[Config Utilities Dynamic Config Client] " + + +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. + 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 = "" + 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 + + 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 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()) + + 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 = "" + 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 + 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(client: DynamicConfigRosClient): + # Force ctk shutdown. + client.gui.quit() + + +def main(): + rospy.init_node("dynamic_config_client") + client = DynamicConfigRosClient() + rospy.on_shutdown(lambda: on_shutdown(client)) + client.spin() + + +if __name__ == "__main__": + 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/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/demos/dynamic_config_gui.py b/config_utilities/demos/dynamic_config_gui.py new file mode 100644 index 0000000..4255df2 --- /dev/null +++ b/config_utilities/demos/dynamic_config_gui.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python3 +from tkinter import * +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] " + +""" +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 = ["Plain YAML", "Type Info (Experimental)"] + 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 = 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. + """ + 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): + """ + 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 + 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() + + 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.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(1, weight=1) + self.columnconfigure(0, weight=1, pad=PAD_X) + + def setup_config_frame(self): + if self.settings.method == "Type Info (Experimental)": + 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) + + 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): + self.current_info = new_info + self.config_frame.set_config_info(new_info) + + # 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): + # 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()) + + +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_refresh_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_refresh_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: + """ + 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 set_config_info(self, new_info): + pass + + def get_config(self): + return {} + + def set_enabled(self, enabled): + pass + + +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.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() + + +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) + + 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): + # TODO(lschmid): Check if only values need to be updated. + self.clear_config_ui() + self.build_config_ui(new_info) + + def get_config(self): + 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 "int" in input_type: + self.add_numeric_value_entry(info, param_name, True) + 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) + 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 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, padx=PAD_X) + frame.columnconfigure(1, weight=0) + self.widgets.append(w3) + + 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) + 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) + 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): + 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() + + +if __name__ == "__main__": + main() 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..db0c9c9 --- /dev/null +++ b/config_utilities/include/config_utilities/dynamic_config.h @@ -0,0 +1,265 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace config { + +/** + * @brief A server interface to manage all dynamic configs. + */ +struct DynamicConfigServer { + using Key = std::string; + + /** + * @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; + 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. 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. + */ + bool setValues(const Key& key, const YAML::Node& values) const; + + /** + * @brief Set the hooks for the dynamic config server. Setting empty hooks will deregister the current hooks. + */ + void setHooks(const Hooks& hooks); + + /** + * @brief Get the info of a dynamic config. + * @param key The unique key of the dynamic config. + */ + YAML::Node getInfo(const Key& key) const; + + private: + size_t hooks_id_ = 0; +}; + +namespace internal { + +/** + * @brief Name-based global registry for dynamic configurations. + */ +struct DynamicConfigRegistry { + using Key = DynamicConfigServer::Key; + + /** + * @brief Server-side interface to dynamic configs. + */ + struct ConfigInterface { + std::function getValues; + std::function setValues; + std::function getInfo; + }; + + // Singleton access. + static DynamicConfigRegistry& instance() { + static DynamicConfigRegistry instance; + return instance; + } + + /** + * @brief Check if a dynamic config with the given key is registered. + */ + bool hasKey(const Key& key) const; + + /** + * @brief Get the interface to a dynamic config with the given key. + * @param key The unique key of the dynamic config. + * @return The interface to the dynamic config, if it exists. + */ + std::optional getConfig(const Key& key) const; + + /** + * @brief Get all keys of the registered dynamic configs. + */ + std::vector keys() const; + + // Dynamic config registration and de-registration. + /** + * @brief Register a dynamic config with the given key. + * @param key The unique key of the dynamic config. + * @param interface The interface to the dynamic config. + * @return True if the registration was successful, false otherwise. + */ + bool registerConfig(const Key& key, const ConfigInterface& interface); + + /** + * @brief De-register a dynamic config with the given key. + * @param key The unique key of the dynamic config. + */ + void deregisterConfig(const Key& key); + + /** + * @brief 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. + * @param hooks_id The id of the server adding the hooks. + * @return The new_id of the server registered hooks. + */ + size_t registerHooks(const DynamicConfigServer::Hooks& hooks, size_t hooks_id); + + /** + * @brief Deregister hooks for a dynamic config server. + */ + void deregisterHooks(size_t hooks_id); + + /** + * @brief Notify all hooks that a config was updated. + */ + void configUpdated(const Key& key, const YAML::Node& new_values); + + private: + DynamicConfigRegistry() = default; + + std::unordered_map configs_; + std::unordered_map hooks_; + size_t current_hooks_id_ = 0; +}; + +} // namespace internal + +/** + * @brief A wrapper class for for configs that can be dynamically changed. + * + * @tparam ConfigT The contained configuration type. + */ +template +struct DynamicConfig { + 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 = {}, Callback callback = {}); + + ~DynamicConfig(); + + DynamicConfig(const DynamicConfig&) = delete; + DynamicConfig& operator=(const DynamicConfig&) = delete; + 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; + + /** + * @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. + */ + 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_; + 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/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/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/checks.h b/config_utilities/include/config_utilities/internal/checks.h index 846aad0..6c4f43f 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(upper_), 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/dynamic_config_impl.hpp b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp new file mode 100644 index 0000000..227645b --- /dev/null +++ b/config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp @@ -0,0 +1,178 @@ +/** ----------------------------------------------------------------------------- + * 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, true)) { + 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; + } + + const auto new_yaml = internal::Visitor::getValues(config).data; + { // 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; +} + +template +void DynamicConfig::setCallback(const Callback& callback) { + std::lock_guard lock(mutex_); + callback_ = callback; +} + +template +bool DynamicConfig::setValues(const YAML::Node& values) { + 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 + + if (callback_) { + callback_(); + } + + // Also notify other clients that the config has been updated. + internal::DynamicConfigRegistry::instance().configUpdated(name_, new_yaml); + 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_); + return internal::Visitor::getInfo(config_).serializeFieldInfos(); +} + +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/internal/field_input_info.h b/config_utilities/include/config_utilities/internal/field_input_info.h new file mode 100644 index 0000000..53d7777 --- /dev/null +++ b/config_utilities/include/config_utilities/internal/field_input_info.h @@ -0,0 +1,170 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +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(const std::string& type_str) : FieldInputInfo(Type::kInt), type_str(type_str) {} + + // Constraints for the field. + // 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; + + 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(const std::string& type_str) : FieldInputInfo(Type::kFloat), type_str(type_str) {} + + // Constraints for the field. + // 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; + + 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..0d96688 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().printing.show_defaults 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..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 "config_utilities/internal/meta_data.h" - // 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 confi 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. @@ -107,7 +96,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..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)); } @@ -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); + if (Settings::instance().printing.show_defaults) { + 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..394726b 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 @@ -69,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 @@ -83,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) { @@ -91,6 +116,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. @@ -99,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 @@ -108,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()); } @@ -118,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> @@ -132,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; } @@ -150,11 +204,11 @@ 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; } @@ -171,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); } @@ -187,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; @@ -208,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; } @@ -283,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/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/include/config_utilities/parsing/ros.h b/config_utilities/include/config_utilities/parsing/ros.h index ad9161c..d096c3c 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,34 @@ 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 value_publishers_; + std::map info_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); + void onSet(const DynamicConfigServer::Key& key, const YAML::Node& new_values); +}; + } // namespace config 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/include/config_utilities/traits.h b/config_utilities/include/config_utilities/traits.h index 90e55e3..4b4c7a3 100644 --- a/config_utilities/include/config_utilities/traits.h +++ b/config_utilities/include/config_utilities/traits.h @@ -52,6 +52,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 +79,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..4e8b0e5 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; } } @@ -162,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"); } @@ -181,8 +184,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 +198,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 +214,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,13 +223,13 @@ 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.is_default) { + if (!info.isDefault()) { is_default = false; break; } @@ -238,22 +242,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.is_default && Settings::instance().indicate_default_values) { + std::string field = yamlToString(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 +281,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 +299,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 +310,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 +323,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 +337,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/dynamic_config.cpp b/config_utilities/src/dynamic_config.cpp new file mode 100644 index 0000000..770dc0f --- /dev/null +++ b/config_utilities/src/dynamic_config.cpp @@ -0,0 +1,163 @@ +/** ----------------------------------------------------------------------------- + * 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(); +} + +bool DynamicConfigServer::setValues(const Key& key, const YAML::Node& values) const { + const auto config = internal::DynamicConfigRegistry::instance().getConfig(key); + if (!config) { + return false; + } + 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); + } + } +} + +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; + } + + 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/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..418ec6a 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"; @@ -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/field_input_info.cpp b/config_utilities/src/field_input_info.cpp new file mode 100644 index 0000000..fd3be3a --- /dev/null +++ b/config_utilities/src/field_input_info.cpp @@ -0,0 +1,304 @@ +/** ----------------------------------------------------------------------------- + * 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 + +#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"] = type_str; + if (min) { + node["min"] = *min; + // Only store the rarer cases. + if (!lower_inclusive) { + node["lower_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) { + min = other_info.min; + 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) { + max = other_info.max; + 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; + } + } +} + +void IntFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { + auto val = YamlParser::fromYaml(min); + if (!val) { + return; + } + this->min = *val; + this->lower_inclusive = lower_inclusive; +} + +void IntFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { + auto val = YamlParser::fromYaml(max); + if (!val) { + return; + } + this->max = *val; + this->upper_inclusive = upper_inclusive; +} + +YAML::Node FloatFieldInputInfo::toYaml() const { + YAML::Node node; + node["type"] = type_str; + if (min) { + node["min"] = *min; + // Only store the rarer cases. + if (!lower_inclusive) { + node["lower_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) { + min = other_info.min; + 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) { + max = other_info.max; + 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; + } + } +} + +void FloatFieldInputInfo::setMin(YAML::Node min, bool lower_inclusive) { + auto val = YamlParser::fromYaml(min); + if (!val) { + return; + } + this->min = *val; + this->lower_inclusive = lower_inclusive; +} + +void FloatFieldInputInfo::setMax(YAML::Node max, bool upper_inclusive) { + auto val = YamlParser::fromYaml(max); + if (!val) { + return; + } + this->max = *val; + this->upper_inclusive = upper_inclusive; +} + +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); + } + } +} + +// Bool. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared(FieldInputInfo::Type::kBool); +} + +// Ints. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("int8"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("int16"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("int32"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("int64"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("uint8"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("uint16"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("uint32"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("uint64"); +} + +// Floats. +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("float32"); +} + +template <> +FieldInputInfo::Ptr createFieldInputInfo() { + return std::make_shared("float64"); +} + +// 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..bf14432 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::yamlToString(value) == internal::yamlToString(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/ros.cpp b/config_utilities/src/ros.cpp new file mode 100644 index 0000000..ef3c602 --- /dev/null +++ b/config_utilities/src/ros.cpp @@ -0,0 +1,109 @@ +/** ----------------------------------------------------------------------------- + * 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->onSet(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) { + 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; + reg_pub_.publish(msg); + + // Latch the current state of the config. + onUpdate(key, server_.getValues(key)); +} + +void RosDynamicConfigServer::onDeregister(const DynamicConfigServer::Key& key) { + value_publishers_.erase(key); + info_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) { + 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; + } + + 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) { + server_.setValues(key, new_values); +} + +} // namespace config 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/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/visitor.cpp b/config_utilities/src/visitor.cpp index 49aaadc..915781b 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,12 +95,12 @@ 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; 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/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; } 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/CMakeLists.txt b/config_utilities/test/CMakeLists.txt index 2f0061e..97e7ec8 100644 --- a/config_utilities/test/CMakeLists.txt +++ b/config_utilities/test/CMakeLists.txt @@ -16,9 +16,11 @@ 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 + tests/field_input_info.cpp tests/inheritance.cpp tests/missing_fields.cpp tests/namespacing.cpp 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/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/src/utils.cpp b/config_utilities/test/src/utils.cpp index 6eae451..7e8697a 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: @@ -54,8 +62,8 @@ bool expectEqual(const YAML::Node& a, const YAML::Node& b) { 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; } } @@ -68,11 +76,11 @@ 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])); - 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; } } @@ -100,4 +108,5 @@ void TestLogger::print() const { std::cout << internal::severityToString(message.first) << ": " << message.second << std::endl; } } + } // namespace config::test diff --git a/config_utilities/test/tests/asl_formatter.cpp b/config_utilities/test/tests/asl_formatter.cpp index 00b20f6..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) { @@ -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': @@ -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'). @@ -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': @@ -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: @@ -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/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/dynamic_config.cpp b/config_utilities/test/tests/dynamic_config.cpp new file mode 100644 index 0000000..a300f77 --- /dev/null +++ b/config_utilities/test/tests/dynamic_config.cpp @@ -0,0 +1,216 @@ +/** ----------------------------------------------------------------------------- + * Copyright (c) 2023 Massachusetts Institute of Technology. + * All Rights Reserved. + * + * AUTHORS: Lukas Schmid , Nathan Hughes + * AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology + * YEAR: 2023 + * LICENSE: BSD 3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * -------------------------------------------------------------------------- */ + +#include "config_utilities/dynamic_config.h" + +#include + +#include "config_utilities/test/default_config.h" +#include "config_utilities/test/utils.h" + +namespace config::test { + +DefaultConfig modified_config() { + DefaultConfig config; + config.i = 2; + config.f = 3.2f; + config.vec = {7, 8, 9}; + config.sub_config.i = 3; + return config; +} + +TEST(DynamicConfig, CheckRegistered) { + DynamicConfigServer server; + + // No dynamic configs registered. + EXPECT_EQ(server.registeredConfigs().empty(), true); + + // Register a dynamic config. + { + auto dyn1 = DynamicConfig("dynamic_config_1"); + auto dyn2 = DynamicConfig("dynamic_config_2", modified_config()); + const auto registered = server.registeredConfigs(); + EXPECT_EQ(registered.size(), 2); + EXPECT_TRUE(std::find(registered.begin(), registered.end(), "dynamic_config_1") != registered.end()); + EXPECT_TRUE(std::find(registered.begin(), registered.end(), "dynamic_config_2") != registered.end()); + + // Check names unique. + auto logger = TestLogger::create(); + auto dyn3 = DynamicConfig("dynamic_config_1"); + EXPECT_EQ(logger->numMessages(), 1); + EXPECT_EQ(logger->lastMessage(), "Cannot register dynamic config: key 'dynamic_config_1' already exists."); + } + + // Dynamic configs should deregister automatically. + EXPECT_EQ(server.registeredConfigs().empty(), true); +} + +TEST(DynamicConfig, SetGet) { + DynamicConfig dyn("dyn"); + + DynamicConfigServer server; + + // Get values. + auto values = server.getValues("dyn"); + EXPECT_TRUE(expectEqual(values, DefaultConfig::defaultValues())); + + // Set values. + std::string yaml_str = R"( + i: 7 + f: 7.7 + vec: [7, 7, 7] + sub_ns: + i: 7 + )"; + auto yaml = YAML::Load(yaml_str); + server.setValues("dyn", yaml); + + // Check actual values. + auto config = dyn.get(); + EXPECT_EQ(config.i, 7); + EXPECT_EQ(config.f, 7.7f); + EXPECT_EQ(config.vec, std::vector({7, 7, 7})); + EXPECT_EQ(config.sub_config.i, 7); + + // Check serialized values. + values = server.getValues("dyn"); + EXPECT_EQ(values["i"].as(), 7); + EXPECT_EQ(values["f"].as(), 7.7f); + EXPECT_EQ(values["vec"].as>(), std::vector({7, 7, 7})); + EXPECT_EQ(values["sub_ns"]["i"].as(), 7); + EXPECT_EQ(values["u8"].as(), 4); // Default value. + + // Check invalid key. + values = server.getValues("invalid"); + EXPECT_TRUE(values.IsNull()); + server.setValues("invalid", DefaultConfig::defaultValues()); + EXPECT_EQ(dyn.get().i, 7); +} + +TEST(DynamicConfig, Hooks) { + auto server = std::make_unique(); + std::string logs; + + // Register hooks. + DynamicConfigServer::Hooks hooks; + hooks.onRegister = [&logs](const std::string& key) { logs += "register " + key + "; "; }; + hooks.onDeregister = [&logs](const std::string& key) { logs += "deregister " + key + "; "; }; + hooks.onUpdate = [&logs](const std::string& key, const YAML::Node& new_values) { logs += "update " + key + "; "; }; + server->setHooks(hooks); + + // Register a dynamic config. + auto a = std::make_unique>("A"); + auto b = std::make_unique>("B"); + DefaultConfig config; + a->set(config); // Should be identical, so not trigger update. + config.i = 123; + b->set(config); // Should trigger update. + b.reset(); + a.reset(); + EXPECT_EQ(logs, "register A; register B; update B; deregister B; deregister A; "); + + // Update hooks. + hooks.onRegister = [&logs](const std::string& key) { logs += "register " + key + " again; "; }; + hooks.onDeregister = nullptr; + server->setHooks(hooks); + logs.clear(); + + // Register a dynamic config. + auto c = std::make_unique>("C"); + c.reset(); + EXPECT_EQ(logs, "register C again; "); + + // Deregister hooks. + server.reset(); + logs.clear(); + + // Register a dynamic config. + auto d = std::make_unique>("D"); + d.reset(); + EXPECT_EQ(logs, ""); +} + +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..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,9 +306,8 @@ Config[config::test::Talker](): )"""; - Settings().print_width = 40; + Settings().printing.width = 40; const std::string modules = internal::ModuleRegistry::getAllRegistered(); - std::cout << modules << std::endl; EXPECT_EQ(modules, expected); Settings().restoreDefaults(); } 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..1187e28 --- /dev/null +++ b/config_utilities/test/tests/field_input_info.cpp @@ -0,0 +1,226 @@ +/** ----------------------------------------------------------------------------- + * 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: int32 + min: 0 + lower_exclusive: true + - type: field + name: f + unit: s + value: 2.1 + default: 2.1 + input_info: + type: float32 + min: 0 + - type: field + name: d + unit: m/s + value: 3.2 + default: 3.2 + input_info: + type: float64 + min: 0 + max: 4 + upper_exclusive: true + - type: field + name: b + value: true + default: true + input_info: + type: bool + - type: field + name: u8 + value: 4 + default: 4 + input_info: + type: uint8 + max: 5 + - 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: int32 + min: 0 + lower_exclusive: true + - type: config + name: SubSubConfig + field_name: sub_sub_config + fields: + - type: field + name: i + value: 1 + default: 1 + input_info: + type: int32 + min: 0 + lower_exclusive: true + - type: config + name: SubSubConfig + field_name: sub_sub_config + fields: + - type: field + name: i + value: 1 + default: 1 + input_info: + type: int32 + min: 0 + lower_exclusive: true +)"; + // Epect near equal for floating point values. + expectEqual(info, YAML::Load(expected), 1e-6); +} + +} // 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(); }; ``` 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/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/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/README.md b/docs/README.md index daa6297..5e6c698 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! 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); -```