启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz

本文记录我测试启元 Q1 时的第一阶段工作:安装桌面调试工具、搭建 ROS 2 Humble 容器、准备 SDK 消息环境,并在 RViz 中加载 Q1 模型、检查关节状态和 TF。

验证范围:URDF 模型显示与运动学链路。 本阶段已经看到模型并完成关节话题验证;Gazebo/MuJoCo 动力学仿真、机器人行走以及真机接口控制仍属于后续工作。

文章目录

    • 一、这次部署要跑通什么
    • 二、我的开发环境
    • 三、准备官方资源和工作目录
    • [四、安装并启动 PrimeConsole](#四、安装并启动 PrimeConsole)
      • [4.1 安装 deb 包](#4.1 安装 deb 包)
      • [4.2 从终端启动](#4.2 从终端启动)
      • [4.3 图标加载失败是否影响启动](#4.3 图标加载失败是否影响启动)
    • [五、部署 ROS 2 Humble 容器](#五、部署 ROS 2 Humble 容器)
      • [5.1 检查 Docker 和图形会话](#5.1 检查 Docker 和图形会话)
      • [5.2 用 Dockerfile 固化依赖](#5.2 用 Dockerfile 固化依赖)
      • [5.3 配置 Compose](#5.3 配置 Compose)
      • [5.4 构建、启动和进入容器](#5.4 构建、启动和进入容器)
    • [六、SDK 准备到哪一步](#六、SDK 准备到哪一步)
    • [七、把 Q1 模型接入 ROS 2](#七、把 Q1 模型接入 ROS 2)
      • [7.1 为什么没有直接编译原模型包](#7.1 为什么没有直接编译原模型包)
      • [7.2 URDF、关节状态和 TF 如何配合](#7.2 URDF、关节状态和 TF 如何配合)
      • [7.3 处理网格相对路径](#7.3 处理网格相对路径)
      • [7.4 完整的 ROS 2 启动文件](#7.4 完整的 ROS 2 启动文件)
      • [7.5 RViz 配置](#7.5 RViz 配置)
    • 八、启动并逐项验证
      • [8.1 启动窗口](#8.1 启动窗口)
      • [8.2 在第二个终端检查话题](#8.2 在第二个终端检查话题)
      • [8.3 核对连杆与关节数量](#8.3 核对连杆与关节数量)
      • [8.4 本次验证结果](#8.4 本次验证结果)
    • 九、这次遇到的坑与排查顺序
      • [9.1 RViz 报 nouveau / dri3 错误](#9.1 RViz 报 nouveau / dri3 错误)
      • [9.2 一直显示 Waiting for robot_description](#9.2 一直显示 Waiting for robot_description)
      • [9.3 RViz 里只有坐标轴,没有机器人外观](#9.3 RViz 里只有坐标轴,没有机器人外观)
      • [9.4 Fixed Frame 不存在](#9.4 Fixed Frame 不存在)
      • [9.5 KDL 提示根连杆带有惯量](#9.5 KDL 提示根连杆带有惯量)
      • [9.6 容器重建后,昨天装的包不见了](#9.6 容器重建后,昨天装的包不见了)
      • [9.7 GUI 无法连接显示服务](#9.7 GUI 无法连接显示服务)
      • [9.8 run.sh 启动了容器,却没有进入](#9.8 run.sh 启动了容器,却没有进入)
    • 十、再次启动与停止
    • 十一、从模型验证走向物理仿真和真机

一、这次部署要跑通什么

刚开始接触 Q1 时,我下载了 PrimeConsole、Q1 SDK 和机器人模型包。三个资源看起来都与"开发机器人"有关,但它们承担的任务不同。

PrimeConsole 是桌面调试入口;SDK 提供程序使用的接口定义与示例;URDF 描述模型结构,供可视化和后续仿真使用。官方开发者社区分别提供这些资源,可以从启元机器人下载页获取。

我把第一阶段的目标拆成四步:

  1. 确认 PrimeConsole 能安装并启动。
  2. 在现有 Ubuntu 24.04 电脑上准备独立的 Humble 开发环境。
  3. 在 RViz 中显示 Q1,打开关节滑块窗口。
  4. 检查 /robot_description、/joint_states、/tf 和 /tf_static,确认模型背后的数据链路。

这四步完成后,后续调试 SDK、设计动作或接入物理仿真时,就有了一个可以检查模型和通信的基础环境。

二、我的开发环境

项目 本次环境
开发电脑 蛟龙 16 Pro,AMD64
宿主系统 Ubuntu 24.04
宿主 ROS 2 Jazzy
容器系统 Ubuntu 22.04
容器 ROS 2 Humble
基础镜像 osrf/ros:humble-desktop-full
自定义镜像 ros2_humble_q1:latest
PrimeConsole 1.0.2,Linux deb 包
本地 SDK 目录 sdk_q1-v1.0.0.0
模型目录 q1_v3
图形显示 宿主 X11/XWayland 接口,容器通过 X11 socket 显示窗口
RViz 渲染 设置 LIBGL_ALWAYS_SOFTWARE=1,使用软件渲染

Q1 SDK 随包指南使用 Ubuntu 22.04 + ROS 2 Humble。我的宿主系统已经配置了 Ubuntu 24.04 + Jazzy,因此使用容器准备 Humble。

这里的关键是让系统、ROS 发行版和依赖保持一致:不要把面向 Ubuntu 22.04 的 Humble deb 软件源混进 Ubuntu 24.04 的系统环境。 这不代表同一台电脑只能使用一个 ROS 版本,容器可以把两套用户空间环境分开。ROS 发行版对应的系统要求可查阅 ROS 2 官方安装文档源码。

另外,echo $ROS_DISTRO 显示的是当前终端加载的 ROS 环境。它返回 humble,不能单独证明网络通信、模型加载或机器人连接已经正常。

三、准备官方资源和工作目录

从官方下载页获取 Linux 版 PrimeConsole、Q1 SDK 和 Q1 URDF,解压后,我的主要文件如下:

text 复制代码
/home/mi/Q1/
├── PrimeConsole-Linux-1.0.2.deb
├── sdk_q1-v1.0.0.0/
│   ├── aimdk_msgs/
│   ├── examples/
│   └── PrimeBot_SDK快速入门与集成指南.md
├── q1_v3/
│   ├── urdf/
│   │   └── q1_v3.urdf
│   ├── meshes/
│   ├── display.launch.py
│   └── q1.rviz
└── ros2_humble/
    ├── Dockerfile
    └── docker-compose.yml

其中 display.launch.py、q1.rviz 和 Docker 配置是本次复现需要准备的文件,完整内容见下文。复现时,把 /home/mi/Q1 替换成自己的工作目录。

我的模型目录名是 q1_v3,但其中 package.xml 仍有旧包名,且声明了 catkin。因此不能仅凭文件夹名称判断它已经是 ROS 2 包。本篇直接读取 URDF 与网格,不依赖原包的 ROS 1 启动文件。

四、安装并启动 PrimeConsole

4.1 安装 deb 包

以下命令在宿主机执行:

bash 复制代码
cd /home/mi/Q1
sudo apt install ./PrimeConsole-Linux-1.0.2.deb

我的原始操作使用了 dpkg -i。如果采用这种安装方式后出现缺少依赖,可以再让 apt 处理依赖:

bash 复制代码
sudo dpkg -i PrimeConsole-Linux-1.0.2.deb
sudo apt -f install

两种安装方式择一即可。安装完成后检查包状态:

bash 复制代码
dpkg-query -W -f='${Status} ${Version}\n' primeconsole

本机实际输出:

text 复制代码
install ok installed 1.0.2

相比只检查包名有没有出现在列表中,install ok installed 更能说明安装状态。

图1 PrimeConsole 1.0.2 安装包、安装状态与启动文件

4.2 从终端启动

bash 复制代码
cd /opt/PrimeConsole
./PrimeConsole

我从终端启动,是为了同时观察窗口和启动日志。截图中可以看到机器人控制、Skill 验证、语音 TTS、动作和表情等区域。

但界面右上角仍显示"未连接",动作与表情列表也是空的。因此,这张截图能证明客户端已经打开,尚不能证明真机连接和摄像头取流成功。

图2 PrimeConsole 主界面,连接状态仍为"未连接"

4.3 图标加载失败是否影响启动

启动日志中出现了如下信息,截图中的资源路径没有完整显示:

text 复制代码
[createWindow] 窗口图标加载失败: /opt/PrimeConsole/resources/app.asar/build/icon...

同一轮启动已经打开主窗口,所以本次该信息没有阻止主界面启动。它涉及窗口图标资源,不能据此判断 ROS 通信或视频功能是否异常;具体缺失原因还需要完整路径与安装包内容才能确认。

图3 再次启动后的真实日志,完整显示 icon.ico 资源路径

如果遇到的现象是"整个窗口打不开",应保存完整终端输出,优先检查明确报出的缺失库或权限问题。不要看到 Electron 就一次性安装一长串旧发行版库名。

如果此前激活过 Conda,可先退出当前环境再复测;退出次数取决于实际嵌套层级。本文没有证据表明 Conda 是本次图标提示的原因。

五、部署 ROS 2 Humble 容器

5.1 检查 Docker 和图形会话

在宿主机的图形桌面终端执行:

bash 复制代码
docker --version
docker compose version
docker info
echo "$DISPLAY"

本文以 Docker Engine 和 Compose 插件已安装、当前用户能运行 Docker 命令为前提。尚未安装时,按 Docker 官方 Ubuntu 安装步骤完成配置。

DISPLAY 在我的环境中为 :0,其他电脑可能不同。它为空时,需要先解决图形会话问题,而不是直接照抄 :0。如果使用 Wayland 桌面,这套方案还需要可用的 XWayland 和相应 X11 socket。

5.2 用 Dockerfile 固化依赖

先在宿主机创建配置目录,再在 /home/mi/Q1/ros2_humble/Dockerfile 中写入下面的内容:

bash 复制代码
mkdir -p /home/mi/Q1/ros2_humble
dockerfile 复制代码
FROM osrf/ros:humble-desktop-full

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        ros-humble-joint-state-publisher \
        ros-humble-joint-state-publisher-gui \
    && rm -rf /var/lib/apt/lists/*

本次镜像需要补装 joint_state_publisher 和 joint_state_publisher_gui,它们是两个独立的软件包。把安装命令写进 Dockerfile,可以让重建后的环境继续具备这些工具。

如果只进入运行中的容器执行 apt 安装,安装内容会留在该容器的可写层。停止、再启动同一个容器通常会保留这些内容;删除并重建容器则不会保留。

5.3 配置 Compose

下面是针对本篇模型显示整理的精简配置。原测试环境还挂载了 SDK 和编译目录,此处仅保留 RViz 所需内容,并限制为本机通信。

保存为 /home/mi/Q1/ros2_humble/docker-compose.yml:

yaml 复制代码
services:
  humble:
    build: .
    image: ros2_humble_q1:latest
    container_name: ros2_humble
    network_mode: host
    environment:
      DISPLAY: ${DISPLAY:?Please run from a graphical desktop terminal}
      QT_X11_NO_MITSHM: "1"
      LIBGL_ALWAYS_SOFTWARE: "1"
      ROS_DOMAIN_ID: "88"
      ROS_LOCALHOST_ONLY: "1"
    volumes:
      - /tmp/.X11-unix:/tmp/.X11-unix:rw
      - ${Q1_ROOT:-/home/mi/Q1}/q1_v3:/Q1/q1_v3:ro
    command: sleep infinity
    stdin_open: true
    tty: true

几个配置项分别解决不同问题:

配置 用途
DISPLAY、X11 socket 挂载 让容器窗口显示在宿主桌面
QT_X11_NO_MITSHM=1 避免 Qt 使用 X11 的 MIT-SHM 扩展
LIBGL_ALWAYS_SOFTWARE=1 请求 Mesa 使用软件渲染
ROS_DOMAIN_ID=88 将这组 ROS 节点放到指定 DDS 域;宿主节点需要使用其他域才能隔离
ROS_LOCALHOST_ONLY=1 本篇只做本机模型测试,限制 ROS 发现范围
模型目录挂载 宿主编辑文件,容器读取模型与配置

软件渲染变量的含义可参考 Mesa 官方环境变量说明。后续连接真机时,需要重新检查 localhost 限制、网卡与机器人域配置。

5.4 构建、启动和进入容器

在宿主机执行:

bash 复制代码
cd /home/mi/Q1/ros2_humble
export Q1_ROOT=/home/mi/Q1

# 在图形桌面终端,为本地 root 用户开放本次 X11 访问。
xhost +si:localuser:root

docker compose config
docker compose build
docker compose up -d
docker exec -it ros2_humble bash

Q1_ROOT 用于选择模型目录。工作目录不同的读者需要修改这个值,并在后续使用 Compose 时保持一致。

如果已经有名为 ros2_humble 的容器,先用 docker ps -a 确认它属于哪个环境,再复用或调整配置,避免创建同名容器。我的现有环境直接复用即可。

进入容器后执行:

bash 复制代码
source /opt/ros/humble/setup.bash
echo "$ROS_DISTRO"
ros2 pkg prefix robot_state_publisher
ros2 pkg prefix joint_state_publisher_gui
ros2 pkg prefix rviz2

ROS_DISTRO 应输出 humble,其余命令应返回包所在路径。本机对应软件版本为:

软件包 本次安装的上游版本
joint_state_publisher 2.4.0
joint_state_publisher_gui 2.4.0
robot_state_publisher 3.0.3
rviz2 11.2.28

图4 容器内环境:Ubuntu 22.04.5、Humble、显示变量与关键 ROS 软件包版本

镜像标签与 apt 软件源会更新,复现时建议另外保存 docker image inspect 的镜像信息和 dpkg-query -W 输出,便于对照差异。

六、SDK 准备到哪一步

SDK 和模型可视化是两条相关但独立的准备工作。本篇显示 Q1 模型不需要编译整个 SDK,也不需要启动机器人端服务。

我的原开发容器已将 SDK 映射到 /Q1/sdk_q1-v1.0.0.0,并将 /var/tmp/q1_sdk_build 映射为宿主机持久化目录。如果需要复用下面的消息包编译命令,在 Compose 的 volumes 中额外加入:

yaml 复制代码
      - ${Q1_ROOT:-/home/mi/Q1}/sdk_q1-v1.0.0.0:/Q1/sdk_q1-v1.0.0.0:ro
      - /var/tmp/q1_sdk_build:/var/tmp/q1_sdk_build

应用配置并重新进入容器后,在容器内执行:

bash 复制代码
source /opt/ros/humble/setup.bash
cd /Q1/sdk_q1-v1.0.0.0

colcon --log-base /var/tmp/q1_sdk_build/log build \
  --packages-select aimdk_msgs \
  --build-base /var/tmp/q1_sdk_build/build \
  --install-base /var/tmp/q1_sdk_build/install \
  --cmake-args -DCMAKE_BUILD_TYPE=Release

source /var/tmp/q1_sdk_build/install/setup.bash
ros2 interface show aimdk_msgs/msg/Bms

本地编译目录中已存在 aimdk_msgs 安装产物,并已验证可以在 Python 中导入 Bms 和 PlayTts 类型。这说明自定义消息环境可用,尚不能推导出完整 C++ 示例、视频功能或真机控制全部跑通。

图5 aimdk_msgs 安装路径,以及 Bms、PlayTts 消息类型的导入检查

后续启动本文的 RViz 链路,加载 /opt/ros/humble/setup.bash 即可。首次复现可以先跳过本节,减少不必要的变量。

七、把 Q1 模型接入 ROS 2

7.1 为什么没有直接编译原模型包

检查本地模型包可看到:

xml 复制代码
<buildtool_depend>catkin</buildtool_depend>

这是 ROS 1 包的构建声明。原包的 catkin 构建配置和 ROS 1 launch 文件,不能直接作为本篇 Humble 的启动方案。

图6 本地模型文件、catkin 声明,以及 26 个 link、22 个活动关节和 3 个固定关节的统计

但其中的 URDF 和 STL 网格可以继续使用。本次只需另外编写 ROS 2 Python launch 文件,启动相关节点,无需先把整个描述包迁移成 ament 包。

7.2 URDF、关节状态和 TF 如何配合

URDF 记录连杆、关节、坐标关系及模型资源;joint_state_publisher_gui 通过滑块产生关节位置;robot_state_publisher 结合 URDF 与关节位置计算 TF,RViz 再据此摆放各连杆。
#mermaid-svg-oJh3r796lnRfpOII{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-oJh3r796lnRfpOII .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oJh3r796lnRfpOII .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oJh3r796lnRfpOII .error-icon{fill:#552222;}#mermaid-svg-oJh3r796lnRfpOII .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oJh3r796lnRfpOII .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oJh3r796lnRfpOII .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oJh3r796lnRfpOII .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oJh3r796lnRfpOII .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oJh3r796lnRfpOII .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oJh3r796lnRfpOII .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oJh3r796lnRfpOII .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oJh3r796lnRfpOII .marker.cross{stroke:#333333;}#mermaid-svg-oJh3r796lnRfpOII svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oJh3r796lnRfpOII p{margin:0;}#mermaid-svg-oJh3r796lnRfpOII .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oJh3r796lnRfpOII .cluster-label text{fill:#333;}#mermaid-svg-oJh3r796lnRfpOII .cluster-label span{color:#333;}#mermaid-svg-oJh3r796lnRfpOII .cluster-label span p{background-color:transparent;}#mermaid-svg-oJh3r796lnRfpOII .label text,#mermaid-svg-oJh3r796lnRfpOII span{fill:#333;color:#333;}#mermaid-svg-oJh3r796lnRfpOII .node rect,#mermaid-svg-oJh3r796lnRfpOII .node circle,#mermaid-svg-oJh3r796lnRfpOII .node ellipse,#mermaid-svg-oJh3r796lnRfpOII .node polygon,#mermaid-svg-oJh3r796lnRfpOII .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oJh3r796lnRfpOII .rough-node .label text,#mermaid-svg-oJh3r796lnRfpOII .node .label text,#mermaid-svg-oJh3r796lnRfpOII .image-shape .label,#mermaid-svg-oJh3r796lnRfpOII .icon-shape .label{text-anchor:middle;}#mermaid-svg-oJh3r796lnRfpOII .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oJh3r796lnRfpOII .rough-node .label,#mermaid-svg-oJh3r796lnRfpOII .node .label,#mermaid-svg-oJh3r796lnRfpOII .image-shape .label,#mermaid-svg-oJh3r796lnRfpOII .icon-shape .label{text-align:center;}#mermaid-svg-oJh3r796lnRfpOII .node.clickable{cursor:pointer;}#mermaid-svg-oJh3r796lnRfpOII .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oJh3r796lnRfpOII .arrowheadPath{fill:#333333;}#mermaid-svg-oJh3r796lnRfpOII .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oJh3r796lnRfpOII .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oJh3r796lnRfpOII .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oJh3r796lnRfpOII .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oJh3r796lnRfpOII .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oJh3r796lnRfpOII .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oJh3r796lnRfpOII .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oJh3r796lnRfpOII .cluster text{fill:#333;}#mermaid-svg-oJh3r796lnRfpOII .cluster span{color:#333;}#mermaid-svg-oJh3r796lnRfpOII 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-oJh3r796lnRfpOII .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oJh3r796lnRfpOII rect.text{fill:none;stroke-width:0;}#mermaid-svg-oJh3r796lnRfpOII .icon-shape,#mermaid-svg-oJh3r796lnRfpOII .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oJh3r796lnRfpOII .icon-shape p,#mermaid-svg-oJh3r796lnRfpOII .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oJh3r796lnRfpOII .icon-shape .label rect,#mermaid-svg-oJh3r796lnRfpOII .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oJh3r796lnRfpOII .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oJh3r796lnRfpOII .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oJh3r796lnRfpOII :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Q1 URDF
robot_state_publisher
robot_description
joint_state_publisher_gui
RViz RobotModel
joint_states
tf 与 /tf_static
STL 网格

固定关节的变换通过 /tf_static 发布,活动关节对应的变换通过 /tf 更新。/robot_description 使用 transient local 持久性,让后启动的兼容订阅者也能获取描述。这些行为可查阅 robot_state_publisher 的 Humble 文档。

7.3 处理网格相对路径

我的 URDF 中存在这种引用:

xml 复制代码
<mesh filename="../meshes/pelvis.STL" />

模型被读取成字符串后,再由 ROS 节点传给 RViz,网格加载器未必知道这个相对路径原本对应哪个 URDF 文件。因此,本次在加载时将相对路径解析成容器内的绝对 file:// URI。

这是一种适合当前目录结构的处理方式。正式做成 ROS 2 描述包时,也可以安装模型资源并使用 package:// 引用。

7.4 完整的 ROS 2 启动文件

保存为 /home/mi/Q1/q1_v3/display.launch.py:

python 复制代码
"""Q1 model visualization; place beside urdf/, meshes/ and q1.rviz."""
from pathlib import Path
import xml.etree.ElementTree as ET

from launch import LaunchDescription
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue


def generate_launch_description():
    root_dir = Path(__file__).resolve().parent
    urdf_path = root_dir / 'urdf' / 'q1_v3.urdf'
    rviz_path = root_dir / 'q1.rviz'
    tree = ET.parse(urdf_path)

    # Resolve mesh paths relative to the URDF file; leave package:// URLs alone.
    for mesh in tree.getroot().iter('mesh'):
        filename = mesh.get('filename', '')
        if filename and '://' not in filename:
            mesh_path = (urdf_path.parent / filename).resolve()
            if not mesh_path.is_file():
                raise FileNotFoundError(f'Mesh not found: {mesh_path}')
            mesh.set('filename', mesh_path.as_uri())

    description = ET.tostring(tree.getroot(), encoding='unicode')
    return LaunchDescription([
        Node(
            package='robot_state_publisher', executable='robot_state_publisher',
            name='robot_state_publisher', output='screen',
            parameters=[{'robot_description': ParameterValue(description, value_type=str)}],
        ),
        Node(
            package='joint_state_publisher_gui', executable='joint_state_publisher_gui',
            name='joint_state_publisher_gui', output='screen',
        ),
        Node(
            package='rviz2', executable='rviz2', name='rviz2', output='screen',
            arguments=['-d', str(rviz_path)],
        ),
    ])

这份代码以 launch 文件自身所在目录定位模型,对每个 mesh 路径分别处理,并在资源缺失时给出明确的文件名。URDF 源文件不会被改写。

我早期的启动文件还给 joint_state_publisher_gui 传了 robot_description 参数,而笔记记录的是通过话题接收的方式。整理后的代码统一采用本次安装版本实际支持的 /robot_description 订阅链路;该版本的官方实现也能看到这一逻辑。

7.5 RViz 配置

保存为 /home/mi/Q1/q1_v3/q1.rviz:

yaml 复制代码
Panels:
  - Class: rviz_common/Displays
    Name: Displays
Visualization Manager:
  Class: ""
  Displays:
    - Class: rviz_default_plugins/Grid
      Name: Grid
      Enabled: true
      Cell Size: 1
      Plane: XY
    - Class: rviz_default_plugins/TF
      Name: TF
      Enabled: true
      Show Axes: true
      Show Arrows: true
      Show Names: false
      Marker Scale: 0.15
    - Class: rviz_default_plugins/RobotModel
      Name: RobotModel
      Enabled: true
      Visual Enabled: true
      Collision Enabled: false
      Description Source: Topic
      Description Topic:
        Value: /robot_description
        Reliability Policy: Reliable
        Durability Policy: Transient Local
        History Policy: Keep Last
        Depth: 1
  Global Options:
    Fixed Frame: pelvis
    Background Color: 48; 48; 48
    Frame Rate: 30
  Tools:
    - Class: rviz_default_plugins/Interact
    - Class: rviz_default_plugins/MoveCamera
    - Class: rviz_default_plugins/Select
  Views:
    Current:
      Class: rviz_default_plugins/Orbit
      Distance: 2.5
      Pitch: 0.3
      Yaw: 0.5
      Focal Point:
        X: 0
        Y: 0
        Z: 0.6

最关键的设置如下:

RViz 选项 设置
Fixed Frame pelvis
RobotModel → Description Source Topic
Description Topic /robot_description
Reliability Reliable
Durability Transient Local

本模型的根连杆是 pelvis,所以先用它作为固定坐标系。模型显示阶段尚未建立 map 或 odom,不能因为这些名称常见就直接填入。

初始配置的描述话题使用了 Volatile;整理版改为 Transient Local,与发布端的持久性设置对应,使后启动的 RViz 也能接收已发布的描述。

八、启动并逐项验证

8.1 启动窗口

在容器内执行:

bash 复制代码
source /opt/ros/humble/setup.bash
ros2 launch /Q1/q1_v3/display.launch.py

应看到 RViz 和关节滑块两个窗口。截图中的 Q1 模型已显示,RobotModel → Status 为 Ok,并可看到关节坐标轴。

图7 RViz 中的 Q1 模型与 TF 坐标轴

图8 关闭 TF 显示后的完整 Q1 外观,RobotModel 状态为 Ok

做一次小幅关节滑动,观察对应连杆姿态是否变化。模型上的红、绿、蓝坐标轴是 TF 可视化;如果坐标轴太密,暂时关闭 TF Display,可以更清楚地看模型外观。

图9 关节滑块窗口与模型并排显示,当前姿态来自正在运行的关节状态发布节点

8.2 在第二个终端检查话题

打开一个新的宿主机终端,进入同一个容器:

bash 复制代码
docker exec -it ros2_humble bash
source /opt/ros/humble/setup.bash

ros2 node list
ros2 topic list
ros2 topic info /robot_description
ros2 topic echo /joint_states --once
ros2 topic echo /tf --once
ros2 topic echo /tf_static --once --qos-durability transient_local

读取完整 URDF 描述时,可以使用:

bash 复制代码
ros2 topic echo /robot_description --once \
  --qos-durability transient_local

这些命令分别回答:节点是否存在、消息是否发布、关节名称是否正确、动态和静态变换是否都能收到。只看到窗口或只看到话题名称,还不足以完成这一步验证。

图10 运行中的 ROS 节点、模型相关话题与 robot_description 发布订阅数量

8.3 核对连杆与关节数量

本地 URDF 的统计结果是:

text 复制代码
link:26
joint:25
revolute joint:22
fixed joint:3

因此,/joint_states 中出现 22 个活动关节,与 URDF 的 25 个关节定义并不矛盾:另外 3 个是固定关节。

可在容器内自行统计:

bash 复制代码
python3 - <<'PY'
from collections import Counter
import xml.etree.ElementTree as ET

root = ET.parse('/Q1/q1_v3/urdf/q1_v3.urdf').getroot()
print('links:', len(root.findall('link')))
print('joints:', len(root.findall('joint')))
print('types:', dict(Counter(j.attrib['type'] for j in root.findall('joint'))))
PY

统计只针对本次本地模型文件。模型包升级后应重新核对,不能把这个数字当作所有版本的固定结论。

8.4 本次验证结果

检查项 本次结果 能说明什么
PrimeConsole 安装 install ok installed 1.0.2 deb 包已安装
PrimeConsole 主窗口 已打开,状态"未连接" 客户端可启动
ROS 发行版 humble 当前容器终端加载了 Humble
RViz RobotModel Status: Ok 截图中的模型可正常显示
URDF 描述 26 个 link、25 个 joint 模型结构可解析
/joint_states 22 个名称及对应位置 活动关节状态链路可用
/tf 与 /tf_static 均能收到变换 动态与静态 TF 链路可用
物理仿真与真机 本阶段未验证 还需后续测试

文章整理时,我还在独立 ROS 域中重新启动了上文的 launch 与 RViz 配置,用只读脚本收取四类话题。检查得到 22 个关节状态名称、单条动态 TF 消息中的 22 个变换,以及静态 TF 消息中的 3 个变换;该结果与本地 URDF 的活动、固定关节数量一致。

图11 只读订阅脚本收到的 22 个关节名称及位置(单位:弧度,来自可视化链路)

图12 只读订阅收到的 22 个动态 TF 与 3 个静态 TF,图中列出部分动态变换及全部静态变换

九、这次遇到的坑与排查顺序

9.1 RViz 报 nouveau / dri3 错误

首次运行记录中出现了:

text 复制代码
libGL error: glx: failed to create dri3 screen
libGL error: failed to load driver: nouveau

当时容器尚未配置可用的 GPU 加速链路,错误出现在图形驱动初始化阶段。我在 Compose 中加入:

yaml 复制代码
LIBGL_ALWAYS_SOFTWARE: "1"

重新创建容器后,RViz 能显示模型。这验证了软件渲染是本次环境的一条可用路径;仅凭上述两行错误,还不能把根因唯一归结为某一个 NVIDIA 软件包。

如果变量是在当前终端中临时 export 的,它只影响之后从该终端启动的进程。把它写进 Compose 后,需要执行 docker compose up -d 应用容器配置。

9.2 一直显示 Waiting for robot_description

本次日志先出现:

text 复制代码
Waiting for robot_description to be published on the robot_description topic...
Centering

短暂等待可能只是节点启动先后顺序造成的。本次随后出现 Centering,并且确实收到了 /joint_states,因此启动链路已经继续运行。

如果长期等待,按下面顺序检查:

bash 复制代码
ros2 node list
ros2 param get /robot_state_publisher robot_description
ros2 topic info /robot_description --verbose

先确认发布节点没有退出、参数已设置,再检查话题、域和 QoS。不要只根据一行 Waiting 日志判断程序已经卡死。

9.3 RViz 里只有坐标轴,没有机器人外观

先看 RobotModel 的具体错误。如果是 Could not load resource,优先检查网格 URI、容器内路径和文件名大小写。

bash 复制代码
ls /Q1/q1_v3/meshes/pelvis.STL

Linux 文件名区分大小写,pelvis.STL 与 pelvis.stl 是不同路径。宿主机文件存在,也要确认容器挂载后能够看到。

robot_state_publisher 负责计算与发布坐标变换;实际读取 STL 来绘制外观的是 RViz 的相关显示组件。TF 正常而外观缺失,完全可能是资源路径问题。

9.4 Fixed Frame 不存在

如果 RViz 使用了 map,但当前节点只建立以 pelvis 为根的机器人坐标树,就会报固定坐标系相关错误。

本篇先将 Fixed Frame 设为 pelvis。之后接入导航或世界坐标系时,再根据真实 TF 树调整。

9.5 KDL 提示根连杆带有惯量

复核本次日志还发现:

text 复制代码
The root link pelvis has an inertia specified in the URDF,
but KDL does not support a root link with an inertia.

这条警告在模型成功显示时仍然存在。当前链路继续解析了连杆,并发布了关节变换,因此不能把它写成"完全没有任何警告"。

图13 原运行日志摘录:软件渲染变量、OpenGL 初始化、描述接收及 KDL 根连杆惯量警告

本阶段保留原模型,以完成可视化验证。若后续需要基于 KDL 进行相关计算,可评估添加一个无惯量的虚拟根连杆,并用固定关节连接 pelvis;修改之后需要重新核对根坐标系和模型用途。不要为消除一条警告,直接删掉后续物理仿真可能需要的惯性数据。

9.6 容器重建后,昨天装的包不见了

先区分"重启原容器"和"删除后创建新容器"。docker compose down 会删除服务容器,之后新建的容器从镜像开始,临时 apt 安装的内容不会自动回来。

本次把两个关节发布工具加入 Dockerfile,就是为了解决这个问题。项目文件则通过宿主目录挂载保存。

9.7 GUI 无法连接显示服务

先在宿主机检查 DISPLAY 与 /tmp/.X11-unix,再检查容器内是否收到正确的环境变量和 socket 挂载。

bash 复制代码
# 宿主机
echo "$DISPLAY"
ls /tmp/.X11-unix

# 容器内
echo "$DISPLAY"
ls /tmp/.X11-unix

本篇使用 xhost +si:localuser:root 授权本地 root 用户。不同 X11/XWayland 和 Docker 用户映射环境可能需要不同认证配置;遇到授权错误时应核对实际容器用户与 X11 认证,避免直接照搬一个 DISPLAY 值。

9.8 run.sh 启动了容器,却没有进入

我的原始笔记把 ./run.sh 写成了"启动并进入"。实际脚本中,默认分支只执行 docker compose up -d,进入容器需要另外执行:

bash 复制代码
./run.sh enter

本文统一使用 docker exec -it ros2_humble bash,避免把宿主终端误认为容器终端。

十、再次启动与停止

之后继续测试时,在宿主机执行:

bash 复制代码
cd /home/mi/Q1/ros2_humble
export Q1_ROOT=/home/mi/Q1
xhost +si:localuser:root
docker compose up -d
docker exec -it ros2_humble bash

容器内执行:

bash 复制代码
source /opt/ros/humble/setup.bash
ros2 launch /Q1/q1_v3/display.launch.py

在运行 launch 的终端按 Ctrl+C,即可停止本次启动的节点。若还要停止容器,在宿主机的 Compose 目录执行:

bash 复制代码
docker compose stop

所有依赖这次授权的图形程序关闭后,可以在宿主机撤销该 X11 访问授权:

bash 复制代码
xhost -si:localuser:root

十一、从模型验证走向物理仿真和真机

这次完成后,我已经有了可以显示 Q1、检查关节命名和 TF 的环境。接下来的工作需要逐步增加验证范围:

阶段 重点检查 应保留的证据
当前:RViz 模型验证 网格、关节、TF、坐标方向 模型截图、话题样本、日志
下一步:动力学仿真 质量与惯量、碰撞体、接触、执行器和控制器 仿真配置、状态曲线、失败记录
真机只读接入 网络发现、消息类型、状态反馈与单位 节点列表、状态样本、固件与 SDK 版本
真机受控动作验证 模式、关节映射、限位、频率与停止机制 指令与反馈时间戳、实际动作记录

RViz 中拖动滑块,验证的是模型姿态变化。进入动力学仿真后,还要处理重力、接触和控制器;进入真机后,还要核对 SDK 的实际接口、关节顺序和运行模式。模型显示正常是这些工作的起点。

下一篇将围绕 SDK 的接口使用展开,先从电池与触摸等状态读取入手,再验证语音服务,逐步把开发环境连接到真实机器人能力。

启元开发者社区:启元机器人开发者社区

相关推荐
skywalk81631 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
翔云1234562 小时前
RocksDB中,客户端每写入一次数据,需要访问几次磁盘
运维·数据库·mysql
爱莉希雅&&&3 小时前
GitLab 完整学习笔记
linux·运维·服务器
风笙9783 小时前
[文件系统]文件系统挂载与卸载详解
linux
GeW3 小时前
新质生产力路线图解读:数据库性能优化与Linux调优专场
linux·数据库
小杨爱科普4 小时前
制造业上自动化改造,改造部分的专利到底归谁
运维·经验分享·自动化
陈希1235 小时前
Linux 服务器性能排查手册(2026):CPU、内存、磁盘IO、网络四大瓶颈定位
linux·服务器·网络
方便面不加香菜5 小时前
Docker Container --容器及容器的操作案例详解
运维·docker·容器
测试界的小码农8 小时前
【Linux 命令实战(03):文件创建、复制、移动与删除,掌握 touch、mkdir、cp、mv、rm】
linux·服务器·php