hu vs. ros2cli and rqt¶
hu is the command-line toolset for the hiroz stack. This page compares it to ros2cli (the standard ROS 2 command-line tools) and rqt (the Qt-based GUI tools), covering both what hu replaces and what it adds.
Feature matrix¶
| Capability | ros2cli | rqt | hu |
|---|---|---|---|
| Measurement | |||
| Publish rate | ros2 topic hz |
— | hu meter hz |
| Bandwidth | ros2 topic bw |
— | hu meter bw |
| End-to-end delay | — | — | hu meter delay |
| Introspection | |||
| Echo messages | ros2 topic echo |
— | hu meter echo |
| Publish messages | ros2 topic pub |
— | hu meter pub |
| List topics / nodes / services / actions | four separate commands | rqt_graph | hu meter list |
| Entity info | four separate commands | rqt_graph | hu meter info |
| Call a service | ros2 service call |
— | hu meter service call <name> --yaml <yaml> --msg-type <type> |
| Action introspection | ros2 action |
— | hu meter action |
| Parameters | ros2 param |
rqt_reconfigure | hu meter param |
| Observation | |||
| Live graph change events (streaming) | — | — | hu monitor watch |
| Graph snapshot | multiple commands | rqt_graph | hu monitor graph |
| Log stream | ros2 topic echo /rosout |
rqt_console | hu monitor log |
| Log level get | ros2 node get-logger-levels (Jazzy+) |
rqt_logger_level | hu monitor log-level <node> |
| Log level set | ros2 node set-logger-levels (Jazzy+) |
rqt_logger_level | hu monitor log-level <node> <level> |
| General | |||
| Machine-readable output | — (human text only) | — | --json on every command |
| Daemon-free operation | no (requires _ros2_daemon) |
no | yes |
| Works without a ROS 2 install | no | no | yes |
| Extensible via plugins | no | yes (rqt plugins) | yes (.wasm plugins) |
| Live multi-topic rate dashboard | — | rqt_topic | hu (interactive TUI) |
Measurement accuracy: hu meter hz vs. ros2 topic hz¶
ros2 topic hz deserializes every message in Python before counting it. This is a known bottleneck: see ros2cli #871, #1043, #843.
hu meter hz subscribes at the raw Zenoh byte layer. It records arrival timestamps from the transport without deserializing any payload.
At moderate rates (≤ 500 Hz, any payload size) both tools report the same rate to within measurement noise — the Python overhead is not the limiting factor and the differential is under a few percent.
At high rates the Python GIL becomes the bottleneck. The hiroz test suite measures this directly with test_hz_python_saturation: a yield_now publisher saturates the CPU to produce a burst stream; each tool is pinned to a separate CPU to isolate the measurement.
xychart-beta
title "Rate measurement: ~97 kHz burst publisher"
x-axis ["Ground truth", "hu meter hz", "ros2 topic hz"]
y-axis "Measured rate (Hz)" 0 --> 100000
bar [96829, 90971, 1398]
hu numbers measured from internal testing on a release binary, via test_hz_python_saturation in hiroz-tests. ros2 topic hz figure from a prior run on identical hardware; ros2 was not available in the benchmark shell. These are internal measurements, not a reproducible public benchmark — treat the specific numbers as illustrative of the order-of-magnitude gap rather than exact figures for your hardware.
| Metric | Measured value |
|---|---|
| Ground truth (time-avg) | 96,829 Hz |
hu meter hz (sliding window) |
90,971 Hz |
ros2 topic hz |
~1,400 Hz |
| hu advantage | ≥ 65× |
ros2 topic hz saturates below 1–2 kHz because rclpy deserializes every message inside the Python GIL. hu meter hz tracks the arrival stream an order of magnitude closer to the true rate. At the rates common in robot perception pipelines (image at 30 fps, lidar at 10–20 Hz, IMU at 100–400 Hz) both tools agree closely; the gap opens above ~500 Hz. hu meter bw has the same property: it counts bytes at the Zenoh subscription layer without deserializing.
No daemon¶
See Why hu? for the full explanation of why ros2cli's daemon model causes problems and how hu's daemon-free design avoids them. The table below lists the specific failure modes this replaces, for reference when comparing against ros2cli.
| Failure mode | Trigger | Symptom | Recovery |
|---|---|---|---|
| Stale domain ID (#1238) | Change ROS_DOMAIN_ID or RMW_IMPLEMENTATION in a new terminal |
Daemon silently queries the wrong domain | pkill -f _ros2_daemon |
| Silent daemon death (#502, #702) | Strict firewall rules or long idle period | All ros2 commands return empty results |
pkill -f _ros2_daemon then re-run |
| Container / WSL2 incompatibility (#934) | Daemon health check fails in certain runtimes | ros2 topic list returns nothing |
None — re-enter with a clean environment |
DDS Discovery Server incompatibility¶
flowchart LR
subgraph DDS["DDS CLI path"]
A["ros2 topic list"] --> B["DDS participant discovery"]
B --> C["Fast-DDS Discovery Server"]
C --> D["❌ Incompatible topology<br>(returns empty / hangs)<br>rmw_fastrtps#499"]
end
subgraph ZenohPath["hu path"]
E["hu meter list topics"] --> F["Zenoh liveliness index<br>(read from router)"]
F --> G["✅ Full graph<br>regardless of router mode"]
end
When using the Fast-DDS Discovery Server (FASTRTPS_DEFAULT_PROFILES_FILE with a <discovery_server> profile), ros2 topic list, ros2 node info, and ros2 topic echo all fail silently because ros2cli uses standard DDS participant discovery, which is incompatible with the topology the Discovery Server imposes (rmw_fastrtps#499).
hu reads the Zenoh liveliness index directly from the router — no DDS layer, no Discovery Server concept. This advantage is scoped: if your network uses rmw_fastrtps_cpp or rmw_cyclonedds_cpp, hu cannot see those nodes at all (see When ros2cli is the right choice).
Machine-readable output¶
Every hu command accepts --json and emits newline-delimited JSON. This makes it composable with jq, shell scripts, CI test harnesses, and logging pipelines without fragile text parsing.
ros2cli outputs human-formatted text with no stable machine-readable format. Parsing ros2 topic list output requires string splitting on / and filtering out blank lines; parsing ros2 topic info requires column-counting. Both break across ROS 2 versions.
# Filter to sensor_msgs topics only
hu meter list topics --json | jq '.[] | select(.type | contains("sensor_msgs"))'
# Extract all publisher node names for /scan
hu meter info topic /scan --json | jq '.publishers[].node'
# Check that hz is within bounds in a CI script
rate=$(hu meter hz /camera/image_raw --duration 5 --json | jq '.rate_hz')
[ "$(echo "$rate > 25" | bc)" = "1" ] || exit 1
# Stream graph events to a log file
hu monitor watch --json >> /var/log/ros-graph-events.jsonl
Live graph events: hu monitor watch¶
ros2cli has no command that streams graph change events. To detect when a node appears or disappears you must poll ros2 node list in a loop, introducing latency proportional to your polling interval and burning CPU during quiet periods.
hu monitor watch subscribes to Zenoh liveliness tokens, which are the mechanism hiroz and rmw_zenoh_cpp use to announce entity existence. It emits a JSON event the moment a node, topic, service, or action appears or disappears — with sub-millisecond latency after the transport propagates the change.
Use cases that are impossible or expensive with ros2cli polling:
- Launch sequencing: wait for a specific node to appear before starting a dependent node, without a timed sleep.
- Integration test teardown: assert that all nodes disappear within N ms of sending a shutdown signal.
- Supervision: trigger a restart when a critical node disappears unexpectedly.
- Debugging intermittent crashes: record all graph events with timestamps to reconstruct the sequence of node deaths.
rqt_graph shows a static canvas that refreshes periodically. It has no event stream and no scripting interface.
Log inspection: hu monitor log vs. ros2 topic echo /rosout¶
The standard way to read ROS 2 logs from the CLI is ros2 topic echo /rosout, which has several friction points:
- It requires knowing the message type (
rcl_interfaces/msg/Log) and outputs raw YAML, including binary stamp fields. - There is no built-in level filter; filtering by severity requires a
greporjqpipe on the YAML output. - There is no node filter; watching logs from a single node requires post-processing.
- The output is not structured — timestamps are split across
secandnanosecfields.
hu monitor log decodes /rosout at the CDR layer and presents a clean stream:
rqt_console provides a GUI log viewer with level and node filters. hu monitor log covers basic log streaming from a terminal with optional count limiting.
Log level control: hu monitor log-level vs. ros2cli vs. rqt_logger_level¶
| Operation | ros2cli | rqt | hu |
|---|---|---|---|
| Get all logger levels for a node | ros2 node get-logger-levels /node (Jazzy+) |
rqt_logger_level | hu monitor log-level <node> |
| Set a logger level | ros2 node set-logger-levels /node name level (Jazzy+) |
rqt_logger_level | hu monitor log-level <node> <level> |
| Works on Humble | no (get/set-logger-levels added in Jazzy) |
yes | yes |
hu monitor log-level works on Humble nodes because it calls the GetLoggerLevels / SetLoggerLevels services directly via Zenoh, without relying on a ros2cli verb that was only added in Jazzy.
# Set the planner's root logger to DEBUG
hu monitor log-level /planner DEBUG
# Read back all active loggers
hu monitor log-level /planner
Plugin extensibility¶
| Step | ros2cli | rqt | hu |
|---|---|---|---|
| Register a new command | Add Python entry-point in setup.cfg |
Write Qt plugin descriptor | Drop a .wasm file in ~/.local/share/hu/plugins/ |
| Install | pip install into the same Python env as ros2cli |
Build and install Qt plugin package | Copy file — no registration step |
| Isolation | Shared Python runtime | Shared Qt runtime | Fully sandboxed per-plugin WASM instance |
| Zenoh session setup | N/A | N/A | Host opens sessions declared in plugin's manifest; plugin never handles connection setup |
| Works in hermetic / offline envs | no — requires pip | no — requires Qt | yes — single binary copy |
# Drop a .wasm file and it becomes a plugin
cp ./my-debug-tool.wasm ~/.local/share/hu/plugins/
hu plugin list # shows all .wasm plugins found in search path
Plugins have access to the live ROS graph, raw CDR subscriptions, publishers, liveliness tokens, queryables, and named Zenoh sessions — all proxied through the host. A bridge plugin that needs two independent sessions (e.g. Humble and Jazzy) declares both in its manifest and the host opens them before the first event fires.
When ros2cli is the right choice¶
hu requires a Zenoh router and is designed for hiroz and rmw_zenoh_cpp networks. It does not work with rmw_fastrtps_cpp or rmw_cyclonedds_cpp — if your nodes use a non-Zenoh RMW, ros2 topic hz will see messages that hu meter hz cannot.
Some ros2cli verbs have no hu equivalent and are not planned:
| ros2cli command | Status in hu |
|---|---|
ros2 launch |
not planned — launch is orthogonal to tooling |
ros2 pkg |
not planned — package management is a build-system concern |
ros2 interface show |
not planned — use hu meter echo --raw or the type description service |
ros2 doctor |
not planned — use hu monitor graph for connectivity diagnosis |
ros2 run |
not planned |
For existing rclcpp/rclpy codebases using DDS, ros2cli remains the right tool. For new Rust code on rmw_zenoh_cpp or pure hiroz networks, hu covers the common inspection workflows with better accuracy, no daemon, and machine-readable output.
Command reference: side by side¶
Rate and bandwidth¶
# Publish rate
ros2 topic hz /scan
hu meter hz /scan
# Bandwidth
ros2 topic bw /scan
hu meter bw /scan
# End-to-end message delay (no ros2cli equivalent)
hu meter delay /scan
Message inspection¶
# Echo messages
ros2 topic echo /chatter
hu meter echo /chatter
# Publish a message
ros2 topic pub /enable std_msgs/msg/Bool '{data: true}'
hu meter pub /enable --msg-type std_msgs/msg/Bool --yaml '{data: true}'
Graph introspection¶
# List topics
ros2 topic list
hu meter list topics
# Topic info
ros2 topic info /scan --verbose
hu meter info topic /scan
# Node info
ros2 node info /lidar_driver
hu meter info node /lidar_driver
# Live change events (no ros2cli equivalent)
hu monitor watch
# Graph snapshot
hu monitor graph
Services and parameters¶
# Call a service
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts '{a: 3, b: 7}'
hu meter service call /add_two_ints --yaml '{a: 3, b: 7}' --msg-type example_interfaces/srv/AddTwoInts_Request
# Get a parameter
ros2 param get /talker use_sim_time
hu meter param get /talker use_sim_time
# Set a parameter
ros2 param set /talker use_sim_time true
hu meter param set /talker use_sim_time true
# Bulk-load parameters from a YAML file
ros2 param load /talker params.yaml
hu meter param load /talker params.yaml