只会 Python 能不能开发机器人?unitree_sdk2_python 接入路径
- 掌握 unitree_sdk2_python 的安装路径与依赖构成,知道 cyclonedds 那个坑为什么会踩
- 从文件树判断 Python 版和 C++ 版的真实关系:不是绑定,是同协议的独立实现
- 想清楚 Python 在机器人开发里的合理边界,学会按控制回路的实时性要求分层
有个场景这两年越来越常见:一个人写了好几年 Python,做数据、做模型、做后端,突然想碰机器人。翻了一圈资料,看到的全是 CMake、头文件、交叉编译、实时内核,心里就凉了半截——机器人开发好像长期就是 C++ 的地盘,自己这套工具链根本插不进去。
但真去看官方仓库,会发现事情没那么绝对。宇树在 C++ 的 unitree_sdk2 之外,另外维护了一个 unitree_sdk2_python。这篇我们就把这个仓库读透,回答两个具体的问题:Python 能做到哪一步,以及哪些地方它确实不合适。
先把话说在前面:本文的全部依据是公开仓库里的 README、目录结构和示例文件名,没有真机验证,也不涉及任何运行效果的描述。凡是提到「跑起来会怎样」的地方,都是仓库文档里写的,不是我实测的。
装起来这一步,坑就藏在依赖里
README 给的安装方式很朴素:
cd ~
sudo apt install python3-pip
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
pip3 install -e .
依赖只有四项:Python 3.8 以上、cyclonedds(README 里锁定了一个具体的小版本)、numpy、opencv-python。以官方仓库当前文档为准,这几项可能随更新变化。
这份依赖清单本身就很有信息量。numpy 说明数据是以数组形态在流动,opencv-python 对应仓库里那几个摄像头示例。而真正的主角是第一项——cyclonedds。
README 特意留了一条 FAQ,讲的就是装不上的情况:
Could not locate cyclonedds. Try to set CYCLONEDDS_HOME or CMAKE_PREFIX_PATH
官方给的解法是先自己把 CycloneDDS 源码编译安装一遍,然后 export CYCLONEDDS_HOME 指过去,再回来装 SDK。
这条 FAQ 值得停一秒。一个 Python 包,装不上的原因是找不到一个 C 库——这就把话说明白了:你没有真正离开 C 的世界,你只是站在它上面。 DDS 那一层始终是原生代码,Python 侧拿到的是它的绑定。我们在这套 SDK 的架构拆解里说过,CycloneDDS 是整个体系的地基,Python 版换的是上层语言,地基没换。
顺带说一句:如果你习惯了 pip install 一把梭,这里的心理准备要提前做好。机器人生态里,纯 Python 的包很少,多数都会在某个位置连到原生库。
它是绑定,还是另写了一遍?
这是我读这个仓库时最想搞清楚的问题。如果它是 C++ 库的绑定,那它的能力上限就等于 C++ 版;如果是独立实现,就要单独看它实现到了哪。
直接看文件树,答案很清楚。整个包目录 unitree_sdk2py/ 下面全是 .py 文件,没有 .a 静态库,没有 pybind 之类的绑定层代码,也没有把 C++ 那边的 libunitree_sdk2.a 拉过来。C++ 仓库里那个塞了近千个文件的 thirdparty/ 目录,在这里也不存在——CycloneDDS 是靠 pip 装的 Python 包引进来的。
再看核心几个包:
unitree_sdk2py/core/ channel.py、channel_config.py、channel_name.py
unitree_sdk2py/rpc/ client.py、client_base.py、client_stub.py
server.py、server_base.py、server_stub.py
lease_client.py、lease_server.py
request_future.py、internal.py
unitree_sdk2py/idl/ 按消息族分:unitree_api、unitree_go、unitree_hg
std_msgs、geometry_msgs、sensor_msgs、nav_msgs
unitree_sdk2py/utils/ crc.py、thread.py、future.py、timerfd.py
bqueue.py、hz_sample.py、joystick.py、singleton.py
clib_lookup.py、lib/crc_aarch64.so、lib/crc_amd64.so
把这张表和 C++ 版对一下就明白了。C++ 那边的 robot/channel/、robot/client/、robot/server/、robot/internal/,在这里变成了 core/ 和 rpc/——同样的概念,同样的租约机制(lease_client / lease_server),同样的请求-响应存根(stub),但是用 Python 重新写了一遍。
所以结论是:这不是绑定,是同一套通信协议的第二个实现。README 那句话也印证了这一点,它说 Python 接口在数据结构和控制方式上与 unitree_sdk2 保持一致,通过请求-响应或话题订阅/发布来完成状态获取与控制。注意「保持一致」这个措辞——是接口对齐,不是代码复用。
idl/ 目录是这个判断最硬的证据。里面躺着一堆 _LowCmd_.py、_LowState_.py、_MotorState_.py、_IMUState_.py 这样的文件,路径形如 idl/unitree_go/msg/dds_/_SportModeState_.py。这些是从 IDL 定义生成的 Python 消息类。两边各自生成一份,共享的是协议,不是二进制。
这里还有个细节:消息族分了 unitree_go 和 unitree_hg 两套。前者里有 _SportModeState_、_HeightMap_、_UwbState_、_Go2FrontVideoData_;后者里有 _HandCmd_、_HandState_、_PressSensorState_、_MainBoardState_。两套消息族名字撞车的不少(_LowCmd_、_LowState_、_IMUState_ 两边都有),引错了族,话题对不上,程序不会报错但就是收不到数据。这是新手最容易卡住的地方之一,写 import 的时候盯紧路径里那一段。
唯一的例外在 utils/ 里:lib/crc_aarch64.so 和 lib/crc_amd64.so,配一个 clib_lookup.py 负责按架构加载。CRC 校验是每条底层指令都要算一遍的东西,把它留在原生代码里,是个很实在的取舍。
从示例读出「怎么写」
example/ 目录的组织方式,几乎复刻了 C++ 版的思路。
最底下是 example/helloworld/,三个文件:publisher.py、subscriber.py、user_data.py。README 说得很直白——两个终端分别跑发布端和订阅端,就能看到数据流出来,而传输的数据结构定义在 user_data.py 里,你可以按需要自己定义。
这个示例跟机器人没有半点关系,它是在教你 DDS。如果你是 Python 出身第一次碰这套东西,起点就在这儿,不在那些能让机器狗站起来的脚本里。 关于话题、QoS 和这套通信模型本身,DDS 通信那篇展开得更细。
往上是按抽象层级分的示例。高层这一档,README 给的命令长这样:
python3 ./example/high_level/read_highstate.py enp2s0
python3 ./example/high_level/sportmode_test.py enp2s0
末尾那个 enp2s0 是机器人所连网口的名字,要按自己机器的实际情况换。这个参数出现在几乎每一条示例命令里——从摄像头到手柄到避障开关,全都要传网口名。这也是 DDS 的特性决定的:它靠网络发现对端,你得告诉它从哪块网卡出去。
sportmode_test.py 里列了一组测试方法,README 直接把它们贴出来了:
test.StandUpDown() # 站起与趴下
# test.VelocityMove() # 速度控制
# test.BalanceAttitude() # 姿态控制
# test.TrajectoryFollow() # 轨迹跟踪
# test.SpecialMotions() # 特殊动作
注释掉四个、留一个,这种示例写法本身就是一种提醒:一次只试一样。
低层那一档,README 前面加了一句很重的话——先用 App 把高层运动服务(sport_mode)关掉,防止指令冲突。这不是客套。高层服务本身就在持续往关节发指令,你的程序再发一份,两股指令打架,后果落在硬件上。这个高层与低层的分野是整套 SDK 里最需要想清楚的概念,高层控制与低层控制那篇专门讲了它。
低层示例做的事,README 描述得也很克制:让某条腿的髋关节保持零度位置,并且明确写了为安全起见把增益设得很小(kp=10、kd=1),另一个关节持续输出一个很小的力矩。默认参数取得这么保守,本身就是态度。
其余的示例按能力铺开:wireless_controller/ 读手柄按键状态,front_camera/camera_opencv.py 用 OpenCV 取前置摄像头画面(README 提醒要在有图形界面的系统上跑,按 ESC 退出),obstacles_avoid_switch/ 循环开关避障,vui_client/ 循环调节音量和灯光亮度。型号目录则有 a2、as2、b2、b2w、g1、go2、go2w、h1、h1_2、h2、r1,各自再分 high_level/ 和 low_level/。
需要强调一句:某个型号目录下没有某个模块,只说明这个 Python SDK 没有暴露这项能力,不能反推成这台机器人做不到。仓库里没写的事,我们就不写。
另一条 Python 路线:unitree_dds_wrapper
宇树还有一个仓库叫 unitree_dds_wrapper,也是 Python,也是走 DDS。两个放在一起看,定位差异就出来了。
它的 README 自述是:为简化与宇树机器人的通信而做,提供由 IDL 生成、并填好了默认值的 {robot}_pub / {robot}_sub 类。用法长这样:
from unitree_dds_wrapper.publisher import Publisher
from unitree_dds_wrapper.subscription import Subscription
from unitree_dds_wrapper.idl import unitree_go
msg_type = unitree_go.msg.dds_.LowState_
pub = Publisher(message=msg_type, topic="rt/test_dds")
sub = Subscription(message=msg_type, topic="rt/test_dds")
对比之下有三个明显区别。
第一,它直接把话题名当字符串写在构造参数里。unitree_sdk2_python 那边有 core/channel_name.py 在管命名,这边把话题暴露给你自己填。灵活,但也意味着拼错了就是静默失败。
第二,它的文件树里根本没有 rpc/ 这个包。也就是说,请求-响应那条路径在这里不存在,它只做发布订阅这一条。unitree_sdk2_python 是两条路径都有的。
第三,它的 robots/ 目录按机器人分:g1、go2、h1、hg、trihand,每个下面是 _pub.py / _sub.py,其中 h1 和 hg 还各带一个 simple_controller.py。消息族也多了 unitree_arm、unitree_hx、unitree_hand,以及 tf2_msgs、trajectory_msgs。安装步骤里还要求装 pinocchio(一个刚体运动学/动力学库),对应包里的 utils/pin.py——这一项需要走 conda,在纯 pip 环境里是个额外门槛。
我不去猜作者当初的分工考虑,但从使用者角度看,这两个仓库的气质不一样:一个是对齐官方 SDK 的完整接口层,一个是贴着 DDS 的轻量收发工具。选哪个,取决于你是要用官方定义好的能力客户端,还是要自己直接摆弄话题。
Python 能吃下哪一段
到这儿可以下判断了。以下都是工程角度的推论,不是官方结论。
适合交给 Python 的:
- 高层指令下发。「站起来」「按这个速度走」「切个模式」——这类指令是事件性的,发一条等一个结果,语言开销无关紧要。
- **状态监控与数据采集。**订阅状态话题、落盘、画图、跑统计。
numpy在依赖里不是摆设,Python 的数据生态在这一段几乎没有对手。 - **和 AI 模型对接。**这是当下最实际的动机。视觉模型、语言模型、策略网络,训练侧的工具链基本长在 Python 上,让机器人接进具身智能这条线,Python 是成本最低的入口。
- **快速原型。**改一行就能重跑,不用等编译。验证想法的阶段,这个循环速度本身就是生产力。
不适合交给 Python 的,主要是一件事:高频硬实时的控制回路。
原因不玄。控制回路要求的是每一拍都准时,而不是平均够快。Python 这边有几个因素会打断这种准时性:GIL 让多线程在解释器层面互相等待,垃圾回收会在你无法预知的时刻插进来,解释执行本身的耗时也不像编译代码那样稳定。这些加起来的结果不是「慢一点」,而是抖动——绝大多数周期都正常,偶尔来一拍迟到。
对普通服务端程序,偶尔迟到一拍无所谓;对控制回路,这一拍的迟到会直接变成硬件上的抖动,轻则动作难看,重则出事。控制回路的时间抖动,最终是要由电机和结构件来承担的。
有意思的是,仓库作者显然清楚这件事。utils/ 里有 timerfd.py(Linux 的定时器文件描述符,用来做精准的周期唤醒)、thread.py、hz_sample.py(从名字看是做频率采样的)、bqueue.py(带缓冲的队列)。这些工具的存在,恰恰说明在 Python 侧把节奏控稳是件需要专门下功夫的事——如果轻松,就不用专门写这些。
顺便说清楚:这不是在贬低 Python。任何语言都有它的适用区间,把一门语言用在它不擅长的位置上,是选型的问题,不是语言的问题。
务实的做法是分层,不是二选一
所以「只会 Python 能不能开发机器人」这个问题,答案是能,但要接受一个前提:你得把系统分层,然后只占其中合适的那几层。
一个可行的分工是这样的:
┌──────────────────────────────────────────┐
│ Python:决策与感知 │
│ AI 模型推理、任务规划、状态监控、数据落盘 │
│ 节奏:事件驱动,或者不高的固定周期 │
├──────────────────────────────────────────┤
│ 高层指令接口(sport / loco 客户端) │
│ 发一条指令,等一个结果 │
├──────────────────────────────────────────┤
│ 硬实时控制回路 │
│ 官方运动服务,或你自己用 C++ 写的那一段 │
│ 节奏:严格周期,不允许抖动 │
└──────────────────────────────────────────┘
这个分层不是我发明的,它就是 SDK 里高层 / 低层那条分界线的自然延伸。官方把稳态行走这类硬实时的活儿封装成了运动服务,你在上面下高层指令,中间那段抖动风险就不由你承担了。
真到了必须自己写低层控制的时候——比如你在做自己的步态或者自研控制器——那一段用 C++ 写,跟 Python 侧通过 DDS 话题交换数据。DDS 本来就是跨语言的,这是它的强项,两边各写各的,靠消息定义对齐。
如果你现在正站在门口,我建议的顺序是:先把 helloworld 那两个脚本跑通,确认自己理解了发布订阅;再去读状态话题,把机器人的数据接出来看;然后才是发高层指令。至于低层控制,等你把前面三步都摸熟了再碰,而且碰之前一定先关掉高层服务。
三件事值得记住:装不上多半是 CycloneDDS 的问题,去看 FAQ 那段;Python 版是独立实现不是绑定,能力以它自己暴露的为准;高层可以放心用 Python,低层的每一拍要留给能保证准时的那一层。 这一卷其他文章会把这些线索一条条接下去。