机器人为什么回应晚了?从 Q1 的一次触摸讲时间戳
摸一下机器人的头,等了半天没反应;人已经转身,它才显示一个开心表情。要查这种问题,很容易先去看网络和表情服务。可即使两边都正常,一条在队列里等久了的触摸消息,也足够制造这次迟到的回应。
Q1 SDK 的《接口说明》恰好提到这个细节:触摸消息到了以后,先看时间戳,超过业务允许的期限就丢弃,文档举的例子是500毫秒。
看到这里,问题就变成了三个:500毫秒从哪一刻算?两条触摸顺序颠倒了怎么办?消息到达时还没过期,轮到执行时过期了,又该在哪儿拦住?下面用一组能算清楚的时间,把这几件事拆开。
依据 Q1 SDK v1.0.0.0。文中的触摸场景和事件回放为离线演示。
1. 先把这80毫秒算清楚
功能先按最简单的方式写:订阅 /aima/hal/touch/state,收到单击,调用表情服务。链路很短,看上去没有什么能出问题的地方。
实际多了几段等待。传感器感知触摸,生产端组织消息,通信链路传输,接收程序安排回调。网络很快也没用,回调可能正排在别的工作后面。日志里一句“收到触摸”,把前面这些过程全压成了一个时刻。
假定一次触摸发生在相对时刻 1000 毫秒,消息里的 stamp 也记录了这个时刻。应用在 1080 毫秒收到它,两者相减,消息已经“活了”80 毫秒。如果同样的事件直到 1800 毫秒才到达,年龄就变成800毫秒。消息内容没有变,适合使用它的时机却变了。
程序如果只记接收时刻,就不知道消息此前已经等了多久。多留一个事件时刻,80和800毫秒的差别才会显出来。
上述计算假设 stamp 记录事件时刻,且两端时钟可比较。Q1 的字段注释只写“世界时间戳”,具体在哪一环填入仍需核实:采集时填写,差值才接近事件年龄;发送时填写,就漏掉了此前的处理时间。
2. 为什么500毫秒要由业务来定
按文档的做法,用当前系统时间减去 header.stamp,再与业务阈值比较。计算不难,真正要决定的是:晚到多少,这次回应就失去意义了。
文档里的500毫秒只是一个例子。温度记录晚到800毫秒,往往仍有保存价值;用户摸头后期待一个即时表情,同样的800毫秒可能已影响体验。区别在于,前者记录过去,后者要回应现在。
假如我们的互动选择500毫秒作为窗口,处理过程可以很直观:先计算消息年龄,再判断是否落在窗口内。80毫秒的事件继续进入交互逻辑,800毫秒的事件不再触发表情,但可以留下日志。丢掉的是这次动作触发机会,不一定是这条消息的全部信息。

图 1:新鲜度看一条消息距离现在多久,顺序看多条消息彼此怎样排列。80、800 和 500 毫秒为教学值;时间差计算假设生产端与消费端使用可比较的时钟。
窗口也不能一路往小了调。缩到正常的调度抖动都容不下,用户会觉得机器人经常不理人。比较稳妥的做法,是一起记录消息年龄和实际处理结果:哪些迟到已经影响体验,哪些丢弃反而造成漏回应。500毫秒可以拿来开始观察,后面要用这些记录修正。
SDK 自带的 get_touch_state.py 主要订阅并打印消息,适合确认链路与字段。把它改成动作触发器时,应用就多承担了一项职责:决定哪些事件仍值得转成动作。
3. 都没过期,为什么一条保留、一条丢掉
只有年龄还不够。设两个事件分别发生在1900和1980毫秒,后一个先到,前一个稍后才到。两条消息都没有超过500毫秒,但到达顺序与发生顺序相反。
这时需要先问:应用在处理独立事件,还是维护“最新状态”?
如果是记录两次独立的触摸,两次交互都可能有意义。较早的那次晚到,不代表它自动失效;应用可以保留它,或在一个短窗口内重新排序。如果维护的是当前位置,情况就不同了:已经用较新的位置更新画面,再被旧位置覆盖,画面会倒退。这样的业务通常只允许更新的状态替换当前状态。
把规则用反,结果也很具体:触摸计数器会少记一次,位置显示会往回跳。所以在写“只接受更大的时间戳”之前,先看这个变量保存的到底是一次事件,还是一个当前状态。
为了把区别看清楚,我用一个小脚本回放了六次到达。输入是合成数据,只有一个发布者、一个会话,并假定每个独立事件有唯一的递增序列号。两种应用策略共用500毫秒窗口,但只有“最新状态”策略拒绝倒退的更新。
| 到达记录 | stamp | 接收时刻 | 消息年龄 | 独立触摸事件策略 | 最新状态策略 |
|---|---|---|---|---|---|
| A,序号1 | 1000 | 1080 | 80 | 保留 | 更新 |
| B,序号2 | 1200 | 2000 | 800 | 过期 | 过期 |
| C,序号4 | 1980 | 2040 | 60 | 保留 | 更新 |
| D,序号3 | 1900 | 2080 | 180 | 保留 | 不覆盖 C |
| C 再次到达,序号4 | 1980 | 2100 | 120 | 重复 | 重复 |
| E,序号5 | 2500 | 2200 | −300 | 检查时钟 | 检查时钟 |
单位均为毫秒。表中结果来自本地脚本,不是设备日志;两种策略是教学设计。
D 正好暴露这个分歧:它仍新鲜,所以独立事件策略保留;它比 C 旧,所以状态策略不采用。C 再次到达则是另一回事——同一次触摸若触发两遍回应,用户会看到一次输入对应两次动作,单靠年龄检查拦不住它。

图 2:按合成输入计算的消息年龄。重复的 C 仍落在有效窗口内,E 甚至落在零的左侧;仅凭年龄小,无法判断一条消息就应该执行。
这组回放把判重记录保存在一个有限集合里,便于观察六次到达。持续运行的应用还要按发布者和会话区分事件身份,并清理过期记录,避免状态无限增长。
4. 接收时查过一次,为什么还不够
再把链路往后延长一步。假设触摸发生在 1000 毫秒,回调在 1080 毫秒收到它,检查结果是 80 毫秒,随后把“显示开心表情”放进应用自己的待办队列。应用直到 1600 毫秒才准备发起表情调用,这次互动的年龄已经变成 600 毫秒。
前面的检查没有算错,只是检查得太早。若待办只保存“显示开心”,丢掉了原始事件时间,后面的模块就无法发现它已经超过选定的 500 毫秒窗口。
这时该补的,是待办里少掉的两项信息:原始事件时间,以及它属于哪一次互动。真正发起表情调用前,再查一次年龄和互动编号。人已经开始下一件事,旧回应就可以退出,不必非要补做。
这次检查管到应用发起请求为止。请求交给设备后如果还要排队,是否能撤回、能否设置执行期限,要由服务提供相应能力。
5. 负300毫秒,通常先查时钟
E 的年龄是−300毫秒。按简单的 age < 500 判断,它似乎“非常新鲜”,但这个负值首先提示:两个时间值可能不能直接相减。
时间的基准必须先对齐。墙上时间用于表示现实日期,单调时钟适合测量经过的时长,仿真时间则可能暂停或改变推进速度。即使两个程序在同一台机器上,也可能选用了不同的时钟。跨设备还要考虑同步误差:发送端比接收端快300毫秒,就能制造出一条看似来自未来的消息。
接入时应先对齐字段含义与时钟,再按已知同步误差设置容差。教学脚本把所有负值单独标记,方便先把异常找出来。
MessageHeader 里的 sequence 可以辅助判重,但要连同发布者和会话一起识别,重启后重新建立状态。另一个时间字段 meas_stamp 的注释同时出现“接收”和“测量”两种措辞。接入时应先确认生产端怎样填写,再决定怎样计算时间差。
6. 关节控制不能照搬触摸规则
把视线从触摸输入转到关节控制,时间戳又多了一层约束。
Q1 接口说明要求关节指令的 header.stamp 来自机载系统时钟并严格单调递增,底层总线驱动会丢弃乱序或重复时间戳的数据包。这里比较的是指令序列的时间顺序,不能简单沿用“独立触摸事件都可以保留”的策略。
可以用三个教学时间戳理解:依次发送100、102、101毫秒。第三帧即使刚刚到达,也比已经接收的第二帧更旧;按文档规定,它不满足时序约束。若第三帧仍是102毫秒,则是不允许的重复。把数据包重新送到,并不会使旧指令重新成为合法的新指令。
为什么输入和输出的要求不同?触摸是对外界事件的报告,应用可以决定是否还要回应;关节指令参与安排物理执行,接收方还必须约束它们的顺序。文档建议关节指令以500—1000 Hz下发,即每帧间隔约1—2毫秒;低于100 Hz还可能触发硬件 Watchdog。不能把触摸示例里的500毫秒窗口,当成这种控制链路可接受的停顿。
上层的职责也就清楚了:在发送前判断命令是否仍适合当前状态,底层再检查指令顺序。旧命令即使排得整齐,也应先经过这层时效判断。
7. 排查迟到的回应,从哪儿下手
如果要给开头的触摸功能补日志,事件时间、接收时刻和发起表情请求的时刻都值得留下。接收减事件时间,查到达之前的等待;发起减接收时刻,查应用自己的队列。序号重复、时钟异常再分别记录,几种问题就不会挤在一个“延迟”数字里。
而处理规则可以很简单:仍属于当前互动、仍在时间窗口内,就回应;已经失去语境,记进日志。这比把所有收到的消息迟早做完,更符合“摸一下就回应”的原意。
依据:启元开发者社区 SDK 下载,Q1 SDK v1.0.0.0 的 MessageHeader.msg、触摸示例及《接口说明》触摸时效建议与关节指令章节。欢迎在启元开发者社区交流实现经验。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)