WendyOS Docs

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 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 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 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 services

Render 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 dot

Watch 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 10

Monitor a topic's publish rate:

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:

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:

{
  "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 true

Call a service

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:

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

wendy device ros2 action cancel /navigate

Lifecycle 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 configure

Components

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 1

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

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:

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.

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

On this page