User_芊芊君子头像
启元 Q1 二开实战:一个指令中控台,串起 8 个 SDK 接口封面图

启元 Q1 二开实战:一个指令中控台,串起 8 个 SDK 接口

SDK 二开的最后一步,是从"调通单个接口"走到"写一个完整应用"。本文记录给启元 Q1 写一个指令中控台的全过程:浏览器输入"笑一个",中控台解析关键词、规划动作状态机路径、按 500ms 限频节奏调用 8 个 SDK 服务(语音、表情、灯带、预设动作、速度控制),再把每一步的响应码实时回显到面板。七个演示指令全部实测通过,其中"挥挥手"触发了接口文档里最隐蔽的一个行为:直连切换被 -1001 拒绝后按官方迁移规则 BFS 改道,全程 7.1 秒,本文涉及代码全部开源。 仓库地址:https://atomgit.com/user3401/q1_command_center

在这里插入图片描述

一、从调通接口到写完整应用

启元 Q1 的 SDK 提供 16 个 ROS2 服务端和一批状态话题,官方示例把每个接口都演示了一遍:play_emotion.py 播表情、mc_locomotion_velocity.py 控制行走、play_lights.py 驱动灯带。跟着示例走,每个接口都能跑通。

但示例之间是孤立的。真实的应用不会只调一个接口:用户说"笑一个",机器人要做的不止是播一个表情,而是"回一句话、做个表情、亮个灯"的组合。把多个接口组合起来,问题就从一个变成了三个:

  1. 模式问题:预设动作要求机器人处于 BIPED_WHOLE_BODY_CTRL 模式,行走要求 BIPED_WALK_RUN。当前模式不满足时怎么过去?直接调 SetMcAction 可能被拒绝(-1001,无迁移规则)。
  2. 节奏问题:SetMcAction 有 500ms 限频(-10001),动作切换有物理过渡期。连发指令会撞墙。
  3. 反馈问题:每一步调用的结果(受理、拒绝、限频重试)如果不回显,出了问题无从排查。

这三个问题正是单接口示例和完整应用之间的距离。本文的指令中控台 q1_command_center 就是为了补上这段路:一条指令进来,编排器负责模式、节奏、反馈三件事,浏览器面板实时可见。

在这里插入图片描述

二、环境搭建与部署

整套工程对环境的要求只有一张表:

组件版本说明
Ubuntu22.04.5WSL2 亦可
ROS 2Humble
Q1 SDKv1.0.0.0开发者社区下载页获取,aimdk_msgs 需编译
机器人侧q1_simulator 或真机无真机用开源模拟器扮演服务端
setuptools58.2.0必须 ≤58.x,新版会破坏 ament 构建
fastapi / uvicorn0.162.0 / 0.54.0面板依赖,纯 wheel 安装

2.1 前置环境(配置过可直接跳过)

从一台干净的 WSL2 到"能编译 SDK",完整路线:

# 1 ROS2 Humble(apt 源走 TUNA 镜像;密钥从 keyserver 的 80 端口取,
#    raw.githubusercontent.com 不可达也不影响)
sudo apt update && sudo apt install -y software-properties-common curl
sudo add-apt-repository -y universe
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys \
    C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
echo "deb [arch=$(dpkg --print-architecture)] \
https://mirrors.tuna.tsinghua.edu.cn/ros2/ubuntu jammy main" \
    | sudo tee /etc/apt/sources.list.d/ros2.list
sudo apt update && sudo apt install -y ros-humble-desktop ros-dev-tools
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc

# 2 SDK 要求的内核网络参数(DDS 大传输的缓冲上限,官方 FastDDS 配置依赖)
printf "net.core.rmem_max=25165824\nnet.core.wmem_max=25165824\n" \
    | sudo tee /etc/sysctl.d/99-primebot-sdk.conf
sudo sysctl --system

# 3 Q1 SDK:开发者社区下载页取 zip,解压后编译消息包(实测 21.7s)
cd ~/workspace && unzip sdk_q1*.zip && mv sdk_q1-v1.0.0.0 sdk_q1
cd ~/workspace/sdk_q1 && colcon build --packages-select aimdk_msgs

# 4 机器人侧替身 q1_simulator(无真机时的仿真服务端,依赖 aimdk_msgs)
cd ~/workspace && git clone https://atomgit.com/user3401/q1_simulator.git
source ~/workspace/sdk_q1/install/setup.bash
cd ~/workspace/q1_simulator && colcon build

第 1 步执行 apt-key 时会有一条"apt-key is deprecated"警告,Humble 时代可忽略。

2.2 部署中控台:三步

部署三步,可直接照做:

# 1. 依赖(注意 setuptools 版本,pip 装东西前先看一眼)
pip3 install --user fastapi uvicorn
pip3 show setuptools | grep Version   # 不是 58.x 就降级

在这里插入图片描述

# 2. 编译中控台(SDK 的 install 要先进环境,aimdk_msgs 从这来)
source /opt/ros/humble/setup.bash
source ~/workspace/sdk_q1/install/setup.bash
cd q1_command_center && colcon build && source install/setup.bash

在这里插入图片描述

# 3. 两个终端分别起机器人侧与中控台
# 终端 1:
source /opt/ros/humble/setup.bash 
source ~/workspace/sdk_q1/install/setup.bash 
source ~/workspace/q1_simulator/install/setup.bash
ros2 run q1_simulator simulator       # 终端 1:机器人侧(真机则跳过)

# 终端 2:
# 同上三条 source,再加 
source ~/workspace/q1_command_center/install/setup.bash
ros2 run q1_command_center center     # 终端 2:中控台

在这里插入图片描述

在这里插入图片描述

浏览器打开 http://localhost:8080 就是面板。这里有一个环境细节值得单独说:q1_simulator 与 aimdk_msgs 在两个不同的工作区里,两条 install 都要 source。只 source SDK 那条,ros2 run 会报 Package 'q1_simulator' not found,这是本文实测撞到的坑(根因是 ROS2 的包查找只认 AMENT_PREFIX_PATH 里已有的前缀)。

在这里插入图片描述

在这里插入图片描述

三、总体设计:一个进程,两条流水线

在这里插入图片描述

Q1 指令中控台总体架构:下行指令流(实线)从浏览器经解析、编排到服务调用,上行状态流(虚线)把动作态、输入源、电量、位移实时送回面板。

中控台是一个 ROS2 节点 + 一个 Web 服务的单进程组合:

  • 下行(指令流):浏览器 POST /api/command → FastAPI → 意图解析(关键词匹配 commands.json 配置表)→ 编排器按步骤序列依次调用 SDK 服务;
  • 上行(状态流):GetMcAction 动作态、GetCurrentInputSource 输入源、Bms 电量订阅、odom→pelvis TF 位移,汇成 /api/state,面板每 500ms 轮询刷新。

单进程里跑两个"世界",线程模型值得交代:rclpy 的 executor 在后台线程转,负责收发 DDS 消息;uvicorn 在主线程转,负责 HTTP。两个世界交接的地方有一个容易踩的坑:rclpy 的 Future.result() 不会阻塞等待,直接在工作线程里调它,拿到的多半是 None。正确做法是挂回调、等事件:

def call(self, client, request, timeout=3.0):
    """后台 executor 处理响应,本线程用 Event 等待完成。"""
    done = threading.Event()
    future = client.call_async(request)
    future.add_done_callback(lambda _f: done.set())
    if not done.wait(timeout):
        return None          # 服务超时
    return future.result()

独立写小脚本验证接口时则是另一套写法:没有常驻 executor,用 rclpy.spin_until_future_complete(node, future) 补转。两种场景两种写法,混用就会写出"永远等不到响应"的程序(本文实测踩过,详见仓库 notes)。

四、接口清单: 8 个服务与 1 个话题

中控台消费了机器人侧 16 个服务端中的 8 个,外加 1 个速度话题。全部接口的关键字段与坑位如下(版本 v1.0.0.0,写码前逐个用 ros2 interface show 预检过):

接口用途关键字段 / 坑
GetMcAction查当前动作态空闲判断必须读 action_desc,current_action.value=0 不可判空闲
SetMcAction切换动作态业务码在 response.state.value:1 受理 / -1001 无迁移规则 / -10001 限频 / -1003 未定义
SetMcPresetMotion预设动作需先处 BIPED_WHOLE_BODY_CTRL;3001 招手、3002 握手、3004 挥手、3046 跳舞A
SetMcInputSource输入源增删1001=ADD、1003=DELETE;先删残留再注册(官方示例同款流程)
GetCurrentInputSource查速度控制权未受控时 input_source.name 为空串
PlayTts语音播报优先级用 TtsPriorityLevel.INTERACTION_L6;成功看 tts_resp.is_success
PlayEmotion播表情Q1 不支持 ID 10,官方示例将其补丁为 3003;成功判定见下文坑位
LedStripCommand灯带模式常量挂在 Request 类上(LED_RAINBOW=2、LED_CUSTOM_BREATH=254)
/aima/mc/locomotion/velocity速度指令source 必须与注册的输入源同名;50Hz 心跳;下限 |v|≥0.2 或 =0

其中 PlayEmotion 和 LedStripCommand 的成功判定有一个共同的坑:响应外层是 CommonResponse,里面还套着一层 header。写成 resp.header.code 会直接 AttributeError,正确的是 resp.header.header.code == 0(或 resp.header.status.value == 1)。本文第一版就栽在这里,表情服务调用在面板上直接看到异常回显,两分钟定位。

这类字段级细节,接口文档里有但散在各章。写应用前逐个预检一遍服务定义(一条命令的事),比运行时排查快得多。

五、核心实现一:解析层用配置驱动

意图解析刻意做成零 ROS 依赖的纯数据模块:commands.json 一张配置表,读者加指令不改代码。

{
  "name": "smile",
  "keywords": ["笑一个", "笑一下"],
  "summary": "表情 3003 + 蓝色呼吸灯",
  "steps": [
    { "type": "tts", "text": "哈哈,被你逗笑了" },
    { "type": "emotion", "id": 3003 },
    { "type": "led", "mode": "custom_breath", "r": 0, "g": 100, "b": 255, "period": 1500 }
  ]
}

匹配逻辑:精确命中优先,否则取文本中包含的最长关键词(“帮我挥个手吧"命中"挥个手”)。八种步骤类型覆盖了本文用到的全部能力:tts / emotion / led / motion(预设动作)/ walk(速度控制)/ stand / halt / wait。

设计取向说明:这里用的是规则表而不是大模型意图识别,是因为本篇的论证重点是接口编排而不是语义理解。配置表结构里 intent 到 steps 的映射是稳定的,前面接 ASR 结果还是接大模型输出,解析层接口不变——这是留给读者的扩展口。

一个真实的迭代细节:初版关键词表里没有"挥个手",实测"帮我挥个手吧"漏识别——包含匹配的前提是关键词真的出现在文本里,补词后通过。规则表方案的可维护性正体现在这类修补只需改一行 JSON,不用动代码。

六、核心实现二:编排器的三个工程细节

编排器是中控台的核心,SequenceRunner 把每条指令的步骤序列翻译成安全的 SDK 调用。先看全景:一条「挥挥手」从受理到完成,7.1 秒里发生了一次被拒、两次改道跳、四轮过渡确认

在这里插入图片描述

6.1 模式保障:直连被拒不报错,BFS 改道

预设动作前必须进入 BIPED_WHOLE_BODY_CTRL。实测的机器人初始态是 BIPED_STAND_DEFAULT,查迁移规则表会发现:这两个状态之间没有直连边。直接调:

code = node.set_action('BIPED_WHOLE_BODY_CTRL')
# 实测返回:-1001(no transition rule from BIPED_STAND_DEFAULT ...)

官方客户端 mc_action_switcher.py 的做法是按规则表 BFS 规划路径。中控台同款处理,区别在于把过程打了出来:

def _ensure_action(self, target, no):
    desc, status = self.node.query_action()
    if desc == target and status in (MC_RUNNING, MC_IDLE):
        self.log('状态', f'已在 {target},跳过切换')
        return
    code = self._set_action_checked(target, no)     # 直连
    if code == 1:
        self._wait_transition(target, no)
        return
    if code != CODE_NO_ROUTE:                        # -1001 才改道
        raise _StepError(no, f'切换 {target} 被拒(code={code})')
    route = plan_route(desc, target)                 # BFS 最短路径
    self.log('改道', f'直连被拒(-1001),BFS 改道:{" → ".join(route)}')
    for hop in route[1:]:
        self._set_action_checked(hop, no)
        self._wait_transition(hop, no)               # 每跳确认过渡完成

在这里插入图片描述

编排器的模式保障逻辑:先查态、再直连、被 -1001 拒绝后按官方规则表 BFS 改道,每跳都等过渡完成再走下一步。

实测("挥挥手"指令,从初始态开始):直连被拒 → BFS 给出 STAND_DEFAULT → WALK_RUN → WHOLE_BODY_CTRL → 两跳各等过渡完成 → 预设动作 3004 受理,指令全程 7.1 秒。其中两跳切换各自花费约 2 秒,是动作过渡的物理时间,省不掉,只能等。

把规则表本身画出来,改道的几何一目了然:

在这里插入图片描述

McAction 迁移图(核心子图):三个高亮态是指令链路的工作模式;红色虚线是"规则表无此边"的直连(实测 -1001),橙色实线是 BFS 给出的改道路径——改道不是猜测,是按官方迁移规则表算出的最短路径。

6.2 限频合规:主动间隔 550ms,被动重试兜底

SetMcAction 限频 500ms。编排器做了两层:主动层,每次调用前检查距上次的间隔,不足 550ms 就等足;被动层,仍然收到 -10001 时等 600ms 重试(至多 3 次)。主动层让限频几乎不会发生,被动层保证极端情况不崩。

限频行为用仓库里的最小探针(notes/rate_limit_probe.py)实测,两次运行两种结果:

# 运行 1:起始 STAND_DEFAULT,第一次调用真实触发迁移
当前动作态: BIPED_STAND_DEFAULT
第 1 次 SetMcAction(BIPED_WALK_RUN) → 1
立刻第 2 次(BIPED_STAND_DEFAULT) → -10001     ← 500ms 限频,确定性复现
等 700ms 后再切 → -1001                        ← 过渡期中连原状态也不是合法起点
过渡完成后再切 → 1

# 运行 2:起始已是 WALK_RUN(第一次为幂等受理,无过渡产生)
第 1 次 → 1(幂等)
立刻第 2 次 → -10001
等 700ms 后切 STAND → 1                       ← 无过渡进行中,直连合法

运行 1 的第三行是本文最有价值的意外收获:过渡期里连"切回原状态"都会被拒。根因在状态机语义:TRANSITION 期间当前态还是旧值、迁移目标挂起中,此时任何非规则边的目标(包括旧状态自己)都不是合法起点。这解释了为什么"等过渡完成再走下一步"不是可选的优化,而是正确性的必要条件。

6.3 过渡确认与输入源生命周期

过渡确认的实现是轮询:每 0.5s 调一次 GetMcAction,直到 action_desc == 目标 且 status == RUNNING,超时 20s。真机的物理过渡可达数秒,这个等待省略了就是下一发指令撞墙。

速度控制则完整对齐官方 mc_locomotion_velocity.py 的五段生命周期:DELETE 清残留 → ADD 注册(name=command_center, priority=80, timeout=1000ms)→ 50Hz 心跳发布 → 归零 → DELETE 释放。timeout=1000ms 是看门狗:心跳一旦断供,速度控制权 1 秒内自动回收,所以发布循环里 sleep 的精度直接关系到安全。"过来"指令实测:

21:42:54 状态 | 直连切换 BIPED_WALK_RUN 受理
21:42:57 状态 | BIPED_WALK_RUN 已 RUNNING(过渡完成)
21:42:59 速度 | 0.3 m/s × 2.0s 已执行,期望位移 ≈0.6m
21:42:59 步骤 | (3/3) stand
21:43:01 状态 | BIPED_STAND_DEFAULT 已 RUNNING(过渡完成)

面板上的 odom 位移读数:x=0.615m。期望 0.6m,多出的 1.5cm 来自 50 次循环里 sleep 的调度开销(每步 0.02s 实际略长)。误差 2.5%,对位姿演示足够;较真的读者可以把发布改成 ROS timer 驱动,精度会更好。

在这里插入图片描述

「过来」指令的 RViz 画面:模型沿 x 轴前移约 0.6m,与面板 odom 读数一致;行走期间输入源卡片显示 command_center 持有速度控制权,结束后自动释放。

七、运行效果:面板、指令与全链路实测

七个演示指令全部实测通过:

说什么干什么实测耗时
你是谁TTS 自我介绍0.0s
笑一个TTS + 表情 3003 + 蓝色呼吸灯0.0s
挥挥手挥手 3004(含 -1001 自动改道)7.1s
握个手握手 3002(已在目标模式则跳过切换)2.0s
过来前进 0.3m/s × 2s(实测位移 0.615m)→ 站定6.6s
跳个舞彩虹灯 + 跳舞 3046 + 表情7.6s
停急停:清速度 + 释放输入源 + 回站立2.5s

在这里插入图片描述

左侧四张状态卡实时显示动作态、输入源、电量(BMS 实采 64%)与位移;右侧日志区逐步回显每条指令的解析、改道、限频与完成事件。

面板之外,全部能力等价于 REST 调用,脚本可以直接集成:

curl -X POST localhost:8080/api/command \
     -H "Content-Type: application/json" -d '{"text":"笑一个"}'
# {"ok":true,"message":"已受理:smile"}

curl localhost:8080/api/state | python3 -m json.tool

两个防御性行为也实测确认:未配置的指令(如"去洗碗")会被拒绝并提示支持列表;一条指令执行中再发新指令,返回"上一条指令还在执行"(编排器单飞行模式,避免两条指令的状态机操作交错)。

八、总结与边界

把中控台从设计写到实测,对"二开"这件事的体感可以归结为一句话:单个接口是文档问题,接口组合才是工程问题。模式保障、限频合规、过渡确认这三件事,接口文档都写了,但只有真把它们串进一条指令链里,才知道每一条都是必答题。七个指令的实测数据都在上面,仓库里还有完整的执行日志与限频探针,可复现。

边界也要说清楚:本文机器人侧用的开源模拟器在接口语义上与真机对齐(服务名、业务码、状态机、限频、看门狗行为一致),但物理反馈是模拟的(预设动作播放的是关键帧动画,位移是速度积分)。换真机时中控台代码零修改,切换点只有一条启动命令;速度控制的底盘表现(打滑、惯性)需要在真机上重新验证,这是仿真验证与真机部署之间永远存在的一步。

下一个自然的扩展是把 asr_result 话题接进解析层,让"说话"替代"打字",指令链路的其余部分不用动。得益于解析层与执行层的解耦,接入只需十几行——订阅语音识别结果,把文本交给与 Web 面板同一个入口:

# 扩展骨架:语音指令接入(加入 RobotClient 即可,编排链路零修改)
from rclpy.qos import qos_profile_sensor_data
from aimdk_msgs.msg import AsrResult

self.create_subscription(AsrResult, '/aima/agent/asr_result',
                         self.on_asr, qos_profile_sensor_data)

def on_asr(self, msg):
    if msg.is_final and msg.confidence > 0.6:      # 只取定稿、置信度达标
        ok, message = self.runner.submit(msg.text)  # 与 Web 同一入口
        self.get_logger().info(f'[ASR] "{msg.text}" → {message}')

本篇的 Q1 SDK 与接口文档来自启元机器人开发者社区,SDK 下载、接口文档与开发者交流都在社区完成,配套的开源模拟器同样来自社区的官方开源组织。

参考资料

  1. 启元机器人开发者社区(Q1 SDK 与文档下载):https://dev.primebot.com/
  2. 本文配套仓库 q1_command_center:https://atomgit.com/user3401/q1_command_center
  3. 官方开源组织(AtomGit,含 q1_simulator):https://atomgit.com/primebot
  4. ROS 2 Humble 文档(executor 与服务调用模型):https://docs.ros.org/en/humble/

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/user340/article/details/169168459

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--