Wendy for ROS 2
Inspect and debug live ROS 2 systems on devices running Wendy Agent, including Unitree robots, with no SSH or ROS 2 app deployment required.
wendy device ros2 brings the full ros2 command-line experience to a remote
device. List nodes and topics, echo messages, watch publish rates, render the
connectivity graph, read and set parameters, drive actions and lifecycle nodes,
and record rosbags, all from your machine, over the same secure connection the
CLI already uses.
To process video from a robot in a Python app, follow Read a Robot Camera with OpenCV. To run a simulated G1 with a browser view, follow Deploy a G1 MuJoCo Simulation.
Standard, upstream ROS 2. WendyOS runs real ROS 2 from stock containers, Humble
by default, configurable per app. Choose your middleware with frameworks.ros2.rmw:
CycloneDDS (default), Fast DDS, RTI Connext, or GurumDDS.
For ROS 2 inside a robot VM, use the Unitree simulator templates and sim-to-real walkthrough.
How it works
The agent discovers running ROS 2 app containers (those deployed with a
frameworks.ros2 config in wendy.json) and starts CLI sidecars in their DDS
domains. When no ROS 2 app is running, it automatically starts a standalone CLI
sidecar on the host network to reach the device's system DDS graph. This works
with Unitree robots without deploying an app or installing the ros2 CLI on
the host. Your commands run inside the sidecar over gRPC.
The system sidecar uses a versioned ROS Humble image with Fast DDS, UDP
discovery, and pinned unitree_go and unitree_api message types. The device downloads the image on first use and caches it for
later commands. It uses domain 0 by default; pass --domain to select another
domain.
ROS 2 app discovery is app-local by default: Wendy injects ROS_LOCALHOST_ONLY=1
so unrelated apps and robots do not discover each other. A bridge or inspection
tool that intentionally needs the device's wider ROS 2 graph can opt in with
"discoveryScope": "host" in frameworks.ros2; pair it with a host-network
entitlement so DDS can reach the device interfaces.
No SSH, no source /opt/ros/<distro>/setup.bash, and no extra ports. If a device
runs apps across multiple RMW implementations (for example CycloneDDS and Fast
DDS), results are tagged with the RMW they came from, such as [cyclonedds].
Scaffold a ROS 2 app
wendy init writes the frameworks.ros2 block for you. Pass --framework ros2,
plus any of the --ros2-* flags to override the defaults:
wendy init \
--app-id my-ros2-app \
--target wendyos \
--language swift \
--framework ros2 \
--ros2-rmw cyclonedds \
--ros2-discovery-scope host \
--assistant skipAny --ros2-* flag on its own implies --framework ros2, and values are
validated up front, so a mistyped RMW or distro fails at init rather than at
deploy time. Run wendy init interactively on a WendyOS target and it asks
whether the app uses ROS 2, then walks through middleware, discovery scope, and
domain ID.
Existing projects need no scaffolding — add the frameworks.ros2 key to
wendy.json by hand and wendy run picks it up on the next deploy. See
wendy.json for every field.
Prerequisites
- A device running Wendy Agent, with a system DDS graph or ROS 2 apps to inspect.
A running app with
frameworks.ros2is optional. See Multi-app deployments for an app-based talker/listener stack. - A device selected as your default (or pass
--device). See Discovering devices.
Use wendy device ros2 topics to inspect a Unitree robot's system graph directly.
The system sidecar's first use requires internet access to download its image.
Every subcommand accepts --domain to override the ROS_DOMAIN_ID. By default it
is 0 for the system graph or read from the running app's ROS 2 config. That includes
auto-derived domains: an app whose frameworks.ros2 omits domainId gets a
stable domain hashed from its appId (in the 0 to 232 range), and the CLI
follows it automatically. Every subcommand also honors the global --json flag
for machine-readable output.
Explore the graph
List what's running and how it's connected:
# Running nodes
wendy device ros2 nodes
# Topics (add --all for publisher/subscriber counts)
wendy device ros2 topics
wendy device ros2 topics --all
# Type, endpoints, and QoS for a single topic
wendy device ros2 topic info /chatter
# Services
wendy device ros2 servicesRender the node/topic connectivity graph as ASCII, or as Graphviz dot to pipe
into your own tooling:
wendy device ros2 graph
wendy device ros2 graph --format dotWatch live data
Stream deserialized messages from a topic, optionally stopping after a fixed count:
# Stream until ctrl-c
wendy device ros2 echo /chatter
# Stop after 10 messages
wendy device ros2 echo /chatter --count 10Monitor a topic's publish rate:
wendy device ros2 hz /chatterecho and hz need the topic's generated message packages inside the CLI
sidecar. The stock system image includes standard ROS 2 messages, but custom
types such as unitree_go/msg/LowState require additional packages. A topic can
appear in topics and topic info even when its message package is unavailable
for decoding.
Inspect LiDAR in Wendy Chat
Wendy Chat and MCP clients can use the read-only ros2_lidar_summary tool
(MCP clients enable it with wendy_tools and {"groups":["robotics"]}) for
sensor_msgs/msg/PointCloud2 and sensor_msgs/msg/LaserScan. It subscribes with
sensor-compatible QoS and decodes data on the device, returning nearest returns
by angular sector, bounded XYZ samples, and frame, timestamp, freshness, and
coverage information. Update both the local CLI and device agent to use it.
The CLI exposes the same geometry sampler:
wendy device ros2 lidar sample /utlidar/cloud_deskewed --scope host --domain 0 --sample-points 64It returns JSON summaries. Use complete PointCloud2 messages and matching IMU/TF streams for SLAM, either through a host-network ROS app or a recorded ROS bag.
First use ros2_topics to discover the actual topic and message type. Use
ros2_topic_info to inspect publishers and endpoint QoS if data is missing.
For example, after discovering /utlidar/cloud_deskewed on the robot's host
graph in domain 0, pass these arguments to ros2_lidar_summary:
{
"scope": "host",
"domain_id": 0,
"topic": "/utlidar/cloud_deskewed",
"message_type": "sensor_msgs/msg/PointCloud2",
"count": 1,
"duration_seconds": 10,
"min_z": -1,
"max_z": 2,
"min_range": 0.05,
"max_range": 20,
"sample_points": 32
}Keep the scope and domain consistent with discovery. The default scope: "app"
preserves the app's ROS 2 environment; host inspection requires an explicit
domain_id. Topic names are examples: a robot may publish a different cloud or
a laser scan. For a discovered scan topic, set message_type to
sensor_msgs/msg/LaserScan.
Directions refer to the output frame's axes, not automatically to the robot.
If the discovered cloud is in odom or map, verify the robot's body frame and
set target_frame to that frame to obtain robot-relative sectors. For example,
add "target_frame": "base_link" only when that is the robot's verified body
frame. The tool requests TF at the source measurement timestamp; a missing
transform is reported rather than treating untransformed points as body-relative.
Height filters use meters in the output frame. Choose them for the sensor and
the obstacle heights of interest.
The tool limits observation duration, message count, decoded points, sample
points, and output bytes. Inspect its coverage and filtering diagnostics before
interpreting distances: an empty sector, dropped points, or no messages do not
establish clear space. Raw samples are a small selection, not a complete cloud.
Custom fields such as /utlidar/range_info need their documented meaning before
they can be described as directional distances.
The returned source_age_seconds is a signed timestamp offset against
clock_basis, not proof that the publisher and device clocks are synchronized.
Source freshness remains unverified without that clock relationship; do not
compare ROS stamps to the chat session clock. Set use_sim_time: true when the
sensor uses a verified ROS simulation clock. A LiDAR summary is a bounded
observation, not a continuously running collision-avoidance system.
See Robot diagnostics with MCP for the
broader observation and navigation workflow.
Parameters
Read parameters across the whole system or a single node, and change them live, no restart required:
# List all parameters (or just one node's)
wendy device ros2 params
wendy device ros2 params --node /talker
# Get and set a single parameter
wendy device ros2 param get /talker use_sim_time
wendy device ros2 param set /talker use_sim_time trueCall a service
wendy device ros2 call /reset std_srvs/srv/EmptyPass a request payload as the optional third argument when the service type needs one.
Actions
List action servers, inspect one, and send goals with live feedback:
wendy device ros2 action list
wendy device ros2 action info /navigate
# Send a goal; add --feedback to stream feedback while it runs
wendy device ros2 action send_goal /navigate nav2_msgs/action/NavigateToPose "{pose: ...}" --feedbackCancel all in-flight goals on an action server (a Wendy extension; the upstream
ros2 action CLI has no cancel verb):
wendy device ros2 action cancel /navigateLifecycle nodes
Inspect and drive managed (lifecycle) nodes through their state machine:
# Lifecycle nodes on the system
wendy device ros2 lifecycle nodes
# Current state and available transitions for one node
wendy device ros2 lifecycle get /lifecycle_talker
wendy device ros2 lifecycle list /lifecycle_talker
# Trigger a transition (configure, activate, deactivate, cleanup, shutdown)
wendy device ros2 lifecycle set /lifecycle_talker configureComponents
Manage composable nodes in a running component container:
wendy device ros2 component list
wendy device ros2 component load /ComponentManager composition composition::Talker
wendy device ros2 component unload /ComponentManager 1Record rosbags
Record topics to a bag on the device, list what's been recorded, and pull a bag back to your machine:
# Record specific topics (ctrl-c to stop and finalize)
wendy device ros2 bag record /camera/image_raw /imu
# Optionally name the bag (defaults to an auto-named, timestamped bag)
wendy device ros2 bag record /scan --output drive-test
# List bags on the device
wendy device ros2 bag list
# Download a bag to a local directory
wendy device ros2 bag download drive-test ./local-bagsRun bag download with no arguments to pick from the device's bags
interactively. On a device running apps across multiple RMW implementations,
recording all topics captures one RMW's graph and warns about the others;
record explicit topic lists per RMW if you need both.
Health checks
Run ros2 doctor against the device's ROS 2 environment:
wendy device ros2 doctorAnything else
For any ros2 subcommand not wrapped above, use the raw passthrough. Flags meant
for Wendy (like --domain) go before the ros2 command word; everything after is
handed straight to ros2:
wendy device ros2 exec topic echo /chatter --once
wendy device ros2 --domain 42 exec node info /talkerVisualize in Foxglove
To see this data in real time on plots and panels, bridge the device's topics into Foxglove Studio with Wendy for Foxglove.
Related
- Multi-app deployments: deploy a ROS 2
stack with
wendy.jsonservices ordocker-compose.yml. - Entitlements: the host-network access ROS 2 discovery requires.
Native Unitree audio packets
Standalone inspection can decode unitree_go/msg/AudioData on discovered audio
input and output topics, plus unitree_api audiohub requests and responses.
With no ROS app running, sample packet metadata through the system CLI:
wendy device ros2 exec run wendy_ros2_inspection audio sample /audioreceiver --count 3Add --include-data to retain each packet as base64. The helper also provides
publish <topic> --time-frame <value> --data-base64 <payload> for a single
already encoded packet. Confirm topic direction and payload format against the
robot firmware. AudioData contains only time_frame and bytes, so the inspector
does not infer a codec, sample rate or channel count and does not expose an ALSA
microphone or speaker. With apps running, use MCP ros2_topic_sample in explicit
host scope to inspect native audio independently of the app's packages.