← 返回文章库

机器人的手怎么接进系统?四个灵巧手 service 仓库里的模块化设计

最后更新 2026-08-23
⏱ 约 14 分钟 🟡 涉接线/强电
你将学到
  • 看懂 serial2dds 这一层适配器在解决什么问题,以及它如何让末端执行器变成可替换模块
  • 对比四个灵巧手服务仓库的串口侧与 DDS 侧差异,理解「变的部分」和「不变的部分」怎么切
  • 学会把这个模式搬到自己的项目里:多品牌硬件接入统一系统时,边界该画在哪

一台人形机器人身上,手往往不是造这台机器人的公司自己做的。

这话听起来有点反直觉,但只要想一想手是个什么东西就明白了:它要在巴掌大的空间里塞进多路驱动、传动、位置反馈,还得扛住抓取时的冲击载荷。这是一个专业化程度极高的独立部件,做得好的公司通常只做这一件事。整机厂商去自研一只手,和去自研一颗轴承没有本质区别——能做,但多半不划算。

于是问题就来了:不同厂家的手,怎么接到同一台机器人上?

这不是一个假设性问题。宇树在 GitHub 上开源了一组仓库,专门干这件事。我把它们的仓库描述抄在下面,你先别急着看内容,先看句式:

dex1_1_service        Serial2dds service for Unitree Dex1-1 Hand
brainco_hand_service  Serial2dds service for Brainco Revo2 Hand
linker_hand_service   serial2dds service for linker hand
dfx_inspire_service   Unitree Robot RH56DFX Inspire Hand Controller.

四个仓库,四只不同来源的手,前三个的描述几乎是同一个模板套出来的,中间那个词一模一样:serial2dds

串口转 DDS。就这么六个字母,把一个相当经典的架构问题给答完了。

先把本文的边界说清楚:下面所有结论都来自这几个公开仓库的 README 与文件树,我没有这些硬件,也没有做过任何真机验证。凡是涉及运行效果的地方,我都会标明「README 里写的是」。硬件参数、抓握能力这类东西本文一律不谈,那不是从代码结构里能读出来的信息。

serial2dds:一个只做翻译的进程

把这个模式拆开,它其实只有三块:

[ 手的控制板 ] ←—— 串口 ——→ [ service 进程 ] ←—— DDS ——→ [ 你的程序 ]

中间那个 service 进程,一端连着串口,另一端连着 DDS 网络。它不做运动规划,不做抓取决策,只做一件事:把两种语言互相翻译

上层程序想让右手闭合,它往一个 DDS 话题发一条消息;service 收到之后,按这只手的串口协议组帧、算校验、写串口。反过来,service 不停地从串口读回状态,打包成 DDS 消息发出去,上层订阅就能收到。

这就是标准的适配器模式(Adapter)。教科书里讲适配器时通常举的例子是「把不兼容的接口包一层」,听起来像个小技巧。但在这里,它带来的后果一点也不小:

换一只手,等于换一个 service 进程,主控代码一行不动。

上层看到的永远是「往某个话题发指令、从某个话题读状态」。至于这条指令最后是变成了 Modbus RTU 的一帧、还是变成了宇树自家电机协议的一帧,上层完全不需要知道,也完全不应该知道。

串口那一侧:三个完全不同的世界

适配器有没有价值,取决于被适配的两边差多远。看看这四个仓库在串口侧各自面对的是什么。

Dex1-1(宇树自家的,README 里的原话是一个平行二指夹爪,由一颗宇树 M4010 电机驱动)——它的串口协议其实是宇树的电机协议。这一点从文件树里看得很清楚:

include/unitreeMotor/unitreeMotor.h
include/unitreeMotor/include/motor_msg_A1B1.h
include/unitreeMotor/include/motor_msg_GO-M8010-6.h
include/unitreeMotor/include/motor_msg_HG.h
include/crc/crc32.h
include/crc/crc_ccitt.h
include/serialPort/SerialPort.h
lib/libUnitreeMotorSDK_Arm64.so
lib/libUnitreeMotorSDK_Linux64.so

三种电机报文格式的头文件、两种 CRC 校验、一个串口封装、外加一个预编译的电机 SDK 动态库。这只夹爪在软件上被当成「一颗电机」来对待。

Linker Hand 走的是另一条路。README 说得很直白:串口通信用的是 Modbus RTU。所以它的安装依赖只有一行:

sudo apt install libmodbus-dev

Modbus 是工业现场用了几十年的老协议,寄存器读写那一套。用它意味着这只手的控制板把自己伪装成了一个标准的工业从站设备。顺带一提,这四个仓库里,只有它在组织元数据里标的语言是 C,其余三个是 C++。

Brainco Revo2 又是第三种情况:README 里写的是厂商自己提供了 C 和 Python 的 SDK,这个仓库做的事情是把串口消息转成 DDS 消息,好让它能配合 unitree_sdk2unitree_sdk2_python 使用。README 末尾还指了一句:厂商那边也有一个自己适配的同类项目可以参考。

Inspire RH56DFX 的仓库描述干脆没提 serial2dds,写的是 Controller。但打开 README,做的是同一件事。

三种串口世界:自家电机协议、标准工业协议、厂商私有 SDK。波特率、帧格式、指令集,没有一样是通用的。如果没有中间这一层,主控程序里就得为每一种手写一套 if,而且每接一个新品牌就要动一次主控。

DDS 那一侧:四个仓库长得几乎一样

现在看另一端。这是我读这几个仓库时觉得最舒服的地方——变化被死死地关在了串口那一侧

话题命名先看:

rt/dex1/left/cmd      rt/dex1/left/state
rt/dex1/right/cmd     rt/dex1/right/state

rt/brainco/left/cmd   rt/brainco/left/state
rt/brainco/right/cmd  rt/brainco/right/state

rt/linker/left/cmd    rt/linker/left/state
rt/linker/right/cmd   rt/linker/right/state

rt/inspire/cmd        rt/inspire/state

前三组是同一个模具:rt/<品牌>/<左右>/<cmd 或 state>。每只手一个 USB 转串口设备,每个设备产生一对话题。你把 brainco 换成 linker,代码逻辑一个字都不用改。

inspire 是唯一的例外,它只有一对话题。README 解释了原因:那条消息里的数组同时装着两只手的关节值,前半段索引对应右手(小指、无名指、中指、食指、拇指弯曲、拇指旋转),后半段对应左手。是一次发两只手,而不是分开发。

这个差异挺有意思的。我不去猜作者当时的考虑,但从接口设计的角度看,按物理设备划分话题(一只手一对话题)比按逻辑整体划分话题(两只手挤一条消息)要更容易扩展:单臂配置、左右手用不同品牌、只装一只手做测试——这些场景在前一种命名下都是自然的,在后一种下就得约定「另一半填什么」。四个仓库里三个采用了前一种,剩下那个的默认分支名还停在 master 而不是 main。这大概率是个收敛过程。

消息类型也是复用的。Inspire 那个仓库的 README 写明:上层发 unitree_go::msg::dds::MotorCmds_,收 unitree_go::msg::dds::MotorStates_。这两个类型不是这个仓库定义的,它们躺在 unitree_sdk2 里:

include/unitree/idl/go2/MotorCmds_.hpp
include/unitree/idl/go2/MotorStates_.hpp

灵巧手复用了电机组的消息定义——一只手在数据层面被抽象成「一组电机」。这个选择省掉了一整套新 IDL 的定义、编译和版本维护。代价是语义不够贴切:README 老实交代了,当前只有参数 q(位置)有意义,其他字段都是保留的。

BrainCo 和 Linker 的 README 则各自补了一张归一化约定:手指位置归一化到 [0, 1],Linker 那份还额外说明 0 是张开、1 是握拢,速度和力矩同样归一化到 [0, 1],两份都建议把所有手指的速度设成 1.0。手指索引也各给了一张表,比如 BrainCo 是 [Thumb, Thumb_aux, Index, Middle, Ring, Pinky],Linker 是 ["Thumb", "Thumb Abduction", "Index", "Middle", "Ring", "Little"]

归一化是这层适配器真正的价值所在。 各家手的行程范围、编码器量程、指令单位必然不一样,但归一化到 01 之后,上层写的就是「这根手指收到八成」,而不是「往寄存器写 3200」。

从 README 里读出来的工程细节

这几个仓库的 README 短得可怜,但含金量不低。挑几处说。

编译与依赖

四个仓库的构建方式高度一致,都是 CMake 那一套。Dex1-1 的安装段是这样的:

sudo apt update
sudo apt install libserialport-dev
sudo apt install libspdlog-dev libboost-all-dev libyaml-cpp-dev libfmt-dev
cd ~
git clone https://github.com/unitreerobotics/dex1_1_service
cd dex1_1_service
mkdir build && cd build
cmake ..
make -j6

Linker 更简单,装完 libmodbus-dev 之后直接 ./build.sh。(依赖包名可能随仓库更新变化,以官方仓库当前文档为准。)

四个仓库的 FAQ 第一条几乎是同一条:make 报错找不到 unitree/idl/go2/MotorCmds_.hpp,解决办法是先去把 unitree_sdk2 编译安装到系统目录。这说明它们不是独立软件,而是挂在 SDK 生态上的插件——关于这套 SDK 本身的分层,我在unitree_sdk2 架构拆解那篇里完整拆过。

还有个细节值得停一秒:Dex1-1 和 Inspire 两个仓库的 include/ 下,都各自躺着一对同名文件:

include/dds/Publisher.h
include/dds/Subscription.h

unitree_sdk2 里也有一对:include/unitree/dds_wrapper/common/Publisher.hSubscription.h。同名文件在多个仓库里各存一份,是很典型的「小仓库不想为两个薄封装引入完整依赖」的做法。好处是每个 service 仓库都能独立编译,坏处是这几份副本以后可能会各自漂移。

启动参数

Dex1-1 的服务端把帮助信息直接抄进了 README:

Unitree Dex1-1 Gripper Server:
  -h [ --help ]                produce help message
  -v [ --version ]             show version
  -n [ --network ] arg (=eth0) dds networkInterface
  -c [ --calibration ]         calibrate the gripper motor

BrainCo 那个的参数表几乎一样,只是长参数名叫 --network_interface

--network 这个参数是 DDS 世界的标志性配置。 DDS 靠多播做节点发现,所以它必须知道该在哪块网卡上广播。Dex1-1 的 FAQ 里专门写了一条相关的坑:如果往机器人的 Type-C 口插了一个带网口的 USB 扩展坞,系统里会多出一个以太网接口,默认的 eth0 就可能不再是正确的那一个。README 给的排查办法是用 ifconfigip addr 看哪个接口拿到了 192.168.123.* 网段的地址,然后显式指定,比如 eth1

这条 FAQ 后面还跟了一句我觉得特别实在的提醒:开机自启脚本 setup_autostart.sh 里没有指定网卡参数,用的是默认值。所以手动启动能跑通、开机自启却连不上,是完全可能发生的——得去改脚本里的 ExecStart 行。

Inspire 的启动方式则暴露了另一种风格差异:H1 上用 -s 参数传串口名,G1 上串口名是硬编码在源码里的,README 里明说了不匹配就直接改源码。这种「一个可配、一个写死」的不对称,在真实工程里比设计文档里常见得多。

校准与自启

Dex1-1 多了一个别人没有的步骤:-c 参数进校准流程。README 里贴的过程是——先手动把夹爪捏紧,然后按 s 加回车,逐个电机确认。它还贴了一段服务启动时的日志,里面能看到扫描到的串口列表、检测到的电机 ID、左右手归属,以及各自对应的话题名。

顺便说,这段日志里有个约定挺容易踩:电机 ID 为 0 对应右手,ID 为 1 对应左手。0 不是左边,这种反直觉的映射,不看 README 是猜不到的。

四个仓库都提供了 setup_autostart.sh。Linker 的 README 还把 systemd 的管理命令列全了:

sudo systemctl status linker_hand.service
sudo journalctl -u linker_hand.service -f
sudo systemctl stop linker_hand.service
sudo systemctl restart linker_hand.service
sudo systemctl disable linker_hand.service

这条线索很关键:它们被设计成系统服务,而不是你手动敲一下的调试程序。 开机自启、独立进程、systemd 托管、日志走 journal——这是把一个硬件驱动当基础设施来部署的标准做法。

为什么非要独立成一个进程?

这是我认为整个设计里最值得琢磨的一点。技术上,把串口读写直接编译进主控程序完全可行,为什么要多一个进程、多一层序列化、多一跳网络?

仓库里没有写理由,我不去猜作者的考虑,但从工程角度看,至少有三个好处是明摆着的。

故障隔离。 串口设备是出了名的不老实:拔松了、供电掉了、驱动崩了、USB 枚举顺序变了。Dex1-1 的 FAQ 里就列了两种典型失败——扫不到电机、扫不到任何 ttyUSB 口,分别对应夹爪没通电和串口板没插好。这类错误如果发生在主控进程内部,轻则抛异常,重则把整个进程拖死。可如果一台正在行走的人形机器人因为手上的串口松了而运动控制线程停摆,那就是事故。拆成独立进程之后,最坏情况是手不动了,腿还在正常跑。

实时性分离。 串口是慢速阻塞设备。一次读写在毫秒量级,还可能超时重试。运动控制循环则要求严格的周期性,抖动一点都很难受。让这两种时间尺度差着数量级的东西共享同一个线程调度,是自找麻烦。拆开之后,service 进程可以慢慢等串口,主控循环该多快还多快,两边只在 DDS 上以消息为单位交汇。

部署灵活。 README 里的注释写着这些服务跑在 PC2(用户开发计算单元)上。一旦通信走的是 DDS,进程跑在哪台机器上就不再重要了——同一块板子、机器人上另一块板子、甚至同一网段的开发机,对订阅者来说没有区别。这也是 Dex1-1 仓库里能出现 test/test_gripper_sub.py 的原因:DDS 之上语言是自由的,C++ 写的 service,Python 写的订阅端,照样能对话。想深入 DDS 这套通信机制本身,可以顺着这一卷的其他文章往下读。

顺带一提,Dex1-1 的仓库里还塞了一份 urdf/dex1_1.urdf 和一整套 STL 网格文件。驱动仓库顺手把模型也给了,做仿真和可视化的人省一道找模型的功夫。

这个模式你自己的项目也用得上

讲到这儿,你应该已经看出来了:这套东西跟「灵巧手」其实没什么关系。它是任何「多品牌硬件接入统一系统」场景的通用解法

判断标准很简单——当你的系统里出现了这样一类部件:功能定位固定(都是「一只手」)、但实现方式各家不同(串口协议全不一样)、而且以后还会增加新型号,那就该在它和主控之间插一层适配器。

具体怎么切,有三个动作可以直接照搬:

  1. 先定义上层语言,再去适配下层。 先想清楚主控只想说什么(「右手收到八成」),把这个接口钉死,然后让每个厂商的 service 各自去满足它。反过来做——先看厂商协议再设计接口——一定会把厂商的私有概念泄漏到上层。
  2. 把量纲统一掉。 归一化到 [0, 1] 这个决定看着朴素,但它是让「换一只手」真正无痛的关键。只要上层还在写具体的行程值,你就没有真正解耦。
  3. 进程边界画在慢速设备前面。 凡是阻塞的、会掉线的、依赖物理连接的东西,都尽量隔到独立进程里去。

当然,这不是说所有项目都得这么干。你用 ESP32 做一台小车,用 PCA9685 驱动几路舵机的时候,把驱动直接写进主循环完全没问题——多一层抽象反而是负担。同样,你在传感器图鉴里挑一颗测距模块接上去,直接读就完了。

分水岭在于部件是否会被替换、以及替换的成本由谁承担。小项目里舵机型号定了就不会变,硬编码是效率最高的选择。可一旦你的系统要面对「客户 A 要装这家的手、客户 B 要装那家的」,硬编码的成本就会以指数形式还回来。这四个仓库的存在,本身就是这个规模临界点已经越过的证据。

读完这几个仓库,记住三件事

第一,仓库描述有时候比 README 信息量还大。 Serial2dds service for ... 这一行句式在四个仓库里重复出现,架构模式就写在里面了。看一组仓库的时候,先把描述横着排一遍再逐个打开,往往能提前看见共性。

第二,好的适配层,变化只发生在一侧。 这四个仓库的串口侧差得很远——自家电机协议、Modbus RTU、厂商 SDK 各一套;DDS 侧却高度一致——同一套话题命名模板、复用同一组消息类型、同样的归一化约定。判断一个抽象层做得好不好,就看你换一个实现时,上层要改几行。

第三,进程边界是一种设计手段,不只是部署细节。 把串口驱动拆成 systemd 托管的独立服务,换来的是故障隔离、时间尺度解耦和部署自由。这三样在小项目里都不值钱,在一台要连续运行的机器人上,样样都是硬需求。

最后再强调一遍:以上全部来自公开仓库的文档与文件结构,没有真机验证。如果你手上恰好有这些硬件,README 里那几条 FAQ 大概率就是你会踩的前三个坑,可以提前读一遍。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明