← Blog·Engineering Updated

The unitree_sdk2 in 2026: A practitioner's map of Unitree's public SDK

Where the SDK actually lives, what's in it, what isn't, and the seven control surfaces you need to know before you ship code.

qtvue engineering· Engineering team··Updated Jun 22, 2026·14 min read

qtvue · SDK deep-dive

unitree-sdk2-deep-dive

qtvue research

TL;DR. The unitree_sdk2 is the official C++ and Python SDK across the full Unitree range (Go2, B2, G1, G1-D, R1, H1, H2, Arms). It speaks Cyclone DDS over UDP multicast on a local LAN, exposes seven control surfaces (low-level joint, high-level sport, arm, hand, lidar, audio, video), and ships with unitree_ros2 for the ROS 2 ecosystem. The official docs are fragmented — this article is the map we wish existed.

1. Where the SDK lives

The public SDK is split across four GitHub repos maintained by Unitree and Unitree-affiliated contributors:

RepoWhat it ships
unitree_sdk2C++ core (builds on Ubuntu 20.04 / 22.04)
unitree_sdk2_pythonPython bindings (CPython 3.8+)
unitree_ros2ROS 2 Humble / Jazzy DDS-XRCE bridges
unitree_sim_isaaclabIsaac Lab environment files + URDF

All four are mirrored on a single internal CI we maintain; in practice, we clone them as a sparse checkout into our customer repos because the release tags drift between the C++ and Python repos.

Gotcha #1. The C++ repo is tagged vX.Y but the Python bindings are tagged independently. A v2.4.0 C++ tag may ship with v2.3.1 Python bindings. Pin both tags in your requirements.txt / CMakeLists.txt and treat them as a coupled pair.

2. Transport: Cyclone DDS over UDP multicast

The SDK assumes a direct Ethernet (or Wi-Fi) connection between your control host and the robot's onboard compute. It uses Cyclone DDS with UDP multicast on 239.255.0.1:8888 by default. Every state stream (joint states, IMU, LiDAR, battery) and every command stream (joint commands, locomotion goals, arm trajectory points) goes over this one multicast group.

In production this has two practical consequences:

  1. You need a clean Layer 2. Multicast doesn't survive routers, and many corporate switches drop multicast by default. We tell customers to put the robot on its own VLAN with the control host, with the switch port configured for multicast-flood or with IGMP snooping properly enabled.
  2. You can't run two SDK instances on the same VLAN. Two hosts sending locomotion commands at the same time will fight over the robot. This is the most common customer support issue we see. We always run the SDK on a dedicated NUC or Jetson, not on a shared workstation.
# Explicit DDS configuration — pin the interface, force IPv4, set QoS
import cyclonedds
from cyclonedds.domain import DomainParticipant
from cyclonedds.core import Qos, Policy

qos = Qos(
    Policy.Reliability.BestEffort,
    Policy.History.KeepLast(10),
    Policy.Durability.Volatile,
)
# Tell Cyclone DDS which interface to use on multi-NIC hosts
cyclonedds.config_set("Local./Network/Interfaces", "eth0")

3. The seven control surfaces

When we say "the SDK" we mean a federated set of modules. Each one talks to a separate DDS topic and exposes its own message types:

┌──────────────────────────────────────────────────────┐
│                  unitree_sdk2 surfaces                │
├──────────┬───────────────┬───────────────────────────┤
│ LowState │ <sport>       │ SportClient (high-level    │
│ LowCmd   │ <hand>        │ locomotion, arm, hand,    │
│          │ <arm>         │ LiDAR, audio, video)      │
├──────────┼───────────────┼───────────────────────────┤
│ MotorCmd │ <lowlevel>    │ Direct joint torque /     │
│ MotorState│              │ position / velocity PD    │
├──────────┼───────────────┼───────────────────────────┤
│ ArmCommand│ <arm_sdk>    │ 6+ DoF arm trajectory     │
│          │              │ (g1, h1, z1)              │
├──────────┼───────────────┼───────────────────────────┤
│ LidarScan│ <lidar>       │ 4D LiDAR point cloud      │
├──────────┼───────────────┼───────────────────────────┤
│ AudioData│ <audio>      │ Microphone array +        │
│          │              │ speaker playback          │
├──────────┼───────────────┼───────────────────────────┤
│ VideoFrame│<videohub>    │ RGB-D camera stream        │
└──────────┴───────────────┴───────────────────────────┘

The most common mistake is mixing the high-level SportClient calls with low-level MotorCmd writes in the same process. The SDK doesn't enforce a single-writer invariant — both work — but they fight at runtime and you'll see the robot twitch erratically. We always isolate them: one process owns low-level joints (or none does), one process owns high-level goals, one process owns the arm.

4. The Python bindings

The Python bindings wrap the C++ core with pybind11. They expose the same message types as dataclasses and the same clients as high-level Python classes. The trade-off is GC pressure — every state callback allocates a new message, and Python's GC can stall the 200 Hz control loop if you're not careful.

Two patterns that work:

# 1. Use the pre-allocated read buffer for hot paths
from unitree_sdk2py.core.channel import ChannelSubscriber
from unitree_sdk2py.idl.unitree_hg import LowState_

sub = ChannelSubscriber("rt/lowstate", LowState_)
sub.Init()
while not shutdown_event.is_set():
    msg = sub.Read()       # reuses an internal buffer
    process(msg)            # no per-frame allocation
    time.sleep(0.005)       # 200 Hz
# 2. For non-hot paths (telemetry), batch into lists
states = []
def cb(msg: LowState_):
    states.append(msg.tick)
    if len(states) >= 1000:
        flush(states)        # every ~5s at 200 Hz
        states.clear()

5. The four footguns

These are the bugs we hit on real engagements. Each one has cost us or a customer at least a week of debugging.

Footgun 1 — Walk(true) doesn't fully enable sport mode. You need to call BalanceStand(true) first, wait for the robot to settle (~0.5s), then call Walk(true). Skip the balance step and the robot tips over on its first Move().

Footgun 2 — StopMove() doesn't actually stop.StopMove() issues a single zero-velocity command. The high-level controller interpolates to a stop over ~0.3s. If you're on a real- time control loop, you need to also send Move(0, 0, 0) repeatedly until the robot is stationary. Or just use SportClient.Damp() — the SDK's emergency damping state.

Footgun 3 — Joint index mismatch between Go2 and G1. Go2 has 12 joints in a quadruped arrangement (FR/FL/HR/HL × 3 each). G1 has 23–43 joints in a humanoid arrangement. The LowCmd message flattens joints into a single array; the index ordering is documented per-platform. There is no shared kJointNames constant — copy the order from your platform's URDF.

Footgun 4 — Multicast TTL leaks. The default DDS config uses TTL=1. On a multi-switch setup this is fine. But if you ever route traffic through a Linux bridge with brctl, the bridge will reset the TTL on forwarding and you'll see the multicast loop back. Symptom: the control host sees its own commands. Fix: set TTL=64 in the DDS QoS or use a separate VLAN.

6. ROS 2 — the unitree_ros2 layer

unitree_ros2 wraps the SDK with proper ROS 2 topics and TF trees. For most customers, this is the right entry point — you get RViz, ros2 bag, ros2 topic echo, and the entire ROS 2 ecosystem for free. The trade-off is one extra hop of latency (~2 ms) and the need to build a Cyclone DDS / Fast DDS bridge correctly.

We have a working ros2 humble setup with cyclonedds configured that most engagements start from. The unitree_ros2 repo includes example launch files for each platform.

7. What we ship

For customer engagements, we wrap the SDK with our own safety layer:

  1. Heartbeat watchdog — if the control host misses three consecutive state updates, the robot goes to Damp().
  2. Velocity limiter — caps Move(x, y, yaw) to the platform's published max × 0.8.
  3. Geofence — uses LiDAR SLAM pose to enforce a polygon. If the robot leaves the polygon, it stops and awaits human approval.
  4. Emergency stop — physical E-stop button wired to a watchdog GPIO that calls Damp() directly, bypassing the SDK.

We open-source these patterns at github.com/UDGOK/qtvue under MIT.

Where to start

If you're picking up the SDK for the first time:

  1. Clone unitree_sdk2_python and run the example/sport_client example with your Go2 on the bench.
  2. Switch to unitree_ros2 and ros2 topic echo /lowstate.
  3. Pick a target (autonomous patrol? manipulation? imitation learning?) and read the matching unitree_sim_isaaclab env.
  4. Reach out if you're stuck — hello@qtvue.com or our intake form.

The SDK is good. It's not perfect, and the docs are sparse. But once you understand the seven surfaces and the four footguns, you can ship real work on top of it.

Taggedunitree_sdk2ros2ddssdk
QE

Written by

qtvue engineering

Engineering team

The qtvue engineering team works on Unitree integration projects every week. We wrote this guide because the official SDK docs are fragmented and Chinese-first, and we wanted a single English reference we could hand to new engineers.

Working on something Unitree-shaped?

Five-question form, three minutes, reply within 1 business day.

Submit your use case →