启元Q1机器人SDK实操:跑通触摸消息,用ROS 2图形界面完成调试与模型展示

第一次打开机器人 SDK,我最想确认的是:应用程序怎样收到机器人的反馈,收到以后又该在哪里写自己的处理逻辑?启元 Q1 的头部触摸事件适合作为起点。传感器编号和事件类型都能直接读出来,回调也很短,可以把注意力放在接口接入上。

这篇文章从一条 TouchState 消息开始。先把官方消息包编译出来,用 Python 接收并核对序号,再打开 ROS 2 图形工具观察节点与字段。最后加载官方 Q1 模型,亲手调节肩肘滑条,看到关节角怎样变成屏幕中的姿态。两组实验分别覆盖消息接入和模型显示,便于为后续开发准备可复用的起点。

这次实验在一台远程 Ubuntu 服务器上完成。触摸输入由本机测试程序生成,统一带有 sdk_local_test 标记;官方 SDK 提供消息类型和订阅示例,自写 Python 节点负责接收与统计。模型部分使用官方 Q1 URDF,由关节状态驱动 RViz 中的姿态显示。文中的截图和动图来自实际运行的终端与 ROS 图形界面,另绘的流程图和 QoS 示意图用于解释消息路径与匹配关系。

图 1:测试消息在 Ubuntu 上生成,经话题送入 Python 订阅回调;Mac 负责远程操作,ROS 节点在同一台 Ubuntu 主机上通信。实际运行结果见后文截图。

阅读本文需要基本的 Linux 命令行和 Python 知识。安装与消息包构建使用官方资源,正文给出完整的发布与简洁订阅程序,并结合测试消息的生成逻辑与实际画面解释结果。阅读时可以分别关注消息怎样发出、回调收到什么,以及关节角怎样反映到模型上。

本文配套项目版本为 v1.0.0,包含安装与构建、Python 消息收发、QoS 检查和 RViz 模型演示代码。各文件的用途在第七节统一说明,资料来源与代码仓库链接列在文末。

目录

  • [一、搭好环境,让 SDK 的消息类型先跑起来](#一、搭好环境,让 SDK 的消息类型先跑起来)
    • [1. 明确开发机、运行环境和图形工具的分工](#1. 明确开发机、运行环境和图形工具的分工)
    • [2. 先构建这次真正用到的消息包](#2. 先构建这次真正用到的消息包)
    • [3. 分清安装成功、构建成功和当前终端可用](#3. 分清安装成功、构建成功和当前终端可用)
  • [二、读懂 TouchState:一条触摸消息里有什么](#二、读懂 TouchState:一条触摸消息里有什么)
    • [1. 先确认接口方向,再看消息定义](#1. 先确认接口方向,再看消息定义)
    • [2. 用编号、事件和序号读懂一次反馈](#2. 用编号、事件和序号读懂一次反馈)
    • [3. 区分事件反馈与定频测试数据](#3. 区分事件反馈与定频测试数据)
  • [三、用 Python 接住消息,完成第一次收发验证](#三、用 Python 接住消息,完成第一次收发验证)
    • [1. 给每个终端加载同一套运行环境](#1. 给每个终端加载同一套运行环境)
    • [2. 先运行官方示例,再看测试消息怎样生成](#2. 先运行官方示例,再看测试消息怎样生成)
    • [3. 把接入逻辑收进一个可读的 Python 回调](#3. 把接入逻辑收进一个可读的 Python 回调)
    • [4. 从"打印出来了"进一步核对十条消息](#4. 从“打印出来了”进一步核对十条消息)
  • [四、打开 ROS 图形工具,把消息流看清楚](#四、打开 ROS 图形工具,把消息流看清楚)
    • [1. 用一段持续消息流观察节点关系](#1. 用一段持续消息流观察节点关系)
    • [2. 展开 Topic Monitor,观察实际字段变化](#2. 展开 Topic Monitor,观察实际字段变化)
    • [3. 图形工具也有自己的订阅配置](#3. 图形工具也有自己的订阅配置)
  • 五、弄清两个常见问题,让接入过程可复现
    • [1. 编译成功,Python 却找不到 aimdk_msgs](#1. 编译成功,Python 却找不到 aimdk_msgs)
    • [2. 能发现发布者,却一条也收不到](#2. 能发现发布者,却一条也收不到)
    • [3. 保留那次只收到九条的记录](#3. 保留那次只收到九条的记录)
  • [六、走进 RViz,亲手调节 Q1 模型的肩肘关节](#六、走进 RViz,亲手调节 Q1 模型的肩肘关节)
    • [1. 找到模型,先分清 link 与 joint](#1. 找到模型,先分清 link 与 joint)
    • [2. 保留官方模型,做最小的 ROS 2 启动适配](#2. 保留官方模型,做最小的 ROS 2 启动适配)
    • [3. 看懂滑条到模型之间的数据流](#3. 看懂滑条到模型之间的数据流)
    • [4. 手动调整左肩和左肘,逐项对照变化](#4. 手动调整左肩和左肘,逐项对照变化)
    • [5. 连续姿态演示中的关节选择与状态更新](#5. 连续姿态演示中的关节选择与状态更新)
  • [七、从这次实测出发,继续做自己的 Q1 应用](#七、从这次实测出发,继续做自己的 Q1 应用)
    • [1. SDK 帮忙省下了哪些工作](#1. SDK 帮忙省下了哪些工作)
    • [2. 把消息接入和模型显示分别核对](#2. 把消息接入和模型显示分别核对)
    • [3. 看懂项目文件,再继续开发](#3. 看懂项目文件,再继续开发)
  • 参考资料

一、搭好环境,让 SDK 的消息类型先跑起来

1. 明确开发机、运行环境和图形工具的分工

Q1 SDK 的快速入门文档要求 Ubuntu 22.04 与 ROS 2 Humble。这次使用的 SDK 来自启元机器人社区下载页,版本为 v1.0.0.0。Mac 上通过 Cursor 连接 Linux,编译和 ROS 节点都运行在 Linux 上。

项目 本次实际环境
系统 Ubuntu 22.04.4 LTS,x86_64
Python 3.10.12
ROS ROS 2 Humble,安装 Desktop 包
消息通信实现 rmw_fastrtps_cpp,包版本 6.2.10
图形工具 rqt_graph 1.3.2、rqt_topic 1.5.1、RViz2 11.2.29
模型与关节工具 Q1 URDF v1.0.0(包内 q1_v3),robot_state_publisher 3.0.3、joint_state_publisher_gui 2.4.0
Q1 SDK v1.0.0.0
本文配套示例项目 v1.0.0

这里选 ROS Desktop,是为了直接使用图形工具。rqt_graph 用来看节点和话题关系,Topic Monitor 用来看字段值;后文再用 RViz2 显示官方 Q1 模型,观察关节角变化怎样反映到姿态上。

远程操作时,最容易混淆的是"在哪台机器上执行"。这次 Mac 上的 Cursor 负责编辑和终端操作,Python、ROS 节点、图形程序都在 Ubuntu 上运行。浏览器中出现 ROS 窗口,也只是把服务器的图形会话传到眼前。因此,检查 Python 版本、安装依赖、运行编译命令,都应在远程终端完成。


2. 先构建这次真正用到的消息包

先按 ROS 官方 Humble 安装文档配置 Ubuntu 的语言环境、软件源与 ROS 软件源,再安装本文使用的工具和构建依赖:

bash 复制代码
sudo apt-get update
sudo apt-get install -y --no-remove --no-install-recommends \
  build-essential python3-dev python3-pip unzip \
  ros-humble-desktop ros-humble-rqt-graph ros-humble-rqt-topic \
  python3-colcon-common-extensions \
  ros-humble-rosidl-default-generators ros-humble-rosidl-default-runtime \
  ros-humble-rmw-fastrtps-cpp

export PRIMEBOT_LAB_DIR="$HOME/primebot-sdk-lab"
mkdir -p "$PRIMEBOT_LAB_DIR"

将从社区下载的 SDK v1.0.0.0 解压到这个工作目录,使消息包位于 $PRIMEBOT_LAB_DIR/sdk_q1-v1.0.0.0/aimdk_msgs。这里的 primebot-sdk-lab 是自行创建的 ROS 工作空间,用于保存构建产物。接下来加载 ROS 并编译消息包:

bash 复制代码
source /opt/ros/humble/setup.bash
cd "$PRIMEBOT_LAB_DIR"
colcon build --base-paths sdk_q1-v1.0.0.0/aimdk_msgs \
  --packages-select aimdk_msgs --executor sequential \
  --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
python3 -c 'from aimdk_msgs.msg import TouchState; print(TouchState.TOUCH_EVENT_SINGLE_CLICK)'

最后一行输出 1,说明当前 Python 已能导入 SDK 生成的消息类型。本次消息包构建耗时 1 分 44 秒,日志中有 setuptools 配置弃用警告,但构建成功,随后导入通过。本次完成的是 aimdk_msgs 消息包的构建与导入验证。

TouchState.msg 是接口定义;Python 代码导入的 TouchState,则来自构建后可用的消息类型支持。先完成这一步,后面的发布器、官方示例和图形工具才能使用同一种消息结构。这里把构建范围限制在 aimdk_msgs,也方便把消息接口问题与 SDK 其他模块的依赖分开检查。

图 2:实际环境与构建记录。1 package had stderr output 对应构建警告,随后完成了消息类型导入。


3. 分清安装成功、构建成功和当前终端可用

这三个状态需要分别确认。系统安装了 ROS,只说明基础工具可用;colcon build 成功,说明工作空间产出了消息包;当前终端执行导入成功,才说明运行 Python 时能找到该包。下面的表格可以帮助判断自己已经完成了哪一步。

检查位置 本例看到的结果 能确认的内容
环境与软件包记录 Ubuntu 22.04、ROS 2 Humble Desktop 使用的系统与工具版本
构建日志末尾 aimdk_msgs 完成构建 本次选择的消息包构建成功
Python 导入命令 打印事件常量 1 当前 shell 的 Python 能使用该消息类型

安装时可以同时记下实际的软件包版本,并将 SDK 版本与测试结果对应保存。本次使用 SDK v1.0.0.0,下载后核对了压缩包的 SHA-256。更换 SDK 或 ROS 环境后,重新确认消息类型导入与接口定义,能更快判断变化来自环境还是应用代码。


二、读懂 TouchState:一条触摸消息里有什么

1. 先确认接口方向,再看消息定义

本例使用的话题是 /aima/hal/touch/state,消息类型为 aimdk_msgs/msg/TouchState。在 ROS 2 中,发布节点把某种类型的消息写入话题,订阅节点收到消息后执行回调;这次的回调只打印内容。

官方《接口说明》将这个接口的方向标为 Robot→Outer:机器人向外提供触摸反馈,应用侧订阅。本次测试使用本机发布器构造可重复的输入,重点观察订阅程序如何接收并解释这类事件消息。

对于刚接触 ROS 的读者,可以先记住三个具体对象:话题名决定程序订阅哪个通道,消息类型规定字段结构,回调函数决定收到数据后执行什么。后面更换回调中的打印逻辑时,前两项仍需与接口保持一致。

加载 SDK 环境后,可以直接查看接口:

bash 复制代码
ros2 interface show aimdk_msgs/msg/TouchState

图 3:终端实际查看 TouchState 接口,展示时仅滤去空行。消息定义列出字段和事件枚举,具体机型支持哪些事件还需对照接口文档。


2. 用编号、事件和序号读懂一次反馈

本例关注以下字段:

字段 本次用法
header.frame_id 测试程序填入 sdk_local_test,便于识别数据来源
header.sequence 测试程序从 1 开始递增,接收端据此核对是否收齐
sensor_id 1,对应 Q1 头部触摸传感器
event_type 1,即 TOUCH_EVENT_SINGLE_CLICK

这里有一个容易被忽略的约定:SDK 的 MessageHeader.msg 将 frame_id 定义为消息关联的坐标系名称。本例借用它写入 sdk_local_test,作为这组本机实验的输入标记。开发正式应用时,应遵循 SDK 对该字段的坐标系定义。

sensor_id 与 event_type 需要一起判断。前者回答"哪个传感器",后者回答"发生了什么事件";只看事件值 1,会把回调与具体部位的关系藏起来。序号则用于接收核对,和事件类别不是一回事。

SDK 的 TouchState.msg 中还定义了双击、滑动等事件常量,随包的《接口说明》目前列出的 Q1 支持项为头部单击。本次选择的 sensor_id=1 与 TOUCH_EVENT_SINGLE_CLICK 正好对应这一项;具体机型的支持范围以接口文档为准。


3. 区分事件反馈与定频测试数据

接口文档描述的是事件触发上报。本次测试发布器采用 1 Hz 的输入频率,便于逐条观察序号变化和回调输出。

按照官方文档,触摸消息在检测到事件时发送。观察实际事件流时,应结合触摸操作、节点状态和接口配置判断;本文测试程序则按固定间隔生成输入,适合检查消息接入过程。

这一点也决定了本文怎样读结果:递增序号用来检查测试消息是否进入应用,固定的传感器编号和单击常量用来检查字段是否正确。header.stamp 和 header.meas_stamp 都由测试发布器填入同一时刻的程序时间,用于构成完整的测试消息。


三、用 Python 接住消息,完成第一次收发验证

1. 给每个终端加载同一套运行环境

后续命令都在 Ubuntu 上执行。每开一个新终端,先加载 ROS 和刚才构建的 SDK 工作空间,并设置本机实验使用的通信环境:

bash 复制代码
export PRIMEBOT_LAB_DIR="$HOME/primebot-sdk-lab"
source /opt/ros/humble/setup.bash
source "$PRIMEBOT_LAB_DIR/install/setup.bash"
export ROS_DOMAIN_ID=71
export ROS_LOCALHOST_ONLY=1
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp

Domain ID 要在实验进程间保持一致。ROS_LOCALHOST_ONLY=1 将本次通信限定在当前 Linux 主机;跨设备通信时,应按官方组网说明配置对应的网络与发现参数。

这里的两个 source 有不同职责:先加载系统 ROS 环境,再叠加刚构建的 SDK 工作空间。环境变量设置对当前终端启动的程序生效,另开终端后仍需加载。特别是图形工具,它们也是 ROS 节点,同样需要正确的消息包与 Domain 配置。

本次实验先启动订阅器,再启动有限次数的发布器。测试程序等待兼容订阅端,最长等待 5 秒;发现订阅端后再预热 0.5 秒。这样给发现与匹配过程留出时间,再从接收端核对本轮实际到达的消息。


2. 先运行官方示例,再看测试消息怎样生成

先在终端 A 运行 SDK 自带的订阅示例:

bash 复制代码
python3 "$PRIMEBOT_LAB_DIR/sdk_q1-v1.0.0.0/examples/python/get_touch_state.py"

我随后在同一主机上启动自写发布器,使用 Reliable 策略,以 1 Hz 调用发布 10 次。下面给出完整程序,保存为 publish_touch.py,加载前述 ROS 与 SDK 环境后即可运行。程序包含参数检查、等待兼容订阅端、消息构造和结束汇总;默认参数与本文十条消息实验一致。

python 复制代码
#!/usr/bin/env python3
"""Publish marked local TouchState test messages."""

import argparse
import json
import math
import sys
import time

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, QoSProfile, ReliabilityPolicy

from aimdk_msgs.msg import TouchState


TOPIC = "/aima/hal/touch/state"
TEST_FRAME = "sdk_local_test"
DISCOVERY_TIMEOUT_SECONDS = 5.0
DISCOVERY_SETTLE_SECONDS = 0.5
DRAIN_SECONDS = 0.5
MAX_SEQUENCE = (1 << 32) - 1


def positive_count(value: str) -> int:
    count = int(value)
    if not 1 <= count <= MAX_SEQUENCE:
        raise argparse.ArgumentTypeError(f"count must be between 1 and {MAX_SEQUENCE}")
    return count


def positive_rate(value: str) -> float:
    rate = float(value)
    if not math.isfinite(rate) or rate <= 0:
        raise argparse.ArgumentTypeError("rate must be a finite positive number")
    return rate


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--count", type=positive_count, default=10)
    parser.add_argument("--rate", type=positive_rate, default=1.0, help="messages per second")
    parser.add_argument("--reliability", choices=("reliable", "best-effort"), default="reliable")
    parser.add_argument("--allow-unmatched", action="store_true", help="publish even if discovery finds no compatible subscriber")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    reliability = (
        ReliabilityPolicy.RELIABLE
        if args.reliability == "reliable"
        else ReliabilityPolicy.BEST_EFFORT
    )
    qos = QoSProfile(depth=10, reliability=reliability, durability=DurabilityPolicy.VOLATILE)
    node = None
    sent = 0
    reason = "error"
    exit_code = 1
    try:
        rclpy.init(args=[])
        node = Node("sdk_local_touch_publisher")
        publisher = node.create_publisher(TouchState, TOPIC, qos)
        print(f"LOCAL TEST publisher: {TOPIC}; reliability={args.reliability}", flush=True)
        discovery_deadline = time.monotonic() + DISCOVERY_TIMEOUT_SECONDS
        while publisher.get_subscription_count() == 0 and time.monotonic() < discovery_deadline:
            rclpy.spin_once(node, timeout_sec=0.1)
        has_subscriber = publisher.get_subscription_count() > 0
        if not has_subscriber and not args.allow_unmatched:
            reason = "no_compatible_subscriber"
            exit_code = 2
        else:
            if has_subscriber:
                # Experiment warm-up; this does not guarantee Best Effort delivery.
                settle_deadline = time.monotonic() + DISCOVERY_SETTLE_SECONDS
                while time.monotonic() < settle_deadline:
                    rclpy.spin_once(node, timeout_sec=0.1)
            next_send = time.monotonic()
            while sent < args.count:
                remaining = next_send - time.monotonic()
                if remaining > 0:
                    rclpy.spin_once(node, timeout_sec=min(remaining, 0.1))
                    continue
                msg = TouchState()
                msg.header.frame_id = TEST_FRAME
                msg.header.sequence = sent + 1
                msg.header.stamp = node.get_clock().now().to_msg()
                msg.header.meas_stamp = msg.header.stamp
                msg.sensor_id = 1
                msg.event_type = TouchState.TOUCH_EVENT_SINGLE_CLICK
                publisher.publish(msg)
                sent += 1
                print(f"sent sequence={sent} sensor=1 event=SINGLE_CLICK source={TEST_FRAME}", flush=True)
                next_send += 1.0 / args.rate
            drain_deadline = time.monotonic() + DRAIN_SECONDS
            while time.monotonic() < drain_deadline:
                rclpy.spin_once(node, timeout_sec=0.1)
            reason = "count_reached"
            exit_code = 0
    except (KeyboardInterrupt, ExternalShutdownException):
        reason = "interrupted"
        exit_code = 130
    except Exception as exc:
        print(f"Publisher failed: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
    finally:
        if node is not None:
            node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()
        print(json.dumps({"sent": sent, "reason": reason, "source": TEST_FRAME}), flush=True)
    return exit_code


if __name__ == "__main__":
    raise SystemExit(main())

保持终端 A 的官方订阅器运行,在加载同一套环境的终端 B 中,进入保存 publish_touch.py 的目录并执行:

bash 复制代码
python3 publish_touch.py --count 10 --rate 1 --reliability reliable

程序会打印逐条发送记录,并在最后输出 sent、reason 和 source 的 JSON 汇总。正常完成时,sent 为 10,reason 为 count_reached;如果等待 5 秒仍找不到兼容订阅器,则以 no_compatible_subscriber 结束,便于先检查订阅器和环境配置。

每调用一次 publish,计数变量 sent 加 1,下一条消息的序号随之递增。这样,发布端记录的是调用次数,接收端记录的是实际到达的序号,两侧结果可以对照。时间字段来自测试程序的 ROS 时钟。


3. 把接入逻辑收进一个可读的 Python 回调

官方示例输出了 TouchState received,同时打印 frame_id、序号、传感器编号和事件值。确认类型和通信正常后,我把回调缩减为头部单击判断与打印,方便后续添加应用逻辑。下面是本次实际运行的完整简洁订阅程序。读者可以将它保存为自己创建的 touch_watch_simple.py:

python 复制代码
#!/usr/bin/env python3
"""Print Q1 TouchState events; sdk_local_test identifies generated test input."""
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from aimdk_msgs.msg import TouchState


def on_touch(msg: TouchState) -> None:
    if msg.sensor_id == 1 and msg.event_type == TouchState.TOUCH_EVENT_SINGLE_CLICK:
        print(
            f"head click: sequence={msg.header.sequence}, "
            f"source={msg.header.frame_id}",
            flush=True,
        )


def main() -> None:
    rclpy.init()
    node = Node("q1_touch_watch")
    node.create_subscription(
        TouchState,
        "/aima/hal/touch/state",
        on_touch,
        qos_profile_sensor_data,
    )
    print("Waiting for TouchState; local fixture uses source=sdk_local_test", flush=True)
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == "__main__":
    main()

加载前述环境后,在保存代码的目录执行 python3 touch_watch_simple.py。当话题中出现符合头部单击条件的消息,程序会打印 head click: sequence=...。本次本机测试中,后面的 source 显示为 sdk_local_test。回调使用 SDK 的事件常量,后续替换打印逻辑时,可以保留这一层条件判断。

这段程序可以沿着执行顺序读:rclpy.init() 初始化通信,Node 创建节点,create_subscription 将消息类型、话题、回调与 QoS 绑定起来,rclpy.spin(node) 让程序持续处理回调。按 Ctrl+C 退出后,finally 中释放节点并关闭 ROS 上下文。这个结构与 ROS 2 官方 Python 发布订阅教程一致。

回调中的 if 判断传感器编号与事件类型,再打印序号和 source。用于数量统计时,我另外增加了测试标记过滤,让统计结果只包含这轮实验生成的头部单击消息。


4. 从"打印出来了"进一步核对十条消息

为了核对接收情况,我在计数接收器中加入测试标记过滤、超时和结果汇总。计数接收器 watch_touch.py 负责记录,run_checks.sh 负责执行整组检查。本轮的设置如下:

配置项 本次取值
输入 带 sdk_local_test 标记的头部单击测试消息
发布端 Reliable,1 Hz,发布调用 10 次
接收端 Best Effort,统计目标为 10 条
超时 从计数接收器开始等待起计 20 秒
核对内容 接收数量及序号 1 到 10

接收器收齐指定数量后结束等待,到达超时仍不足目标数量则返回失败。

本次接收结果如下:

图 4:实际接收日志的终端回看。上半部分是计数接收器收到的 10 条消息,序号为 1 到 10;下半部分是独立 GUI 演示轮中官方示例的单条日志摘录。核对接收结果要看接收端记录,发布器的发送次数不能代替它。

计数程序结束时会输出 JSON 汇总:received 是纳入统计的消息数,sequences 保存接收顺序,ignored 记录被过滤的消息,reason 说明为什么结束。读这几个字段,比只看终端最后一次打印更有用。

我把接收和校验分成两步:计数器记录实际到达的序号,达到指定数量后结束;随后检查数量是否为 10,且序列是否恰好等于 [1, 2, ..., 10]。这能把"收到十条"进一步细化为"预期的十个序号按顺序到达",也便于发现重复、缺号或顺序变化。


四、打开 ROS 图形工具,把消息流看清楚

1. 用一段持续消息流观察节点关系

图形观察时,我让官方订阅示例与上面的简洁订阅程序同时运行,用本机发布器持续生成 1 Hz 的测试消息,给展开面板和观察字段留出时间。这是独立于十条计数实验的另一轮运行。

在具有可用图形会话、且已执行同一组环境命令的终端中,分别启动:

bash 复制代码
ros2 run rqt_graph rqt_graph
bash 复制代码
ros2 run rqt_topic rqt_topic

这次使用 Xvfb 为服务器提供图形会话,通过绑定本机回环地址的 noVNC 服务和 SSH 转发查看窗口。在带桌面的 Ubuntu 上,可以直接在图形终端启动这两个工具。远程环境则先确认桌面会话与显示转发可用,再加载与消息节点相同的 ROS 环境。

在 rqt_graph 中将显示模式设为 Nodes/Topics (all),点击刷新,能够看到完整的发布和订阅关系:

图 5:左侧 /sdk_local_touch_publisher 发布到 /aima/hal/touch/state,右侧是官方 /get_touch_state 和自写 /q1_touch_watch。这张图用于检查程序是否接到了预期话题。

这张图把代码中的几个名字对应起来:Node("q1_touch_watch") 对应右侧自写节点,订阅调用中的话题字符串对应中间通道。两个订阅器分别处理同一个发布器送来的消息,便于比较官方示例和自己的回调。

如果观察时少了一个节点,应先确认相应程序仍在运行、环境已加载、Domain 相同,并刷新视图。节点与连线用于观察发现到的通信结构,消息字段是否更新还要继续看数据窗口。


2. 展开 Topic Monitor,观察实际字段变化

节点图出现连线后,还应检查实际消息。进入 Topic Monitor,勾选 /aima/hal/touch/state,展开 header,能看到来源标记和递增序号:

图 6:真实 Topic Monitor 操作动图。sequence 随消息递增,frame_id='sdk_local_test'、sensor_id=1、event_type=1 保持不变。Hz 显示约 1.00,与测试发布频率一致;带宽栏显示为 unknown。

实际阅读窗口时,我会先看 sequence 是否变化,再核对标记、传感器编号和事件值;这样能区分"界面已经打开"与"界面正在收到预期消息"。


3. 图形工具也有自己的订阅配置

这里还有一个与官方 Python 示例不同的默认值:本次使用的 Topic Monitor 按 Reliable 订阅,官方示例使用 Best Effort。当前测试发布器提供 Reliable,可以同时满足两者,因而图形界面与 Python 回调都能接收。这一默认行为可在 rqt_topic 的 Humble 实现中核对。

节点图和字段视图可以一起用:先确认程序接到了哪个话题,再检查传入回调的数据。初次接入时,我会先在这里核对话题名与消息来源,再回到代码里排查处理逻辑。

想确认的问题 更直接的观察位置
哪个节点发布,哪个节点订阅 rqt_graph 的节点、话题与连接
当前收到的字段是什么 Topic Monitor 展开的消息内容
十条消息是否按预期到齐 接收日志中的数量、序号与超时结果
自己的业务条件是否成立 Python 回调中的传感器与事件判断

图形工具让自定义 SDK 消息变得容易观察,日志则把一次运行的结果留下来。两者结合后,再修改回调时就能知道变化发生在通信侧还是处理逻辑中。


五、弄清两个常见问题,让接入过程可复现

下面两个故障是我主动设置的排查实验。每组数量验证前,我先结束上一组发布与订阅进程,并关闭 rqt 窗口,让当前话题只包含本组实验的节点。


1. 编译成功,Python 却找不到 aimdk_msgs

我在干净的 shell 中只加载 /opt/ros/humble/setup.bash,rclpy 导入成功,导入 TouchState 则出现:

text 复制代码
ModuleNotFoundError: No module named 'aimdk_msgs'

原因是当前终端只知道 ROS 安装位置,还不知道新构建的 SDK 工作空间。加载这个工作空间的环境后,导入恢复正常:

bash 复制代码
source "$PRIMEBOT_LAB_DIR/install/setup.bash"
python3 -c 'from aimdk_msgs.msg import TouchState; print("overlay loaded: TouchState import OK")'

图 7:两次实际运行的日志。遇到这类错误,我会先检查当前终端的 source,再考虑重新安装或编译。

这个现象提示了一个具体的检查顺序:如果 rclpy 可以导入而 aimdk_msgs 不行,先检查 SDK 工作空间是否加载、路径是否指向本次构建位置。只有确认环境加载正确后仍缺少产物,才需要回到构建日志。反复安装 ROS 无法替一个漏加载工作空间的终端补齐环境。


2. 能发现发布者,却一条也收不到

QoS 可以理解为通信双方对传输方式的约定。本例主要关注 Reliability:发布端提供 Best Effort,而订阅端要求 Reliable 时,这一组合不兼容;Reliable 发布端则可以满足 Best Effort 订阅端。ROS 官方 QoS 兼容说明列出了对应关系。

官方触摸示例使用 qos_profile_sensor_data,包含 Best Effort。它可以匹配接口文档中描述的 Reliable 发布,这组配置本身兼容。

判断兼容性要看发布端提供的策略能否满足订阅端要求,双方不必逐项取同一个名称。本次重点验证 Reliability;如果在自己的工程里还修改了 Durability 等策略,也应一起核对。下图只讨论这一项,不能代替完整 QoS 配置检查。

图 8:QoS 示意,仅比较 Reliability,其他条件保持相同。箭头表示是否兼容;兼容后仍需核对接收序号和超时,不能仅凭匹配成功推断消息收齐。下表单独记录本次实测数量。

我用自写脚本改变双方的 Reliability,保持话题名、消息类型与 Domain ID 一致。最终一轮复验结果如下:

发布端 订阅端 本次实际接收结果
Reliable Best Effort 10 条,序号 1 到 10
Best Effort Reliable 0 条,17 秒超时,提示 RELIABILITY 不兼容
Best Effort Best Effort 10 条,序号 1 到 10

第二组实验中,我将发布端设为 Best Effort、订阅端设为 Reliable,并允许测试发布器在匹配失败后继续发送,以观察接收端的反应。接收器等待 17 秒,终端出现 Last incompatible policy: RELIABILITY,随后汇总为 received: 0、reason: timeout。

下一组将订阅端改为 Best Effort,重新启动双方,话题名、消息类型与 Domain ID 保持一致。最终一轮复测收到了序号 1 到 10,与表中第三行对应。

图 9:终端回看实际日志。上方是不兼容时的警告,中间是修复后一次复测的 10 条结果,下方保留了更早一次只收到序号 2 到 10 的记录。


3. 保留那次只收到九条的记录

首次把双方都设成 Best Effort 时,我收到了序号 2 到 10,共 9 条。随后在发布器发现兼容订阅端后加入 0.5 秒等待,再运行整组检查,收到了完整的 1 到 10。这两轮记录分别对应各自的启动条件与接收结果。

有限条数实验需要把启动顺序和节点就绪状态一起考虑。发布器发现了兼容订阅端以后,还要从目标接收器确认实际到达的序号;尤其是多个订阅器同时存在时,发现某个订阅端与目标计数节点就绪是两件事。

本轮检查依次核对了环境加载、接收数量和序列,最终通过。QoS 不兼容这一组的预期结果,就是接收 0 条并超时;另外两组则需要收到完整的 1 到 10。用这样的预期逐项对照,才能区分预设故障与意外失败。

对开发者而言,这次九条与十条的差异提醒了一个实用习惯:调整通信配置后,既看程序是否开始打印,也核对接收总数、首尾序号与超时状态。


六、走进 RViz,亲手调节 Q1 模型的肩肘关节

消息链路跑通以后,我又做了一项独立的模型可视化实验:由关节 GUI 提供角度,在 RViz 中观察 Q1 的姿态更新。这个过程把关节名称、转动方向与模型坐标关系对应起来,方便理解每个数值在机器人结构中的位置。


模型来自启元机器人社区下载页的 Q1 URDF v1.0.0 ,与 SDK 分开提供。我使用包内的 q1_v3/urdf/q1_v3.urdf。这个模型包含 26 个 link、22 个活动关节,根坐标系是 pelvis;头部和两个腕部关节在该文件中为 fixed,保持固定的连接关系。

URDF 描述机器人各部分怎样连接。link 表示模型中的刚体部分,joint 定义相邻部分的连接关系、关节原点和允许的运动。mesh 文件提供外形,关节状态决定当前姿态;把模型外形加载出来,只完成了显示过程的一部分。

本次文件共有 25 个关节,其中 22 个为 revolute、3 个为 fixed。我核对了 27 处 mesh 引用,对应 24 个不同的 STL 文件,引用的文件都在模型包中。这样的资源检查有助于区分文件路径问题与坐标关系问题。


2. 保留官方模型,做最小的 ROS 2 启动适配

这里遇到一个实际的适配问题:模型包的构建配置使用 ROS 1 的 catkin,附带 launch 也采用 ROS 1 写法,并引用了包里不存在的旧文件名。我的处理是保留原压缩包和原模型,只在派生 URDF 中把 mesh 相对路径改为本机绝对 file:// 路径,再用一个 ROS 2 launch 启动模型。派生文件沿用官方的关节、限位和惯量定义。

RViz2 由前面的 ROS Desktop 提供,模型显示还使用以下两个官方软件包:

bash 复制代码
sudo apt-get install --no-remove --no-install-recommends \
  ros-humble-robot-state-publisher ros-humble-joint-state-publisher-gui

路径适配时,我以原 URDF 所在目录为基准,解析每个 <mesh> 的 filename,确认对应 STL 文件存在,再将其写成 file:// 开头的绝对路径,保存为派生 URDF。原模型与派生文件分别保存,便于核对修改内容。换一台机器后,mesh 路径也应对应到新机器上的实际位置。这一步由 prepare_q1_model.py 完成,q1_model.launch.py 则负责启动手动 GUI 或自动姿态演示所需的节点。

启动配置使用 ROS 2 launch 组织模型节点,并设置以下参数:

节点或配置 本次设置
robot_state_publisher robot_description 参数接收派生 URDF 的完整 XML 内容
joint_state_publisher_gui 读取同一份派生 URDF,生成可调关节面板
rviz2 加载 RobotModel,以 pelvis 为 Fixed Frame
通信环境 Domain 72、ROS_LOCALHOST_ONLY=1、rmw_fastrtps_cpp

模型实验使用 Domain 72,触摸消息实验使用 Domain 71;同一组模型进程采用相同的 Domain。这次服务器的图形会话位于 DISPLAY=:99,并设置 LIBGL_ALWAYS_SOFTWARE=1 使用软件渲染。在自己的 Ubuntu 桌面上操作时,保留当前会话的显示配置即可。


3. 看懂滑条到模型之间的数据流

模型窗口运行时,GUI 发布 /joint_states,robot_state_publisher 根据 URDF 计算各 link 的坐标变换,RViz 再据此显示姿态。RViz 的 RobotModel 从 /robot_description 读取模型,Fixed Frame 设为 pelvis。

环节 在本次模型显示中的作用
Joint State Publisher GUI 把滑条对应的关节名称和角度写入关节状态
/joint_states 承载模型当前的关节状态
robot_state_publisher 结合 URDF 与关节状态,计算 link 之间的坐标变换
TF 与 RViz RobotModel 将变换应用到模型各部分,在窗口中显示姿态

按照 robot_state_publisher 的 Humble 说明,固定关节变换由 /tf_static 提供,活动关节更新通过 /tf 发布。这里把 pelvis 设为 Fixed Frame,是以模型根部作为显示参考,便于集中观察上肢变化。


4. 手动调整左肩和左肘,逐项对照变化

图 10:真实 GUI 调试过程的采样动图。我逐步调节左肩 roll 与左肘滑条,再调回零位附近;数值单位为 rad。动图由实际运行的关节面板与 RViz 窗口采样得到。

这段操作中,左肩 roll 调到约 0.629 rad ,左肘调到约 0.824 rad,均在对应模型限位内。可以看到,上臂方向和前臂姿态随数值一起变化,而腿部站姿保持稳定。GUI 的零位对应关节变量接近 0 时的模型姿态,其中仍包含 URDF 定义的关节原点变换。

在关节面板中,可以先找到 left_shoulder_roll_joint,逐步调整滑条,观察上臂方向;再调整 left_elbow_joint,观察前臂相对于上臂的变化。最后把两个关节调回零位附近,对照初始姿态。界面数值使用弧度,读数与模型限位应按同一单位比较。

动图中的关节 本次调节到的角度(约) 官方模型中的位置限位(rad)
left_shoulder_roll_joint 0.629 rad −0.087266 至 2.7925
left_elbow_joint 0.824 rad −0.87266 至 1.6581

滑条有离散步进,调回零位附近时可能仍有很小的数值差异,所以本文按"零位附近"描述回程。观察重点是选中的关节、面板数值和模型部位能否对应起来。这样在以后阅读模型或编写姿态程序时,关节名称就有了可见的含义。


5. 连续姿态演示中的关节选择与状态更新

手动滑条操作验证了模型资源、关节状态和坐标变换之间的配合。启动时出现了 KDL 不支持根 link 惯量的提示,模型保持官方惯量定义,RViz 中的姿态正常显示。

我另外用程序发布 /joint_states,运行了一次连续姿态演示:一个周期为 8 秒,记录了 161 次发布,并正常结束。程序调节左肩 pitch 和左肘,目标值分别为 −0.55 rad 与 0.80 rad,另外 20 个活动关节保持零位。

观察项目 关节状态来源 调节的关节
图 10 的手动动图 GUI 滑条 左肩 roll、左肘
单独运行的连续演示 程序发布 /joint_states 左肩 pitch、左肘

运行这两种方式时,我分别启动对应的关节状态源,让同一组模型由一个发布源提供姿态。自动程序还会检查模型限位,并在发现额外的关节状态发布者时退出。

这两项观察把同一条数据流展示得更清楚:GUI 与程序都能提供关节角,robot_state_publisher 负责计算变换,RViz 将这些变化显示到官方模型上。手动方式便于逐个认识关节,连续演示则便于观察角度随时间变化时的姿态。


七、从这次实测出发,继续做自己的 Q1 应用

1. SDK 帮忙省下了哪些工作

这次接入中,启元 Q1 SDK 的消息类型和现成订阅示例省去了从头定义接口的工作。把头部单击接入 Python 回调只需几十行代码,其中实际的事件处理集中在一个条件判断里。ROS 2 工具能直接查看自定义消息,排障时也无需先写一套界面。

这次更费时间的是运行环境和通信配置:漏加载工作空间,代码会在导入阶段失败;QoS 不兼容,节点虽然能发现对方,消息仍进不了回调。实验进程还需要保持相同的 Domain。图形工具便于观察连接和字段,接收数量则由日志核对。

对我来说,这套接口最实用的部分,是能够沿用 ROS 2 的工具逐层观察:消息结构可以直接查看,官方示例可以作为接入参照,回调可以逐步替换为自己的逻辑。模型侧也提供了关节与几何资源,不过当前下载包仍需要处理 ROS 1 配置到 ROS 2 启动的适配。开发者拿到资料后,还要为自己的环境完成这些连接工作。


2. 把消息接入和模型显示分别核对

回看这次操作,我会把开发过程中的检查分成两组。消息侧先确认 TouchState 可以导入,再核对话题、事件字段与 QoS,最后用接收数量和序号确认结果。模型侧先确认 URDF 和 mesh 资源,再对照关节面板数值、TF 与 RViz 姿态。

这样分开以后,出现异常时就有具体的观察位置:导入失败从工作空间环境查起,发现节点但收不到消息就核对通信配置,关节数值变化后则对照模型对应部位。本文中的环境加载、QoS 配对和肩肘调节,都沿着这个顺序检查。

这套方法也方便继续修改自己的代码。保留一组输入参数与预期结果,每次改动后先对照这一组观察,能更清楚地判断变化发生在哪个环节。


3. 看懂项目文件,再继续开发

配套项目把环境准备、消息收发和模型显示分成独立文件。下载代码后,可以按下面这张表找到相应入口:

文件 用途
install_ros.sh 配置 ROS 软件源,安装 Humble Desktop、rqt 工具和消息包构建依赖。
build_sdk.sh 下载并校验官方 SDK,编译 aimdk_msgs,确认 Python 能导入消息类型。
publish_touch.py 生成带测试标记和递增序号的头部单击消息,可设置发送数量、频率与 Reliability。
touch_watch_simple.py 订阅触摸话题,判断头部单击事件,并打印序号与来源标记。
watch_touch.py 筛选测试消息,记录接收数量和序号,按数量或超时结束,并输出 JSON 汇总。
run_checks.sh 执行环境加载与三组 QoS 检查,核对预期错误、接收数量及完整序列。
prepare_q1_model.py 下载并校验官方模型,保留原件,适配网格路径,生成派生 URDF 和 RViz 配置。
q1_model.launch.py 组织模型显示节点,启动 RViz 与状态发布器,并选择 GUI 或自动关节状态源。
model_joint_demo.py 在模型限位内周期调节左肩 pitch 与左肘,通过 /joint_states 发布模型姿态。
README.md、VERSIONS.md、VERSION 分别提供完整运行步骤、实测环境与验证记录、示例项目版本号。
HEADLESS.md 说明无头 Ubuntu 服务器的图形会话配置和窗口查看方法。
SOURCES.md 汇总官方 SDK、模型与 ROS 资料的来源。
.gitignore 排除构建产物、下载资源、缓存和本地配置,便于维护源码。

初次运行时,先按 README.md 完成环境准备与消息包构建,再配合使用发布器和订阅器;需要核对整组结果时运行 run_checks.sh。进入模型部分后,由准备脚本处理资源,launch 组织显示节点,关节 GUI 或自动演示程序提供姿态输入。这样,每个文件都对应一个明确的操作环节。

继续写自己的应用时,可以从 touch_watch_simple.py 的回调入手,逐步加入事件处理逻辑。每次调整环境或通信参数,都记录自己使用的版本、关键配置和接收结果,方便比较修改前后的行为。后续也可到启元机器人社区查阅开发资料并交流实操问题。


参考资料

  1. 启元机器人开发者社区:社区首页、SDK 与模型下载页。本文使用 Q1 SDK v1.0.0.0 与 Q1 URDF v1.0.0(包内 q1_v3)。
  2. Q1 SDK v1.0.0.0 包内资料:《PrimeBot SDK 快速入门与集成指南》《接口说明》及 examples/python/get_touch_state.py。
  3. ROS 2 Humble 官方文档:Ubuntu 安装说明、Python 发布订阅教程、QoS 策略与兼容关系。
  4. rqt 图形工具:rqt_graph 官方包资料、rqt_topic 官方包资料、rqt_topic Humble 订阅实现。
  5. 模型状态发布:robot_state_publisher Humble 说明。
  6. 本文配套代码仓库:q1-sdk-ros2-lab · AtomGit,示例项目版本 v1.0.0 ,文章对应提交 4f91c011。
相关推荐
MocapLeader1 小时前
IROS 2026 | 开源、可复现的连续体机器人学习平台:从制造到自主操作
具身智能·机器人学习·连续体机器人·模仿学习·nokov度量动作捕捉·同构遥操作·iros 2026
长弓三石2 小时前
钉钉与飞书机器人接入企业级智能体过程及排障实录
人工智能·机器人·钉钉·飞书·企业级harness
桃花键神2 小时前
GR00T N1 —— 双系统架构如何让一个模型驱动不同人形机器人
系统架构·机器人
Y Shy11 小时前
2026.10 在受限的嵌入式机器人设备中使用LLM Agent
机器人·llm·嵌入式·agent
HA-ming14 小时前
PCSwitch 智能呼叫系统技术拆解:AI 机器人、意图识别与模块化流程如何串起业务自动化
人工智能·机器人·自动化·智能呼叫系统·智能呼叫中心·智能外呼·可视化模块化
BSD_CGQ17 小时前
FSR 压力传感器 I2C 接口实战:地址规划与 MCU 软件滤波的工程取舍
嵌入式硬件·机器人·压力传感器·柔性薄膜压力传感器
火星机器人life18 小时前
从“会动”到“会学”:移动机器人学习型运动规划与控制全景综述
强化学习·运动规划·具身智能·机器人控制
火星机器人life19 小时前
|机器人强化学习系列第 1 篇|机器人为什么需要强化学习?传统控制方法的四个局限
机器人
xwz小王子19 小时前
Nature Machine Intelligence|微型机器人强化学习训练缩短至分钟级:每秒19万次交互
人工智能·机器人