Docker 里使用 ROS2 开发指南
Docker 里使用 ROS2 开发指南 你在 Linux 里用 ROS2 的经验完全适用,唯一的区别是: 代码在 Mac 上编辑,命令在 Docker 容器里运行 。 1. 核心概念:Docker vs 原生 Linux | 你在 Linux 上做的事 | 在 Docker 里对应的操作 | | | | | 打开一个终端 | docker compose exec ros2 go2 bash | | 再开一个终端 | 再执行一次同样的…
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,找到以太网接口名(如 eth0、en0、enp3s0),然后在 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_go、unitree_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/sportmodestate | Go2 -> 你 | 运动状态(位置、速度、姿态、步态) |
rt/lowstate | Go2 -> 你 | 低层状态(关节角度、IMU、电量) |
rt/utlidar/cloud | Go2 -> 你 | 激光雷达点云 |
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 开发注意事项
- 先在 Go2 App 里确认 Sport Mode 已开启 — 高层控制(走路、站立)依赖 Sport Mode
- 不要同时运行多个控制程序 — 多个控制源会冲突,导致机器人行为异常
- 低层控制前必须关闭 Sport Mode — 通过
MotionSwitcherClient切换,先StandDown()让狗趴下再切 - 测试新代码时让 Go2 悬空 — 用支架把 Go2 架起来,避免失控摔坏
- 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 不通?
- 确认 Mac 的以太网 IP 已设为
192.168.123.222,子网掩码255.255.255.0 - 确认网线已连接到 Go2
- macOS Docker Desktop 的
network_mode: host实际上是 Docker VM 的网络,如果不通可以尝试移除network_mode: host并改用macvlan或直接在 Mac 上安装 ROS2
Q: ros2 topic list 看不到 Go2 的话题?
- 确认
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp(echo $RMW_IMPLEMENTATION检查) - 确认
cyclonedds.xml里的网卡名正确 - 确认 Go2 已开机并且 Sport Mode 已启动