启元 Q1 接口地图:从 aimdk_msgs 逆向出 25 个 Service/Topic 的完整清单

摘要

启元 Q1 的 25 个对外接口,不连机器人、不翻完近 50 万字节、按模块切了 6 章的文档,也能一次看清------契约就在 aimdk_msgs 消息包里,编译完即落在本地。本文记录一次完整的离线摸底:为什么放弃按文档目录建立全局印象,转而从 .msg/.srv 源文件反推;_5F 与 /aima/ 各自管什么,抄错一个字符会怎么死;以及一个被手动刷屏逼出来的 dump_interfaces.sh。基于 primebot-q1-sdk 2026-10 main 分支,Ubuntu 22.04 + ROS 2 Humble 离线整理,接口名可用 ros2 interface list 本机复核。

📌 版本声明 :本文依据 primebot-q1-sdk 仓库 aimdk_msgs/ 目录下的 .msg/.srv 源文件与同仓库 接口说明.md(接口元数据标注版本 1.0 Stable)整理,对应 main 分支 2026-10 状态。所有接口名均可通过 ros2 interface list 在本机复核,无需真机。

文章目录

    • 摘要
    • 一、为什么"翻文档"不如"读消息包"
      • [1.1 文档的结构性困境](#1.1 文档的结构性困境)
      • [1.2 消息包才是二开唯一可信的契约](#1.2 消息包才是二开唯一可信的契约)
      • [1.3 按命名空间归拢:两套命名空间风格并存](#1.3 按命名空间归拢:两套命名空间风格并存)
    • [二、接口分类统计:25 个落点是怎么构成的](#二、接口分类统计:25 个落点是怎么构成的)
      • [2.1 一张分类统计表](#2.1 一张分类统计表)
      • [2.2 通信形态构成饼图](#2.2 通信形态构成饼图)
      • [2.3 三种形态背后的设计逻辑](#2.3 三种形态背后的设计逻辑)
    • [三、25 个接口全景表(按可操作性排序)](#三、25 个接口全景表(按可操作性排序))
      • [3.1 运动控制(10 个)](#3.1 运动控制(10 个))
      • [3.2 交互控制(10 个)](#3.2 交互控制(10 个))
      • [3.3 硬件抽象(5 个)](#3.3 硬件抽象(5 个))
    • [四、命名规范专节:`_5F`、`/aima` 与 `/aimdk_5Fmsgs` 各管什么](#四、命名规范专节:_5F、/aima 与 /aimdk_5Fmsgs 各管什么)
      • [4.1 `_5F`:下划线穿的十六进制马甲](#4.1 _5F:下划线穿的十六进制马甲)
      • [4.2 两套命名空间为什么这样分工](#4.2 两套命名空间为什么这样分工)
      • [4.3 命名写错的三种死法](#4.3 命名写错的三种死法)
    • [五、响应模型三层:`header.code` → `status.value` → `reason`](#五、响应模型三层:header.code → status.value → reason)
      • [5.1 三层各管什么](#5.1 三层各管什么)
      • [5.2 CommonState 的生命周期语义](#5.2 CommonState 的生命周期语义)
      • [5.3 reason:失败原因的排错地图](#5.3 reason:失败原因的排错地图)
      • [5.4 判定口诀与一个点到为止的冲突提醒](#5.4 判定口诀与一个点到为止的冲突提醒)
    • [六、输入源 `McInputSource`:谁在控制这台机器人](#六、输入源 McInputSource:谁在控制这台机器人)
      • [6.1 结构三字段](#6.1 结构三字段)
      • [6.2 五大内置信源](#6.2 五大内置信源)
      • [6.3 仲裁三准则与 1000ms 脱锁](#6.3 仲裁三准则与 1000ms 脱锁)
      • [6.4 重名与超时会以什么方式报复你](#6.4 重名与超时会以什么方式报复你)
    • [七、`dump_interfaces.sh` 全解:不接真机导出接口清单](#七、dump_interfaces.sh 全解:不接真机导出接口清单)
      • [7.1 四个自省命令与过滤技巧](#7.1 四个自省命令与过滤技巧)
      • [7.2 完整脚本逐段解读](#7.2 完整脚本逐段解读)
      • [7.3 用 Python 做分模块统计](#7.3 用 Python 做分模块统计)
    • [八、无真机自环验证:`service call` 与 `topic echo`](#八、无真机自环验证:service call 与 topic echo)
      • [8.1 CLI 直调示例](#8.1 CLI 直调示例)
      • [8.2 从 `get_bms_state.py` 学订阅姿势](#8.2 从 get_bms_state.py 学订阅姿势)
      • [8.3 把动作串成时序](#8.3 把动作串成时序)
    • 九、边界与坑:文档不会主动告诉你的事
      • [9.1 命名空间混用:不能靠前缀猜类型](#9.1 命名空间混用:不能靠前缀猜类型)
      • [9.2 QoS 不是摆设](#9.2 QoS 不是摆设)
      • [9.3 srv 与 topic 的边界再强调](#9.3 srv 与 topic 的边界再强调)
      • [9.4 其他适用边界](#9.4 其他适用边界)
    • 十、总结
    • 参考资料

一、为什么"翻文档"不如"读消息包"

1.1 文档的结构性困境

官方 接口说明.md 有近 50 万字节,结构是"模块 → 功能 → 接口"三层。查单个接口它很好用,想建立全局印象就很别扭。我摸底时头一件事就是想让机器人"说话",本以为是个五分钟需求,结果在交互控制模块里翻完了语音控制小节才数清楚:PlayTts、PlayAudioFile、GetVolume、SetVolume、GetMute、SetMute 六个 Service,外加两个 TCP 端口和两个话题。一个"说话"就牵出这么多落点,25 个接口得翻到什么时候。这种"查得到、看不全"的体感,是一切按功能章节组织的文档的通病。文档的另一重成本在检索:50 万字节摊到六大模块,每个接口的背景、字段表、示例动辄上千字,查一个接口要读三段,建一张全局图要翻六章;等你好不容易拼出全貌,SDK 的小版本可能又走了一轮。

更麻烦的是,同一能力会在不同章节反复出现。关节控制就是典型:运控高层接口里有自定义上肢关节控制,运控低层接口里还有全身伺服,两个话题名字只差两层前缀,功能边界却完全不同。按目录顺序读的人,很容易在"哪个是哪个"上白交一轮学费,甚至把高层接口当成底层直控来用,引出剧烈的物理震颤。我读到这两个话题时专门停下来对了一遍前缀,才敢确认哪个能碰、哪个不能乱碰。

1.2 消息包才是二开唯一可信的契约

所以我的做法反过来:先读消息包,再查文档。

这个转折发生在编译之后。SDK 按之前那篇的路子在本机编完,install/ 目录里躺着全部类型;我顺手敲了一条 ros2 interface list,屏幕上几百行类型名滚过去------那一刻很直观:契约根本不用去文档里找,它早就编译到本地了。不需要机器人在线,不需要打开文档,ros2 interface list 与 ros2 interface show 就能把全部类型倒出来。第七节会把这个动作固化成脚本,CI 里同样能跑。

消息包还有一层编译期强制力:.msg/.srv 定义是 C++ 头文件与 Python 类的唯一来源。文档可以滞后,消息包滞后意味着编译直接失败------不一致会以最疼的方式暴露,而不是让你带着错误认知跑很久才发现。

二开的真实契约两端也都在消息包里。你的代码 from aimdk_msgs.msg import Bms,导入的是消息包;调用 ros2 service call /aimdk_5Fmsgs/srv/SetMcAction ...,命中的也是消息包定义的类型。文档只是这套契约的"人类可读投影",投影会歪,契约不会。

什么叫"漂移"?SDK 小版本升级加了新接口、改了字段默认值,文档未必同步;你按三个月前的文档写代码,直到编译报错或运行时 KeyError 才发现契约变了。消息包不会悄悄变------它变了,你的 import 和类型检查会立刻报警。把消息包当基准、文档当注释,是成本最低的对齐方式。

1.3 按命名空间归拢:两套命名空间风格并存

我整理时做的头一个动作,是把类型名按命名空间归拢,滚出来的表很快收敛成两种风格:

命名空间 形态 承载什么 数量
/aimdk_5Fmsgs/srv/ Service 需要"请求-应答"的动作:切模式、播放、查询 15 个
/aima/mc/... Topic 高频流式数据与控制:速度、关节指令 3 个
/aima/hal/... Topic 硬件状态与单帧数据:BMS、触摸、设备列表 4 个
/aima/agent/... Topic AI 层结果:ASR 识别文本 1 个
TCP 端口 Socket 音频原始流:6001 播放、4713 录制 2 个

表按通信形态分完堆,设计意图就摆在脸上了:要"确认结果"的走 Service,要"连续刷新"的走 Topic,要"低延迟原始流"的直接甩出 TCP 端口。 记住这条判断,你就不必每次翻目录。至于这两套名字各自的来历,第四节专门拆解。

图1:官方接口说明 API 列表四列表格与三大模块分组

二、接口分类统计:25 个落点是怎么构成的

2.1 一张分类统计表

归拢命名空间只是理了个外层。再按"通信形态 + 数据方向"展开一遍,25 个落点变成下面这张明细表。它同时是本文后续章节的索引,也可以直接贴进你的项目文档当速查表用。

一级形态 细分 数量 成员
Service(请求-应答) /aimdk_5Fmsgs/srv/ 15 GetMcAction、SetMcAction、SetMcMotion、SetMcPresetMotion、SetMcInputSource、GetCurrentInputSource、PlayTts、PlayAudioFile、PlayEmotion、GetVolume、SetVolume、GetMute、SetMute、ShutdownTask、LedStripCommand
Topic · 控制指令流 /aima/mc/、/aima/hal/ 3 /aima/mc/locomotion/velocity、/aima/mc/custom/joint/command、/aima/hal/joint/command
Topic · 状态反馈流 /aima/hal/ 4 /aima/hal/joint/state、/aima/hal/bms/state、/aima/hal/touch/state、/aima/hal/audio/audio_device_list
Topic · AI 结果流 /aima/agent/ 1 /aima/agent/asr_result
TCP · 原始音频流 端口 6001 / 4713 2 6001(simple-protocol-tcp 播放)、4713(native-protocol-tcp 录制)

合计 15 + 3 + 4 + 1 + 2 = 25。这张表我校对过三遍:先数 Service 的十五项,再数八个话题,音频端口收尾,凑齐 25 才敢往下写。八个话题按数据方向再分成"控制下发"与"状态上报"两族,这是第六节输入源仲裁与第九节 QoS 选型的基础。

读表也有技巧:想找"让机器人动起来"的入口,直接看 Topic · 控制指令流那三行;想查某个能力的成败回执,去 Service 的十五项里按名字找------命名足够规整,PlayTts 播语音、SetVolume 调音量、ShutdownTask 关机,十有八九见名知意。我后来查接口基本不翻文档了,先扫这张表。

📌 口径备注 :相机单帧服务 /aima/hal/camera/CaptureJpegImage 类型是 Service,但前缀位于 /aima/hal/ 之下,是两套命名空间规则交叠的特例;RTSP 视频流则是地址模板而非 ROS 接口。二者在官方清单中归入硬件抽象模块单独列示,第三节全景表按官方模块口径收录。

2.2 通信形态构成饼图

#mermaid-svg-zzs7Yvpw6hG15uJi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zzs7Yvpw6hG15uJi .error-icon{fill:#552222;}#mermaid-svg-zzs7Yvpw6hG15uJi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zzs7Yvpw6hG15uJi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zzs7Yvpw6hG15uJi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zzs7Yvpw6hG15uJi .marker.cross{stroke:#333333;}#mermaid-svg-zzs7Yvpw6hG15uJi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zzs7Yvpw6hG15uJi p{margin:0;}#mermaid-svg-zzs7Yvpw6hG15uJi .pieCircle{stroke:#000000;stroke-width:2px;opacity:0.7;}#mermaid-svg-zzs7Yvpw6hG15uJi .pieOuterCircle{stroke:#000000;stroke-width:1px;fill:none;}#mermaid-svg-zzs7Yvpw6hG15uJi .pieTitleText{text-anchor:middle;font-size:25px;fill:#000000;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-zzs7Yvpw6hG15uJi .slice{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#000000;font-size:17px;}#mermaid-svg-zzs7Yvpw6hG15uJi .legend text{fill:#000000;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:17px;}#mermaid-svg-zzs7Yvpw6hG15uJi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 60% 32% 8% 25 个对外接口的通信形态构成 Service(请求-应答) Topic(流式数据) TCP(原始音频流)

图:25 个接口按通信形态的构成,Service 占六成,是二开动作的主要入口。

2.3 三种形态背后的设计逻辑

三种形态的分工不是文档随手写的,而是交互语义的自然结果。Service 是"我问你答":调用 SetMcAction 切模式,机器人回一个 CommonResponse 告知成败,关系随即释放,适合低频、一次性、需要明确成败反馈的操作。Topic 是"你一直发、我一直听":/aima/hal/bms/state 按 1Hz 往外喷电压电流,适合高频、持续、不需要逐次应答的数据流。

把 BMS 做成 Service,就得每秒主动问一次,带宽浪费了,时序也丢了;把切模式做成 Topic,又会丢失"本次成败"的精确语义。我最早动过把 BMS 包成服务调用的念头,琢磨了两分钟发现每秒轮询有多蠢,才看懂官方为什么把它排成话题。相机单帧抓拍 CaptureJpegImage 佐证了这条规律:调用方要的是"这一帧",一次性拉取比持续订阅更符合语义,所以它是 Service。关节指令是 Topic 而非 Service,同样能从数据特征解释:全身 22 个可动关节按 500Hz 以上节拍刷新,每帧为每个关节携带位置、速度、力矩与 PD 增益五组数值,做成请求-应答会让通信次数和延迟双双爆炸,Topic 的"最新值覆盖"语义才扛得住这种流。TCP 更特殊:48kHz 多声道原始音频每秒数十万字节,且对送达时机有刚性要求,DDS 的可靠重传与尽力而为两头不讨好,官方索性让音频旁路 ROS 2,直接开 6001 播放、4713 录制两条 PulseAudio 通道------这件事的完整推演在系列第十篇,本篇点到为止。

三、25 个接口全景表(按可操作性排序)

下表按"新手多久会用到"重排,不是官方顺序。左列是接口名,右列是建议的调用时机。这个排序是我给自己排的:先弄清谁管模式切换,再弄清谁管指令流,灯语这种锦上添花的放最后。

3.1 运动控制(10 个)

接口 类型 数据类型 用途 什么时候用
/aimdk_5Fmsgs/srv/GetMcAction Service GetMcAction 查询当前运动模式与生命周期阶段 任何控制前先查
/aimdk_5Fmsgs/srv/SetMcAction Service SetMcAction 切换运动模式(站立/走跑/全身控制等) 动作前置
/aimdk_5Fmsgs/srv/SetMcMotion Service SetMcMotion 设置全身动作(模仿类) 播放动作库
/aimdk_5Fmsgs/srv/SetMcPresetMotion Service SetMcPresetMotion 预设上肢动作 仅 WBC 模式可用
/aimdk_5Fmsgs/srv/SetMcInputSource Service SetMcInputSource 注册自定义控制输入源 首次接管控制前
/aimdk_5Fmsgs/srv/GetCurrentInputSource Service GetCurrentInputSource 查当前控制权在谁手里 排查"我的指令没生效"
/aima/mc/locomotion/velocity Topic McLocomotionVelocity 走跑速度控制(前后/横移/转向/俯仰) 遥控、导航跟随
/aima/mc/custom/joint/command Topic McCustomJointCommand 自定义上肢关节控制 挥手、递物、姿态编程
/aima/hal/joint/command Topic JointCommandArray 全身关节指令(低层) 高级模式下的底层控制
/aima/hal/joint/state Topic JointStateArray 全身关节状态反馈 闭环控制必备

这十个接口构成一条完整控制链:先 GetMcAction 确认状态,再 SetMcAction 切模式,注册输入源后持续发速度或关节指令,joint/state 负责把执行结果送回你的闭环。

六个 Service 里,输入源两件套(SetMcInputSource 与 GetCurrentInputSource)是分水岭:没注册输入源之前,你发的速度指令会被仲裁层当成空气------不报错,不回调,什么都不发生。我头一回在消息包里翻到 McLocomotionVelocity 的 source 字段时没当回事,直到对着仲裁规则读了一遍才后怕:字段填错,整条链路安静地假装没收到。

3.2 交互控制(10 个)

接口 类型 数据类型 用途
/aimdk_5Fmsgs/srv/PlayTts Service PlayTts 文本转语音播报(带优先级与打断控制)
/aimdk_5Fmsgs/srv/PlayAudioFile Service PlayAudioFile 播放音频文件
/aimdk_5Fmsgs/srv/PlayEmotion Service PlayEmotion 播放表情/视频(屏幕控制)
/aimdk_5Fmsgs/srv/GetVolume / SetVolume Service GetVolume / SetVolume 音量查询与设置
/aimdk_5Fmsgs/srv/GetMute / SetMute Service GetMute / SetMute 静音状态查询与设置
/aima/hal/audio/audio_device_list Topic AudioDeviceInfoList 列出输入/输出音频设备
/aima/agent/asr_result Topic AsrResult 语音识别结果(含中间结果与置信度)
TCP :6001 Socket PulseAudio simple 协议 音频文件播放(直连,绕过 ROS)
TCP :4713 Socket PulseAudio native 协议 音频录制(直连,拿原始 PCM)
/aimdk_5Fmsgs/srv/ShutdownTask Service ShutdownTask 主动关机(请求体是 TokenCommon)

注意最后一行:关机这种"一发定生死"的操作,官方同样用 Service 承载------要的就是一次确认。另外 PlayTts 自带优先级与打断控制,做迎宾讲解这类连续播报场景时,先用 GetVolume 与 GetMute 把通道状态确认清楚,比反复试错快得多。这两个 getter 我在消息包里对过字段,返回结构和其它查询服务一致,读一遍就能照着写封装。

3.3 硬件抽象(5 个)

接口 类型 数据类型 用途
/aima/hal/bms/state Topic Bms 电池电压/电流/温度/循环次数
/aima/hal/touch/state Topic TouchState 头部触摸事件(按事件触发,不是周期发布)
/aima/hal/camera/CaptureJpegImage Service CaptureJpegImage 取指定相机单帧 JPEG
rtsp://<IP>:<Port>/live_<camera_id> RTSP --- 相机视频流
/aimdk_5Fmsgs/srv/LedStripCommand Service LedStripCommand 整机灯语样式

相机能力被拆成两个落点:单帧抓拍走 Service、连续视频走 RTSP------一个要"这一帧",一个要"一直有",恰好落在两种形态的分界线上,是整套接口设计哲学的缩影。RTSP 那条我在清点脚本里筛不到,它不是 ROS 接口,ros2 interface list 里根本没有它;头一回拿着接口清单找视频流,怎么都找不到,后来才反应过来要去文档里翻地址模板。
#mermaid-svg-ltwaxhPN79FX77GU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ltwaxhPN79FX77GU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ltwaxhPN79FX77GU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ltwaxhPN79FX77GU .error-icon{fill:#552222;}#mermaid-svg-ltwaxhPN79FX77GU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ltwaxhPN79FX77GU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ltwaxhPN79FX77GU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ltwaxhPN79FX77GU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ltwaxhPN79FX77GU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ltwaxhPN79FX77GU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ltwaxhPN79FX77GU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ltwaxhPN79FX77GU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ltwaxhPN79FX77GU .marker.cross{stroke:#333333;}#mermaid-svg-ltwaxhPN79FX77GU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ltwaxhPN79FX77GU p{margin:0;}#mermaid-svg-ltwaxhPN79FX77GU .edge{stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .section--1 rect,#mermaid-svg-ltwaxhPN79FX77GU .section--1 path,#mermaid-svg-ltwaxhPN79FX77GU .section--1 circle,#mermaid-svg-ltwaxhPN79FX77GU .section--1 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section--1 text{fill:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth--1{stroke-width:17;}#mermaid-svg-ltwaxhPN79FX77GU .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-0 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-0 path,#mermaid-svg-ltwaxhPN79FX77GU .section-0 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-0 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-ltwaxhPN79FX77GU .section-0 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-0{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-0{stroke-width:14;}#mermaid-svg-ltwaxhPN79FX77GU .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-1 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-1 path,#mermaid-svg-ltwaxhPN79FX77GU .section-1 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-1 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-1 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-1{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-1{stroke-width:11;}#mermaid-svg-ltwaxhPN79FX77GU .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-2 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-2 path,#mermaid-svg-ltwaxhPN79FX77GU .section-2 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-2 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-2 text{fill:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-2{stroke-width:8;}#mermaid-svg-ltwaxhPN79FX77GU .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-3 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-3 path,#mermaid-svg-ltwaxhPN79FX77GU .section-3 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-3 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-3 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-3{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-3{stroke-width:5;}#mermaid-svg-ltwaxhPN79FX77GU .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-4 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-4 path,#mermaid-svg-ltwaxhPN79FX77GU .section-4 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-4 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-4 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-4{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-4{stroke-width:2;}#mermaid-svg-ltwaxhPN79FX77GU .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-5 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-5 path,#mermaid-svg-ltwaxhPN79FX77GU .section-5 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-5 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-5 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-5{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-5{stroke-width:-1;}#mermaid-svg-ltwaxhPN79FX77GU .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-6 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-6 path,#mermaid-svg-ltwaxhPN79FX77GU .section-6 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-6 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-6 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-6{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-6{stroke-width:-4;}#mermaid-svg-ltwaxhPN79FX77GU .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-7 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-7 path,#mermaid-svg-ltwaxhPN79FX77GU .section-7 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-7 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-7 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-7{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-7{stroke-width:-7;}#mermaid-svg-ltwaxhPN79FX77GU .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-8 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-8 path,#mermaid-svg-ltwaxhPN79FX77GU .section-8 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-8 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-8 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-8{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-8{stroke-width:-10;}#mermaid-svg-ltwaxhPN79FX77GU .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-9 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-9 path,#mermaid-svg-ltwaxhPN79FX77GU .section-9 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-9 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-9 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-9{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-9{stroke-width:-13;}#mermaid-svg-ltwaxhPN79FX77GU .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-10 rect,#mermaid-svg-ltwaxhPN79FX77GU .section-10 path,#mermaid-svg-ltwaxhPN79FX77GU .section-10 circle,#mermaid-svg-ltwaxhPN79FX77GU .section-10 polygon,#mermaid-svg-ltwaxhPN79FX77GU .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-10 text{fill:black;}#mermaid-svg-ltwaxhPN79FX77GU .node-icon-10{font-size:40px;color:black;}#mermaid-svg-ltwaxhPN79FX77GU .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .edge-depth-10{stroke-width:-16;}#mermaid-svg-ltwaxhPN79FX77GU .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-ltwaxhPN79FX77GU .disabled,#mermaid-svg-ltwaxhPN79FX77GU .disabled circle,#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:lightgray;}#mermaid-svg-ltwaxhPN79FX77GU .disabled text{fill:#efefef;}#mermaid-svg-ltwaxhPN79FX77GU .section-root rect,#mermaid-svg-ltwaxhPN79FX77GU .section-root path,#mermaid-svg-ltwaxhPN79FX77GU .section-root circle,#mermaid-svg-ltwaxhPN79FX77GU .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-ltwaxhPN79FX77GU .section-root text{fill:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .section-root span{color:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .section-2 span{color:#ffffff;}#mermaid-svg-ltwaxhPN79FX77GU .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-ltwaxhPN79FX77GU .edge{fill:none;}#mermaid-svg-ltwaxhPN79FX77GU .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-ltwaxhPN79FX77GU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} aimdk_msgs
运动控制
Service
GetMcAction
SetMcAction
SetMcMotion
SetMcPresetMotion
输入源两件套
Topic
locomotion/velocity
custom/joint/command
hal/joint/command
hal/joint/state
交互控制
Service
PlayTts
PlayAudioFile
PlayEmotion
音量与静音
ShutdownTask
Topic
audio_device_list
asr_result
TCP
6001 播放
4713 录制
硬件抽象
hal/bms/state
hal/touch/state
CaptureJpegImage
RTSP 流
LedStripCommand

图:aimdk_msgs 类型树的思维导图,按官方三大模块组织,与上面三张表一一对应。

四、命名规范专节:_5F、/aima 与 /aimdk_5Fmsgs 各管什么

4.1 _5F:下划线穿的十六进制马甲

/aimdk_5Fmsgs/srv/PlayTts 里的 5F 不是版本号,也不是"第 5 版"。0x5F 是下划线 _ 的 ASCII 十六进制码 (十进制 95),所以这个名字展开后是 aimdk_msgs/srv/PlayTts------正好和数据类型 aimdk_msgs/srv/PlayTts 严丝合缝对上。

我在这上面栽过一次。照着文档抄服务名时,我凭直觉把 _5F "翻译"成了下划线,wait_for_service 老老实实等了一分多钟,日志里一片沉默------没有报错,没有拒绝,就是没人应答。排查了半天才发现:服务端注册的名字带转义,客户端等的是一个世界上不存在的名字。从此我记住一条:抄屏幕,不抄记忆。

这意味着:服务名里的 _5F 与消息包名里的 _,是同一个字符的两种写法。ROS 2 话题/服务名允许出现的字符集比类型名更窄,下划线在某些中间环节(网关、可视化工具、URL 路由)会被当成非法字符做二次转义,于是官方在接口名层面统一把 aimdk_msgs 写成 aimdk_5Fmsgs。这是基于命名现象的合理推测,官方并未给出解释;你只需记住结论:写代码时服务名照抄 _5F,类型名照抄 aimdk_msgs,两者都不要手改。

落到代码里,两边的写法也要分清:创建 client 时传服务名 /aimdk_5Fmsgs/srv/PlayTts,import 时写 aimdk_msgs.srv.PlayTts。前者带转义、后者带下划线,混用一次就知道疼。

4.2 两套命名空间为什么这样分工

我一开始的判断是错的:以为 /aima/ 是新版命名、/aimdk_5Fmsgs/ 是旧版残留,还暗自琢磨了一番迁移周期。后来把 25 个接口按形态分完堆才看清------/aima/ 与 /aimdk_5Fmsgs/ 不是新旧关系,也不是两套 SDK,而是数据类型与交互形态不同 :所有"请求-应答"形态的能力都挂在 /aimdk_5Fmsgs/srv/ 下,服务名与 .srv 类型一一对应;所有"流式数据"形态的能力都挂在 /aima/ 下,按域再分 mc(运控)、hal(硬件)、agent(智能体)三个子树。分界线不是版本,是有没有回执。
#mermaid-svg-TIEu9elPI2ags3NX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TIEu9elPI2ags3NX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TIEu9elPI2ags3NX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TIEu9elPI2ags3NX .error-icon{fill:#552222;}#mermaid-svg-TIEu9elPI2ags3NX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TIEu9elPI2ags3NX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TIEu9elPI2ags3NX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TIEu9elPI2ags3NX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TIEu9elPI2ags3NX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TIEu9elPI2ags3NX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TIEu9elPI2ags3NX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TIEu9elPI2ags3NX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TIEu9elPI2ags3NX .marker.cross{stroke:#333333;}#mermaid-svg-TIEu9elPI2ags3NX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TIEu9elPI2ags3NX p{margin:0;}#mermaid-svg-TIEu9elPI2ags3NX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TIEu9elPI2ags3NX .cluster-label text{fill:#333;}#mermaid-svg-TIEu9elPI2ags3NX .cluster-label span{color:#333;}#mermaid-svg-TIEu9elPI2ags3NX .cluster-label span p{background-color:transparent;}#mermaid-svg-TIEu9elPI2ags3NX .label text,#mermaid-svg-TIEu9elPI2ags3NX span{fill:#333;color:#333;}#mermaid-svg-TIEu9elPI2ags3NX .node rect,#mermaid-svg-TIEu9elPI2ags3NX .node circle,#mermaid-svg-TIEu9elPI2ags3NX .node ellipse,#mermaid-svg-TIEu9elPI2ags3NX .node polygon,#mermaid-svg-TIEu9elPI2ags3NX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TIEu9elPI2ags3NX .rough-node .label text,#mermaid-svg-TIEu9elPI2ags3NX .node .label text,#mermaid-svg-TIEu9elPI2ags3NX .image-shape .label,#mermaid-svg-TIEu9elPI2ags3NX .icon-shape .label{text-anchor:middle;}#mermaid-svg-TIEu9elPI2ags3NX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TIEu9elPI2ags3NX .rough-node .label,#mermaid-svg-TIEu9elPI2ags3NX .node .label,#mermaid-svg-TIEu9elPI2ags3NX .image-shape .label,#mermaid-svg-TIEu9elPI2ags3NX .icon-shape .label{text-align:center;}#mermaid-svg-TIEu9elPI2ags3NX .node.clickable{cursor:pointer;}#mermaid-svg-TIEu9elPI2ags3NX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TIEu9elPI2ags3NX .arrowheadPath{fill:#333333;}#mermaid-svg-TIEu9elPI2ags3NX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TIEu9elPI2ags3NX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TIEu9elPI2ags3NX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TIEu9elPI2ags3NX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TIEu9elPI2ags3NX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TIEu9elPI2ags3NX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TIEu9elPI2ags3NX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TIEu9elPI2ags3NX .cluster text{fill:#333;}#mermaid-svg-TIEu9elPI2ags3NX .cluster span{color:#333;}#mermaid-svg-TIEu9elPI2ags3NX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TIEu9elPI2ags3NX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TIEu9elPI2ags3NX rect.text{fill:none;stroke-width:0;}#mermaid-svg-TIEu9elPI2ags3NX .icon-shape,#mermaid-svg-TIEu9elPI2ags3NX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TIEu9elPI2ags3NX .icon-shape p,#mermaid-svg-TIEu9elPI2ags3NX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TIEu9elPI2ags3NX .icon-shape .label rect,#mermaid-svg-TIEu9elPI2ags3NX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TIEu9elPI2ags3NX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TIEu9elPI2ags3NX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TIEu9elPI2ags3NX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Topic 面(流式,高频)
Service 面(请求-应答,低频)
/aimdk_5Fmsgs/srv/*
同步返回 CommonResponse

header.code + status.value
/aima/mc/*

控制指令流
/aima/hal/*

硬件状态流
/aima/agent/*

AI 结果流
FastDDS 单向下发
FastDDS 单向上报

图:Service 面与 Topic 面的分工总览,两套命名空间对应两种交互形态。

判断标准很简单:你要"回执"就找 Service,你要"持续数据"就找 Topic。 想让机器人走起来,速度指令是 Topic(要连续刷),但切到走跑模式是 Service(要确认切换成功)。反过来说,一个能力若你只想触发一次并知道结果,它几乎一定在 /aimdk_5Fmsgs/srv/ 里;你要的是"一直有"的数据,就去 /aima/ 下按域找。

/aima/ 之下三个子域的划分依据是"数据由谁产生":mc 来自运控层,hal 来自硬件抽象层,agent 来自智能体层。订阅反馈时按域找人,比把八个话题名死记下来更可持续。

4.3 命名写错的三种死法

名字写错不会有好报,我按踩坑概率排个序。

最疼的是手改 _5F------就是我在 4.1 栽的那个:把 /aimdk_5Fmsgs/srv/SetMcAction 写成 /aimdk_msgs/srv/SetMcAction,wait_for_service 会一直超时。服务端注册的名字带转义,客户端等一个不存在的名字,日志里只有沉默。不报错、不拒绝、什么都不返回,这种安静的死法最耗时间。

另外两种省流。话题名照抄服务名:想调 PlayTts 却执行 ros2 topic pub /aimdk_5Fmsgs/srv/PlayTts,系统会提示类型无法确定或无人订阅------话题和服务在 ROS 2 里是两张注册表,从来不是同一个频道。猜类型名:给 /aima/hal/bms/state 配 aimdk_msgs/msg/JointStateArray,DDS 会在发现阶段因类型不匹配而拒绝建联,ros2 topic info -v 里能看到类型对不上的端点。

三种死法的共同解药是第七节的自省命令:先 list 再 show,抄屏幕,不抄记忆。

更体系化的做法是把命名校验挂进 CI:每次 SDK 更新后跑一遍 dump_interfaces.sh,与仓库里存档的上一版清单做 diff,新增、删除、字段数变化的类型一目了然。命名规范因此从"人肉记诵"变成"机器把关",这也是把接口地图当工程资产管理的起点。

图2:_5F 转义对照与两套命名空间分工全貌

五、响应模型三层:header.code → status.value → reason

5.1 三层各管什么

所有 Service 返回的不是 success: true,而是三层嵌套:框架层返回码 header.code、业务状态 status.value、失败原因 status.reason。官方给出的判定顺序写得很明确:"框架返回码优先,业务状态码随后"。
#mermaid-svg-iuuNHR4mMlWVy0s3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iuuNHR4mMlWVy0s3 .error-icon{fill:#552222;}#mermaid-svg-iuuNHR4mMlWVy0s3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iuuNHR4mMlWVy0s3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iuuNHR4mMlWVy0s3 .marker.cross{stroke:#333333;}#mermaid-svg-iuuNHR4mMlWVy0s3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iuuNHR4mMlWVy0s3 p{margin:0;}#mermaid-svg-iuuNHR4mMlWVy0s3 .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-iuuNHR4mMlWVy0s3 .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-iuuNHR4mMlWVy0s3 .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-iuuNHR4mMlWVy0s3 .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-iuuNHR4mMlWVy0s3 .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-iuuNHR4mMlWVy0s3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-iuuNHR4mMlWVy0s3 .node rect,#mermaid-svg-iuuNHR4mMlWVy0s3 .node circle,#mermaid-svg-iuuNHR4mMlWVy0s3 .node ellipse,#mermaid-svg-iuuNHR4mMlWVy0s3 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iuuNHR4mMlWVy0s3 .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-iuuNHR4mMlWVy0s3 .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-iuuNHR4mMlWVy0s3 .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iuuNHR4mMlWVy0s3 .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-iuuNHR4mMlWVy0s3 .edgeLabel .label text{fill:#333;}#mermaid-svg-iuuNHR4mMlWVy0s3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} header
status
state
reason
COMMON_RESPONSE
string
message
诊断文本
RESPONSE_HEADER
int64
code
0=SUCCESS 1=FAIL 框架层
COMMON_STATE
int
value
0..400 业务层
int
reason
0..30 故障原因
COMMON_TASK_RESPONSE
uint64
task_id
异步任务追踪
REASON

图:统一响应模型的结构关系,response 与 task_response 两种外壳共用 CommonState。

header.code 是 RPC 框架的调度结论,只有 0(SUCCESS,调度成功)和 1(FAIL,调度失败)两个值,回答的是"你的请求有没有被系统接纳";status.value 是业务层的执行进展,用 CommonState 枚举描述;status.reason 只在失败时有意义,用 0 到 30 的原因码回答"为什么不成"。另有 message 字段承载人类可读的诊断文本,排障时优先读它。带 task_id 的 CommonTaskResponse 是异步任务外壳,用于追踪长耗时动作。

少读一层的代价是实打实的:只看 header.code == 0 就宣布成功,业务层其实在排队;只看 status.value 不读 message,拿着 reason 猜半天方向。三层信息的性价比完全不同:header.code 决定你要不要继续读,status.value 决定成功还是重试,message 决定你下一步查哪里。把 message 接进日志系统的优先级,应该和异常栈一样高。

5.2 CommonState 的生命周期语义

CommonState 的取值不是 0/1 二值,而是一套跨模块的生命周期语义:

value 枚举 含义 你该做什么
0 UNKNOWN 未知 记日志 + 回退
1 SUCCESS 成功 继续
2 FAILURE 执行失败 读 message
3 ABORTED 被抢占/取消 重新排队
4 TIMEOUT 超时 查链路与负载
5 INVALID 参数非法 查限位/状态机
6 IN_MANUAL 人工接管中 停手,等人放开
100 NOT_READY 未就绪 延时重试
200 PENDING 排队中 轮询
300 CREATED 已创建 等执行
400 RUNNING 运行中 可查进度

100/200/300/400 四个"大值"要单独说:它们描述的是异步任务的排队与执行阶段,拿到这些值不该判失败,而该轮询或等待。把"排队中"当成"失败了",是响应解析做成统一库时最容易犯的分类错误------我写判别函数时就把 100 划进失败集里过,对着文档愣了才发现方向反了。对应地,value=6(IN_MANUAL)是最需要立刻停手的值------它意味着有人正在物理接管,此时任何自动指令都不该继续发。

5.3 reason:失败原因的排错地图

当 status.value != 1 时,status.reason 给出 0 到 30 的具体原因码。全表 31 项,下表挑六个最高频的:

reason 标识 含义 典型场景
5 REASON_LOW_BATTERY 低电量限制 切大动作前电量不足
7 REASON_NO_WHITELIST 动作不在白名单 播放未登记的动作
9 REASON_ACTION 当前模式不支持 模式没切对就发指令
15 REASON_TASK_RUNNING 其他任务正在运行 动作没播完又抢播
19 REASON_FORM_MIS_MATCH 需双足站立状态 非站立态下发站立专属动作
28 REASON_NOT_INTERRUPTIBLE 当前动画不可打断 强打断被拒

排障路径建议:先读 message 拿人类可读描述,再拿 reason 对表定位,最后才翻文档。比如拿到 reason=19,基本等于在提醒你------这个动作要求机器人处于双足站立,先把模式切对再说。reason 与 value 必须搭配阅读:参数类失败(如 INVALID)常伴生"当前模式不支持"这类模式原因,资源类失败(如 TIMEOUT)常指向"其他任务正在运行"。把两列连起来读,比单独看任何一列都更接近根因。

5.4 判定口诀与一个点到为止的冲突提醒

把三层判别固化成代码,大致是一个这样的纯函数:

python 复制代码
RETRY_VALUES = {100, 200, 300, 400}   # NOT_READY / PENDING / CREATED / RUNNING
FAILED_VALUES = {2, 3, 4, 5, 6}       # FAILURE / ABORTED / TIMEOUT / INVALID / IN_MANUAL

def judge(resp):
    """三段式判别:先框架码,再业务态,最后诊断文本。"""
    if resp.header.code != 0:
        return "FAILED", f"调度被拦截: {resp.message}"
    v = resp.status.value
    if v == 1:
        return "SUCCESS", ""
    if v in RETRY_VALUES:
        return "RETRY", f"中间态,轮询等待 value={v}"
    if v in FAILED_VALUES:
        return "FAILED", f"reason={resp.status.reason} msg={resp.message}"
    return "UNKNOWN", f"value={v},记日志并回退"

这个纯函数把三层判别收敛到一处:header.code != 0 直接判调度失败,连业务态都不用看;value == 1 判业务成功;四个大值属于异步流程的中间态,应当轮询而非报错;2 到 6 才是真失败,此时读 reason 与 message 定位根因。返回值除了成败还多一态 RETRY,调用方可以据此决定重试间隔------文档口诀只有成败两说,但四个大值明摆着不该算失败,这一态是我自己加的。注意这里的取值表只是通用协议口径,写库时务必给运控类接口留一个协议路由的扩展位。

⚠️ 一个必须知道但本文不展开的冲突 :运动控制类接口的 status.value 遵循 McActionStatus 协议,其中 100 代表 RUNNING ,而通用 CommonState 里 100 是 NOT_READY------同一个数字,两种相反语义。同一套解析逻辑在两类接口上会得出相反结论,这是官方在文档里亲自标出的坑。冲突的根因剖析与协议路由写法,留给系列第九篇《启元 Q1 响应三段式:框架码、业务码、原因码与 100 值冲突坑》专门拆解,本文记住"运控接口要换表"即可。

图3:框架码、业务态、原因码三级判别决策卡

六、输入源 McInputSource:谁在控制这台机器人

6.1 结构三字段

McLocomotionVelocity 的 source 字段不是随手填的字符串,它必须是你通过 /aimdk_5Fmsgs/srv/SetMcInputSource 注册过的输入源标识。McInputSource 结构只有三个字段,但个个关键:

字段 类型 取值 说明
name string 自定义标识 严禁 与内置信源重名;下发指令时 source 也要填这个值
priority int32 0-100 控制层级刻度,高优先级绝对抢占低优先级
timeout int32 默认 1000(ms) 单信源中断超过该值即触发脱锁看门狗

priority 不是均匀刻度,而是按控制层级分段的:100-80 是系统级(急停、安全保护),79-60 是高级控制(遥控器、App 手动),59-40 是中级(语音、手势交互),39-20 是低级------SDK 二次开发信号的默认落点,19-0 是备用(研发调试、离线补丁)。给自研程序定优先级时,先想清楚它该被遥控器"一抢就停",还是宁可和交互层平级竞争。这个选择没有后悔药:优先级填高一时爽,手柄一推你的程序就被静默拦截,日志里连一行警告都没有。

timeout 是安全阀:设得比控制循环周期大一个数量级即可,设得太小(比如 50ms)会把网络抖动误判为断流,频繁触发脱锁。反过来设太大会怎样?程序已经崩了,机器人还傻等满超时才释放控制权------中间这几百毫秒里,你的指令没人发,别人也抢不进来。

6.2 五大内置信源

系统内置五个输入源,全部严禁重名:

输入源标识 预设优先级 超时脱锁(ms) 物理信源
rc 80 1000 工业级实体遥控器
pnc 80 1000 导航规划算法
interaction 80 1000 语音、视觉手势等交互指令源
vr 70 1000 VR 远程操控终端
app_proxy 60 1000 移动端 App 代理转发

SetMcInputSource 的 action 支持 ADD(1001)、MODIFY(1002)、DELETE(1003)、ENABLE(2001)、DISABLE(2002) 五种操作,不同动作对 input_source 的必填字段有区别:增改要填齐 name、priority、timeout,删启停只要 name。两个隐式规则值得记住:ADD 成功的源默认即启用并直接参与仲裁;本接口的所有修改只存内存、不落 YAML,重启即丢,想持久化得改配置文件。我在消息包里对完这几个 action 的枚举值,当场决定把注册动作写进启动脚本------不然每次重启都得手工重来一遍。

MODIFY 的语义也要注意:它不像 DELETE 或 ENABLE/DISABLE 那样只认 name,而是要求连 priority、timeout 一起重填,不存在"只改一个字段"的增量操作。另一个闭环缺口是:接口只能查询当前持锁源,无法枚举全部已注册源,想做输入源审计,只能对照配置文件手工核对------这部分值得在自己的工具层补一个读取能力。

6.3 仲裁三准则与 1000ms 脱锁

输入源仲裁的规则,我用一句话概括:谁的优先级高谁说了算,同级先到先得,断流超过 1000ms 自动让位。

展开说。当前由低优先级源控制时,一旦检测到高优先级源(如 rc)的有效指令,"指挥锁"瞬间切换,低优先级源被自动拦截并静默处理------注意"静默"两个字,你的程序不会收到任何"你被抢了"的通知。多个同优先级源同时发指令时,已持锁者独占,直到它主动停发或超时。每个信源还有一只默认 1000ms 的看门狗,指令中断超过阈值,系统自动释放控制权,按优先级降序寻找下一个就绪候补,或者直接安全停机。

这条设计的工程含义很直接:你的控制循环必须远快于 1Hz,并且在程序异常退出前主动释放控制权,否则机器人会以为你还在控制它。我给自己定的是 20ms 一帧,比 1000ms 阈值快五十倍,网络抖动吃不掉这个余量。

排查"我的指令没生效"时,先查 /aimdk_5Fmsgs/srv/GetCurrentInputSource------它返回当前真正持有物理控制权的源标识;若该值在多个标识间频繁抖动,说明存在信源竞争,机器人必然一顿一顿。官方也建议,在执行提权操作后定期用它校验 name,形成控制权闭环。

还有一个边界细节:GetCurrentInputSource 反映的是"受控归属权",不是"指令活跃度"------主控源超时停机后,在新抢占者出现前它仍返回旧标识。排查时看到旧标识先别急着下结论,它可能只是个"遗产"。只有开机后尚未收到任何合法指令的初始阶段,name 才会上报空字符串。

6.4 重名与超时会以什么方式报复你

重名的后果不是报错,是同层级互冲 :如果你把自定义源命名成 rc,你的指令和手柄指令轮流冲刷覆盖,两条指令在日志里都合法,机器人表现为"一顿一顿"。这类问题难归因,因为没有任何一方"犯错"------日志干净,接口正常,只有机器人在地上一抖一抖。

超时的后果更阴:程序崩溃前没来得及告别,1000ms 后控制权顺延给次级信源,机器人可能突然执行别人的指令。更稳的做法是退出前停发指令并主动 DISABLE,把控制体面地还回去。我在自己的节点里把 DISABLE 挂在了异常退出的 finally 里,宁可多写十行,不赌崩溃时机。

图4:输入源优先级分层与 1000ms 脱锁机制全景

七、dump_interfaces.sh 全解:不接真机导出接口清单

7.1 四个自省命令与过滤技巧

ROS 2 的自省命令分两族:ros2 interface 看类型定义 ,本地静态、无需运行时;ros2 topic/service 看运行时注册,需要节点在线。清点接口清单用前者就够。

先纠正一个高频笔误:ROS 2 并没有 ros2 srv list 这个子命令 ,srv 只是类型的一种分类;列服务要用 ros2 service list(运行时)或从 ros2 interface list 按 /srv/ 过滤(类型全量),把两者写混是最常见的 CLI 笔误。

这节脚本是怎么被逼出来的:我一开始是手动党。ros2 interface list 一敲,几百行滚屏,SDK 自有类型混在 builtin_interfaces、std_msgs 里;ros2 interface show 一个个看字段,grep -c '/srv/' 数个数。手动跑了三四轮,每轮都是复制粘贴加肉眼扫,直到某次 SDK 升级要重新对一遍清单,才下定决心把这套动作固化成脚本。脚本本身没什么高深的,就是把我自己的手工步骤一条条抄进去。

bash 复制代码
# 1) 导出全部类型,只留 aimdk_msgs 命名空间(前缀锚定,排除依赖类型)
ros2 interface list | grep '^aimdk_msgs/' | sort

# 2) 看单个类型的字段定义
ros2 interface show aimdk_msgs/msg/Bms
ros2 interface show aimdk_msgs/srv/SetMcAction   # srv 会输出 --- 分隔的请求/响应两段

# 3) 从类型清单直接统计 msg 与 srv 各有多少
ros2 interface list | grep '^aimdk_msgs/' | grep -c '/srv/'

# 4) 运行时核对:服务与话题的实际注册名(需节点在线)
ros2 service list | grep '_5F' | sort
ros2 topic list -t | grep '/aima'                 # -t 附带消息类型名

这四条命令覆盖四个方向:全量类型导出、单类型字段、分类计数、运行时核对。grep '^aimdk_msgs/' 用行首锚定把 SDK 自有类型从上百个依赖类型里择出来;ros2 interface show 对 msg 输出一段字段,对 srv 输出"---"分隔的请求与响应两段,据此即可判定类型类别;-t 参数让话题列表附带类型名,是核对"话题→类型"映射的捷径。注意事项:所有命令前必须先 source 系统 ROS 和 SDK 的 install/setup.bash,只加载系统 ROS 会报 Cannot determine type------那次就是只 source 了系统 ROS 就开跑,满屏报错,还以为 SDK 没装好。

再补两个过滤技巧:ros2 interface show 的输出常带大段注释,接 awk -F'#' '{print $1}' 可以先剥掉行内注释再数字段;ros2 topic list -t 每行形如"话题名 类型名",配 grep -o '\[.*\]' 能直接抽出类型列,与 interface list 的结果做集合比对,就能找出运行时注册了、类型清单里却没有的异常端点。

7.2 完整脚本逐段解读

下面这个脚本把 SDK 的全部类型清点成一张 Markdown 表,可直接贴进项目文档:

bash 复制代码
#!/usr/bin/env bash
# dump_interfaces.sh ------ 导出 aimdk_msgs 全部消息/服务类型
set -euo pipefail

source /opt/ros/humble/setup.bash
source "${SDK_INSTALL:-$HOME/primebot_sdk/install}/setup.bash"

OUT="${1:-interfaces.md}"

{
  echo "| 类型全名 | 类别 | 字段数 |"
  echo "| :--- | :--- | ---: |"
  # 只保留 aimdk_msgs 命名空间
  ros2 interface list | grep '^aimdk_msgs/' | sort | while read -r t; do
      kind="msg"
      case "$t" in
        */srv/*|*srv*) kind="srv" ;;
      esac
      # interface show 的第一段是类型声明,字段行以 '-' 开头
      n=$(ros2 interface show "$t" 2>/dev/null | grep -cE '^-|^  [a-zA-Z]' || true)
      printf "| \`%s\` | %s | %s |\n" "$t" "$kind" "$n"
  done
} > "$OUT"

echo "已导出:$OUT"
wc -l "$OUT"

逐段说这段脚本在帮我做什么。

开头 set -euo pipefail 是安全三件套:未定义变量即报错、管道中段失败也能被捕获,任何一步翻车立刻中断,避免生成半张表误导后续阅读------我不想对着一张缺行的表排查半小时,最后发现是脚本自己半路死了。

两段 source 分别加载 ROS 2 基础环境和 SDK 自身环境;后者路径支持用 SDK_INSTALL 环境变量覆盖,便于在 CI 里指定预先装好的 SDK 位置,换机器不用改脚本。OUT 接收位置参数,默认输出到当前目录的 interfaces.md,想导出到别处就传个路径进去。

表头三列分别是类型全名、类别、字段数,字段数列右对齐便于扫读。grep '^aimdk_msgs/' 前缀锚定只留 SDK 自有类型,排除 builtin_interfaces、std_msgs 等依赖;紧跟的 sort 保证多次运行输出确定,方便对两次导出做 diff------这条是给"SDK 升级后对比"留的口子。case 分支按路径里是否含 /srv/ 判定服务类型,其余按 msg 计。

字段统计用 grep -cE '^-|^ [a-zA-Z]',同时匹配以 - 开头的字段声明行和缩进的字段名行------因为 ros2 interface show 对嵌套类型的排版并不统一,这个正则对极少数嵌套排版会漏计,误差可接受,追求精确可改用 Python 解析。最后的 printf 把反引号转义进 Markdown,保证类型名以代码格式渲染。

预期输出形态(你的字段数会与这里不同,取决于 SDK 版本):

text 复制代码
| 类型全名 | 类别 | 字段数 |
| :--- | :--- | ---: |
| `aimdk_msgs/msg/Bms` | msg | 17 |
| `aimdk_msgs/msg/TouchState` | msg | 2 |
| `aimdk_msgs/srv/PlayTts` | srv | 3 |

这套脚本的价值在于它不需要机器人 :只要 SDK 编译过,全部类型就在本地 install/ 里,清点是纯本地的静态动作,适合在采购评估、CI 门禁、文档同步三个场景反复跑。

导出结果建议随仓库版本化管理:每次 SDK 升级后重跑并提交,diff 即变更日志。字段数一列还有个隐藏用法------相邻类型字段数接近的,往往是同一族消息的请求/响应变体,读类型时可以先把这一列做聚类,能少读一半 interface show。

7.3 用 Python 做分模块统计

bash 擅长"导出一张表",但如果想按模块(mc / hal / agent)聚合计数,十行 Python 更直白:

python 复制代码
#!/usr/bin/env python3
"""按模块统计 aimdk_msgs 类型分布:输入为 ros2 interface list 的原始输出。"""
import collections
import sys

counts = collections.Counter()
for line in sys.stdin:
    t = line.strip()
    if not t.startswith("aimdk_msgs/"):
        continue
    # 路径形如 aimdk_msgs/<module>/msg/Name 或 aimdk_msgs/<module>/srv/Name
    parts = t.split("/")
    if len(parts) < 4:
        continue
    module, kind = parts[1], parts[2]
    counts[(module, kind)] += 1

for (module, kind), n in sorted(counts.items()):
    print(f"{module:<10} {kind:<4} {n}")
print(f"{'total':<10} {'':<4} {sum(counts.values())}")

用法是 ros2 interface list | python3 count_by_module.py。collections.Counter 让聚合一目了然;路径切分后按段位取模块名与类别,天然对齐官方文档的"模块分类"口径。我拿它的输出和前面那张 15/8/2 统计表交叉验证过:两边对不上就说明有一边数错了------实测对上了,这才敢把 25 这个数写进标题。这个脚本同时是校验 SDK 版本漂移的哨兵:升级后总数或结构变了,diff 一下就知道多了哪些新接口。

图5:四条自省命令构成的无真机接口清点工具箱

八、无真机自环验证:service call 与 topic echo

8.1 CLI 直调示例

官方 CLI 调试指南给的姿势可以直接抄。几条规范先记住:载荷字段必须与 .srv/.msg 定义严格一致;布尔值全小写;字符串"外层单引、内层双引";嵌套对象缺省时用 {} 占位(例如 header: {})。

bash 复制代码
# 1) 切运动模式:201 即位控站立,同步看两层返回码
ros2 service call /aimdk_5Fmsgs/srv/SetMcAction aimdk_msgs/srv/SetMcAction \
  '{header: {}, source: "sdk_rpc", command: {action: {value: 201}}}'

# 2) 查当前模式与生命周期状态
ros2 service call /aimdk_5Fmsgs/srv/GetMcAction aimdk_msgs/srv/GetMcAction \
  '{header: {}}'

# 3) 持续发布速度指令(-r 10 表示 10Hz;source 须为已注册输入源)
ros2 topic pub /aima/mc/locomotion/velocity aimdk_msgs/msg/McLocomotionVelocity \
  '{header: {}, source: "my_app", forward_velocity: 0.2, lateral_velocity: 0.0, angular_velocity: 0.0}' -r 10

# 4) 只收一帧 BMS 反馈,验证订阅链路通不通
timeout 5 ros2 topic echo /aima/hal/bms/state --once

代码块里前两条是 Service 的标准姿势:请求体里 header: {} 占位,source 标明调用方,command.action.value 用枚举数值(201 即位控站立)。注意:位控站立没有平衡调节能力,不会主动抵抗外力,官方文档明确警告需外部固定防摔------很多"机器人自己倒了"的事故,根因就是把位控站立当成了自平衡站立。 话题发布那条,-r 10 指定 10Hz 持续发布,source 必须填第六节注册过的输入源,否则仲裁层不认。用 timeout 包住 echo ... --once 只等一帧就退出,是 CI 里做"链路通不通"探针的标准写法------超时返回非零,恰恰说明订阅链路需要排查。

8.2 从 get_bms_state.py 学订阅姿势

官方 BMS 示例示范了传感器话题的标准订阅姿势,几个细节值得抄进自己的工程。

python 复制代码
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data

from aimdk_msgs.msg import Bms


class BmsEcho(Node):
    def __init__(self):
        super().__init__("bms_echo")
        # 1Hz 传感器流:Best Effort 语义,丢了旧帧不心疼
        self.create_subscription(
            Bms, "/aima/hal/bms/state", self.on_bms, qos_profile_sensor_data
        )

    def on_bms(self, msg: Bms):
        # 官方字段单位:电压 mV、电流 mA、功率 mW,展示层才换算
        self.get_logger().info(
            f"voltage={msg.voltage / 1000:.3f}V "
            f"current={msg.current / 1000:.3f}A "
            f"soc={int(msg.remaining_capacity_percentage)}% "
            f"temp={msg.temperature:.1f}C cycles={msg.cycle_count}"
        )

QoS 选 qos_profile_sensor_data------Best Effort 加浅队列,与 BMS 话题 1Hz 的发布节奏正匹配,换 Reliable 反而可能因历史数据堆积拖延首帧到达。我照抄示例时没多想,后来自己在别的传感器话题上换了 Reliable,首帧延迟变慢,才回头想通这一层。

单位换算要放在展示层:原始字段是 mV、mA、mW,直接打印会得到放大了上千倍的读数------日志里躺着一个没换算单位的数,谁能反应过来是电压还是电流。这是文档里不会提醒的暗坑。

cycle_count 与 remaining_capacity_percentage 是电池健康度的一线指标,做长航时任务时应纳入监控。无真机时可以用 ros2 topic pub 向这个话题灌 mock 数据自环验证,整套回调逻辑不需要机器人参与。顺带一提,Bms 还带 manufacturer、serial_number、protocol_version 等身份字段,做设备资产管理时可以直接从这条 1Hz 话题里采集,不必另建通道。

8.3 把动作串成时序

机器人 velocity Topic SetMcAction 你的程序 机器人 velocity Topic SetMcAction 你的程序 #mermaid-svg-qNeFlmMwpU3xdB5q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qNeFlmMwpU3xdB5q .error-icon{fill:#552222;}#mermaid-svg-qNeFlmMwpU3xdB5q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qNeFlmMwpU3xdB5q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qNeFlmMwpU3xdB5q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qNeFlmMwpU3xdB5q .marker.cross{stroke:#333333;}#mermaid-svg-qNeFlmMwpU3xdB5q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qNeFlmMwpU3xdB5q p{margin:0;}#mermaid-svg-qNeFlmMwpU3xdB5q .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qNeFlmMwpU3xdB5q text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qNeFlmMwpU3xdB5q .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qNeFlmMwpU3xdB5q .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qNeFlmMwpU3xdB5q #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qNeFlmMwpU3xdB5q .sequenceNumber{fill:white;}#mermaid-svg-qNeFlmMwpU3xdB5q #sequencenumber{fill:#333;}#mermaid-svg-qNeFlmMwpU3xdB5q #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qNeFlmMwpU3xdB5q .messageText{fill:#333;stroke:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qNeFlmMwpU3xdB5q .labelText,#mermaid-svg-qNeFlmMwpU3xdB5q .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .loopText,#mermaid-svg-qNeFlmMwpU3xdB5q .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qNeFlmMwpU3xdB5q .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qNeFlmMwpU3xdB5q .noteText,#mermaid-svg-qNeFlmMwpU3xdB5q .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qNeFlmMwpU3xdB5q .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qNeFlmMwpU3xdB5q .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qNeFlmMwpU3xdB5q .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qNeFlmMwpU3xdB5q .actorPopupMenu{position:absolute;}#mermaid-svg-qNeFlmMwpU3xdB5q .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qNeFlmMwpU3xdB5q .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qNeFlmMwpU3xdB5q .actor-man circle,#mermaid-svg-qNeFlmMwpU3xdB5q line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qNeFlmMwpU3xdB5q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每 20ms 中断超 1000ms → 自动脱锁 切 BIPED_WALK_RUN header.code=0, status.value=1 再 GetMcAction 确认 RUNNING 发布速度指令(source=my_app) 仲裁通过,执行

图:从切模式到持续控速的完整时序,脱锁看门狗覆盖整个控制循环。

这张时序图把前两节串了起来:切模式是 Service 的"一问一答",控速是 Topic 的"持续广播",而 1000ms 看门狗横跨两者------控制循环每 20ms 发一帧,比脱链阈值快五十倍,这就是"控制循环必须远快于 1Hz"的量化含义。20ms 对应 50Hz,落在官方给的 50-100Hz 区间下沿;按 1Hz 发的话,看门狗每轮都在脱链边缘试探。

九、边界与坑:文档不会主动告诉你的事

9.1 命名空间混用:不能靠前缀猜类型

/aima/hal/camera/CaptureJpegImage 是唯一"前缀在 /aima/hal/ 下但类型是 Service"的接口;RTSP 视频流又是地址模板而非 ROS 接口,ros2 interface list 里根本翻不到它。判断类型的唯一可信来源是 ros2 interface list 的输出,不是前缀猜的。 写自动化工具(接口文档生成器、类型校验器)时,这一点直接决定正确性。

9.2 QoS 不是摆设

ROS 2 的发布与订阅必须在 QoS 上兼容才能建联,选错的表现是"代码没报错,就是收不到数据":

接口 QoS 频率 选型理由
/aima/mc/locomotion/velocity Reliable / Volatile / Depth=10 50-100 Hz 控制命令必须到达
/aima/mc/custom/joint/command Best Effort / Volatile / Depth=10 100 Hz 新帧覆盖旧帧即可
/aima/hal/joint/command Best Effort / Volatile / Depth=10 1000 Hz(实际可能达不到) 高频伺服流
/aima/hal/joint/state Best Effort / Volatile / Depth=10 高频 闭环反馈
/aima/hal/bms/state Best Effort / Volatile / Depth=10 1 Hz 官方示例用 qos_profile_sensor_data
/aima/hal/touch/state Reliable / Volatile / Depth=1 触发式上报 无触摸不发布,深度 1 就够
/aima/hal/audio/audio_device_list Best Effort / Volatile / Depth=10 1 Hz 设备热更新通知
/aima/agent/asr_result Best Effort / Volatile / Depth=10 事件驱动 关注 is_final 终判
全部 Service Reliable(服务默认) 按调用 低频动作默认可靠优先

规律可以一句话收拢:控制指令流要 Reliable 保必达,高频传感器流用 Best Effort 换低延迟(新数据比旧数据值钱),触发式事件给 Depth=1 就够。最常见的翻车是订阅端手写 Reliable 去接 Best Effort 发布端------ROS 2 的 QoS 兼容矩阵里这一对直接拒绝建联,ros2 topic info -v 中能看到端点存在却互不匹配;代码不报错,就是收不到数据,这种静默失败最折磨人。抄官方示例的 QoS,是最省事的避雷方式。另外底层关节指令还有两硬约束:单帧位置增量不得超过 0.2 rad,退出前必须补一帧 stiffness: 0.0, damping: 5.0 的保护帧,严禁直接断连。

9.3 srv 与 topic 的边界再强调

客户端创建有时序坑:即使 wait_for_service 返回成功,底层链路匹配仍需毫秒级时间,首次请求可能"发出了但服务端没收到" ,跨芯片通信场景更明显。官方建议 Client 在节点初始化时一次性创建并全局复用,杜绝在业务循环里反复创建销毁;必须动态创建时,就绪后额外等约 200ms 或为首次调用加重试。我在自环脚本里吃过这个亏------wait_for_service 明明返回了,紧跟着的 call 还是空手而归,补了 200ms 等待才稳。

9.4 其他适用边界

本文清单基于 aimdk_msgs 源码文件树 而非运行时抓包,SDK 版本更早或更新时接口可能增减,请以 ros2 interface list 实际输出为准。TCP :4713 / :6001 两个音频端口有社区反馈"实际机器人默认没开",需要按需确认;本文只陈述文档所述分配(6001 播放、4713 录制),未做真机验证。官方仅支持 Ubuntu 22.04 + ROS 2 Humble 做开发编译,Mac / Windows 不在支持列表。任何涉及电机控制的代码,上线前必须先读 SDK 根目录的《友好用户使用声明》。

最后提醒时效:本文写于 2026-10,对应 main 分支状态;若你读到时已隔数月,先用第七节脚本跑一遍本地清单,确认接口集合没有增减,再据此校对本文各表。接口地图这东西,本来就该跑出来,不是抄出来。

十、总结

回到开头的问题:为什么我放弃翻文档,转而读消息包。50 万字节的文档回答的是"这个接口怎么用",而摸底阶段真正的问题是"到底有哪些接口"------后者只有消息包能一次答全。25 个落点清完之后,最稳的两个锚点也定了:服务名照抄 _5F、类型名照抄 aimdk_msgs,抄屏幕不抄记忆;响应先看 header.code 再看 status.value,四个大值是排队不是失败。

如果只做一件事,我会把 dump_interfaces.sh 挂进 CI:SDK 每次升级跑一遍,diff 就是变更日志。成本十分钟,收益是文档漂移再也骗不了你。接口清单会过期,但"读消息包"这个动作不会------它不依赖真机、不依赖文档版本,甚至不依赖网络,一台装了 ROS 2 Humble 的 Ubuntu 22.04 就够了。

也要说清这份地图的边界:25 个落点是当前分支的公开接口,MIMIC 与预设动作另走 SetMcMotion / SetMcPresetMotion,不在 SetMcAction 的显式边里;输入源命名、QoS 这些周边规矩同样会随版本动。所以我把第七节的脚本留在仓库里,而不是把表抄进备忘录------表会过期,脚本不会。

下次再摸一套新 SDK,我还是这个顺序:先编译,再 ros2 interface list,然后才是文档。

参考资料

  1. 接口说明.md(SDK 仓库根目录),含 25 个接口的完整协议定义与 CommonState 枚举
  2. aimdk_msgs/ 目录源码:https://github.com/PrimebotTech/primebot-q1-sdk
  3. ROS 2 接口概念文档:https://docs.ros.org/en/humble/Concepts/Basic/About-Interfaces.html
  4. ROS 2 QoS 说明:https://docs.ros.org/en/humble/Concepts/Intermediate/About-Quality-of-Service-Settings.html
  5. 启元机器人开发者社区:https://dev.primebot.com/
相关推荐
汤姆yu1 小时前
MiniMax M3.1 Flash:重新定义「智能体」的性能与成本
ai·大模型·minimax m3.1
Meta Luo1 小时前
552B 的模型,8B 的成本:DeepSeek V4.1 Flash 1017 到底改了什么
人工智能·ai·aigc
Quanqiucard2 小时前
四轮巡检机器人物联网卡怎么选?5 条硬指标 + 4 重干扰
物联网·机器人
鲁邦通物联网2 小时前
低底盘机器人机器人梯控门区通行与夹碰风险控制设计
机器人·巡检机器人·机器人梯控·agv梯控·非侵入式采集·机器人乘梯·机器人自主乘梯
wang_yb3 小时前
让 LLM 输出不再失控:用 Pydantic 校验大模型输出
ai·databook
FlyWIHTSKY4 小时前
Agent阶段一:基础夯实
python·ai·fastapi
硅谷秋水4 小时前
VLMeg:把 VLM 从奖励回归器变成成功检测器
人工智能·机器学习·语言模型·数据挖掘·机器人
Data_AI4Science4 小时前
AI 时代,人更重要了
人工智能·ai·智能体·生物医药·电子实验记录本·ai取代人
MounRiver_Studio4 小时前
原生集成AI 能力的 RISC-V IDE:第三代 MounRiver Studio 正式发布
ide·ai·agent·risc-v