把不同频率的数据流写进同一个 MCAP

相机以 30 Hz 采图,机器人以 100 Hz 返回状态,控制器以 50 Hz 下发指令——这三条数据流应该怎样写进同一个文件?

直觉上,人们常想先把它们拼成一帧,再按某个统一频率保存。这样做会带来等待、补帧和降采样,也会模糊数据真正发生的时间。MCAP 提供了更自然的模型:每条数据流保持自己的节拍,每条消息携带自己的时间戳,在同一容器中独立记录。

本文给出这套录制方案的核心设计。

一份文件,四种频率

讨论“采集频率”前,先明确它指的是哪一层:

频率 含义 决定因素
设备输出频率 相机产生图像、机器人上报状态的节拍 设备、驱动与配置
应用读取频率 应用从接收缓存中选取数据的节拍 采集线程
控制提交频率 控制线程向设备接口提交命令的节拍 控制循环与提交事件
实际记录频率 MCAP 中每秒保存的有效消息数 有效样本、负载与运行状态

假设相机输出 60 FPS,而应用以 30 Hz 读取,稳定运行时每秒会记录约 30 张图像。机器人状态同理:设备可以更新得更快,录制线程只在 100 Hz 的采样点选取最新的有效快照。应用轮询快于设备时,则以版本号判断是否出现了新样本。

因此,配置频率描述调度目标;消息数量和时间戳描述实际结果。

State、目标与 action

机器人链路中有几个相似但用途不同的量:

  • 主端 state:操作者输入设备的实际状态,是遥操作映射的输入。
  • 候选目标:主端 state 经过偏置、坐标或关节映射及合法性检查后得到的从端目标。
  • 从端 state:执行设备返回的实际状态,是数据集中的观测。
  • action:控制线程完成步进限制和最终检查后,提交给设备接口的命令。

action 的记录点放在设备接口的提交处。这样,数据集保存的是控制器真正尝试执行的命令;提交结果、接口裁剪和错误状态随辅助信息一同记录。候选目标则留在控制链路内部,用来表达操作者当前的最新意图。

数据如何穿过系统

接收、控制和录制各自承担一件事:接收层发布不可变快照,控制线程只读取最新状态,录制线程按事件保存完整历史。

flowchart LR
    master[主端接收] --> masterState[主端状态快照]
    masterState --> mapping[读取与映射]
    mapping --> target[最新候选目标]

    slave[从端接收] --> slaveState[有效状态快照]
    target --> control[控制线程]
    slaveState --> control
    control --> submit[最终步进与命令提交]

    slaveState --> stateQueue[(state 队列)]
    submit --> actionQueue[(action 事件队列)]
    camera[各路相机] --> encode[读取与编码]
    encode --> imageQueue[(图像队列)]

    stateQueue --> writer[单一 MCAP writer]
    actionQueue --> writer
    imageQueue --> writer
    writer --> mcap[(MCAP 文件)]

控制线程通过“最新值”缓存获得当下意图;录制线程通过事件队列保留每一次有效样本和命令提交。两种数据结构分别服务于实时性与完整性。

主端映射、从端采样和控制循环可以设置为不同频率。图像编码和磁盘写入始终留在控制线程之外,让控制周期的延迟只取决于控制计算与设备接口。

MCAP 中的多频率时间线

MCAP 本质上是一本按时间索引的事件日志。每类数据使用独立 topic,各自保存消息,数量可以自然不同。

数据流 Topic 示例 触发条件 典型节拍
各路图像 observations/images/<camera> 对应相机读取成功 每路约 30 Hz
从端状态 observations/qpos 快照完整、有效且已更新 目标 100 Hz
action action 控制线程实际提交命令 跟随实际提交频率
辅助信息 对应 metadata topic 与主消息同时生成 跟随对应消息

下面用 70 ms 的理想窗口展示三条数据流如何交错进入同一文件:

sequenceDiagram
    participant I as 图像 · 30 Hz
    participant S as state · 100 Hz
    participant A as action · 50 Hz
    participant M as MCAP
    I->>M: 0 ms · frame 0
    S->>M: 0 ms · sample 0
    A->>M: 0 ms · submit 0
    S->>M: 10 ms · sample 1
    S->>M: 20 ms · sample 2
    A->>M: 20 ms · submit 1
    S->>M: 30 ms · sample 3
    I->>M: 33 ms · frame 1
    S->>M: 40 ms · sample 4
    A->>M: 40 ms · submit 2
    S->>M: 50 ms · sample 5
    S->>M: 60 ms · sample 6
    A->>M: 60 ms · submit 3
    I->>M: 67 ms · frame 2
    S->>M: 70 ms · sample 7

按这一组理想频率计算,一秒会产生约 90 条三路图像、100 条 state 和 50 条 action。实际间隔通常会围绕目标节拍波动,MCAP 直接保存这些真实时间。writer 的物理写入顺序只反映消费队列的先后,下游分析以消息时间戳重建时间关系。

时间戳:为每个字段定义语义

所有 topic 都映射到可比较的主机 Unix 时间;设备原始时钟同时保留,便于追踪和校准。

数据 publish_time log_time 辅助时间信息
图像 设备帧时间映射到主机时钟后的值 应用取得帧的主机时间 原始设备帧时间、单位与时钟域
state 快照中最新分量的接收时间 应用完成快照组装的主机时间 各分量的接收时间与版本
action 命令提交开始时间 提交结束或失败时间 命令序号、来源状态与提交结果

图像消息的 header.stamp 复用 publish_time。单调时钟继续负责线程调度和耗时统计;Unix 时间负责跨 topic 比较。两套时钟各司其职,设备时间经过单位与时钟域映射后再参与对齐。

统一时间基准让消息可以比较,硬件同步则决定采样瞬间能否严格一致。需要分析图像与 state 的关系时,优先选择具有明确采样语义的 publish_time。

有效快照与 action 事件

state 往往由多个分量拼成。一次有效快照需要同时满足完整性、新鲜度和最大时间跨度要求,并携带各分量的时间与版本。100 Hz 是尝试采样的节拍;遇到未更新、过期或缺失的分量时,本轮跳过并计数。

action 则由提交事件驱动。控制线程先读取最新候选目标与有效 state,再执行步进限制和安全检查,最后在调用设备接口时生成事件。同值命令只要实际提交就会留下记录;一个周期包含多个子命令时,事件中分别标明完整提交、部分提交或失败。

这两个规则让数据含义保持清晰:state 描述设备反馈,action 描述控制器提交。

队列、背压与一次完整录制

每个数据源使用独立的有界队列,由单一 writer 公平消费。采集与控制线程采用非阻塞入队,队列满、读取超时、错过调度时隙、重复样本和状态过期分别计数。相机从 60 FPS 正常抽样到 30 Hz,也会与队列溢出造成的丢弃分开统计。

一轮录制生成 <UUID>.mcap 和同名 JSON:

  • 启动时确认所需数据源就绪,再建立录制边界;
  • 停止时先结束生产,再排空队列并关闭文件;
  • JSON 分别汇总各 topic 的首末时间、消息数、实际频率、错误与丢弃计数;
  • 文件完整性、数据语义版本和异常结束状态一并写入元数据。

图像模式仍可单独运行。新增 state 与 action 后,它们只是同一录制容器中的两条新数据流,不会改变相机链路的工作方式。

先忠实记录,再按任务对齐

录制阶段的目标是保留原始有效样本、真实提交事件和可信时间。固定频率的训练样本放到后处理阶段生成:先选择基准时间轴,再定义匹配容差、缺失值处理和插值策略。

验证也沿用同样的分层思路。先检查每个 topic 的频率、间隔分布、时间语义与丢弃统计,再评估跨 topic 的时间关系和多路并发负载。state 反馈频率、action 提交频率与机械臂的运动跟踪性能由不同指标描述。

这套方法把录制从“拼帧”改成“记事件”:在线路径保留每条数据流的真实节拍,离线阶段再根据任务构建统一样本。频率可以不同,但每条消息的来源、时间和结果都有迹可循。




Enjoy Reading This Article?

Here are some more articles you might like to read next: