“三层同步审计”判定掉帧缺失

建立一个"Axis 原始输出---MocapApi---我们的程序"三层同步审计。只有这样才能判定缺失到底发生在哪一层。

基于目前证据,我的判断是:

  • "当前程序收到的每一个 SDK Avatar 事件都没有再丢":证据充分。
  • "MocapApi 是否漏交事件":尚未证明,也没有排除。
  • "Axis 是否本来就没有发出该姿态":尚未证明。
  • "UDP/WiFi 是否丢了报文":尚未证明。
  • "多个 SDK 事件是否最终读取到同一个最新 Avatar 状态":这是目前最值得怀疑、但还没有彻底验证的隐患。
  • 使用 r70、非 Cache、UDP、普通 Windows、WiFi 时,无法给出数学意义上的"永不丢一帧"保证。
  • 工程上可以通过正确调用、原始流旁路、TCP、冗余和缺帧显式处理,把风险降到很低,但必须先定位。

一、第一性原理:一帧到底经过了什么

实际链路不是简单的"MocapApi → 程序":

复制代码
```mermaid
flowchart LR
    A["人体动作"] --> B["无线惯性传感器"]
    B --> C["接收器 / Transceiver"]
    C --> D["Axis Studio融合与骨架求解"]
    D --> E["Axis生成第 N 帧姿态"]
    E --> F["Axis BVH UDP/TCP广播"]
    F --> G["Windows网络栈"]
    G --> H["MocapApi内部接收与解析"]
    H --> I["MocapApi事件队列或最新状态"]
    I --> J["PollApplicationNextEvent"]
    J --> K["通过Avatar句柄读取关节"]
    K --> L["深拷贝NoitomFrame"]
    L --> M["采集FIFO"]
    M --> N["后续处理"]
```

诺亦腾官方说明也明确:MocapApi 不直接与传感器通信,它接收 Axis Studio 输出的 UDP/TCP socket 流;传感器融合和骨架求解在 Axis 中完成。Noitom 官方 GitHub

因此,观察到:

复制代码
100 → 101 → 103

只能说明应用最终没看到 102,不能立刻断定是哪一层丢的。至少有以下可能:

  1. Axis 根本没有生成姿态 102。
  2. Axis 生成了,但没有广播。
  3. UDP 数据报在系统或网络层丢失。
  4. MocapApi 收到数据报,但解析失败。
  5. MocapApi 内部只保留最新状态,102 被 103 覆盖。
  6. 事件存在,但我们没有正确取完。
  7. 我们取到了两个事件,但两个事件的 Avatar 句柄都读取成最新的 103。
  8. 我们读取到了 102,后面的 FIFO/GMR 丢掉了。

现在的六层计数相等已经基本排除了第 8 种,但没有排除第 1~7 种。


二、技术人员的判断哪里是对的

1. 旧代码的参数确实调用错过

PollApplicationNextEvent 的第二个参数是事件数组容量/数量,不是字节数。

旧代码如果是:

复制代码
MCPEvent_t event;
uint32_t count = sizeof(MCPEvent_t);
PollApplicationNextEvent(&event, &count, app);

就相当于:

实际只准备了一个房间,却告诉 SDK 后面准备了几十个房间。

这可能造成越界写、事件破坏或未定义行为。这个错误必须修复,且已经不是争议点。

2. 非 Cache 模式必须正确处理批量事件

诺亦腾官方文档明确要求:

  • pEvent 中每一个 MCPEvent_t.size 都必须初始化;
  • 非 Cache 模式先查询所需事件数量;
  • 分配对应数量的事件数组;
  • 再次调用取出事件;
  • 缓冲区不足时重新取。Noitom MocapApi 官方说明,第 4--5 页

当前源代码已经实现了官方"两段式"分支:

  • 查询 required_count
  • 创建对应数组
  • 初始化每个 event.size
  • 批量读取
  • 处理 Error_InsufficientBuffer
  • 将一批中的 Avatar 事件逐个深拷贝

对应位置是 noitom_client.cpp (line 393)(/C:/Users/RX01296/Desktop/全身控制8_3/gmr-master_PN_xsenscpp最终版/gmr-master_PN/cpp_noitom_g1/src/noitom_client.cpp:393)。

3. 但实际运行默认走的是预分配模式

这里必须质疑当前运行路径。

当前主程序和 bridge 都把:

复制代码
use_preallocated_poll = true;

设为了真。因此实际运行默认走 256 个事件的预分配批量读取,而不是官方文档展示的"查询数量---按数量读取"分支。

预分配模式不是必然错误,因为代码:

  • 初始化了每个事件的 size
  • 提供了真实数组容量;
  • 遇到 Error_InsufficientBuffer 会扩大数组;
  • 没有再把字节数当事件数。

但它不是官方文档示例的原样实现。因此,"调用方式已经百分之百被官方背书"这个说法不成立。它仍然需要与官方两段式方法严格 A/B。


三、当前代码中最危险的隐藏问题:事件不一定等于历史帧快照

官方对事件和句柄的描述非常关键:

  • 事件携带被更新对象的 handle;
  • handle 是 MocapApi 内部对象的索引;
  • 真正的数据需要通过接口和 handle 再去读取。Noitom 官方对象模型

也就是说,事件可能只是:

复制代码
"Avatar对象更新了"
+ Avatar句柄

而不一定包含该时刻完整、不可变的骨架快照。

假设 SDK 内部这样工作:

复制代码
事件1:AvatarHandle=5,实际对应帧100
事件2:AvatarHandle=5,实际对应帧101
事件3:AvatarHandle=5,实际对应帧102

AvatarHandle=5背后的当前状态已经更新到102

应用批量拿到三个事件后依次调用:

复制代码
readAvatarFrame(handle=5);
readAvatarFrame(handle=5);
readAvatarFrame(handle=5);

可能读到:

复制代码
102、102、102

而不是:

复制代码
100、101、102

这种情况下:

复制代码
sdk_avatar_events = 3
frames_deep_copied = 3

依然完全相等,但前两帧已经在 SDK 对象模型内部被"最新状态覆盖"。

这正是以下统计的意义:

复制代码
multi_avatar_batches
batch_duplicate_posture_reads
duplicate_indices

如果出现:

复制代码
multi_avatar_batches > 0
batch_duplicate_posture_reads > 0

那么问题极可能不是我们的 FIFO,而是:

非 Cache 模式的一批 AvatarUpdated 事件并不保存各自独立历史姿态,多个事件读取了同一个最新对象状态。

这个问题不能靠"深拷贝再快一点"解决,因为深拷贝开始前,旧姿态可能已经消失。

需要诺亦腾明确回答:

  1. 一个 AvatarUpdated 事件是否绑定不可变的历史姿态?
  2. 还是只通知某个 Avatar 对象已经更新?
  3. 多个事件带相同 Avatar handle 时,getter 如何定位各自帧?
  4. 非 Cache 模式内部保存多少帧?
  5. 队列满时丢最旧还是丢最新?
  6. GetAvatarPostureIndex() 返回事件对应索引,还是对象当前最新索引?

官方公开文档没有充分说明这几个关键语义。


四、关于 Cache 模式

r70 头文件包含:

复制代码
EnableApplicationCacheEvents
DisableApplicationCacheEvents
ApplicationCacheEventsIsEnabled

但实际 DLL 返回 Error_NotSupported,所以"头文件声明了"不等于"r70 DLL 实现了"。

如果新版 SDK 真正支持 Cache,理论上它可能让 SDK 为事件保留对应的历史数据,从而避免同一 Avatar 对象不断覆盖。但这必须通过下面的测试证明:

复制代码
同一批多个Avatar事件
→ 每次读取GetAvatarPostureIndex
→ 是否得到连续且不同的索引

不能仅凭 EnableApplicationCacheEvents() 返回成功就认为不会覆盖。

另外,当前代码还有一个潜在缺陷:cache_events_enabled_ 的赋值依赖 log_connection_info 条件。如果未来新版 SDK 成功启用 Cache,但关闭连接日志,程序可能仍按非 Cache 分支运行。r70 因为明确不支持 Cache,当前测试暂时不受这个缺陷影响,但升级 SDK 前必须修正。


五、20 次结果重新解释

已有结果:

复制代码
总理论帧数:63040
总缺失:24
帧缺失率:0.0381%
完整运行:6/20
出现缺失:14/20

如果粗略假定每帧独立,单轮 3152 帧保持完整的概率约为:

复制代码
(1 - 0.000381)^3152 ≈ 30.1%

这几乎正好对应实际的 6/20 = 30%

这意味着该缺失率对你这种约一分钟的运行长度来说,不是"偶尔极端异常",而是足以导致大多数运行至少缺一次。

换算:

复制代码
平均约每2627帧缺1帧
50 Hz下约每52.5秒缺1帧
10分钟期望约11.4帧
1小时期望约68.6帧

因此当前版本不适合宣称"逐帧可靠"。

但六层计数相等证明的是:

SDK 已经交给程序的 Avatar 事件,都成功进入了深拷贝、管道及后续处理。

它不能证明:

Axis 生成的每一帧都被 MocapApi 交了出来。


六、能否实现真正的零丢帧

需要区分三个目标。

目标 A:测试中观察不到缺帧

有较高概率实现。通过正确调用、换 TCP、降低争用、升级 SDK、增加冗余,可能做到数小时甚至数十小时没有观察到缺失。

目标 B:将缺失率降到工程上足够低

可以实现,但需要定义数字,例如:

复制代码
连续运行8小时
总姿态帧数约1,440,000
缺失0
重复0
倒序0
端到端最大延迟小于规定值

如果观察到 0 次失败,根据常用的"rule of three",95% 置信上界大约为:

复制代码
p < 3 / N

例如想证明单帧缺失概率低于 10^-6,至少需要约 300 万帧零缺失,即 50 Hz 下约 16.7 小时。实际还应做多天、不同干扰条件测试。

目标 C:数学意义上永不缺帧

在以下条件下无法保证:

复制代码
WiFi
+ UDP
+ 普通Windows调度
+ 闭源MocapApi
+ 无发送端重传
+ 无独立冗余链路

UDP 标准明确不保证交付,也不保证重复保护;需要可靠有序交付时应使用 TCP。RFC 768

而且即使改成 TCP,也只能保证 Axis 到 MocapApi 的字节流可靠、有序;不能保证:

  • 传感器到 Axis 没丢;
  • Axis 每次都生成姿态;
  • Axis 内部没有覆盖;
  • 程序永远不宕机;
  • 延迟永远小于机器人期限。

TCP通过序列号、检测和重传提供可靠有序字节流,但丢包时可能等待重传,形成延迟尖峰。RFC 9293

所以真正可以承诺的应是:

在给定硬件、网络、负载和持续时间下,达到明确的缺帧率与最大延迟指标。

而不是无限时间的绝对零丢帧。


七、所有主要解决方案及对抗性评价

方案 能解决什么 主要质疑 优先级
官方非 Cache 两段式 Poll 排除事件数组容量、初始化和取批错误 仍可能只读到最新 Avatar 状态 最高
预分配大事件数组 减少动态分配和 count/fetch 竞态 不完全等同官方示例;仍有对象覆盖风险 高,需 A/B
收到一批后立即返回 减少持续空 Poll 对 SDK 线程的竞争 可能不能及时排空其他事件 高,需 A/B
持续 Poll 到空 尽快排空 SDK 队列 高频空 Poll、最高优先级线程可能抢占 SDK 内部接收线程 已证明当前配置不足
RegisterEventHandler 回调 避免用户态疯狂轮询 回调可能就在 SDK 接收线程;在回调中读取全部关节可能反而阻塞 SDK 仅实验
SDK Cache 模式 理论上保存事件历史 r70 不支持;新版具体语义未知 升级后重点测试
升级 MocapApi/Axis 可能获得 Cache、bug 修复 ABI、姿态格式、许可证和兼容性风险
TCP Axis→客户端可靠有序,避免 UDP 永久丢包 丢包时增加延迟;不能修复 Axis 上游缺失 很高
原始 BVH UDP 旁路解析 绕过 MocapApi,直接掌握每个数据报 需要正确解析协议;UDP仍可能丢 诊断价值最高
Wireshark/Npcap原始抓包 判定数据报是否到达操作系统 抓到包不等于 MocapApi成功消费;抓包自身也可能丢 必须做
Axis 本地录制作为真值 判断 Axis 是否生成完整姿态 录制路径和广播路径可能不是同一内部队列 必须同时做
双 UDP 端口合并 一路缺失时用另一路补齐 如果两个端口共享 Axis 同一发送循环,缺失会高度相关 条件性
UDP+TCP 双路合并 两种传输机制更独立 实现复杂,时间戳/帧号匹配必须可靠 很有潜力
两个独立无线/采集系统 真正增加上游独立性 成本高、两套姿态需要对齐 高可靠场景
WiFi 7 MLO/FRER 多链路复制、去重,降低单链路丢失 需要端到端设备支持;相关故障仍存在;不是现有 Axis 一键功能 中长期
扩大 socket 接收缓冲 缓解应用调度不及时造成的内核丢包 MocapApi未公开 socket 句柄,可能无法直接设置 需供应商支持
CPU affinity/合理优先级 减少采集线程被其他任务打断 最高优先级忙轮询可能饿死 SDK 内部线程 必须 A/B
不在热路径打印/分配 减小消费时延 当前 FIFO 深度只有2,说明它可能不是主因 保留
缺帧时 hold/rebuffer 防止机器人时间轴持续错位 不能恢复真实丢失姿态;会有短暂停顿 必须作为安全兜底
插值 补齐时间长度 用户目前不希望插值,而且不是真实原始帧 暂不采用
前向纠错/FEC 无重传恢复部分丢包 必须发送端配合,Axis现有协议大概率不支持 供应商级改造

2025 年公开研究表明,把 WiFi 7 多链路和 IEEE 802.1CB FRER 结合,能够提高交付率并约束延迟,但代价是信道效率下降;它依然是概率性可靠性提升,不是无限条件下的绝对零丢失。Ergenç 等,Computer Communications 2026 / 2025 在线发表

工业网络中成熟的思路也是"复制---独立路径传输---按序号消重",例如 IEEE 802.1CB FRERRFC 7198 RTP 双流。但它们有效的核心前提是路径故障尽量不相关。两个 Axis 端口如果共享同一数据源和发送线程,就可能只是"同一份缺失复制两遍"。


八、我建议的决定性实验

不要先继续做 20 次单一程序测试。下一步必须同时取得三份证据:

复制代码
```mermaid
flowchart TB
    A["Axis生成/本地录制"] --> B["原始UDP/TCP数据"]
    B --> C["Wireshark/Npcap抓包"]
    B --> D["MocapApi"]
    D --> E["Avatar事件及posture_index"]
    E --> F["NoitomFrame FIFO"]

    A -.比较帧号和时间.-> C
    C -.比较报文序号.-> E
    E -.比较深拷贝序号.-> F
```

实验 1:建立 Axis 真值

同时执行:

  1. Axis 本地录制动作。
  2. 开启 BVH 广播。
  3. Wireshark/Npcap 抓取目标端口。
  4. MocapApi 审计程序记录:
    • event.fTimestamp
    • posture_index
    • PTP time
    • 每批事件数量
    • 每批 Avatar 事件数
    • 每批读取到的所有 posture index
    • 缺失、重复、倒序的完整列表。

如果 Axis 记录本身缺号,问题在传感器/Axis,不在 SDK 调用。

如果 Axis 完整、原始 UDP 缺号,问题在 Axis 广播、网络栈或 WiFi。

如果原始 UDP 完整、MocapApi 缺号,才能确认 MocapApi/调用方式有问题。

实验 2:专门验证"最新状态覆盖"

每次批量取到事件后记录:

复制代码
batch_event_count
avatar_event_count
每个事件的avatar_handle
每次getter返回的posture_index
每次getter返回的PTP time

关键判据:

复制代码
一批有3个Avatar事件
读取结果却是 [102,102,102]

一旦发生,就证明"多事件句柄读取成最新状态"。这种情况不能靠下游 FIFO 修复。

实验 3:四组随机 A/B

只修改一个变量,运行顺序随机化:

  1. 官方两段式 + 读完一批即返回。
  2. 预分配批量 + 读完一批即返回。
  3. 官方两段式 + 持续排空到空。
  4. 预分配批量 + 持续排空到空。

每组至少 20 次。不要同时修改线程优先级、日志和传输模式,否则无法归因。

实验 4:线程争用 A/B

比较:

复制代码
Normal
Above Normal
Highest

我特别质疑 Highest + yield + 高频空Poll

yield() 只表示当前线程让出一次执行机会,不保证 SDK 内部较低优先级线程立即获得 CPU。如果采集线程优先级高于 MocapApi 内部接收线程,它可能反过来降低可靠性。

重点关联:

复制代码
缺帧发生前后的poll_calls
empty_polls
max_poll_block_ms
CPU占用
SDK内部接收线程调度

实验 5:UDP 与 TCP

使用相同动作、相同帧率、相同程序分别测试 UDP 和 TCP。

判定:

  • TCP 完整、UDP 缺失:网络/UDP接收路径是主要原因。
  • TCP 与 UDP 缺相同索引:Axis 上游或共同 SDK 层更可疑。
  • 原始流完整但两者在 MocapApi 后缺失:SDK内部交付最可疑。

实验 6:双路缺失集合相关性

记录:

复制代码
missing_A
missing_B
intersection
union

真正有价值的不是"两路总共丢多少",而是:

复制代码
P(B也丢 | A已丢)

如果 A 丢失时 B 几乎总是也丢,双路没有意义。如果交集接近零,按 posture_index 合并很有价值。


九、对现有工程的最终审查意见

已经做对的

  • 采集线程和后续处理分离。
  • FIFO 保存独立 NoitomFrame
  • Avatar 数据立即深拷贝。
  • 非 Avatar 事件不再额外等待。
  • 无事件使用 yield()
  • 事件数组不再把字节数当容量。
  • 初始化每个 MCPEvent_t.size
  • 有完整的全链路计数。
  • 有姿态复制前后 index 检查。
  • 下游没有证据显示继续丢帧。

仍然没有证明的

  • Axis 是否生成每个缺失 index。
  • 原始 socket 流是否包含每个缺失 index。
  • AvatarUpdated 是否对应不可变历史帧。
  • r70 非 Cache 模式是否只保存最新 Avatar 状态。
  • 持续排空是否与 SDK 内部线程争用。
  • 预分配模式和官方两段式哪一个在 r70 上更可靠。
  • 回调方式是否优于轮询。
  • SDK 更新是否能真正启用 Cache 并保存独立姿态。

当前最可能的四类原因排序

在没有原始流证据前,我给出的暂定排序是:

  1. MocapApi 非 Cache 对象覆盖或内部事件交付机制:较高可疑。
  2. UDP/WiFi/Windows接收路径偶发丢包:较高可疑。
  3. 高优先级持续空 Poll 与 SDK 内部线程竞争:中高可疑。
  4. Axis 源头本身没有产生/广播某些姿态:中等可疑。

目前没有足够依据把任何一项定为唯一原因。


最终建议

最值得做的不是再改 FIFO,而是按下面顺序推进:

  1. 先完成"Axis录制 + 原始数据抓包 + MocapApi日志"的同步三层审计。
  2. 默认运行路径临时切到完全遵循官方说明的两段式 Poll,且每批深拷贝后立即返回,不强制持续 Poll 到空。
  3. 保留当前预分配版本作为唯一变量进行严格 A/B。
  4. multi_avatar_batchesbatch_duplicate_posture_reads 做逐批详细记录。
  5. 测试 TCP,与 UDP 对照。
  6. 向诺亦腾索要事件/handle/Cache 的精确生命周期契约。
  7. 若确认 MocapApi 内部覆盖,优先尝试新版 SDK Cache;否则考虑直接解析原始 BVH 流。
  8. 真正上机器人前,即使采集达到长期零缺失,也必须加入"发现帧号跳变后暂停推进、重新积累 buffer、重新同步"的安全策略,避免机器人静默累积时间轴误差。

核心判断可以浓缩为一句话:

MocapApi 调用方式曾经确实有问题,当前已经修复了明显的容量错误,但还没有证明 r70 非 Cache 模式能够逐事件保存历史姿态;在查清"原始数据是否存在"和"同 handle 是否只读最新状态"之前,继续优化 poll 频率无法证明能解决零缺帧。

相关推荐
十月的皮皮1 小时前
STM32从零到量产开发:四路继电器工业控制模块开发 -上位机主窗口设计说明
c语言·stm32·单片机·stm32cubemx
专注数据的痴汉1 小时前
「数据下载」武汉统计年鉴(2009-2025)
大数据·人工智能·信息可视化
fanged2 小时前
设备树学习5--读写实操(TODO)
学习
wuyk5552 小时前
第1章:无刷电机核心原理与运行机制
c语言·开发语言·stm32·单片机·嵌入式硬件·机器学习
独隅2 小时前
KMP 全栈开发:用 Koog 框架构建原生 AI Agent
人工智能
SomeB1oody2 小时前
【RustyML入门】2.10. 主成分分析
开发语言·后端·机器学习·rust·教程
APTShark2 小时前
Harness 约束是长出来的,不是写出来的 —— 二阶抽象与驾驭
人工智能
莫凡的博客2 小时前
火山引擎-基础入门阶段细化实操案例
人工智能·php·火山引擎
秦先生在广东2 小时前
Google/skills:Google 官方将工程实践「技能化」,以 Agent Skills 开放标准接入 AI 编程生态
人工智能