BACK_TO_BASE
Engineering Notebook // Build Log
/
23:17:56
/
NOTEBOOK_ENTRY

Docker 里使用 ROS2 开发指南

Docker 里使用 ROS2 开发指南 你在 Linux 里用 ROS2 的经验完全适用,唯一的区别是: 代码在 Mac 上编辑,命令在 Docker 容器里运行 。 1. 核心概念:Docker vs 原生 Linux | 你在 Linux 上做的事 | 在 Docker 里对应的操作 | | | | | 打开一个终端 | docker compose exec ros2 go2 bash | | 再开一个终端 | 再执行一次同样的…

Notebook Time
3 min
Image Frames
0
View Tracks
74
ROS
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

Docker 里使用 ROS2 开发指南

你在 Linux 里用 ROS2 的经验完全适用,唯一的区别是:代码在 Mac 上编辑,命令在 Docker 容器里运行


1. 核心概念:Docker vs 原生 Linux

你在 Linux 上做的事在 Docker 里对应的操作
打开一个终端docker compose exec ros2-go2 bash
再开一个终端再执行一次同样的命令(同一个容器,新 shell)
编辑代码在 Mac 上用 VSCode 编辑 src/ 目录,容器内自动同步
source /opt/ros/humble/setup.bash已自动执行(entrypoint.sh 和 .bashrc 里配好了)
colcon build在容器内执行,和 Linux 完全一样
ros2 run / ros2 topic在容器内执行,和 Linux 完全一样
sudo apt install ...在容器内执行,但容器重建后会丢失(持久安装写进 Dockerfile)

一句话总结:Mac 上编辑文件,容器里跑命令。


2. 日常操作速查

启动 / 停止 / 重建

# 在 Mac 终端(ros2 项目目录下)

docker compose up -d --build   # 构建镜像 + 启动容器(首次或 Dockerfile 改了)
docker compose up -d           # 启动容器(镜像已存在)
docker compose down            # 停止并删除容器
docker compose down -v         # 停止 + 删除编译缓存(build/install/log)
docker compose restart         # 重启容器

进入容器

# 每执行一次 = 开一个新终端(在同一个容器里)
docker compose exec ros2-go2 bash

你可以在 Mac 上开多个终端窗口,每个都执行这条命令,就像在 Linux 上开多个终端一样。

直接运行单条命令(不进入交互 shell)

docker compose exec ros2-go2 ros2 topic list
docker compose exec ros2-go2 ros2 node list
docker compose exec ros2-go2 python3 ~/ros2_ws/src/my_node.py

3. 创建和运行 Python 节点

方式 A:直接写脚本(快速测试推荐)

不需要创建包、不需要 colcon build,适合快速验证想法。

在 Mac 上的 src/ 目录下创建文件,比如 src/my_publisher.py

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class MyPublisher(Node):
    def __init__(self):
        super().__init__('my_publisher')
        self.pub = self.create_publisher(String, '/chatter', 10)
        self.timer = self.create_timer(1.0, self.publish_msg)
        self.count = 0

    def publish_msg(self):
        msg = String()
        msg.data = f'Hello ROS2 #{self.count}'
        self.pub.publish(msg)
        self.get_logger().info(f'Published: {msg.data}')
        self.count += 1


def main():
    rclpy.init()
    node = MyPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

在容器里运行:

python3 ~/ros2_ws/src/my_publisher.py

方式 B:创建 ROS2 Package(正式项目推荐)

# 进入容器
docker compose exec ros2-go2 bash

# 创建包
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python my_go2_pkg \
    --dependencies rclpy std_msgs geometry_msgs

这会在 src/my_go2_pkg/ 生成一个标准包结构(Mac 上的 src/ 目录里能直接看到):

src/my_go2_pkg/
├── my_go2_pkg/
│   └── __init__.py       # <- 你的节点代码放这里
├── resource/
├── test/
├── package.xml
├── setup.cfg
└── setup.py              # <- 注册节点入口

添加节点:在 Mac 上编辑 src/my_go2_pkg/my_go2_pkg/my_node.py,写好代码后编辑 setup.py 注册入口:

entry_points={
    'console_scripts': [
        'my_node = my_go2_pkg.my_node:main',
    ],
},

编译并运行(在容器内):

cd ~/ros2_ws
colcon build --packages-select my_go2_pkg
source install/setup.bash
ros2 run my_go2_pkg my_node

4. 节点间通信

和 Linux 上完全一样,只是每个"终端"都通过 docker compose exec ros2-go2 bash 进入。

Topic(话题)—— 发布/订阅

最常用的通信方式,一对多,异步。

终端 1 — Publisher:

docker compose exec ros2-go2 bash
python3 ~/ros2_ws/src/my_publisher.py

终端 2 — Subscriber(直接用命令行监听):

docker compose exec ros2-go2 bash
ros2 topic echo /chatter

或者写一个 subscriber 脚本 src/my_subscriber.py

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class MySubscriber(Node):
    def __init__(self):
        super().__init__('my_subscriber')
        self.sub = self.create_subscription(String, '/chatter', self.callback, 10)

    def callback(self, msg):
        self.get_logger().info(f'Received: {msg.data}')


def main():
    rclpy.init()
    node = MySubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Service(服务)—— 请求/响应

一对一,同步调用。

# 终端 1:查看有哪些 service
ros2 service list

# 终端 2:调用一个 service(以 turtlesim 为例)
ros2 service call /spawn turtlesim/srv/Spawn "{x: 5.0, y: 5.0, theta: 0.0, name: 'turtle2'}"

常用调试命令

ros2 topic list              # 查看所有话题
ros2 topic echo /topic_name  # 监听某个话题
ros2 topic hz /topic_name    # 查看话题频率
ros2 topic info /topic_name  # 查看话题的发布者/订阅者

ros2 node list               # 查看所有活跃节点
ros2 node info /node_name    # 查看节点详情

ros2 service list            # 查看所有服务
ros2 param list              # 查看所有参数

这些命令在容器内和 Linux 上用法完全一致。


5. 多节点同时运行(Launch 文件)

如果有多个节点要一起启动,用 launch 文件。

在 Mac 上创建 src/my_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle',
        ),
        Node(
            package='turtlesim',
            executable='turtle_teleop_key',
            name='teleop',
            prefix='xterm -e',  # 需要 xterm,也可以去掉这行手动开终端
        ),
    ])

在容器内运行:

ros2 launch ~/ros2_ws/src/my_launch.py

6. 安装新的依赖

临时安装(容器重建后消失)

# 进入容器后
sudo apt update
sudo apt install ros-humble-some-package
pip3 install some-python-lib

永久安装(写进 Dockerfile)

在 Mac 上编辑 Dockerfile,在对应的 RUN apt-get install 里添加包名,然后重建:

docker compose up -d --build

7. 文件在哪 / 什么会保留

内容位置重建容器后
你的源码(src/Mac 本地 src/ 目录永远保留(不在容器里)
编译产物(build/, install/Docker named volume保留(除非 docker compose down -v
容器内 apt install 的包容器文件系统丢失(需写进 Dockerfile)
容器内 pip install 的包容器文件系统丢失(需写进 Dockerfile)

8. 完整工作流示例

以"写一个 publisher 控制 Go2 运动"为例:

# Step 1: Mac 上用 VSCode 编辑 src/go2_walk.py
#         (文件保存后容器内立刻能看到)

# Step 2: 在 Mac 终端进入容器
docker compose exec ros2-go2 bash

# Step 3: 运行
python3 ~/ros2_ws/src/go2_walk.py

# Step 4: 另开一个 Mac 终端,进容器调试
docker compose exec ros2-go2 bash
ros2 topic list
ros2 topic echo /cmd_vel

# Step 5: 改代码?直接在 Mac VSCode 里改,Ctrl+C 停掉容器里的进程,重新 python3 运行

不需要 colcon build,不需要 source,直接 python3 跑脚本就行。 只有创建了正式 ROS2 package 才需要 build。


9. Unitree Go2 使用

9.1 网络连接

Go2 通过以太网用 CycloneDDS 通信。把电脑用网线连到 Go2,配好 IP:

设备IP 地址
Go2 机载电脑192.168.123.161
你的电脑(Mac/容器)192.168.123.xxx(推荐 192.168.123.222

Mac 上设置静态 IP:系统设置 -> 网络 -> 以太网 -> 手动配置 IP 为 192.168.123.222,子网掩码 255.255.255.0

修改 cyclonedds.xml 里的网卡名:进容器执行 ip link,找到以太网接口名(如 eth0en0enp3s0),然后在 Mac 上编辑 cyclonedds.xml

<NetworkInterface name="你的接口名" priority="default" multicast="true" />

9.2 安装 unitree_ros2

# 进入容器
docker compose exec ros2-go2 bash

# 克隆 unitree_ros2(包含 Go2 的消息定义和示例)
cd ~/ros2_ws/src
git clone --recurse-submodules https://github.com/unitreerobotics/unitree_ros2.git

# 安装依赖并编译
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
colcon build
source install/setup.bash

编译完成后,你就有了 Go2 专用的消息类型(unitree_gounitree_api)。

9.3 安装 unitree_sdk2_python

如果你想用 Python SDK 直接控制 Go2(不通过 ROS2 话题),在容器里安装:

cd ~/ros2_ws/src
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
pip3 install -e .

9.4 Go2 的 DDS 话题

Go2 通过 CycloneDDS 直接暴露以下话题(不是标准 ROS2 话题格式,带 rt/ 前缀):

话题方向说明
rt/sportmodestateGo2 -> 你运动状态(位置、速度、姿态、步态)
rt/lowstateGo2 -> 你低层状态(关节角度、IMU、电量)
rt/utlidar/cloudGo2 -> 你激光雷达点云
rt/api/sport/request你 -> Go2高层运动控制指令
rt/lowcmd你 -> Go2低层关节控制指令

9.5 高层控制示例:让 Go2 走路

unitree_sdk2_python 的方式,创建 src/go2_walk.py

#!/usr/bin/env python3
"""
让 Go2 前进 3 秒然后停下。
确保 Go2 已开机、已站立、网线已连接。
"""
import time
import sys
from unitree_sdk2py.core.channel import ChannelFactoryInitialize
from unitree_sdk2py.go2.sport.sport_client import SportClient


def main():
    # 初始化 DDS 通信(参数是网卡名,如 eth0)
    iface = sys.argv[1] if len(sys.argv) > 1 else "eth0"
    ChannelFactoryInitialize(0, iface)

    sport = SportClient()
    sport.SetTimeout(10.0)
    sport.Init()

    print("Standing up...")
    sport.StandUp()
    time.sleep(1.0)

    print("Walking forward...")
    # Move(vx, vy, vyaw) — 前进速度 0.3 m/s
    sport.Move(0.3, 0.0, 0.0)
    time.sleep(3.0)

    print("Stopping...")
    sport.StopMove()
    time.sleep(0.5)

    print("Done!")


if __name__ == '__main__':
    main()

运行:

# 进容器
docker compose exec ros2-go2 bash

# 运行(替换 eth0 为你的网卡名)
python3 ~/ros2_ws/src/go2_walk.py eth0

9.6 用 ROS2 话题控制 Go2

如果你编译了 unitree_ros2,可以用标准 ROS2 方式发话题控制 Go2。

创建 src/go2_ros2_move.py

#!/usr/bin/env python3
"""
通过 ROS2 话题发送运动请求给 Go2。
需要先 colcon build unitree_ros2 并 source install/setup.bash。
"""
import json
import rclpy
from rclpy.node import Node
from unitree_api.msg import Request


class Go2Commander(Node):
    def __init__(self):
        super().__init__('go2_commander')
        self.pub = self.create_publisher(Request, 'api/sport/request', 10)
        self.timer = self.create_timer(0.1, self.send_cmd)
        self.get_logger().info('Go2 commander started')

    def send_cmd(self):
        msg = Request()
        msg.header.identity.api_id = 1008  # Move API ID
        msg.parameter = json.dumps({
            "x": 0.3,    # 前进速度 m/s
            "y": 0.0,    # 横向速度
            "z": 0.0     # 旋转角速度
        })
        self.pub.publish(msg)


def main():
    rclpy.init()
    node = Go2Commander()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

运行(需要先编译 unitree_ros2):

cd ~/ros2_ws && source install/setup.bash
python3 ~/ros2_ws/src/go2_ros2_move.py

9.7 监听 Go2 状态

终端 1(查看运动状态):

docker compose exec ros2-go2 bash
cd ~/ros2_ws && source install/setup.bash
ros2 topic echo /sportmodestate

终端 2(查看低层状态 — 关节、IMU):

docker compose exec ros2-go2 bash
cd ~/ros2_ws && source install/setup.bash
ros2 topic echo /lowstate

9.8 Go2 开发注意事项

  1. 先在 Go2 App 里确认 Sport Mode 已开启 — 高层控制(走路、站立)依赖 Sport Mode
  2. 不要同时运行多个控制程序 — 多个控制源会冲突,导致机器人行为异常
  3. 低层控制前必须关闭 Sport Mode — 通过 MotionSwitcherClient 切换,先 StandDown() 让狗趴下再切
  4. 测试新代码时让 Go2 悬空 — 用支架把 Go2 架起来,避免失控摔坏
  5. ping 测试连通性
    docker compose exec ros2-go2 ping 192.168.123.161
    

10. 常见问题

Q: 为什么我的节点看不到其他节点发的 topic? 确保所有节点都在同一个容器里运行(都用 docker compose exec ros2-go2 bash 进入)。它们共享同一个 ROS2 环境。

Q: 我在容器里 apt install 了一个包,重建后没了? 把它加到 Dockerfile 里,然后 docker compose up -d --build 重建。

Q: 容器里修改了文件但 Mac 上看不到? 只有 src/ 目录是双向同步的。容器里其他目录的修改不会反映到 Mac。

Q: colcon build 报错怎么办? 和 Linux 上一样排查。常见原因:缺依赖(rosdep install --from-paths src --ignore-src -r -y)。

Q: 怎么同时跑两个不同的 ROS2 程序? 开两个 Mac 终端,各自执行 docker compose exec ros2-go2 bash,然后分别运行。就像 Linux 上开两个终端一样。

Q: 容器里 ping 192.168.123.161 不通?

  1. 确认 Mac 的以太网 IP 已设为 192.168.123.222,子网掩码 255.255.255.0
  2. 确认网线已连接到 Go2
  3. macOS Docker Desktop 的 network_mode: host 实际上是 Docker VM 的网络,如果不通可以尝试移除 network_mode: host 并改用 macvlan 或直接在 Mac 上安装 ROS2

Q: ros2 topic list 看不到 Go2 的话题?

  1. 确认 RMW_IMPLEMENTATION=rmw_cyclonedds_cppecho $RMW_IMPLEMENTATION 检查)
  2. 确认 cyclonedds.xml 里的网卡名正确
  3. 确认 Go2 已开机并且 Sport Mode 已启动