对照Q1和T1的SDK时,有一处差异特别容易漏掉:两份 TouchState.msg 逐字节相同,sensor_id=1 的意思却变了。
Q1上是头部,T1上是背部。两种解释就写在同一份文件的注释里。
如果功能是"摸头后显示一个开心表情",代码从Q1搬到T1,可能照常编译、照常进入回调,甚至还能显示表情。只是触发它的部位已经换了。程序没有报错,原来的产品要求却没保住。
下面就沿着这个小功能,核对输入、表情调用和动作定义三处差异。使用的都是Q1/T1 SDK v1.0.0.0,文末给出可离线运行的核心判断。
1. 文件相同,为什么编号含义不同
两包 aimdk_msgs/hal/msg/TouchState.msg 的内容逐字节一致。它们都有消息头、传感器编号 sensor_id、事件类型 event_type,以及单击、双击和滑动等常量。
但相同文件里,恰好写着不同机型的解释:
text
T:id为1表示背部传感器,id为2表示胸部传感器
Q:id为1表示头部传感器
字段类型没变,取值也合法,编译器当然查不出问题。它只认识这个字段能放一个编号,至于编号指向哪块传感器,要结合机型解释。
回到我们的功能。第一版如果写成"编号1单击时播放开心表情",迁移后仍可能命中同一个分支。但产品想保留的究竟是什么?
如果意图是"摸头才回应",T1的编号1就不能直接替代Q1的编号1。如果意图是"用户主动触摸时友好回应",则可以针对T1重新选择背部、胸部中的哪些触摸算有效互动。前一个需求强调部位,后一个强调交互意图,代码看起来相似,迁移决策却不同。
后面的例子选择第二种需求:主动触摸时友好回应。输入先按机型解释成身体部位,再配置哪些部位触发欢迎。这样,从Q1的摸头改成T1的背部触摸,就是一项看得见的产品选择,不会藏在同一个数字1后面。
2. 传入两个表情,最后只发出了一个
触摸输入解释清楚后,下一步是显示开心表情。两个SDK都有 examples/python/play_emotion.py,但默认值和回退时机不同。
Q1示例把 emotion_ids 默认设为 [10]。在表情模式下,发送前会检查列表:只要其中包含10,就把整个待发送列表改成 [3003]。T1示例默认设为 [90],先尝试原始请求;调用返回失败,并且原列表中含90时,再尝试 [3003]。
| 项目 | Q1 示例 | T1 示例 |
|---|---|---|
| 默认表情列表 | [10] |
[90] |
| 首次发送前 | 表情模式且含10时,整体改为 [3003] |
尝试原始列表 |
| 首次失败后 | 该分支没有T1式的第二次尝试 | 表情模式且原列表含90时,再尝试 [3003] |
例如,在Q1示例中传入 [10, 3004],最终发送的是 [3003],不是 [3003, 3004]。如果业务把这个列表当作一段连续表情编排,后一个元素也随整体替换消失了。移植时只检查"接口是否存在",很难发现这样的行为。
T1的先尝试、再回退,则意味着一次业务请求可能对应两次服务调用。若只在业务入口打一条日志,后面出了问题,很难看出失败发生在哪次调用。更合适的记录方式是保留原始意图、首次实际发送值、返回结果以及是否进入回退。
文件里还有一个值得注意的小矛盾:T1回退分支的注释写"ID 10失败",代码实际检查 90 in self.emotion_ids。阅读者如果只扫注释,会得到另一套规则;实际执行条件应以代码为准。
所以业务层最好只提出"显示开心表情",具体编号由目标环境的配置提供。示例里的默认值和回退值,可以拿来理解调用过程;直接把它们搬成全项目的资源规则,容易连同整体替换和失败重试一起搬过去。
资源依据:Q1文档列出3001---3013;T1示例中的90只是尝试值。示例源码不能确认目标设备实际安装了哪些资源。
3. 常量204,在一份包里有效,在另一份里被注释
如果欢迎功能以后还想加一个转身或模仿动作,就会接触 McAction.msg。这份文件把急停、形态切换、四足、双足和Q系列动作放在不同区段里。结构很像,常量却并不完全相同。
T1包有效定义了:
text
int32 BIPED_LOCOMOTION_ROTATE_LOCAL = 204
Q1包的对应位置是:
text
# 双足原地旋转(删除)
# int32 BIPED_LOCOMOTION_ROTATE_LOCAL = 204
反过来,Q1包末尾有有效的 MIMIC_V2=1018 与 MIMIC_OPENSOURCE_V2=1019,T1包中没有这两个定义。只使用一份SDK开发,再把"有这个常量"的假设带到另一份包,就可能在很早的构建阶段碰到问题。
但更隐蔽的风险来自两边都有的定义。两份文件都包含四足动作区段;Q1是人形机器人,文件里存在四足枚举,不能据此推断它具备对应的四足执行能力。协议可能表达多个产品共用的命令集合,具体设备只实现其中一部分。
要接这类动作,检查就得往下多走两步:目标包有定义之后,还要确认设备支持,再确认当前状态允许执行。否则找到一个常量名,距离真正做出那个动作仍然很远。

图 1:两份SDK的静态证据。上层共享命名和协议框架,下面分别列出字段语义、示例路径及枚举定义差异;图中的常量不作为真机能力清单。
4. 三处核对,得到了什么
读者可以从官方页面下载两包v1.0.0.0,按前文的文件路径逐项核对。我用离线脚本比较了触摸文件的哈希,通过Python语法树读取表情默认参数,再扫描三个动作常量是否为有效定义。得到的主要结果如下:
| 核对项 | Q1 | T1 |
|---|---|---|
TouchState.msg 文件内容 |
两者一致 | 两者一致 |
| 表情默认列表 | [10] |
[90] |
BIPED_LOCOMOTION_ROTATE_LOCAL |
无有效定义 | 204 |
MIMIC_V2 |
1018 | 无有效定义 |
MIMIC_OPENSOURCE_V2 |
1019 | 无有效定义 |
以后升级SDK,这几项也值得再查一次。默认值变了,就重看首次发送和失败回退;动作常量变了,就找出依赖它的业务。比起拿所有功能从头试一遍,这张差异表能先缩小范围。
脚本检查文件哈希、默认参数和有效枚举;表情回退路径由源码阅读核对,设备资源与动作能力仍须运行验证。
5. 用同一个函数处理两台机器
前面的问题落到代码里,其实不用做一个很大的框架。把输入部位和输出资源各翻译一次,中间保留同一段欢迎逻辑,就够这个例子使用。
输入端结合明确的机型配置,把原始触摸编号解释成部位与事件。共享业务层只判断"这是不是当前欢迎流程允许的有效触摸",再产生"表达开心"的意图。输出端根据目标机型和版本查找已确认的资源,然后通过该设备对应的SDK调用服务。

图 2:本文提出的应用结构,不是官方SDK内置组件。机型配置负责解释输入和选择资源;公共业务逻辑处理互动意图。未知资源不在图中被自动视为可用。
欢迎流程只用"有效触摸"和"开心表情"这两个概念。编号、SDK版本及资源确认记录放进设备配置,服务调用结果再交回来。后面即使换了资源,也不必沿着所有业务分支找数字。
把这条路径缩成Python代码,核心判断只需要输入映射、业务条件和资源查询:
python
def plan_reply(sensor_parts, accepted_parts, resources, sensor_id, event):
part = sensor_parts.get(sensor_id)
if part is None:
return "unknown_sensor", None
if event != "single_tap" or part not in accepted_parts:
return "ignored", None
resource = resources.get("happy")
if resource is None:
return "resource_unmapped", None
return "ready", resource
Q1的部位映射填 {1: "head"},T1填 {1: "back", 2: "chest"}。两种配置交给同一个函数,输入先得到部位含义,再经过业务判断,最后查找资源。返回 ready 后,调用层才继续处理输出。
本例选择Q1头部单击、T1背部单击触发欢迎。这个选择属于我们的产品规则:T1胸部传感器仍可被识别,但暂不触发这段互动。三个主要用例的结果如下:
| 输入与配置 | 业务判断 | 输出 |
|---|---|---|
| Q1编号1单击,已有演示资源映射 | 头部触摸有效 | 记录Q1开心资源 |
| T1编号1单击,未配开心资源 | 背部触摸有效,资源缺失 | resource_unmapped,不记录播放 |
| T1编号1单击,补上演示资源映射 | 背部触摸有效 | 记录T1开心资源 |
演示使用的 demo:q1:happy 和 demo:t1:happy 是虚构测试资源名,不是设备ID;single_tap 是应用解码后的统一事件标签。程序不导入SDK、不连接设备。
中间一行是这段实现最有用的地方。背部触摸已识别,欢迎规则也允许,最后卡在资源缺失。此时应当补映射,输入和业务逻辑都不用动。"没有回应"终于有了一个具体原因。
把调用写出来,缺少映射和补齐映射的差别就更直观:
python
t1_parts = {1: "back", 2: "chest"}
print(plan_reply(t1_parts, {"back"}, {}, 1, "single_tap"))
# ("resource_unmapped", None)
print(plan_reply(t1_parts, {"back"},
{"happy": "demo:t1:happy"}, 1, "single_tap"))
# ("ready", "demo:t1:happy")
离线演示还检查了胸部触摸未获业务允许、未知传感器和双击三种情况。它们分别停在自己的判断位置,因此"没有回应"可以沿路径查清原因。
真正接设备时,资源配置换成目标环境确认过的ID;收到 ready 后,由调用层通过对应SDK服务发起请求,并检查服务回执。这个替换把设备差异留在两端,欢迎逻辑仍然使用"有效触摸"和"表达开心"这样的业务概念。
6. 最后到设备上确认什么
离线用例已经能检查部位映射、欢迎规则和资源缺失分支。接上目标设备之后,再确认触摸实际来自哪个部位、所选表情是否存在、服务失败时返回什么。几种结果要和前面的分层对应起来,别最后又统统记成"播放失败"。
这次对比里,最值得提前处理的是 sensor_id=1 这样的语义差异。少了一个常量,程序可能直接报错;编号含义变了,程序却能一路跑下去,直到人觉得"怎么碰这里也有反应"才暴露出来。
把身体部位和资源映射写清楚,改动并不大。它让"摸一下,笑一下"先在两台机器人上各自成立,共享代码才有实际意义。
验证范围:本文完成SDK静态比较及离线分支演示,未连接真机。各设备应使用匹配的SDK构建环境;同名包不构成两份消息包混装或整体二进制兼容的证据。
依据:启元官方SDK下载中Q1/T1 v1.0.0.0的 TouchState.msg、play_emotion.py、McAction.msg 与接口说明;机型参考Q1官方产品页。欢迎在启元开发者社区交流跨机型开发经验。