
一、首先明确“丢帧”发生在哪一层最开始不能直接假设问题一定在MocapApi也可能发生在Axis没有生成帧。Axis生成了帧但UDP没有送达。MocapApi收到数据但事件队列发生覆盖或合并。程序轮询不及时。深拷贝时SDK内部对象已经更新。FIFO覆盖。GMR消费不足。机器人通信或机器人buffer丢帧。因此首先增加三层计数SDK Avatar事件数 ↓ 深拷贝NoitomFrame数 ↓ 程序/FIFO输出帧数判断规则是第一层已经少问题发生在MocapApi之前或MocapApi内部。第一层正常、第二层少深拷贝或SDK对象生命周期有问题。第二层正常、第三层少程序FIFO或消费逻辑有问题。三层相等但帧号缺失程序没有主动丢帧缺口进入程序前就已经存在。实验结果长期表现为sdk_avatar_events frames_deep_copied client_frames_returned FIFO pushed FIFO popped但是posture_index仍偶尔跳跃。这说明原程序内部的FIFO和深拷贝没有继续丢帧缺帧已经发生在程序看到Avatar事件之前。二、先修复MocapApi调用热路径1. 无事件时从sleep改为yield原逻辑if (!frame) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); }修改为if (!frame) { std::this_thread::yield(); }依据采集线程已经与GMR分离。Windows的sleep_for(1ms)可能实际暂停超过1毫秒。50 Hz虽然每帧约20 ms但SDK事件可能集中进入队列。固定等待会扩大连续事件的轮询间隔。yield()允许其他可运行线程执行但采集线程能够更快重新获得CPU。这项修改降低了程序主动制造轮询空窗的概率。2. 非Avatar事件立即继续poll原来的隐患是读到Notify或Sensor事件 → 返回外层 → 等待 → 再次poll改为for (;;) { poll SDK事件; if (没有事件) 返回空; if (系统错误) 报错; if (不是AvatarUpdated) continue; 立即深拷贝Avatar姿态; return frame; }依据GMR只需要Avatar姿态。Notify、SensorUpdated等事件不能阻塞后续Avatar事件。SDK返回Error_MoreEvent说明队列里可能还有事件应尽快排空。非Avatar事件不能成为人为的采集节流点。3. 使用预分配事件轮询加入event_deliverypreallocated_poll目的避免热路径频繁分配和释放事件对象。减少堆分配、锁竞争和不可预测停顿。让采集线程尽可能只做poll、检查、深拷贝和入队。4. 每个Avatar事件立即深拷贝每次取得AvatarUpdated后立即将SDK内部姿态复制成独立的NoitomFrame。增加了posture_changes_during_copy用于检测深拷贝期间SDK对象是否已经被下一帧覆盖。测试结果posture_changes_during_copy0说明已复制帧是独立稳定的不存在多个FIFO元素共同引用SDK最新姿态的问题。5. 采集与GMR彻底分离结构保持为MocapApi专用采集线程 ↓ 独立NoitomFrame ↓ FIFO ↓ GMR线程后来进一步把MocapApi采集放入独立进程通过管道传给主程序MocapApi采集进程 ↓ 进程内采集FIFO ↓ 命名管道 ↓ 主程序输入FIFO ↓ GMR这样MocapApi轮询不会被GMR计算、输出线程或其他主程序工作抢占。6. 提升采集调度优先级加入process_priority_high1 acquisition_priority_high1目的减少Windows调度延迟。尤其在Axis、GMR和其他程序同时运行时降低采集线程长时间得不到CPU的概率。三、增加完整诊断统计MocapApi路径增加或完善了poll_calls avatar_events empty_polls other_events more_event_returns duplicate_indices backward_indices max_poll_gap_ms max_poll_block_ms missing_posture_indices posture_changes_during_copy三层/管道/FIFO统计包括sdk_avatar_events frames_deep_copied client_frames_returned pipe_frames_written frames_fifo_pushed frames_fifo_popped max_fifo_depth three_layers_equal这些指标的作用不是单纯显示“掉了几帧”而是确定缺帧发生的边界。最终发现SDK事件数 深拷贝数 管道数 主程序收到数同时仍存在随机posture_index缺口。因此可以排除深拷贝覆盖FIFO覆盖管道丢帧GMR消费导致源帧缺失主程序主动只取最新帧剩下最可疑的边界是Axis BVH输出 → MocapApi内部 → AvatarUpdated事件四、方案A尽可能优化MocapApi路径方案A主要包括预分配poll事件。采集线程独立。采集进程隔离。高进程/线程优先级。非Avatar事件立即排空。无事件使用yield()。每个Avatar事件立即深拷贝。FIFO逐帧传递。三层计数和缺帧明细。效果确实比早期版本好多轮测试出现有些轮次零缺帧 有些轮次缺1数帧但仍然不能稳定保证每轮完整。关键证据是MocapApi只返回31483151个Avatar事件 程序就只能深拷贝31483151帧程序无法复制一个SDK没有交付的事件。因此继续优化FIFO或GMR已经没有理论意义。五、方案D双MocapApi接收路径冗余Axis同时向两个端口广播127.0.0.1:7012 127.0.0.1:7013两个独立MocapApi进程同时接收然后按posture_index合并。实验中出现7012缺失[10971,12096,12162] 7013缺失[10870,10904] 共同缺失[] 合并后缺失0这证明两个路径的随机缺口有时互不重合冗余合并能够恢复完整序列。但也出现过两个路径共同缺失[11825] 合并后仍缺失1因此方案D只能降低随机丢帧概率不能消除共同上游缺帧。它还有这些代价两套MocapApi实例。双倍SDK资源。需要按帧号合并、去重、等待。实时延迟和逻辑复杂度增加。两路仍然经过相同的MocapApi实现共因故障没有被消除。所以D适合作为诊断和备选冗余方案不是最终首选。六、方案C绕过MocapApi直接读取Axis二进制BVH UDP这是昨天最关键的改造。Axis配置两个目标127.0.0.1:7012 → MocapApi对照路径 127.0.0.1:7014 → 原始BVH UDP路径通过同一次take008回放同时比较Axis原始UDP数据包 MocapApi Avatar事件实验出现原始UDP3152个数据包内部缺失0 MocapApi3150个Avatar事件缺失2而且缺失帧号会随机变化。这形成了直接证据Axis已经发出了对应帧本机UDP也收到了但MocapApi没有把所有帧作为Avatar事件交付给程序。因此方案C选择完全绕开这个不稳定边界。七、原始BVH协议解析新增了Axis原始BVH数据源[axis_bvh_raw_source.hpp](/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN - 副本/cpp_xsens_g1/include/axis_bvh_raw_source.hpp)[axis_bvh_raw_source.cpp](/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN - 副本/cpp_xsens_g1/src/axis_bvh_raw_source.cpp)解析依据是Axis二进制BVH格式64字节帧头 59个骨骼 每个骨骼6个float 354个数据项 位置XYZ 欧拉角XYZ解析流程检查帧头标志 → 读取FrameIndex → 读取59骨骼局部位移和欧拉角 → XYZ旋转转四元数 → 按BVH父子层级做前向运动学 → 厘米转换为米 → 转换到现有GMR坐标系 → 提取现有23个身体节点 → 构造独立NoitomFrame为保证不会默默解析错误还检查帧头token 协议版本/格式 DataCount354 WithDisp1 WithReference0 数据包长度配置不匹配时应拒绝数据而不是产生一个看似正常但姿态错误的机器人动作。八、为什么可以直接接入现有GMR方案C没有修改GMR算法。它只是将输入源从MocapApi → NoitomFrame替换为Axis二进制BVH → NoitomFrame下游看到的数据类型没有变化NoitomFrame posture_index timestamp 23个body position quaternion因此以下部分保持不变GMR映射算法IK配置Unitree G1模型MuJoCo模型ZMQgRPC控制算法机器人输出数据结构九、数值正确性验证为了避免“虽然不丢帧但姿态解析错了”新增了离线逐帧对比工具[axis_bvh_offline_compare_main.cpp](/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN - 副本/cpp_xsens_g1/src/axis_bvh_offline_compare_main.cpp)[noitom_capture_audit_main.cpp](/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN - 副本/cpp_xsens_g1/src/noitom_capture_audit_main.cpp)增加--dump-wire比较同一帧经过两条路径得到的姿态路径1Axis原始BVH → 自研解析 路径2Axis → MocapApi → SDK姿态3149个共同帧的对比结果最大位置误差约1.32×10⁻⁶ m 最大旋转误差约0.0685°这个误差量级说明骨骼顺序正确。父子层级正确。XYZ旋转顺序正确。坐标轴变换正确。厘米到米转换正确。与原有GMR输入基本等价。十、Solution C正式接入主程序在[xsens_core_main.cpp](/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN - 副本/cpp_xsens_g1/src/xsens_core_main.cpp)中增加--motion-input axis-raw运行方式.\xsens_core.exe --motion-input axis-raw --noitom-ip 127.0.0.1 --noitom-port 7014 --input-fps 50 --output-fps 50 --transport none --disable-ws --disable-console-metrics --auto-start此时MocapApi完全不参与动作帧获取Axis Studio → 原始BVH UDP → AxisRawBvhSource → NoitomFrame → FIFO → GMR十一、UDP接收稳定性改造原始UDP接收路径设置了约16 MB接收缓冲区rcvbuf_requested16777216目的吸收短时间的线程调度抖动。避免用户态暂时未读取时内核UDP队列过小。不覆盖程序FIFO不只保留最新帧。无数据时继续使用std::this_thread::yield();并将WSAETIMEDOUT WSAEWOULDBLOCK WSA_IO_PENDING997都视为暂时没有数据而不是致命错误。这是因为测试中曾出现[Receiver] raw BVH recv failed WSA997导致采集提前停止。修复后997不会再终止数据源。十二、完善唯一帧守恒统计增加source_received source_unique_frames source_missing_indices source_duplicate_indices source_backward_indices gmr_processed motion_generated unique_frames_conserved其中source_unique_frames source_received - 精确重复帧数Axis回放结束时会重复发送一次末帧12518因此标准take008结果为收到UDP包3152 重复末帧1 唯一姿态帧3151 帧号范围936812518数学上12518 - 9368 1 3151重复末帧不是新的动作帧不能再次推入机器人buffer所以按相同posture_index精确去重。最终三次完整测试全部得到source_received3152 source_unique_frames3151 source_missing_indices0 source_duplicate_indices1 source_backward_indices0 first_posture_index9368 last_posture_index12518 gmr_processed3151 motion_generated3151 unique_frames_conserved1 input_pending0 robot_pending0这证明在三轮离线回放中Axis的3151个唯一帧 程序收到的3151个唯一帧 GMR处理的3151帧 生成的3151帧动作当前程序框架mermaid flowchart LR Suit[动捕服传感器] --|Wi-Fi| Axis[Axis Studio] Axis --|二进制 BVH UDPbr/127.0.0.1:7014| Raw[AxisRawBvhSourcebr/专用采集线程] Axis -.-|可选对照br/127.0.0.1:7012| SDK[MocapApi路径br/保留作诊断/回退] Raw -- Validate[协议与长度校验] Validate -- Parse[解析59骨骼br/局部位置 XYZ欧拉角] Parse -- FK[前向运动学br/局部姿态转全局姿态] FK -- Convert[坐标系和单位转换] Convert -- Frame[独立NoitomFramebr/含posture_index] Frame -- Check[缺失/重复/倒序统计] Check -- FIFO[输入FIFO] FIFO -- GMR[GMR线程] GMR -- Motion[50 Hz机器人动作帧] Motion -- Output[MuJoCo或机器人通信] 采集和消费线程逻辑mermaid flowchart TD Start[启动axis-raw数据源] -- Socket[绑定127.0.0.1:7014br/申请16 MB接收缓冲] Socket -- Receive[接收一个UDP数据包] Receive -- Error{接收结果} Error --|暂时无数据br/timeout/would-block/997| Yield[yield()] Yield -- Receive Error --|真实错误| Stop[记录错误并停止] Error --|收到数据| Header{协议头、长度、格式正确} Header --|否| Reject[拒绝异常包并统计] Reject -- Receive Header --|是| Index[读取posture_index] Index -- Sequence{与上一帧比较} Sequence --|index previous| Duplicate[重复帧计数br/不再推给机器人] Duplicate -- Receive Sequence --|index previous 1| Missing[记录所有缺失帧号] Sequence --|index previous| Backward[记录倒序/重启] Sequence --|连续| Decode[解析姿态] Missing -- Decode Backward -- Decode Decode -- DeepCopy[生成独立NoitomFrame] DeepCopy -- Push[FIFO尾部入队] Push -- GMRThread[GMR从FIFO头部逐帧取出] GMRThread -- Count[gmr_processed] Count -- Motion[生成动作帧] Motion -- Receive 缺帧定位逻辑mermaid flowchart TD Gap[发现posture_index缺口] -- RawCheck{Axis原始UDP是否也缺} RawCheck --|原始UDP不缺br/MocapApi缺| SDKProblem[MocapApi事件交付问题] RawCheck --|原始UDP也缺| Upstream{Axis记录/广播中是否存在该帧} Upstream --|Axis没有生成| Wifi[动捕服Wi-Fi、传感器br/或Axis实时解算问题] Upstream --|Axis已生成但UDP未收到| UDP[UDP交付、套接字缓冲br/或本机调度问题] SDKProblem -- SolutionC[使用axis-raw绕过MocapApi] UDP -- Tune[增大接收缓冲br/提高采集优先级br/检查网络] Wifi -- SuitTest[实时穿戴专项测试] 昨日工作的最终结论原来的FIFO覆盖问题已经排除程序不是只保留最新帧。深拷贝问题已经排除每个Avatar事件均形成独立NoitomFrame复制期间姿态没有变化。GMR消费丢帧已经排除完整测试中唯一源帧数、GMR处理数、动作生成数全部相等。MocapApi仍存在随机少交付Avatar事件的现象优化能够降低概率但不能从根本上保证完整。双MocapApi冗余能改善但共同缺失仍可能存在。Solution C通过读取Axis原始二进制BVH消除了MocapApi事件层的不确定性。自研解析结果与SDK数值基本等价位置与旋转误差远低于实际控制敏感范围。三次完整take008测试均实现3151个唯一帧零缺失、零倒序、全部进入GMR。当前尚未证明的是“动捕服通过Wi‑Fi实时采集时上游永不缺帧”。离线回放验证的是Axis到程序和GMR的链路实时穿戴Wi‑Fi链路仍需要3060分钟专项测试。所以昨天真正完成的不是简单地“换了一个UDP输入”而是通过分层计数和双路径对照把缺帧边界定位到了MocapApi事件交付层然后用协议等价、数值验证过的原始BVH输入替换了这个不稳定边界同时保持GMR和机器人控制逻辑不变。