Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions config_utilities/include/config_utilities/dynamic_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct DynamicConfigServer {
};

DynamicConfigServer() = default;
explicit DynamicConfigServer(const Hooks& hooks);
virtual ~DynamicConfigServer();
DynamicConfigServer(const DynamicConfigServer&) = delete;
DynamicConfigServer(DynamicConfigServer&&) = default;
Expand Down Expand Up @@ -205,7 +206,8 @@ struct DynamicConfigRegistry {
} // namespace internal

/**
* @brief A wrapper class for for configs that can be dynamically changed.
* @brief A wrapper class for for configs that can be dynamically changed. If the dynamic config is const, it will be
* read-only of the underlying config, which can still be changed from external sources.
*
* @tparam ConfigT The contained configuration type.
*/
Expand All @@ -217,10 +219,12 @@ struct DynamicConfig {
* @brief Construct a new Dynamic Config, wrapping a config_uilities config.
* @param name Unique name of the dynamic config. This identifier is used to access the config on the client side.
* @param config The config to wrap.
* @param callback A callback function that is called whenever the config is updated. This is only called if the
* values of the config change, not on every set request.
*/
explicit DynamicConfig(const std::string& name, const ConfigT& config = {}, Callback callback = {});

~DynamicConfig();
virtual ~DynamicConfig();

DynamicConfig(const DynamicConfig&) = delete;
DynamicConfig& operator=(const DynamicConfig&) = delete;
Expand Down Expand Up @@ -248,15 +252,15 @@ struct DynamicConfig {

private:
const std::string name_;
ConfigT config_;
mutable ConfigT config_;
mutable std::mutex mutex_;
Callback callback_;
const bool is_registered_;

std::string setValues(const YAML::Node& values);
std::string setValues(const YAML::Node& values) const;
YAML::Node getValues() const;
YAML::Node getInfo() const;
internal::DynamicConfigRegistry::ConfigInterface getInterface();
internal::DynamicConfigRegistry::ConfigInterface getInterface() const;
void moveMembers(DynamicConfig&& other);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ void DynamicConfig<ConfigT>::setCallback(const Callback& callback) {
}

template <typename ConfigT>
std::string DynamicConfig<ConfigT>::setValues(const YAML::Node& values) {
std::string DynamicConfig<ConfigT>::setValues(const YAML::Node& values) const {
if (values.Type() != YAML::NodeType::Map || values.size() == 0) {
return "";
}
Expand Down Expand Up @@ -164,7 +164,7 @@ YAML::Node DynamicConfig<ConfigT>::getInfo() const {
}

template <typename ConfigT>
internal::DynamicConfigRegistry::ConfigInterface DynamicConfig<ConfigT>::getInterface() {
internal::DynamicConfigRegistry::ConfigInterface DynamicConfig<ConfigT>::getInterface() const {
internal::DynamicConfigRegistry::ConfigInterface interface;

interface.get = [this]() { return getValues(); };
Expand Down
4 changes: 3 additions & 1 deletion config_utilities/src/dynamic_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@

namespace config {

bool DynamicConfigServer::Hooks::empty() const { return !onRegister && !onDeregister; }
bool DynamicConfigServer::Hooks::empty() const { return !onRegister && !onDeregister && !onUpdate; }

DynamicConfigServer::DynamicConfigServer(const Hooks& hooks) { setHooks(hooks); }

bool DynamicConfigServer::hasConfig(const Key& key) const {
return internal::DynamicConfigRegistry::instance().hasKey(key);
Expand Down
44 changes: 29 additions & 15 deletions config_utilities_ros/app/gui_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import signal
import yaml
from threading import Thread
import rclpy
import sys

import rclpy
from rclpy.node import Node
from ros2node.api import get_node_names
from config_utilities_msgs.srv import SetConfig
from config_utilities_ros.gui import DynamicConfigGUI

Expand Down Expand Up @@ -46,16 +48,19 @@ def get_available_servers_and_keys(self):
and t[1][0] == "config_utilities_msgs/srv/SetConfig"
]

available_nodes = [n[2] for n in get_node_names(node=self, include_hidden_nodes=False)]

servers = {}
for config in configs:
# We assume that no other node will use config_utilities messages with the same name.
ind = config.rfind("/")
server = config[:ind]
key = config[ind + 1 :]
if server not in servers:
servers[server] = [key]
else:
servers[server].append(key)
for server in available_nodes:
if not config.startswith(f"{server}/"):
continue
key = config[len(server) + 1 :]
if server not in servers:
servers[server] = [key]
else:
servers[server].append(key)
break
return servers

def set_request(self, server, key, data):
Expand Down Expand Up @@ -94,18 +99,27 @@ def shutdown(self):


def main():
rclpy.init()
gui = RosDynamicConfigGUI()
signal.signal(signal.SIGINT, lambda sig, frame: gui.shutdown())

parser = argparse.ArgumentParser(
description="Webserver hosting dynamic configuration GUI."
)
parser.add_argument("--debug", "-d", action="store_true")
parser.add_argument(
"--host", type=str, default="localhost", help="Host to run the webserver on."
)
parser.add_argument(
"--port", "-p", type=int, default=5000, help="Port to run the webserver on."
)
parser.add_argument(
"--no-open-browser",
action="store_true",
help="Do not open the web browser automatically.",
)
args, _ = parser.parse_known_args()

# TODO(lschmid): Expose GUI args in the future.
gui.run(debug=args.debug)
rclpy.init()
gui = RosDynamicConfigGUI()
signal.signal(signal.SIGINT, lambda sig, frame: gui.shutdown())
gui.run(debug=args.debug, host=args.host, port=args.port, open_browser=not args.no_open_browser)
gui.shutdown()


Expand Down
221 changes: 130 additions & 91 deletions config_utilities_ros/config_utilities_ros/gui/templates/index.html
Original file line number Diff line number Diff line change
@@ -1,107 +1,146 @@
<!DOCTYPE html>
<html lang="en">
<!-- Head -->
<head>
<title>Config Utilities Dynamic Configs</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
</head>

<!-- Head -->
<!-- Scripts -->
<script src="{{ url_for('static', filename='js/config_table.js') }}"></script>
<script src="{{ url_for('static', filename='js/build_fields.js') }}"></script>
<script src="{{ url_for('static', filename='js/selection_panes.js') }}"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
// Reload the page when the AJAX request is complete.
$(document).ajaxStop(function () {
window.location.reload(false);
});
// Send post requests via Ctrl+Enter
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === "Enter") {
post("/submit", readConfigData());
}
});
</script>

<head>
<title>Config Utilities Dynamic Configs</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<!-- Body -->

<!-- Scripts -->
<script src="{{ url_for('static', filename='js/config_table.js') }}"></script>
<script src="{{ url_for('static', filename='js/build_fields.js') }}"></script>
<script src="{{ url_for('static', filename='js/selection_panes.js') }}"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
// Reload the page when the AJAX request is complete.
$(document).ajaxStop(function () {
window.location.reload(false);
});
</script>
<body>
<header>
<h1>Config Utilities Dynamic Config GUI</h1>
</header>

<!-- Body -->
<!-- The config -->
{% if config_name %}
<div class="config-pane">
<table id="config-table" class="config-table">
<tr>
<th>{{ config_name }}</th>
<th>Value</th>
<th>Default</th>
</tr>
<script>
const configData = {{ config_data | tojson }};
buildFields(configData);
</script>
</table>
</div>
{% endif %}

<body>
<header>
<h1>Config Utilities Dynamic Config GUI</h1>
</header>
<!-- Interaction with the config -->
<div class="config-pane">
<button
name="refreshBtn"
class="config-button"
type="submit"
onclick="post('/refresh');"
>
Refresh
</button>
<button
name="submitBtn"
class="config-button"
type="submit"
onclick="post('/submit', readConfigData());"
>
Submit
</button>
</div>

<!-- The config -->
{% if config_name %}
<div class="config-pane">
<table id="config-table" class="config-table">
<tr>
<th>{{ config_name }} </th>
<th>Value</th>
<th>Default</th>
</tr>
<!-- Config Key selection -->
<div class="config-pane" id="key-pane">
<span class="selection-header">Config:</span>
<script>
const configData = {{ config_data | tojson }};
buildFields(configData);
const availableKeys = {{ available_keys | tojson }};
const activeKey = "{{ active_key }}";
buildSelectionPane(availableKeys, activeKey, 'key-pane');
</script>
</table>
</div>
{% endif %}


<!-- Interaction with the config -->
<div class="config-pane">
<button name="refreshBtn" class='config-button' type="submit" onclick="post('/refresh');">Refresh</button>
<button name="submitBtn" class='config-button' type="submit"
onclick="post('/submit', readConfigData());">Submit</button>
</div>


<!-- Config Key selection -->
<div class="config-pane" id="key-pane">
<span class="selection-header">Config:</span>
<script>
const availableKeys = {{ available_keys | tojson }};
const activeKey = "{{ active_key }}";
buildSelectionPane(availableKeys, activeKey, 'key-pane');
</script>
</div>
</div>

<!-- Config Server selection -->
<div class="config-pane" id="server-pane">
<span class="selection-header">Server:</span>
<script>
const availableServers = {{ available_servers | tojson }};
const activeServer = "{{ active_server }}";
buildSelectionPane(availableServers, activeServer, 'server-pane');
</script>
</div>
<!-- Config Server selection -->
<div class="config-pane" id="server-pane">
<span class="selection-header">Server:</span>
<script>
const availableServers = {{ available_servers | tojson }};
const activeServer = "{{ active_server }}";
buildSelectionPane(availableServers, activeServer, 'server-pane');
</script>
</div>

<!-- Error Pane -->
{% if error_message %}
<div class="config-pane" id="error-pane"
style="color: #b71c1c; background: #ffebee; border: 1px solid #b71c1c; padding: 10px; display: block;">
{% for error in error_message %}
<div style="margin-bottom: 0px; margin-top: 0px;">
<span style="font-size: 1.2em; margin-right: 10px;">&#9888;</span> {{ error }}
<!-- Error Pane -->
{% if error_message %}
<div
class="config-pane"
id="error-pane"
style="
color: #b71c1c;
background: #ffebee;
border: 1px solid #b71c1c;
padding: 10px;
display: block;
"
>
{% for error in error_message %}
<div style="margin-bottom: 0px; margin-top: 0px">
<span style="font-size: 1.2em; margin-right: 10px">&#9888;</span> {{
error }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}

<!-- Warning Pane -->
{% if warning_message %}
<div class="config-pane" id="warning-pane"
style="color: #ff9800; background: #fff3e0; border: 1px solid #ff9800; padding: 10px; display: block;">
{% for warning in warning_message %}
<div style="margin-bottom: 0px; margin-top: 0px;">
<span style="font-size: 1.2em; margin-right: 10px;">&#9888;</span> {{ warning }}
<!-- Warning Pane -->
{% if warning_message %}
<div
class="config-pane"
id="warning-pane"
style="
color: #ff9800;
background: #fff3e0;
border: 1px solid #ff9800;
padding: 10px;
display: block;
"
>
{% for warning in warning_message %}
<div style="margin-bottom: 0px; margin-top: 0px">
<span style="font-size: 1.2em; margin-right: 10px">&#9888;</span> {{
warning }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}

<!-- Message Pane -->
{% if message %}
<div class="config-pane" id="message-pane">
<div class="selection-header" display="block">Messages:</div>
<span id="message-text">{{ message }}</span>
</div>
{% endif %}
</body>
<!-- Message Pane -->
{% if message %}
<div class="config-pane" id="message-pane">
<div class="selection-header" display="block">Messages:</div>
<span id="message-text">{{ message }}</span>
</div>
{% endif %}
</body>
</html>