一、最开始我们面对的是什么
原始目标是让穿动捕服的人控制G1机器人,或者先控制MuJoCo里的G1模型。
原始设想的数据链路是:
动捕服
↓
Axis Studio / Noitom
↓
人体骨骼数据
↓
GMR IK重定向
↓
G1 29自由度动作
↓
gRPC
↓
机器人控制器
↓
ONNX
↓
LowCmd
↓
电机
原系统希望把人体动作转换成G1能够执行的29自由度关节动作,然后通过网络发送出去。
最初认为主要问题是gRPC:
- gRPC运行在HTTP/2和TCP之上。
- 接口模型偏RPC,不完全适合连续动作流。
- 线程、Stream和缓存机制比较复杂。
- 网络或接收端变慢时,可能积压旧动作。
- 机器人可能正在执行已经过时的动作。
因此,最初目标是:
保留动作数据结构、GMR、ONNX和控制算法,只替换通信层。
二、最初为什么选择ZeroMQ
人体动作不是典型的“请求—响应”数据。它不是:
电脑问机器人一个问题
机器人返回一个答案
而是:
电脑不断产生动作帧
机器人不断接收动作帧
所以ZeroMQ比gRPC更适合做轻量动作流通信。
当时最初设计为:
protobuf MocapFrame
↓
ZeroMQ PUB
↓
ZeroMQ SUB
↓
机器人
最开始的需求偏向低延迟,因此提出:
HWM=1ZMQ_CONFLATE- 只保留最新动作
- 丢弃旧的
frame_id - 网络慢时不排队
这个方案的原则是:
机器人最关心当前动作,不关心已经过去的历史动作。
如果收到:
Frame 100
Frame 101
Frame 102
Frame 103
但机器人只能及时处理一帧,就直接使用:
Frame 103
这适合“低延迟、最新状态优先”的控制系统。
三、为什么后来不能继续使用PUB/SUB和CONFLATE
后来需求发生了根本变化:
可以慢,可以等待,但电脑发出的每一帧必须全部收到,不能主动丢弃。
这个需求和“最新帧优先”是冲突的。
ZMQ_CONFLATE的作用就是主动覆盖旧帧:
Frame 1
Frame 2
Frame 3
Frame 4
最终队列可能只剩:
Frame 4
这显然不满足“所有帧都要拿到”。
所以我们重新确定了优先级:
第一优先级:完整性
第二优先级:顺序
第三优先级:实时性
也就是说:
如果程序处理不过来,就让帧排队、让延迟增加,但不能为了降低延迟而扔掉数据。
因此通信架构从最初的:
PUB/SUB
HWM=1
CONFLATE
最新帧覆盖旧帧
调整成了现在的:
PUSH/PULL
阻塞发送
FIFO顺序队列
不主动丢弃
这不是简单改了一个参数,而是系统目标发生变化后,重新选择了正确的通信模型。
四、为什么保留protobuf
我们没有修改原来的 MocapFrame。
原因很简单:
MocapFrame是动作数据内容,而gRPC、ZeroMQ只是运输工具。
可以把它理解成:
MocapFrame = 箱子里的货物
gRPC/ZMQ = 运货的卡车
我们只换卡车,不动货物。
这样有几个好处:
- 不需要重写GMR输出数据。
- 不需要重写机器人接收后的解析逻辑。
- 保持C++和其他语言之间兼容。
- 以后改DDS、共享内存或者其他通信方式时,仍可以继续使用。
- 避免动作数据结构和通信改造同时变化,降低风险。
现在的逻辑是:
GMR输出
↓
生成MocapFrame protobuf
↓
protobuf序列化成二进制
↓
加传输Header
↓
ZMQ发送
接收端反过来:
ZMQ收到数据
↓
检查Header
↓
检查CRC
↓
解析protobuf
↓
得到原来的MocapFrame
五、为什么增加MotionFrameHeader
protobuf里有动作内容,但通信层仍需要自己的管理信息。
因此增加了传输Header,包含:
magic
version
message_type
frame_id
capture_timestamp
send_timestamp
payload_size
joint_count
crc32
每个字段都有明确作用。
1. magic
用于判断收到的数据是不是我们的动作协议,如果magic不正确,说明可能是:
- 连接到了错误端口。
- 收到了错误数据。
- 数据内容损坏。
- 发送端和接收端协议不一致。
这种数据不能送进机器人。
2. version
标识协议版本。
以后增加字段或者改变格式时,可以判断新旧程序是否兼容。
3. message_type
区分消息类型,例如:
动作帧
机器人状态
心跳
现在主要使用动作帧,但协议保留扩展能力。
4. frame_id
给电脑实际发送的每个动


344

被折叠的 条评论
为什么被折叠?



