---
title: Wendy for ROS 2
description: 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](../../guides/tutorials/python/robot-camera-opencv/). To run a simulated G1 with a browser view, follow [Deploy a G1 MuJoCo Simulation](../../guides/tutorials/python/g1-mujoco-simulation/).

<Callout type="info">
**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.
</Callout>

For ROS 2 inside a robot VM, use the [Unitree simulator templates](../../installation/unitree-simulators/) and [sim-to-real walkthrough](../../integrations/sim-to-real/).

## 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.

<Callout type="info">
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]`.
</Callout>

## 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:

```bash
wendy init \
  --app-id my-ros2-app \
  --target wendyos \
  --language swift \
  --framework ros2 \
  --ros2-rmw cyclonedds \
  --ros2-discovery-scope host \
  --assistant skip
```

Any `--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`](../../apps/wendy.json/#frameworks) 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.ros2` is optional. See
  [Multi-app deployments](../../guides/multi-app-deployments/) for an app-based
  talker/listener stack.
- A device selected as your default (or pass `--device`). See
  [Discovering devices](../../device/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:

```bash
# 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 services
```

Render the node/topic connectivity graph as ASCII, or as Graphviz `dot` to pipe
into your own tooling:

```bash
wendy device ros2 graph
wendy device ros2 graph --format dot
```

## Watch live data

Stream deserialized messages from a topic, optionally stopping after a fixed
count:

```bash
# Stream until ctrl-c
wendy device ros2 echo /chatter

# Stop after 10 messages
wendy device ros2 echo /chatter --count 10
```

Monitor a topic's publish rate:

```bash
wendy device ros2 hz /chatter
```

`echo` 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:

```sh
wendy device ros2 lidar sample /utlidar/cloud_deskewed --scope host --domain 0 --sample-points 64
```

It 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`:

```json
{
  "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](../../integrations/robot-diagnostics/) 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:

```bash
# 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 true
```

## Call a service

```bash
wendy device ros2 call /reset std_srvs/srv/Empty
```

Pass 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:

```bash
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: ...}" --feedback
```

Cancel all in-flight goals on an action server (a Wendy extension; the upstream
`ros2 action` CLI has no cancel verb):

```bash
wendy device ros2 action cancel /navigate
```

## Lifecycle nodes

Inspect and drive managed (lifecycle) nodes through their state machine:

```bash
# 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 configure
```

## Components

Manage composable nodes in a running component container:

```bash
wendy device ros2 component list
wendy device ros2 component load /ComponentManager composition composition::Talker
wendy device ros2 component unload /ComponentManager 1
```

## Record rosbags

Record topics to a bag on the device, list what's been recorded, and pull a bag
back to your machine:

```bash
# 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-bags
```

Run `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:

```bash
wendy device ros2 doctor
```

## Anything 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`:

```bash
wendy device ros2 exec topic echo /chatter --once
wendy device ros2 --domain 42 exec node info /talker
```

## Visualize 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](../../integrations/foxglove/).

## Related

- [Multi-app deployments](../../guides/multi-app-deployments/): deploy a ROS 2
  stack with `wendy.json` services or `docker-compose.yml`.
- [Entitlements](../../device/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:

```sh
wendy device ros2 exec run wendy_ros2_inspection audio sample /audioreceiver --count 3
```

Add `--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.
