Skip to content

Commit 4e65041

Browse files
authored
Feature/gui quality of life (#58)
* add discovery of actual nodes in ROS2 * add Ctrl+Enter capture to send requests * add better handling for const dynamic configs and servers * expose host and port as args * fix indentation error
1 parent 03d14ea commit 4e65041

File tree

5 files changed

+173
-114
lines changed

5 files changed

+173
-114
lines changed

config_utilities/include/config_utilities/dynamic_config.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ struct DynamicConfigServer {
6767
};
6868

6969
DynamicConfigServer() = default;
70+
explicit DynamicConfigServer(const Hooks& hooks);
7071
virtual ~DynamicConfigServer();
7172
DynamicConfigServer(const DynamicConfigServer&) = delete;
7273
DynamicConfigServer(DynamicConfigServer&&) = default;
@@ -205,7 +206,8 @@ struct DynamicConfigRegistry {
205206
} // namespace internal
206207

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

223-
~DynamicConfig();
227+
virtual ~DynamicConfig();
224228

225229
DynamicConfig(const DynamicConfig&) = delete;
226230
DynamicConfig& operator=(const DynamicConfig&) = delete;
@@ -248,15 +252,15 @@ struct DynamicConfig {
248252

249253
private:
250254
const std::string name_;
251-
ConfigT config_;
255+
mutable ConfigT config_;
252256
mutable std::mutex mutex_;
253257
Callback callback_;
254258
const bool is_registered_;
255259

256-
std::string setValues(const YAML::Node& values);
260+
std::string setValues(const YAML::Node& values) const;
257261
YAML::Node getValues() const;
258262
YAML::Node getInfo() const;
259-
internal::DynamicConfigRegistry::ConfigInterface getInterface();
263+
internal::DynamicConfigRegistry::ConfigInterface getInterface() const;
260264
void moveMembers(DynamicConfig&& other);
261265
};
262266

config_utilities/include/config_utilities/internal/dynamic_config_impl.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ void DynamicConfig<ConfigT>::setCallback(const Callback& callback) {
113113
}
114114

115115
template <typename ConfigT>
116-
std::string DynamicConfig<ConfigT>::setValues(const YAML::Node& values) {
116+
std::string DynamicConfig<ConfigT>::setValues(const YAML::Node& values) const {
117117
if (values.Type() != YAML::NodeType::Map || values.size() == 0) {
118118
return "";
119119
}
@@ -164,7 +164,7 @@ YAML::Node DynamicConfig<ConfigT>::getInfo() const {
164164
}
165165

166166
template <typename ConfigT>
167-
internal::DynamicConfigRegistry::ConfigInterface DynamicConfig<ConfigT>::getInterface() {
167+
internal::DynamicConfigRegistry::ConfigInterface DynamicConfig<ConfigT>::getInterface() const {
168168
internal::DynamicConfigRegistry::ConfigInterface interface;
169169

170170
interface.get = [this]() { return getValues(); };

config_utilities/src/dynamic_config.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939

4040
namespace config {
4141

42-
bool DynamicConfigServer::Hooks::empty() const { return !onRegister && !onDeregister; }
42+
bool DynamicConfigServer::Hooks::empty() const { return !onRegister && !onDeregister && !onUpdate; }
43+
44+
DynamicConfigServer::DynamicConfigServer(const Hooks& hooks) { setHooks(hooks); }
4345

4446
bool DynamicConfigServer::hasConfig(const Key& key) const {
4547
return internal::DynamicConfigRegistry::instance().hasKey(key);

config_utilities_ros/app/gui_node.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import signal
55
import yaml
66
from threading import Thread
7-
import rclpy
87
import sys
8+
9+
import rclpy
910
from rclpy.node import Node
11+
from ros2node.api import get_node_names
1012
from config_utilities_msgs.srv import SetConfig
1113
from config_utilities_ros.gui import DynamicConfigGUI
1214

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

51+
available_nodes = [n[2] for n in get_node_names(node=self, include_hidden_nodes=False)]
52+
4953
servers = {}
5054
for config in configs:
51-
# We assume that no other node will use config_utilities messages with the same name.
52-
ind = config.rfind("/")
53-
server = config[:ind]
54-
key = config[ind + 1 :]
55-
if server not in servers:
56-
servers[server] = [key]
57-
else:
58-
servers[server].append(key)
55+
for server in available_nodes:
56+
if not config.startswith(f"{server}/"):
57+
continue
58+
key = config[len(server) + 1 :]
59+
if server not in servers:
60+
servers[server] = [key]
61+
else:
62+
servers[server].append(key)
63+
break
5964
return servers
6065

6166
def set_request(self, server, key, data):
@@ -94,18 +99,27 @@ def shutdown(self):
9499

95100

96101
def main():
97-
rclpy.init()
98-
gui = RosDynamicConfigGUI()
99-
signal.signal(signal.SIGINT, lambda sig, frame: gui.shutdown())
100-
101102
parser = argparse.ArgumentParser(
102103
description="Webserver hosting dynamic configuration GUI."
103104
)
104105
parser.add_argument("--debug", "-d", action="store_true")
106+
parser.add_argument(
107+
"--host", type=str, default="localhost", help="Host to run the webserver on."
108+
)
109+
parser.add_argument(
110+
"--port", "-p", type=int, default=5000, help="Port to run the webserver on."
111+
)
112+
parser.add_argument(
113+
"--no-open-browser",
114+
action="store_true",
115+
help="Do not open the web browser automatically.",
116+
)
105117
args, _ = parser.parse_known_args()
106118

107-
# TODO(lschmid): Expose GUI args in the future.
108-
gui.run(debug=args.debug)
119+
rclpy.init()
120+
gui = RosDynamicConfigGUI()
121+
signal.signal(signal.SIGINT, lambda sig, frame: gui.shutdown())
122+
gui.run(debug=args.debug, host=args.host, port=args.port, open_browser=not args.no_open_browser)
109123
gui.shutdown()
110124

111125

Lines changed: 130 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,146 @@
11
<!DOCTYPE html>
22
<html lang="en">
3+
<!-- Head -->
4+
<head>
5+
<title>Config Utilities Dynamic Configs</title>
6+
<link
7+
rel="stylesheet"
8+
href="{{ url_for('static', filename='css/style.css') }}"
9+
/>
10+
</head>
311

4-
<!-- Head -->
12+
<!-- Scripts -->
13+
<script src="{{ url_for('static', filename='js/config_table.js') }}"></script>
14+
<script src="{{ url_for('static', filename='js/build_fields.js') }}"></script>
15+
<script src="{{ url_for('static', filename='js/selection_panes.js') }}"></script>
16+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
17+
<script>
18+
// Reload the page when the AJAX request is complete.
19+
$(document).ajaxStop(function () {
20+
window.location.reload(false);
21+
});
22+
// Send post requests via Ctrl+Enter
23+
document.addEventListener("keydown", function (event) {
24+
if (event.ctrlKey && event.key === "Enter") {
25+
post("/submit", readConfigData());
26+
}
27+
});
28+
</script>
529

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

11-
<!-- Scripts -->
12-
<script src="{{ url_for('static', filename='js/config_table.js') }}"></script>
13-
<script src="{{ url_for('static', filename='js/build_fields.js') }}"></script>
14-
<script src="{{ url_for('static', filename='js/selection_panes.js') }}"></script>
15-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
16-
<script>
17-
// Reload the page when the AJAX request is complete.
18-
$(document).ajaxStop(function () {
19-
window.location.reload(false);
20-
});
21-
</script>
32+
<body>
33+
<header>
34+
<h1>Config Utilities Dynamic Config GUI</h1>
35+
</header>
2236

23-
<!-- Body -->
37+
<!-- The config -->
38+
{% if config_name %}
39+
<div class="config-pane">
40+
<table id="config-table" class="config-table">
41+
<tr>
42+
<th>{{ config_name }}</th>
43+
<th>Value</th>
44+
<th>Default</th>
45+
</tr>
46+
<script>
47+
const configData = {{ config_data | tojson }};
48+
buildFields(configData);
49+
</script>
50+
</table>
51+
</div>
52+
{% endif %}
2453

25-
<body>
26-
<header>
27-
<h1>Config Utilities Dynamic Config GUI</h1>
28-
</header>
54+
<!-- Interaction with the config -->
55+
<div class="config-pane">
56+
<button
57+
name="refreshBtn"
58+
class="config-button"
59+
type="submit"
60+
onclick="post('/refresh');"
61+
>
62+
Refresh
63+
</button>
64+
<button
65+
name="submitBtn"
66+
class="config-button"
67+
type="submit"
68+
onclick="post('/submit', readConfigData());"
69+
>
70+
Submit
71+
</button>
72+
</div>
2973

30-
<!-- The config -->
31-
{% if config_name %}
32-
<div class="config-pane">
33-
<table id="config-table" class="config-table">
34-
<tr>
35-
<th>{{ config_name }} </th>
36-
<th>Value</th>
37-
<th>Default</th>
38-
</tr>
74+
<!-- Config Key selection -->
75+
<div class="config-pane" id="key-pane">
76+
<span class="selection-header">Config:</span>
3977
<script>
40-
const configData = {{ config_data | tojson }};
41-
buildFields(configData);
78+
const availableKeys = {{ available_keys | tojson }};
79+
const activeKey = "{{ active_key }}";
80+
buildSelectionPane(availableKeys, activeKey, 'key-pane');
4281
</script>
43-
</table>
44-
</div>
45-
{% endif %}
46-
47-
48-
<!-- Interaction with the config -->
49-
<div class="config-pane">
50-
<button name="refreshBtn" class='config-button' type="submit" onclick="post('/refresh');">Refresh</button>
51-
<button name="submitBtn" class='config-button' type="submit"
52-
onclick="post('/submit', readConfigData());">Submit</button>
53-
</div>
54-
55-
56-
<!-- Config Key selection -->
57-
<div class="config-pane" id="key-pane">
58-
<span class="selection-header">Config:</span>
59-
<script>
60-
const availableKeys = {{ available_keys | tojson }};
61-
const activeKey = "{{ active_key }}";
62-
buildSelectionPane(availableKeys, activeKey, 'key-pane');
63-
</script>
64-
</div>
82+
</div>
6583

66-
<!-- Config Server selection -->
67-
<div class="config-pane" id="server-pane">
68-
<span class="selection-header">Server:</span>
69-
<script>
70-
const availableServers = {{ available_servers | tojson }};
71-
const activeServer = "{{ active_server }}";
72-
buildSelectionPane(availableServers, activeServer, 'server-pane');
73-
</script>
74-
</div>
84+
<!-- Config Server selection -->
85+
<div class="config-pane" id="server-pane">
86+
<span class="selection-header">Server:</span>
87+
<script>
88+
const availableServers = {{ available_servers | tojson }};
89+
const activeServer = "{{ active_server }}";
90+
buildSelectionPane(availableServers, activeServer, 'server-pane');
91+
</script>
92+
</div>
7593

76-
<!-- Error Pane -->
77-
{% if error_message %}
78-
<div class="config-pane" id="error-pane"
79-
style="color: #b71c1c; background: #ffebee; border: 1px solid #b71c1c; padding: 10px; display: block;">
80-
{% for error in error_message %}
81-
<div style="margin-bottom: 0px; margin-top: 0px;">
82-
<span style="font-size: 1.2em; margin-right: 10px;">&#9888;</span> {{ error }}
94+
<!-- Error Pane -->
95+
{% if error_message %}
96+
<div
97+
class="config-pane"
98+
id="error-pane"
99+
style="
100+
color: #b71c1c;
101+
background: #ffebee;
102+
border: 1px solid #b71c1c;
103+
padding: 10px;
104+
display: block;
105+
"
106+
>
107+
{% for error in error_message %}
108+
<div style="margin-bottom: 0px; margin-top: 0px">
109+
<span style="font-size: 1.2em; margin-right: 10px">&#9888;</span> {{
110+
error }}
111+
</div>
112+
{% endfor %}
83113
</div>
84-
{% endfor %}
85-
</div>
86-
{% endif %}
114+
{% endif %}
87115

88-
<!-- Warning Pane -->
89-
{% if warning_message %}
90-
<div class="config-pane" id="warning-pane"
91-
style="color: #ff9800; background: #fff3e0; border: 1px solid #ff9800; padding: 10px; display: block;">
92-
{% for warning in warning_message %}
93-
<div style="margin-bottom: 0px; margin-top: 0px;">
94-
<span style="font-size: 1.2em; margin-right: 10px;">&#9888;</span> {{ warning }}
116+
<!-- Warning Pane -->
117+
{% if warning_message %}
118+
<div
119+
class="config-pane"
120+
id="warning-pane"
121+
style="
122+
color: #ff9800;
123+
background: #fff3e0;
124+
border: 1px solid #ff9800;
125+
padding: 10px;
126+
display: block;
127+
"
128+
>
129+
{% for warning in warning_message %}
130+
<div style="margin-bottom: 0px; margin-top: 0px">
131+
<span style="font-size: 1.2em; margin-right: 10px">&#9888;</span> {{
132+
warning }}
133+
</div>
134+
{% endfor %}
95135
</div>
96-
{% endfor %}
97-
</div>
98-
{% endif %}
136+
{% endif %}
99137

100-
<!-- Message Pane -->
101-
{% if message %}
102-
<div class="config-pane" id="message-pane">
103-
<div class="selection-header" display="block">Messages:</div>
104-
<span id="message-text">{{ message }}</span>
105-
</div>
106-
{% endif %}
107-
</body>
138+
<!-- Message Pane -->
139+
{% if message %}
140+
<div class="config-pane" id="message-pane">
141+
<div class="selection-header" display="block">Messages:</div>
142+
<span id="message-text">{{ message }}</span>
143+
</div>
144+
{% endif %}
145+
</body>
146+
</html>

0 commit comments

Comments
 (0)