Code and lab notes for making a Clearpath Husky A200 and a Unitree Go2 EDU quadruped work together over campus Wi‑Fi. The Husky reads the dog's odometry and sends it motion and posture commands, even though the two robots run different ROS 2 distributions (Jazzy ↔ Foxy) and the campus network blocks multicast, which normally breaks ROS 2 discovery.
Husky bring-up, mapping and datasets live in HuskyA200. This repository holds everything that involves both robots.
| Capability | Status |
|---|---|
Go2 state → ROS 2 (/go2/odom, 50 Hz) via unitree_sdk2 |
✅ Working on the Go2's Jetson |
| Go2 → Husky odometry over Wi‑Fi | ✅ ros2 topic echo /go2/odom on the Husky |
Husky → Go2 stand_up / stand_down (/go2/command) |
✅ Verified on the real dog (2026-10-13) |
Husky → Go2 velocity (/go2/cmd_vel) |
🟡 Reaches go2_bridge; walking test while standing is next |
| One-command startup (launch scripts) | ⬜ Currently 4 terminals |
flowchart LR
subgraph G["Unitree Go2 EDU · Jetson · ROS 2 Foxy"]
MCU["Motion MCU<br/>Unitree DDS, domain 0"] <--> BR["go2_bridge<br/>(unitree_sdk2)"]
BR <-->|"ROS_DOMAIN_ID=1"| ZG["zenoh-bridge-ros2dds<br/>(connect)"]
end
subgraph H["Clearpath Husky A200 · Ubuntu 24.04 · ROS 2 Jazzy"]
ZH["zenoh-bridge-ros2dds<br/>(listen, TCP 7447)"] <--> APP["Husky ROS 2 nodes / CLI"]
end
ZG <-->|"/go2/* only<br/>TCP over campus Wi-Fi"| ZH
Key design decisions:
- zenoh instead of plain DDS discovery.
zenoh-bridge-ros2ddscarries ROS 2 traffic over a single unicast TCP link. That works without multicast and across Foxy ↔ Jazzy. An allow-list keeps it to/go2/*. - Separate ROS domain on the Go2. The dog's own services use DDS domain 0. Running our nodes there flooded
go2_bridgewithbad_alloc, so all of our ROS nodes on the Go2 useROS_DOMAIN_ID=1. - Topics, not services, across robots. ROS services don't cross the zenoh bridge with Fast DDS, so remote posture commands use the
/go2/commandtopic. - SDK-first CycloneDDS.
unitree_sdk2bundles a newer CycloneDDS than ROS Foxy.CMakeLists.txtputs the SDK's headers first and sets an RPATH so the rightlibddscloads at runtime.
Husky-Go2-MultiRobot/
├── go2_bridge/ ROS 2 package that runs on the Go2's Jetson (C++, unitree_sdk2)
│ ├── src/go2_bridge.cpp
│ └── config/go2_zenoh.json5 zenoh allow-list shared by both robots
├── go2_tools/go2_state_test/ SDK-only state reader, no ROS (skipped by colcon)
└── docs/ Dated lab notes: setup, problems, root causes, results
On the Go2 (ROS 2 Foxy, unitree_sdk2 installed to /opt/unitree_robotics):
mkdir -p ~/go2_ws/src && cd ~/go2_ws/src
git clone https://github.com/greenaandblue/Husky-Go2-MultiRobot.git
cd ~/go2_ws && source /opt/ros/foxy/setup.bash
colcon build --packages-select go2_bridge --parallel-workers 1Daily startup. Start the four terminals in the order A → C → B → D:
# A — Go2: go2_bridge
source /opt/ros/foxy/setup.bash && source ~/go2_ws/install/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp ROS_DOMAIN_ID=1
ros2 run go2_bridge go2_bridge --ros-args -r __ns:=/go2 -p network_interface:=eth0 -p max_vx:=0.2
# C — Husky: zenoh bridge (listen)
source /opt/ros/jazzy/setup.bash && source /etc/clearpath/setup.bash
zenoh-bridge-ros2dds -c ~/go2_zenoh.json5 -l tcp/0.0.0.0:7447
# B — Go2: zenoh bridge (connect to the Husky)
source /opt/ros/foxy/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp ROS_DOMAIN_ID=1
zenoh-bridge-ros2dds -c ~/go2_ws/src/Husky-Go2-MultiRobot/go2_bridge/config/go2_zenoh.json5 \
-e tcp/<husky-ip>:7447
# D — Husky: test
ros2 topic echo /go2/odom --once
ros2 topic pub --once -w 1 /go2/command std_msgs/msg/String "{data: stand_up}"
ros2 topic pub --once -w 1 /go2/command std_msgs/msg/String "{data: stand_down}"If the Husky prints Waiting for at least 1 matching subscription(s) forever, terminal B or A isn't running.
| Name | Type | Direction | Description |
|---|---|---|---|
/go2/odom |
nav_msgs/Odometry |
out, 50 Hz | Pose from rt/sportmodestate, twist rotated into the body frame (go2/odom → go2/base_link) |
/go2/cmd_vel |
geometry_msgs/Twist |
in | SportClient::Move(vx, vy, wz), clamped by max_vx / max_vy / max_wz; stops after cmd_timeout without input |
/go2/command |
std_msgs/String |
in | stand_up (StandUp → BalanceStand), stand_down, recovery_stand, stop |
/go2/stand_up, /stand_down, /stop |
std_srvs/Trigger |
service | Local use on the Go2 only |
Parameters: network_interface (required, e.g. eth0), max_vx 0.5 m/s, max_vy 0.3 m/s, max_wz 0.8 rad/s, cmd_timeout 0.5 s, odom_frame, base_frame, odom_rate 50 Hz.
| Document | Contents |
|---|---|
| docs/2026-10-12_husky_go2_comm_summary.md | One-page progress summary (EN + 中文), updated 2026-10-13 |
| docs/2026-10-12_husky_go2_comm_detailed.md | Full setup log: system configuration, one-time setup on both robots, chronological problems / root causes / fixes, troubleshooting table |
This repository is the home for all future Husky ↔ Go2 work. Conventions:
| What | Where |
|---|---|
| New ROS 2 packages | Top level, one folder per package (e.g. go2_bridge/) |
| Launch / startup scripts | scripts/ |
| Shared configs used by both robots | Inside the package that owns them, or config/ |
| Lab notes and test results | docs/YYYY-MM-DD_<topic>.md; images in docs/images/ |
| Tools not built by colcon | go2_tools/ (has COLCON_IGNORE) |
- Walking test:
/go2/cmd_velfrom the Husky while the dog is standing - Measure Wi‑Fi gaps on
/go2/cmd_vel; tunecmd_timeout(≈1.0 s) - Launch scripts for both robots to replace the 4-terminal startup
- Persistent Go2 network fixes (
ipv4.never-default,power_save off) and NTP time sync - Verify the
/go2/odomtwist frame; relatego2/odomto the Husky's TF tree for joint tasks - Restrict the zenoh port (currently open without authentication)
Huihang Liu · Computer Engineering, Penn State · @greenaandblue