A Qt-based ROS 2 widget for starting, stopping, and monitoring both ROS nodes and arbitrary shell processes.
dispatcher builds its UI from a YAML file. Each configured item is launched in its own detached tmux session, and the terminal button can attach a gnome-terminal window to that session when available.
The dispatcher executable uses the following ROS parameters:
| Name | Type | Default | Description |
|---|---|---|---|
dispatcher_config_path |
string |
"" |
Path to the dispatcher YAML file. |
initial_configuration |
string |
"" |
Configuration name to select on startup. |
start_checked_on_startup |
bool |
false |
Start all checked process items after loading the initial configuration. |
ssh_timeout_sec |
int |
10 |
Timeout used when building remote SSH commands. |
target_loop_rate_hz |
double |
100.0 |
Main process/status polling rate. |
The top-level dispatcher YAML supports:
| Key | Required | Description |
|---|---|---|
workspace |
Yes | Workspace path used by ROS process items before sourcing install/setup.bash. |
nodes |
Yes | Process and category definitions rendered in the main panel. |
configurations |
No | Named runtime configurations. Each entry may be a simple name or a map with name, cmd_prefix, environment_variables, and icon. |
cmd_prefix |
No | Default command prefix for the implicit all configuration. |
environment_variables |
No | Default environment variables for the implicit all configuration. |
hide_unconfigured_processes |
No | If true, items missing in the active configuration are hidden instead of disabled. |
scripts |
No | Script button definitions shown in the scripts panel. |
variables |
No | Variable selectors used for $VARIABLE command substitution. |
Each entry in nodes can be:
- A ROS process item. If
typeis omitted, the item is treated as ROS by default. - A shell process item with
type: shell. - A collapsible category with
type: categoryand anitemsarray. Entries initemsfollow the same rules, including the default to ROS whentypeis omitted.
Common item fields include:
| Key | Description |
|---|---|
name |
UI label and tmux-session base name. |
cmd |
Command used for the implicit all configuration. |
configurations |
Per-configuration command definitions. |
start_checked |
Whether the item starts checked in the UI. Optional, defaults to false. |
stop_tmux_cmd |
Stop sequence sent to tmux. Optional, defaults to C-C. |
hostname / user |
Optional remote execution target for local commands or configuration entries. |
use_cmd_prefix |
Enables or disables command-prefix injection. |
use_environment_variables |
Enables or disables environment-variable injection. |
attach_on_start |
Opens a terminal automatically after launch. |
ROS process items can additionally define node_name plus an optional
namespace, or a ros_nodes array with the same monitoring fields for
online-state monitoring. An omitted namespace resolves to the root namespace,
so name: talker matches the graph's /talker; a name that is already
absolute, such as /fcat/fcat, carries its own namespace.
Shell process items use pgrep on the item name to infer online state.
Script entries support:
| Key | Description |
|---|---|
name |
Button label. |
cmd or configurations |
Script command definition. |
row, column |
Grid placement in the scripts panel. |
icon |
Optional Qt resource path for the button icon. |
use_terminal |
Whether to wrap execution in gnome-terminal --. |
Variable entries support:
| Key | Description |
|---|---|
name |
Variable name used in commands, for example $FCAT_LOOP_RATE_HZ. |
choices |
Selectable values exposed in the UI. |
The following examples build incrementally from a minimal configuration to a
full-featured setup. Each one corresponds to a YAML file in config/
and a screenshot in doc/.
Four examples run without any custom packages, using only the demos that ship
with ROS 2. Each is self-contained and includes an rqt_graph button. Install
the demo packages if needed:
sudo apt install ros-$ROS_DISTRO-demo-nodes-cpp \
ros-$ROS_DISTRO-demo-nodes-py \
ros-$ROS_DISTRO-turtlesim \
ros-$ROS_DISTRO-rqt-graphLaunch any of them from the root of the colcon workspace containing
dispatcher:
source install/setup.bash
ros2 run dispatcher dispatcher --ros-args \
-p dispatcher_config_path:=src/dispatcher/config/<example>.yaml \
-p start_checked_on_startup:=trueconfig/example_ros2_talker_listener.yaml
is the publisher/subscriber "hello world" plus topic introspection scripts. The
cpp and python configurations swap between demo_nodes_cpp and
demo_nodes_py, and a LOG_LEVEL variable is substituted into every command.
Launch it with -p initial_configuration:=cpp.
config/example_ros2_namespaces.yaml runs
two talker/listener pairs remapped into /demo1 and /demo2 with -r __ns:=,
each grouped in a collapsible type: category — /demo1 is expanded and
/demo2 collapsed below. The root_pair_launch_file item shows a single entry
monitoring two nodes at once. This file defines no configurations, so the
selector at the top stays empty.
config/example_ros2_services.yaml starts
add_two_ints_server under cpp/python configurations alongside a two-node
launch-file item, with script buttons that call the service and run the demo
clients. Launch it with -p initial_configuration:=cpp. Note the second item's
label elided to introspec...unch_file: item names longer than the checkbox
width are shortened in the middle.
config/example_ros2_turtlesim.yaml mixes
type: shell items whose status comes from pgrep (turtlesim_node, teleop)
with a ROS item monitored through the graph (draw_square). teleop sets
attach_on_start so it opens its own terminal for keystrokes, and the scripts
panel is filled with buttons that publish to topics and call services.
The talker/listener example is the smallest starting point:
workspace: .
variables:
- name: LOG_LEVEL
choices:
- info
- debug
- warn
configurations:
- name: cpp
icon: :/icons/source_code.png
- name: python
icon: :/icons/application.png
nodes:
- name: talker
ros_nodes:
- namespace: /
name: talker
configurations:
- name: cpp
cmd: ros2 run demo_nodes_cpp talker --ros-args --log-level $LOG_LEVEL
- name: python
cmd: ros2 run demo_nodes_py talker --ros-args --log-level $LOG_LEVEL
start_checked: true
- name: listener
ros_nodes:
- namespace: /
name: listener
configurations:
- name: cpp
cmd: ros2 run demo_nodes_cpp listener --ros-args --log-level $LOG_LEVEL
- name: python
cmd: ros2 run demo_nodes_py listener --ros-args --log-level $LOG_LEVEL
start_checked: true
scripts:
- name: rqt_graph
cmd: ros2 run rqt_graph rqt_graph &
icon: :/icons/plot.png
row: 0
column: 0
use_terminal: falseSeveral details in those files are worth calling out because they are easy to get wrong when writing a config from scratch:
- A script with
use_terminal: falsemust end in&. Such a script runs as a blockingsystem("bash -c <cmd>")call on the Qt main thread, so without the&the entire dispatcher UI freezes until the command exits. Scripts withuse_terminal: trueare wrapped ingnome-terminal --, which returns immediately, so those do not need it. - Avoid script commands that can block forever.
ros2 topic pub --oncewaits indefinitely for a matching subscriber andros2 service callwaits indefinitely for the service, so pressing such a button while the target node is stopped leaves a process spinning. Bound them with--max-wait-time-secs Nortimeout N. - A non-root
namespacemust be spelled out. Status matching compares against the fully-qualified name from the ROS graph, so a node remapped with-r __ns:=/demohas to be monitored asnamespace: /demo. An omittednamespaceresolves to the root namespace, and anamethat is already absolute (name: /fcat/fcat) carries its own. - Do not monitor the same node from two items. Both items turn green when
either one is started, which makes the status lights meaningless. Give each
item a distinct set of
ros_nodes. - Launch a multi-node item with a launch file, not
cmd_a & cmd_b. The stop button sendsC-Cto the tmux session, which only reaches the foreground process and leaves the backgrounded node orphaned. - Commands cannot contain single quotes. Process commands are sent as
tmux send-keys -t <session> '<cmd>' Enterand scripts are run asbash -c '<cmd>', so a literal'closes the wrapper. Use double quotes instead, and wrap the whole YAML value in single quotes when the command contains': '(as inros2 service call ... "{a: 2, b: 3}"), which YAML would otherwise read as a nested mapping. - Keep shell metacharacters out of process item names. A process item's name
becomes its tmux session name and is interpolated unquoted into commands like
tmux has-session -t <index>_<name>. Only spaces are sanitized (to underscores), so a name likeroot pair (launch file)makes every tmux call for that item fail withsh: Syntax error: "(" unexpected. Script names are not affected. - A
type: shellitem's name must match the process it starts. Status comes frompgrep <name>, which matches against the kernel's 15-charactercommfield. Naming an itemturtle_teleop_keyfails —pgreprefuses patterns longer than 15 characters — whileteleopmatches the truncatedturtle_teleop_kas a substring. - Beware YAML booleans in service request fields.
ros2 service callparses its request with YAML 1.1, which reads bareoff,on,yes, andnoas booleans."{r: 255, g: 0, b: 0, width: 5, off: 0}"fails withattribute name must be string, not 'bool'; omit the field or quote the key.
Dispatcher cleans up after itself on a normal quit or SIGINT/SIGTERM: it
stops each item, kills the tmux sessions, and removes /tmp/dispatcher.lock. If
the process is SIGKILLed instead, none of that runs, which leaves behind:
- A stale lock file. The next launch aborts with
Could not get lock on file /tmp/dispatcher.lock; an instance of Dispatcher appears to already be running. Remove it withrm /tmp/dispatcher.lock. - Orphaned tmux sessions still running your nodes. Inspect with
tmux lsand clear them withtmux kill-server(ortmux kill-session -t <name>to be selective).
config/example_configurations.yaml
shows the simplest multi-configuration setup. Two named configurations
(config_A and config_B) control which processes are available and which
commands they run. Processes that lack a definition for the active configuration
are grayed out.
| config_A | config_B |
|---|---|
![]() |
![]() |
configurations:
- config_A
- config_B
nodes:
- name: both_configs
configurations:
- name: config_A
cmd: echo "Running config_A"
- name: config_B
cmd: echo "Running config_B"
- name: only_config_A
configurations:
- name: config_A
cmd: echo "Only running config_A"
- name: single_config
cmd: echo "Running single_config"config/example_scripts.yaml adds a scripts
panel with one-click action buttons placed in a grid. Each button can
optionally display an icon and choose whether to open in a terminal.
scripts:
- name: kst
cmd: kst2 &
icon: :/icons/plot.png
row: 0
column: 0
use_terminal: false
- name: htop
cmd: htop
icon: :/icons/terminal.png
row: 0
column: 1
use_terminal: true
- name: rqt_graph
cmd: ros2 run rqt_graph rqt_graph &
row: 1
column: 0
use_terminal: falseconfig/example_variables.yaml introduces
variable selectors shown as drop-downs in the UI. References like
$FCAT_LOOP_RATE_HZ in any command are substituted with the selected value
at launch time.
variables:
- name: FCAT_LOOP_RATE_HZ
choices:
- 1000
- 500
- 250
- 100
nodes:
- name: fcat
ros_nodes:
- name: /fcat/fcat
cmd: ros2 launch robot_bringup fcat_offline.py --rate $FCAT_LOOP_RATE_HZ
start_checked: true
scripts:
- name: "Print Hostname and LOOP_RATE"
cmd: cat /etc/hostname && echo Loop rate $FCAT_LOOP_RATE_HZ
row: 0
column: 0
use_terminal: falseconfig/example_shell.yaml combines several
features: type: shell processes that use pgrep for status,
hide_unconfigured_processes: true to hide (rather than gray out) items
without a command for the active configuration, and configuration entries with
custom icons.
hide_unconfigured_processes: true
configurations:
- name: online
icon: :/icons/wifi.png
- name: offline
icon: :/icons/wifi-no.png
nodes:
- name: fcat
ros_nodes:
- namespace: /fcat
name: fcat
configurations:
- name: online
cmd: ros2 launch ... --rate $FCAT_LOOP_RATE_HZ
- name: offline
cmd: ros2 launch ... --rate $FCAT_LOOP_RATE_HZ
start_checked: true
- name: my-ping3
type: shell
configurations:
- name: online
cmd: ping asimov-dev
- name: offline
cmd: ping asimov-dev
start_checked: trueconfig/example_category.yaml groups
processes into collapsible categories using type: category. Each category
can hold any mix of ROS and shell items and can be expanded or collapsed in
the UI.
| collapsed | expanded |
|---|---|
![]() |
![]() |
nodes:
- name: commander
namespace: /commander
node_name: commander
cmd: ros2 run commander commander
start_checked: true
- name: motor control
type: category
items:
- name: fcat
type: ros
ros_nodes:
- namespace: /fcat
name: fcat
configurations:
- name: online
cmd: ros2 launch ... fcat_online.py
- name: offline
cmd: ros2 launch ... fcat_offline.py
start_checked: true
- name: sensors
type: category
items:
- name: ping1_but_this_is_a_very_long_name_to_demonstrate_elided
type: shell
configurations:
- name: online
cmd: ping google.com
- name: offline
cmd: ping asimov-dev
start_checked: trueCategories can similarly be used to group the buttons generated with the scripts key. This allows the used to cluster buttons, and to toggle their visibility by collapsing them. As with nodes, the syntax is to use type: category in the script item YAML definition.
workspace: .
nodes:
- name: commander
namespace: /commander
node_name: commander
cmd: ros2 run commander commander
start_checked: true
scripts:
# Regular uncategorized scripts
- name: htop
cmd: htop
icon: :/icons/terminal.png
row: 0
column: 0
use_terminal: true
# Categorized visualization tools
- name: Visualization Tools
type: category
items:
- name: kst
cmd: kst2 &
icon: :/icons/plot.png
row: 0
column: 0
use_terminal: false
- name: rqt_graph
cmd: ros2 run rqt_graph rqt_graph &
row: 0
column: 1
use_terminal: false
- name: plotjuggler
cmd: ros2 run plotjuggler plotjuggler &
row: 1
column: 0
use_terminal: false
# Categorized monitoring tools
- name: System Monitoring
type: category
items:
- name: rqt_console
cmd: ros2 run rqt_console rqt_console &
icon: :/icons/terminal.png
row: 0
column: 0
use_terminal: false
- name: rqt_top
cmd: ros2 run rqt_top rqt_top &
row: 0
column: 1
use_terminal: false
# Another regular script after categories
- name: rviz2
cmd: ros2 run rviz2 rviz2 &
row: 1
column: 0
use_terminal: falseconfig/example_remote_session.yaml
demonstrates running commands on a remote host over SSH. Adding hostname
(and optionally user) to a process or script causes the dispatcher to wrap
the command in ssh hostname "command". Per-configuration environment
variables are also shown.
configurations:
- name: online
environment_variables:
RMW_IMPLEMENTATION: rmw_cyclonedds_cpp
CYCLONEDDS_URI: /etc/cyclonedds_online.xml
- name: offline
environment_variables:
RMW_IMPLEMENTATION: rmw_cyclonedds_cpp
CYCLONEDDS_URI: /etc/cyclonedds_offline.xml
nodes:
- name: fcat
configurations:
- name: online
cmd: ros2 launch ... fcat_online.py
hostname: asimov-dev.jpl.nasa.gov
- name: offline
cmd: ros2 launch ... fcat_offline.py
start_checked: trueFrom this package directory after building:
source /opt/ros/jazzy/setup.bash
source install/setup.bash
ros2 run dispatcher dispatcher --ros-args \
-p dispatcher_config_path:=/path/to/dispatcher/config.yaml \
-p initial_configuration:=offline \
-p start_checked_on_startup:=trueIf initial_configuration is omitted, the first configured entry is used. The
start_checked_on_startup parameter defaults to false; when enabled,
dispatcher starts each available item whose YAML start_checked value is
true after applying that initial configuration.
The terminal button opens a command like:
gnome-terminal -t commander -- tmux a -t 3_commanderYou can attach manually from any terminal. First, you can list them with
tmux lsFor example, for example_category.yaml config:
$ tmux ls
1_commander: 1 windows (created Tue Mar 31 13:00:05 2026)
1_fcat: 1 windows (created Tue Mar 31 13:00:05 2026)
1_ping1_but_this_is_a_very_long_name_to_demonstrate_elided: 1 windows (created Tue Mar 31 13:00:05 2026)
2_ping1_2: 1 windows (created Tue Mar 31 13:00:05 2026)
3_ping2: 1 windows (created Tue Mar 31 13:00:05 2026)Then, attach to the desired session with:
tmux a -t 1_fcatInstall the dependencies using rosdep:
rosdep install --from-paths src --ignore-src -r -yBuild dispatcher. From this package directory, run:
source /opt/ros/jazzy/setup.bash
colcon build --base-paths . --packages-select dispatcher \
--cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=ONRun the gtest suite with:
source /opt/ros/jazzy/setup.bash
colcon test --base-paths . --packages-select dispatcherTo print the collected test results:
colcon test-result --verboseThis project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.












