资讯动态

Day13 unitree_G1人形机器人传输问题

发布时间:2026/8/14 16:48:03 来源:尧图企业网站定制
今天看的是为什么Axis显示动捕设备是96 Hz但我们的程序通过BVH/MocapApi只能得到约6064帧/秒重点讲我们如何从发现现象开始一步步修改程序、增加证据、排除可能原因最后把问题定位到Axis普通BVH/MocapApi输出边界。一、最开始我们看到了什么第一次动捕服成功连接后链路已经能跑通动捕服 ↓ PNLink ↓ Axis Studio ↓ MocapApi ↓ GMR ↓ ZMQ ↓ MuJoCoAxis里的人物会跟随真人MuJoCo里的G1也会动。但终端不断出现[NoitomJump] prev_posture_index100 posture_index102 delta2 missing_posture_indices[101]大白话就是上一帧编号是100 下一次收到的编号是102 编号101没有出现同时程序统计的接收频率只有约6064 Hz但Axis设备页面显示帧率动捕设备96于是产生了最初的疑问是Axis本来就只发约64帧还是我们的程序、GMR、ZMQ或MuJoCo弄丢了约三分之一这时不能直接下结论因为从Axis到MuJoCo中间有很多环节任何一层都可能造成少帧。二、第一步不靠“看起来”建立分层计数一开始只看终端里的NoitomJump无法知道帧究竟丢在哪里。所以我们的第一个核心思路是给流水线中的每一层都安装“水表”。最终形成以下计数Axis/MocapApi实际交给程序多少帧 ↓ source_received GMR处理了多少帧 ↓ gmr_processed 生成了多少机器人动作 ↓ motion_generated ZMQ成功发送多少帧 ↓ zmq_sent MuJoCo接收多少帧 ↓ received MuJoCo实际应用多少帧 ↓ applied同时记录source_missing_indices input_pending robot_pending gaps invalid stale这样我们就能问是输入时就少了 还是GMR处理时少了 还是ZMQ发送时少了 还是MuJoCo接收时少了这一步是整个排查最关键的基础。三、建立AuditCore检查电脑发送端我们增加了AuditCore。它记录source_received source_missing_indices first_posture_index last_posture_index gmr_processed motion_generated zmq_sent grpc_sent input_pending robot_pending max_input_queue max_robot_queue为什么需要它因为如果最终看到source_received1000 gmr_processed650就说明GMR或输入队列丢了350帧。如果看到motion_generated1000 zmq_sent650就说明ZMQ发送端丢了350帧。如果它们都相等就说明问题不在这些层。四、建立AuditViewer检查MuJoCo接收端发送端说“我发送成功了”还不够因为接收端可能没拿全。所以MuJoCo端又增加received applied pending gaps invalid stale latest_frame_id delay_ms最重要的是gaps我们给每个ZMQ动作帧增加连续编号frame_id1 frame_id2 frame_id3如果MuJoCo收到1 2 4那么gaps1这样可以直接判断ZMQ通信有没有漏掉动作帧。五、先排查MuJoCo是不是处理不过来第一次看到明显延迟时最容易怀疑会不会MuJoCo渲染太慢导致动作积压或丢失于是我们观察Viewerreceived applied pending gaps invalid测试结果出现过received1862 applied1862 pending0 gaps0 invalid0 delay≈0.3 ms这意味着收到1862帧实际应用1862帧没有尚未处理的积压frame_id没有断号Header、CRC、protobuf没有损坏本机ZMQ延迟很低。所以排除MuJoCo渲染导致丢帧 MuJoCo接收后没有应用 MuJoCo队列持续堆积 数据解析错误导致丢弃六、排查ZMQ是否把帧弄丢了项目早期的设计是PUB/SUB HWM1 CONFLATE 只保留最新帧这种设计追求低延迟但会主动抛弃旧帧。后来对方提出新要求可以慢一点可以等待但所有数据都必须拿到。这和“最新帧优先”完全相反。所以我们重新思考如果不能丢历史帧 就不能使用只保留最新帧的机制于是改成阻塞式PUSH/PULL FIFO先进先出队列 不使用CONFLATE 不主动覆盖旧帧 退出前排空队列然后比较motion_generated zmq_sent received applied正式测试结果motion_generated1862 zmq_sent1862 received1862 applied1862 gaps0 invalid0 pending0因此确认只要动作已经进入ZMQ发送阶段就完整到达并应用到了MuJoCo。所以约96→63 Hz不是ZMQ造成的。七、排查GMR是不是只能处理约64 Hz下一种可能是Axis可能已经给了96 Hz但GMR IK太慢每秒只能算64帧。原来如果采集和GMR都放在一个循环中读取一帧 执行一次IK 发送一帧 再读取下一帧那么GMR一旦计算慢前面的MocapApi就无法及时继续读取。因此我们把它们拆开MocapApi采集线程 ↓ Input FIFO ↓ GMR线程 ↓ Robot FIFO ↓ ZMQ发送线程这样采集线程只负责尽快拿数据不负责执行IK。判断依据是source_received gmr_processed input_pending max_input_queue实测多次得到source_received1864 gmr_processed1864 input_pending0以及source_received1267 gmr_processed1267 input_pending0以及source_received628 gmr_processed628 input_pending0说明MocapApi实际收到多少帧GMR就处理多少帧没有帧长期留在输入队列GMR没有把96 Hz降成64 Hz程序实际进入GMR之前就只有约64 Hz。因此排除GMR性能问题。八、排查ZMQ发送线程是不是跟不上GMR即使GMR能够处理全部数据ZMQ线程也可能发送太慢。所以检查motion_generated zmq_sent robot_pending max_robot_queue正常停止时得到motion_generatedzmq_sent robot_pending0这说明GMR生成的动作全部发出没有动作遗留在发送队列停止时执行了队列排空ZMQ没有把输入频率降成64 Hz。九、排查插值器是不是吃掉了帧我们曾看到source_received628 motion_generated626表面看少了2帧。所以我们检查插值器逻辑。插值器必须先拿到前后参考帧才能生成中间动作。程序启动时最初少量帧用于初始化因此可能固定出现输入628 输出626但如果96→64是插值器造成的应该看到输入约960 输出约640实际不是。实际是进入插值器之前就只有628帧因此固定少2帧属于初始化每秒少约32帧不是插值器造成的插值器不是根源。十、排查代码里是否写死了60 Hz下一步检查采集程序是否存在sleep_for(16ms);或者if(frame_id % 3 0) { skip(); }或者只读取队列中最新的一帧实际采集逻辑是有MocapApi事件就立即读取没有事件时才短暂休眠约1 ms没有固定60 Hz定时器没有每隔几帧跳过没有LatestBuffer覆盖输入没有CONFLATE用于MocapApi输入。所以排除程序主动限频。十一、排查-InputFps 96和-OutputFps 96运行命令中使用-InputFps 96 -OutputFps 96一开始可能会误以为既然这里写了96为什么实际不是96日志显示[Interpolator] input_fps96 output_fps96 ratio1.000这些参数只告诉插值器如何处理已经收到的数据不能控制Axis的BVH端口发送多少事件。大白话就是在自己程序里写“我要96帧”不能命令Axis凭空多发32帧。所以这两个参数不是原因。十二、测量MocapApi读取一帧到底多慢还存在一种可能MocapApi读取一帧花15毫秒所以程序最多只能读取约64帧/秒。于是我们增加avg_avatar_read_ms max_avatar_read_ms实际结果avg_avatar_read_ms≈0.05 ms max_avatar_read_ms通常小于0.5 ms96 Hz每帧的时间预算约为1000 / 96 ≈ 10.42 ms而我们读取只需要约0.05 ms说明读取函数本身非常快远没有占满10.42 ms。所以排除骨骼节点读取太慢 MocapApi函数耗时导致64 Hz NoitomFrame转换太慢十三、排查大量日志是否拖慢程序当时终端疯狂输出[NoitomJump]一份日志中甚至统计到11820条 18737条控制台打印确实可能很慢所以我们提出假设可能不是MocapApi慢而是打印几万行日志把采集线程拖慢了。于是增加log_jump_details false;关闭每一次跳号的详细打印只保留周期汇总。重新测试后source_recv_rate仍约6065 Hz avg_posture_delta仍约1.5 skipped_frames仍约3038/秒 avg_avatar_read_ms仍约0.05 ms说明关闭日志没有改变实际频率。因此排除日志输出造成降频。十四、排查多线程日志互相干扰终端中曾出现[Metrics]和[NoitomJump]粘在同一行一开始容易误以为数据处理发生了混乱。实际上是多个线程同时写控制台采集线程正在打印NoitomJump 监控线程正在打印Metrics文字交叉不等于数据帧交叉也不等于ZMQ损坏。关闭逐跳日志后审计数据仍然一致证明这只是日志显示问题不是动作数据问题。十五、尝试更激进地读取MocapApi事件我们又提出一个合理怀疑MocapApi内部可能一次缓存了多个事件但程序每次只取一个导致其余事件被覆盖。于是进行了两类尝试调整PollApplicationNextEvent调用方式缓存关节数组和事件读取结果。但修改后出现程序收不到动作 MuJoCo机器人保持站立 程序原生崩溃 退出码-1073741819回退到原来稳定实现后机器人重新跟随抬手 程序恢复正常同时SDK报告cache_eventsunsupported这说明当前MocapApi r70没有提供我们设想的事件缓存能力那种优化方式与当前SDK并不兼容。所以我们没有为了“看起来更快”保留危险修改而是回退稳定方案。这一步的结论是不能通过简单增加poll次数或错误缓存SDK对象来找回不存在的Avatar事件。十六、排查GenLock是否锁到了较低频率Axis普通BVH设置中曾开启GenLock我们怀疑它可能让输出跟随其他同步时钟从96 Hz变成较低频率。于是关闭GenLock保持其他设置不变重新运行同样测试。结果仍然source_recv_rate约6064 Hz avg_posture_delta约1.5没有明显变化。因此排除GenLock是主要原因。十七、排查7003端口或TCP是否随机掉包普通BVH使用TCP 127.0.0.1:7003这里有两个重要事实1. 这是本机连接Axis和我们的C程序运行在同一台电脑上。127.0.0.1不经过外部网线也不经过交换机。所以Axis → MocapApi程序这一段不是PNLink网线传输。2. TCP具有有序可靠传输如果网络来不及典型表现更可能是延迟堆积 读取阻塞 队列增长而不是长期稳定地只得到约三分之二的应用层事件同时队列保持0。再加上多次测试都得到约6264 Hz比例非常稳定。因此随机TCP丢包的可能性极低也无法解释固定接近2/3的比例。十八、测试高级BVH 7005我们看到Axis还有高级BVH数据广播因此提出新的假设普通BVH 7003可能只输出约64 Hz高级BVH 7005可能输出完整96 Hz。为了不破坏已经稳定的7003我们采用并行验证普通BVH保留7003 高级BVH另开7005检查端口127.0.0.1:7005处于监听运行程序连接7005日志显示connected via TCP 127.0.0.1:7005但20秒内一直是posture_index0 source_recv_rate0 gmr_rate0 zmq_send_rate0 received0说明TCP连接确实建立了但7005没有发送当前MocapApi r70能够解析的Avatar事件高级BVH不是普通BVH接口的直接替代品可能需要专用高级协议或另一套SDK。因此排除把端口改成7005就能直接获得96 Hz然后恢复稳定的7003程序重新正常动作。十九、排查Axis界面有没有输出频率选项接着检查Axis设置。1. 工作模式页面只有自动检测 全身 全身带手套 上半身 下半身 手臂 手套而且是灰色不可修改。它是穿戴类型识别不是输出帧率。2. 设备页面显示帧率动捕设备96但同样是灰色只读。这说明96由当前硬件自动报告不是普通BVH广播的可调参数。3. 普通BVH页面可以设置二进制格式 骨骼模板 旋转顺序 位移 TCP/UDP IP 端口但没有看到Output FPS Broadcast Rate Downsample Every Frame Skip Frame因此在当前Axis Studio界面内没有找到把普通BVH输出改成96 Hz的入口。二十、最终用数学关系确认边界最有说服力的是固定时长统计。20秒测试审计结果first_posture_index615147 last_posture_index617082 source_received1267 source_missing_indices669内部索引跨度617082 - 615147 1 1936折算频率1936 / 20 96.8 Hz程序实际收到1267 / 20 63.35 Hz索引没有出现的数量1936 - 1267 669恰好等于source_missing_indices66910秒复测结果first_posture_index754158 last_posture_index755124 source_received628 source_missing_indices339内部索引跨度755124 - 754158 1 967折算967 / 10 96.7 Hz 628 / 10 62.8 Hz两次独立测试结果基本相同内部索引约9697 Hz 实际Avatar事件约63 Hz比例大约为63 / 96 ≈ 0.66非常接近三分之二。这比“偶尔卡顿”更像稳定的输出策略或两种不同的时钟定义。二十一、最后是怎么锁定问题边界的我们把所有计数排成一条线Axis内部posture_index跨度约96 Hz ↓ MocapApi实际Avatar事件约63 Hz ↓ MotionInput收到全部记录 ↓ GMR处理与收到数量相等 ↓ ZMQ发送与生成数量相等 ↓ MuJoCo接收与ZMQ发送数量相等 ↓ MuJoCo应用与接收数量相等真正发生数量变化的最早边界是Axis内部姿态索引 ↓ 普通BVH/MocapApi Avatar事件进入我们程序之后数量都能对上。所以问题不在GMR protobuf ZMQ FIFO MuJoCo 日志 线程 本机TCP而是在Axis普通BVH输出策略 或 MocapApi AvatarUpdated事件生成机制 或 posture_index本身的语义二十二、为什么不能简单说“Axis丢帧”虽然我们定位到了Axis/MocapApi边界但仍然要严谨。posture_index可能表示Axis内部每次姿态解算的编号而BVH接口可能只保证按另一个频率发布Avatar事件如果协议本来就规定内部96 Hz、对外64 Hz那么索引跳号不是网络丢帧而是主动降采样。所以现在最准确的说法不是Axis丢了三分之一的数据。而是Axis设备侧姿态索引按约96 Hz增长但普通BVH/MocapApi只产生约6264个可读取的Avatar事件每秒。现有程序完整处理了所有实际交付的事件尚需厂商协议或官方说明确认这是广播降采样还是posture_index与BVH帧率本来就采用不同定义。二十三、整个排查过程形成的方法论我们最终形成了一套可以复用的排查体系。第一层先明确现象不要只说“感觉掉帧” 必须明确 哪个编号跳了 每秒收到多少 在哪一层看到的第二层每层设置计数器输入多少 处理多少 生成多少 发送多少 接收多少 应用多少第三层通过等式定位边界AB说明A到B没丢 BC说明问题在B到C第四层观察队列pending持续增长 → 后面处理不过来 pending始终很小 → 上游本来就没给那么多第五层测量耗时不能猜“读取可能慢” 要测出avg_avatar_read_ms第六层一次只改变一个变量我们依次单独测试关闭逐帧日志 关闭GenLock 拆分线程 更改MocapApi轮询 切换7005 恢复7003每次只改一项才能知道结果由什么造成。第七层危险优化必须能回退MocapApi优化导致崩溃和无数据后立即回退到稳定实现。判断标准不是“代码更漂亮”而是机器人是否跟随 计数是否对齐 测试是否通过 是否出现崩溃第八层使用固定时长复测通过10秒、20秒等固定测试避免不同测试时长无法比较。第九层区分“没产生”和“传输丢失”这是这次最重要的认识Axis没有通过接口交付 ≠ ZMQ传输时丢失程序只能保证拿到的全部处理 生成的全部发送 发送的全部接收 接收的全部应用不能保证获得上游未通过接口公开的数据。二十四、最终形成的完整问题框架mermaid flowchart TD A[发现现象br/设备显示96 Hz但程序约63 Hz] -- B[给每层增加计数] B -- C[AuditCorebr/输入/GMR/生成/发送] B -- D[AuditViewerbr/接收/应用/gaps/invalid] C -- E[检查GMR与FIFO] D -- F[检查ZMQ与MuJoCo] E -- G[source_received等于gmr_processed] F -- H[sent等于received等于applied] G -- I[排除GMR和队列] H -- J[排除ZMQ和MuJoCo] I -- K[测量MocapApi读取耗时] K -- L[约0.05 ms排除读取性能] L -- M[关闭日志、关闭GenLock] M -- N[频率仍约63 Hz] N -- O[测试高级BVH 7005] O -- P[TCP可连但无Avatar事件] P -- Q[恢复普通BVH 7003] Q -- R[检查Axis设备和工作模式] R -- S[96为只读设备频率无广播频率选项] S -- T[固定时长数学复核] T -- U[问题边界定位到Axis普通BVH/MocapApi] 二十五、一句话总结整个思路我们不是看到NoitomJump就直接说Axis丢帧而是先给Axis输入、GMR、动作生成、ZMQ发送、ZMQ接收和MuJoCo应用分别计数再通过FIFO积压、frame_id、CRC、读取耗时和固定时长测试逐层排除GMR性能、程序限频、日志开销、线程阻塞、ZMQ丢帧、MuJoCo丢帧、GenLock和端口配置最终发现数量差异在数据进入程序之前就已存在从而把问题边界定位到Axis普通BVH/MocapApi输出机制并保留“主动降采样或索引语义不同”两种严谨解释。最终方法可以浓缩为先分层 → 再计数 → 找第一个数量发生变化的位置 → 测性能 → 看队列 → 做单变量实验 → 用固定时长复测 → 区分上游未输出与下游传输丢失 → 最后才下结论

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价