目录
++[目录... 1](#目录... 1)++
++[一、Turtlesim简介... 2](#一、Turtlesim简介... 2)++
++[1.1核心可执行节点和主要接口... 2](#1.1核心可执行节点和主要接口... 2)++
++[1.2说明与局限... 2](#1.2说明与局限... 2)++
++[二、创建新的工作空间... 3](#二、创建新的工作空间... 3)++
++[2.1 创建功能包turtle_exercsie. 3](#2.1 创建功能包turtle_exercsie. 3)++
++[2.2 仿真功能的示例-话题控制... 3](#2.2 仿真功能的示例-话题控制... 3)++
++[2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹... 3](#2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹... 3)++
++[2.3 仿真功能的示例-服务调用... 9](#2.3 仿真功能的示例-服务调用... 9)++
++[2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环... 9](#2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环... 9)++
++[2.4 仿真功能的示例-动作反馈... 14](#2.4 仿真功能的示例-动作反馈... 14)++
++[2.4.1 创建动作接口功能包-小海龟通过指令话多边形... 14](#2.4.1 创建动作接口功能包-小海龟通过指令话多边形... 14)++
++[第三章 实际开发中的选型指南... 18](#第三章 实际开发中的选型指南... 18)++
++[3.1、 话题(Topic)与服务(Service)的优缺点对比... 18](#3.1、 话题(Topic)与服务(Service)的优缺点对比... 18)++
++[3.2、 结合你的案例来分析... 18](#3.2、 结合你的案例来分析... 18)++
++[3.3、 实战指南:开发时如何选择?... 19](#3.3、 实战指南:开发时如何选择?... 19)++
一、Turtlesim简介
**turtlesim(小乌龟模拟器)** 是 ROS2 官方提供的轻量级二维教学仿真工具,并非真实机器人或完整物理仿真器,其设计目的是帮助初学者直观理解 ROS2 的核心概念------节点(Node)、话题(Topic)、服务(Service)、动作(Action)及参数(Parameter)。
turtlesim 启动后会弹出一个蓝色背景的 2D 窗口,其中有一只可程序控制运动的小乌龟(turtle),常用于演示 ROS2 通信机制与命令行工具的使用。
1.1核心可执行节点和主要接口
- turtlesim_node:主仿真节点,负责渲染画布、维护乌龟位姿,订阅速度指令并发布位姿信息。
- turtle_teleop_key:键盘控制节点,将方向键映射为 geometry_msgs/msg/Twist消息发布到 /turtle1/cmd_vel。
- draw_square/ mimic:示例程序,分别演示自动画正方形和运动跟随。
主要通信接口
- 话题 Topic
- /turtle1/cmd_vel--- 订阅 geometry_msgs/Twist,接收线速度与角速度指令控制乌龟运动。
- /turtle1/pose--- 发布 turtlesim/msg/Pose,广播乌龟当前坐标 (x, y)、朝向 θ及速度。
- 服务 Service
- /spawn--- 生成新乌龟(指定坐标、朝向、名称)。
- /kill--- 删除指定乌龟。
- /reset--- 重置画布与乌龟至初始状态。
- /turtle1/set_pen--- 设置乌龟画笔颜色、线宽及是否落笔。
- /turtle1/teleport_absolute/ teleport_relative--- 绝对/相对瞬移。
- 动作 Action
- /turtle1/rotate_absolute--- 控制乌龟旋转到指定朝向,带反馈与可取消。
- 参数 Parameter
- /turtlesim节点提供 background_r、background_g、background_b参数,可在运行时动态修改画布背景色。
启动方式示例
终端1:启动仿真器
ros2 run turtlesim turtlesim_node
终端2:启动键盘控制(鼠标聚焦此终端后用↑↓←→控制)
ros2 run turtlesim turtle_teleop_key
1.2说明与局限
turtlesim 不具备物理仿真能力(无重力、碰撞、摩擦力等),不能替代 Gazebo 等物理仿真器。它仅用于 ROS2 基础概念学习与通信机制验证,是进入真实机器人或 Gazebo 仿真前的"Hello World"级入门工具。
二、创建新的工作空间
2.1 创建功能包turtle_exercsie
在 ROS 2 中创建一个新的工作空间ros2_ws1,并创建一个名称是turtle_exercise的功能包,下面示例使用Python功能包进行。
首先在终端执行如下命令:创建功能包和代码存放文件夹src;进入src文件里,创建我的小海龟练习功能包文件turtle_exercise;
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~# ls ros2_ws ros_ws root@LHAYR:~# mkdir -p ~/ros2_ws1/src root@LHAYR:~# cd ros2_ws1/src root@LHAYR:~/ros2_ws1/src# ros2 pkg create --build-type ament_python --license MIT turtle_exercise going to create a new package package name: turtle_exercise 。。。。。。。。 root@LHAYR:~/ros2_ws1/src# |
2.2 仿真功能的示例-话题控制
话题(Topic)是一种基于发布者--订阅者(Publisher--Subscriber)模型的通信机制,允许节点通过发布和订阅消息实现松耦合的数据交换。在 ROS2 中,话题是节点间传输数据流的主要方式,特别适用于连续、高频、实时性要求较高的数据(如传感器数据、控制指令等)。
在 Turtlesim 仿真环境中,可以通过话题实时控制小海龟的运动状态,例如发布速度指令驱动其移动,或订阅位姿话题实时获取其在画布中的坐标与朝向。借助话题机制,系统能够以较低延迟完成对小海龟运动的实时控制与状态监控,因此话题也被认为是各类 ROS2 仿真与控制任务中最常用、最核心的通信工具之一。
本案例通过 Turtlesim 提供的速度控制话题 /turtle1/cmd_vel,控制小海龟沿螺旋线轨迹运动,效果如图 X-X 所示。其实现原理为:在保持角速度恒定的前提下,使线速度随时间线性增大,从而使小海龟在旋转的同时不断向外"扩散",形成螺旋线轨迹。具体实现步骤如下:
URDF 文件主要包括加载机器人模型和发布机器人关节状态两个过程。机器人模型的发布需要使用机器人状态发布器(Robot State Publisher)。机器人状态发布器将机器人的状态发布到tf2,使得ROS2能够了解机器人内部各连杆和关节的坐标系,从而确定机器人的状态。
2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹
在turtle_exercise功能包里,创建一个节点代draw_spiral.py,代码写入内容如下:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| * * 创建draw_spiral.py root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1/src/turtle_exercise/turtle_exercise root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# touch draw_spiral.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# chmod +x draw_spiral.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# ls init.py draw_spiral.py root@LHAYR:~/ros2_ws1/src #code . |
| * * vs code 打开draw_spiral.py,代码写入内容如下 |
| #!/usr/bin/env python3 import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class SpeedPublisher(Node): def init(self): super().init('draw_spiral') # 声明参数,默认控制名为 'turtle1' 的小海龟 self.turtle_name = self.declare_parameter('turtle_name', 'turtle1').get_parameter_value().string_value # 创建发布者,向对应小海龟的 cmd_vel 话题发布速度指令 self.publisher = self.create_publisher(Twist, f'{self.turtle_name}/cmd_vel', 10) # 创建定时器,每 0.1 秒触发一次回调函数 self.timer = self.create_timer(0.1, self.on_timer) # 初始化 Twist 消息 self.cmd = Twist() self.cmd.linear.x = 0.0 # 初始线速度为 0 self.cmd.angular.z = 3.0 # 初始角速度为 3.0 def on_timer(self): # 每次定时器触发,线速度增加 0.03,使运动半径逐渐变大 self.cmd.linear.x += 0.03 self.publisher.publish(self.cmd) def main(args=None): rclpy.init(args=args) speed_publisher = SpeedPublisher() try: rclpy.spin(speed_publisher) except KeyboardInterrupt: pass finally: speed_publisher.destroy_node() rclpy.shutdown() if name == 'main': main() |
| * * 修改setup.pyw文件,将螺旋运动控制节点添加到entry_points中 |
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', , |
|
|
在turtle_exercise功能包里,先创建一个launch文件夹,再创建一个spiral_launch.py的launch文件,代码写入内容如下: 用launch文件直接启动小海龟turtlesim_node和控制节点draw_spiral.py
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| * 先创建文件夹,在创建文件 |
| root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# cd .. root@LHAYR:~/ros2_ws1/src/turtle_exercise# mkdir -p launch root@LHAYR:~/ros2_ws1/src/turtle_exercise# cd launch root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# touch spiral_launch.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# ls spiral_launch.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# |
| * 在vs code编译器中,打开spiral_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='sim' ), Node( package='turtle_exercise', executable='draw_spiral', name='drawspiral', parameters=\[ {"turtlename": "turtle1"} ) ]) |
| * 修改setup.py文件,把launch文件路径添加到data_files变量中 |
| 顶部添加: |-----------------------------------------------------------------------------| | import os from glob import glob from setuptools import find_packages, setup | 在data_files添加('share/' + package_name + '/launch', glob('launch/*launch.py')) |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | data_files= ('share/ament_index/resource_index/packages', \['resource/' + package_name), ('share/' + package_name, 'package.xml'), ('share/' + package_name + '/launch', glob('launch/*launch.py')) ], |
|
编译,加载,运行
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1# colcon build Starting >>> turtle_exercise Finished <<< turtle_exercise 0.58s Summary: 1 package finished 0.66s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle_exercise spiral_launch.py INFO launch: All log files can be found below /root/.ros/log/2026-07-10-17-05-33-959101-LHAYR-3451 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 3454 INFO draw_spiral-2: process started with pid 3455 turtlesim_node-1 INFO 1783674334.278782642 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO 1783674334.290728612 sim: Spawning turtle turtle1 at x=5.544445, y=5.544445, theta=0.000000 turtlesim_node-1 WARN 1783674390.233078662 sim: Oh no! I hit the wall! (Clamping from x=6.048432, y=-0.020131) turtlesim_node-1 WARN 1783674390.249716780 sim: Oh no! I hit the wall! (Clamping from x=5.781527, y=-0.022364) |
| 运行结果如下: |
|
|
升级版本,小海龟路径是五角星,修改draw_spiral.py代码
python
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.srv import SetPen, TeleportAbsolute
import math
class StarDrawer(Node):
def __init__(self):
super().__init__('star_drawer')
# 1. 创建发布者
self.cmd_pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
# 2. 定义状态机变量
# 你的逻辑:4 -> 8 -> 8 -> 8 -> 8 -> 4 (共6段动作)
self.segment_lengths = [4.0, 8.0, 8.0, 8.0, 8.0, 4.0]
self.current_segment = 0
# 物理参数
self.linear_speed = 2.0 # 前进速度 m/s
self.angular_speed = 1.5 # 转弯速度 rad/s (约86度/秒)
self.turn_angle = 144.0 * (math.pi / 180.0) # 目标转角:144度
# 计时器
self.timer = self.create_timer(0.05, self.control_loop) # 20Hz 控制频率
self.elapsed_time = 0.0
# 初始化状态
self.state = 'MOVING' # MOVING 或 TURNING
self.reset_turtle()
def reset_turtle(self):
"""强制将海龟重置到中心并朝向正右方"""
client = self.create_client(TeleportAbsolute, '/turtle1/teleport_absolute')
while not client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('等待重置服务...')
req = TeleportAbsolute.Request()
req.x = 5.544445 # 窗口中心 X
req.y = 5.544445 # 窗口中心 Y
req.theta = 0.0 # 【关键】强制朝向正右方 (0弧度)
future = client.call_async(req)
rclpy.spin_until_future_complete(self, future)
self.get_logger().info("已重置位置和朝向!开始画星...")
def control_loop(self):
twist = Twist()
dt = 0.05 # 定时器间隔
if self.current_segment >= len(self.segment_lengths):
# 画完了,停止
twist.linear.x = 0.0
twist.angular.z = 0.0
self.cmd_pub.publish(twist)
self.get_logger().info("五角星绘制完成!")
self.destroy_timer(self.timer)
return
target_dist = self.segment_lengths[self.current_segment]
if self.state == 'MOVING':
# --- 直线运动阶段 ---
if self.elapsed_time < (target_dist / self.linear_speed):
twist.linear.x = self.linear_speed
twist.angular.z = 0.0
else:
# 距离到了,准备转弯
twist.linear.x = 0.0
twist.angular.z = 0.0
self.state = 'TURNING'
self.elapsed_time = 0.0 # 重置计时器用于转弯
elif self.state == 'TURNING':
# --- 旋转阶段 ---
turn_duration = self.turn_angle / self.angular_speed
if self.elapsed_time < turn_duration:
twist.linear.x = 0.0
twist.angular.z = self.angular_speed # 向右转 (正值在TurtleSim中通常是逆时针,若方向反了改为负值)
# 注意:TurtleSim中 angular.z > 0 是逆时针(左转),< 0 是顺时针(右转)
# 五角星外角144度通常是向一个方向连续转。
# 如果画出来是反的,把下面这行改成 -self.angular_speed
twist.angular.z = -self.angular_speed
else:
# 转弯结束,进入下一段直线
twist.linear.x = 0.0
twist.angular.z = 0.0
self.state = 'MOVING'
self.elapsed_time = 0.0
self.current_segment += 1
self.cmd_pub.publish(twist)
self.elapsed_time += dt
def main(args=None):
rclpy.init(args=args)
node = StarDrawer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
|--------|
| |
| 运行结果如下 |

2.3 仿真功能的示例-服务调用
服务是一种基于请求一响应模型的通信机制,在ROS2中使用服务可以调用预定义的功能来实现特定的任务。在Turtlesim仿真环境中,提供了多个不司的服务,如生成小海龟,重置环境,移除小海龟等。
在 Turtlesim 仿真环境中,/spawn 服务允许我们通过发送请求来动态生成新的小海龟,并精确指定其名称、位置和朝向。
🛠️ 核心原理:/spawn 服务
- 服务类型:turtlesim/srv/Spawn
- 请求参数:
- x, y (float32): 小海龟生成的坐标位置。
- theta (float32): 小海龟的初始朝向角度(弧度制)。
- name (string): 小海龟的唯一标识名称。
- 响应参数:
- name (string): 返回生成成功的小海龟名称。
2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环
在vs code编译器中,在src/turtle_exercise/turtle_exercise/路径下,创建一个服务端文件spawn.py(~/ros2_ws1/src/turtle_exercise/turtle_exercise/spawn.py )打开该文件,输入下面代码:
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.srv import Spawn, SetPen, Kill
from turtlesim.msg import Pose
import math
import time
class OlympicRingsDrawer(Node):
def __init__(self):
super().__init__('olympic_rings_drawer')
# 1. 定义奥运五环的配置 (名称, 颜色RGB, 中心X, 中心Y)
# 注意:turtlesim 窗口大小为 11x11,这里调整了坐标使其居中
self.rings_config = [
("blue_ring", (0, 120, 255), 3.0, 6.5),
("black_ring", (0, 0, 0), 5.5, 6.5),
("red_ring", (255, 0, 0), 8.0, 6.5),
("yellow_ring", (255, 255, 0), 4.25, 5.0),
("green_ring", (0, 255, 0), 6.75, 5.0)
]
self.radius = 1.2 # 环的半径
self.linear_vel = 2.0 # 线速度
self.angular_vel = self.linear_vel / self.radius # 角速度 = v / r
# 2. 创建服务客户端
self.spawn_client = self.create_client(Spawn, '/spawn')
self.kill_client = self.create_client(Kill, '/kill')
# 3. 创建速度发布者字典和画笔设置客户端字典
self.vel_pubs = {}
self.pen_clients = {}
# 4. 启动初始化流程
self.init_sequence()
def init_sequence(self):
"""按顺序执行:清背景 -> 杀默认海龟 -> 生成新海龟 -> 设颜色 -> 开始画"""
self.set_background_white()
self.kill_default_turtle()
# 延迟生成海龟,确保背景设置完成
self.create_timer(0.5, self.spawn_all_turtles)
def set_background_white(self):
"""将背景设置为白色"""
# turtlesim 没有直接的背景服务,但可以通过参数设置
# 或者在 launch 文件中设置。这里我们使用 ros2 param 命令行替代
# 实际上更好的做法是在 launch 里设置,但为了单文件逻辑,这里做个提示
self.get_logger().info("请将仿真背景设置为白色(可通过 launch 参数或按键 'W')")
def kill_default_turtle(self):
"""杀死默认的 turtle1"""
while not self.kill_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('等待 /kill 服务...')
req = Kill.Request()
req.name = 'turtle1'
self.kill_client.call_async(req)
def spawn_all_turtles(self):
"""批量生成小海龟并设置画笔"""
for name, color, x, y in self.rings_config:
self.spawn_turtle(name, x + self.radius, y, 1.5708) # 从圆的右侧(3点钟方向)开始,朝上
time.sleep(0.2) # 稍微等待,避免服务冲突
self.set_pen_color(name, color)
# 所有海龟生成完毕后,开始画圆
self.create_timer(0.1, self.draw_rings)
def spawn_turtle(self, name, x, y, theta):
"""生成一只小海龟"""
while not self.spawn_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info(f'等待 /spawn 服务以生成 {name}...')
req = Spawn.Request()
req.name = name
req.x = float(x)
req.y = float(y)
req.theta = float(theta)
self.spawn_client.call_async(req)
# 为这只海龟创建速度发布者
self.vel_pubs[name] = self.create_publisher(Twist, f'/{name}/cmd_vel', 10)
def set_pen_color(self, name, color):
"""设置小海龟的画笔颜色"""
# 注意:turtlesim 原生不支持通过服务动态改颜色,
# 但在某些修改版或通过参数可以。这里我们假设环境支持 /<name>/set_pen
# 如果原生不支持,颜色可能不生效,但不影响画圆。
# 标准做法是通过 launch 参数设置,这里用通用服务名代替
try:
pen_client = self.create_client(SetPen, f'/{name}/set_pen')
if pen_client.wait_for_service(timeout_sec=1.0):
req = SetPen.Request()
req.r = color[0]
req.g = color[1]
req.b = color[2]
req.width = 6
pen_client.call_async(req)
except Exception as e:
self.get_logger().warn(f"设置 {name} 颜色失败,可能不支持该服务: {e}")
def draw_rings(self):
"""控制所有海龟做圆周运动"""
twist = Twist()
twist.linear.x = self.linear_vel
twist.angular.z = self.angular_vel
for name, pub in self.vel_pubs.items():
pub.publish(twist)
def main(args=None):
rclpy.init(args=args)
node = OlympicRingsDrawer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
修改launch启动文件:spawn_launch.py
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription( # 1. 启动 turtlesim 仿真器,设置背景为白色 Node( package='turtlesim', executable='turtlesim_node', name='sim', parameters=\[ {'background_r': 255}, {'background_g': 255}, {'background_b': 255} ), # 2. 启动奥运五环绘制节点 Node( package='turtle_exercise', executable='spawn', name='olympic_rings_drawer', output='screen' ) ]) |
修改 setup.py文件
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', 'spawn = turtle_exercise.spawn:main', # 【确保有这一行】 , }, |
编译、加载、运行
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # 1. 进入工作空间并编译 cd ~/ros2_ws1 colcon build --packages-select turtle_exercise # 2. 刷新环境变量 source install/setup.bash # 3. 启动奥运五环 ros2 launch turtle_exercise spawn_launch.py |
| root@LHAYR:~/ros2_ws1# colcon build Starting >>> turtle_exercise Finished <<< turtle_exercise 0.75s Summary: 1 package finished 0.91s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle_exercise spawn_launch.py INFO launch: All log files can be found below /root/.ros/log/2026-07-10-20-02-36-318110-LHAYR-4818 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 4821 INFO spawn-2: process started with pid 4822 turtlesim_node-1 INFO 1783684956.584694516 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO 1783684956.595242317 sim: Spawning turtle turtle1 at x=5.544445, y=5.544445, theta=0.000000 spawn-2 INFO 1783684956.654626313 olympic_rings_drawer: 请将仿真背景设置为白色(可通过 launch 参数或按键 'W') turtlesim_node-1 INFO 1783684957.429776807 sim: Spawning turtle blue_ring at x=4.200000, y=6.500000, theta=1.570800 |

2.4 仿真功能的示例-动作反馈
动作(Action)由两个核心角色组成,就像服务(Service)由服务端和客户端组成一样。但动作的角色分工更复杂,因为它是"异步长任务":
2.4.1 创建动作接口功能包-小海龟通过指令话多边形
在工作空间 `ros2_ws1/src` 下创建 `interfaces_exercise` 功能包(接口包必须使用ament_cmake:
root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1/src
root@LHAYR:~/ros2_ws1/src# ros2 pkg create --build-type ament\_cmake --license MIT interfaces\_exercise
going to create a new package
package name: interfaces_exercise
。。。。。。
creating folder ./interfaces_exercise/src
creating folder ./interfaces_exercise/include/interfaces_exercise
creating ./interfaces_exercise/CMakeLists.txt
root@LHAYR:~/ros2_ws1/src#
创建 action 文件夹与 Move.action
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1/src# cd interfaces_exercise root@LHAYR:~/ros2_ws1/src/interfaces_exercise# mkdir action root@LHAYR:~/ros2_ws1/src/interfaces_exercise# touch action/Move.action root@LHAYR:~/ros2_ws1/src/interfaces_exercise# |
修改Move.action 动作接口:分为三部分:Goal(目标)、Result(结果)、Feedback(实时反馈),用vs code打开目录,。
|-------------------------------------------------------------------------------------------------------------------------------------|
| # Goal:需要绘制的多边形边数 int32 order --- # Result:最终所有顶点坐标序列 float32\[\] sequence --- # Feedback:当前已绘制的顶点坐标序列 float32\[\] partial_sequence |
修改 CMakeLists.txt,添加下面标红的语句。
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cmake_minimum_required(VERSION 3.8) project(interfaces_exercise) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() # find dependencies find_package(ament_cmake REQUIRED) find_package(rosidl_default_generators REQUIRED) # uncomment the following section in order to fill in # further dependencies manually. # find_package(<dependency> REQUIRED) # 生成接口文件 rosidl_generate_interfaces(${PROJECT_NAME} "action/Move.action" ) if(BUILD_TESTING) |
修改 package.xml,添加下列4条标红的语句
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>interfaces_exercise</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="root@todo.todo">root</maintainer> <license>MIT</license> <buildtool_depend>ament_cmake</buildtool_depend> <buildtool_depend>rosidl_default_generators</buildtool_depend> <depend>action_msgs</depend> <exec_depend>rosidl_default_runtime</exec_depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <member_of_group>rosidl_interface_packages</member_of_group> <export> <build_type>ament_cmake</build_type> </export> </package> |
创建~/ros2_ws1/src/turtle_exercise/turtle_exercise/action_move.py 在action_move.py文件内输入下面代码
python
import rclpy
import time
import math
from rclpy.node import Node
from rclpy.action import ActionServer
from geometry_msgs.msg import Twist
from interfaces_exercise.action import Move
class TurtleMoveActionServer(Node):
def __init__(self):
super().__init__('action_move_server')
self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
self._action_server = ActionServer(
self,
Move,
'/turtle_move',
self.execute_callback
)
self.get_logger().info("✅ 动作服务端已启动,等待指令:/turtle_move")
def execute_callback(self, goal_handle):
order = goal_handle.request.order
self.get_logger().info(f'开始绘制 {order} 边形')
feedback = Move.Feedback()
result = Move.Result()
sequence = []
angle = 2 * math.pi / order
for i in range(order):
# 前进
twist = Twist()
twist.linear.x = 1.2
self.pub.publish(twist)
time.sleep(1.0)
# 记录顶点并反馈
sequence.append(float(i + 1))
feedback.partial_sequence = sequence
goal_handle.publish_feedback(feedback)
self.get_logger().info(f'反馈:已到达第 {i+1} 个顶点')
# 转弯
twist.linear.x = 0.0
twist.angular.z = angle
self.pub.publish(twist)
time.sleep(1.0)
# 停止
self.pub.publish(Twist())
goal_handle.succeed()
result.sequence = sequence
self.get_logger().info("✅ 多边形绘制完成!")
return result
def main(args=None):
rclpy.init(args=args)
server = TurtleMoveActionServer()
rclpy.spin(server)
rclpy.shutdown()
if __name__ == '__main__':
main()
创建launch文件,路径: ~/ros2_ws1/src/turtle_exercise/launch/action_move_launch.py
在action_move_launch.py文件内输入如下代码
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from launch import LaunchDescription from launch_ros.actions import Node from launch.actions import DeclareLaunchArgument, ExecuteProcess from launch.substitutions import LaunchConfiguration def generate_launch_description(): return LaunchDescription( DeclareLaunchArgument('order', default_value='6'), Node( package='turtlesim', executable='turtlesim_node', name='sim' ), Node( package='turtle_exercise', executable='action_move', name='action_move_node' ), ExecuteProcess( cmd=\[ 'ros2 action send_goal --feedback /turtle_move', 'interfaces_exercise/action/Move', '\\'{"order":', LaunchConfiguration('order'), '}\\'' , shell=True ) ]) |
setup.py 最终配置:添加'action_move = turtle_exercise.action_move:main',
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', 'spawn = turtle_exercise.spawn:main', 'action_move = turtle_exercise.action_move:main', , }, |
编译,加载,运行
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1 root@LHAYR:~/ros2_ws1# colcon build 0.216s WARNING:colcon.colcon_ros.prefix_path.ament:The path '/root/ros2_ws1/install/turtle_exercise' in the environment variable AMENT_PREFIX_PATH doesn't exist Starting >>> interfaces_exercise Starting >>> turtle_exercise Finished <<< interfaces_exercise 0.33s Finished <<< turtle_exercise 0.79s Summary: 2 packages finished 0.94s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle\_exercise action\_move\_launch.py order:=6 INFO launch: All log files can be found below /root/.ros/log/2026-07-10-21-52-27-126167-LHAYR-6574 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 6577 INFO action_move-2: process started with pid 6578 INFO turtle_move-3: process started with pid 6579 turtlesim_node-1 INFO 1783691547.401203064 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO |

第三章 实际开发中的选型指南
通过"画五角星"和"画奥运五环"这两个具体的案例,对比话题(Topic)和服务(Service),说明你已经真正跨入了 ROS 2 开发的大门。在 ROS 2 中,话题和服务就像是机器人的"神经系统"和"肌肉系统",它们各有分工。下面我为你详细梳理它们的优缺点,以及在实际开发中的选型指南。
3.1、 话题(Topic)与服务(Service)的优缺点对比
表格
| 对比维度 | 话题(Topic)通信 | 服务(Service)通信 |
|---|---|---|
| 通信模型 | 发布/订阅模型(Publish/Subscribe) | 请求/响应模型(Request/Response) |
| 数据流向 | 单向(一对多、多对一、多对多) | 双向(一对一) |
| 是否阻塞 | 非阻塞:发布者发完就忘,不管有没有人听 | 阻塞/异步等待:客户端发出请求后,必须等服务端处理完并返回结果 |
| 数据特性 | 连续、高频、流式数据 | 离散、低频、状态/指令数据 |
| 核心优点 | 解耦极强,实时性高,适合持续的数据流传输 | 逻辑清晰,有明确的结果反馈,适合任务确认 |
| 核心缺点 | 无法确认对方是否收到,没有返回值 | 如果服务端卡死,客户端会一直等待(阻塞) |
3.2、 结合你的案例来分析
3.2.1. 为什么"画五角星"用话题(Topic)?
- 场景特征:控制小海龟移动需要高频、连续地发送速度指令(例如每 0.1 秒发一次)。
- 选择理由:话题是"单向非阻塞"的。你的控制节点只管疯狂往 /cmd_vel 话题里发速度指令,不需要等 turtlesim_node 回复"我收到了"。这保证了机器人控制的实时性。如果用服务来发速度指令,每次发完都要等回复,系统会因为频繁的网络握手而严重卡顿,五角星就画歪了。
3.2.2. 为什么"画奥运五环"用服务(Service)?
- 场景特征:生成海龟(/spawn)、设置画笔颜色(/set_pen)、清除背景(/clear)都是一次性、低频的操作。
- 选择理由:服务是"请求-响应"的。当你调用 /spawn 时,你需要明确知道这只海龟有没有成功生成,如果名字重复了,服务端还会返回错误提示。这种需要"确认结果"的任务,用服务最合适。
3.3、 实战指南:开发时如何选择?
在实际的机器人功能开发中,你可以用以下**"三个灵魂拷问"**来决定使用哪种通信机制:
💡 拷问 1:这个数据是"连续流"还是"一次性指令"?
- 选话题(Topic):如果数据像水流一样源源不断。
- 例子:激光雷达数据、摄像头图像、底盘实时速度、电机编码器反馈。
- 选服务(Service):如果数据像按门铃,按一下响一次。
- 例子:拍照保存、切换导航地图、开关机械爪、查询电池电量。
💡 拷问 2:发送方需不需要"确认回执"?
- 选话题(Topic):发出去就不管了,追求极致的速度。
- 例子:避障时的紧急刹车指令(发出去最重要,等回执就来不及撞墙了)。
- 选服务(Service):必须等对方说"OK"才能进行下一步。
- 例子:调用机械臂抓取杯子。你必须等服务端返回 success=True,才能执行下一个"放下杯子"的动作。
💡 拷问 3:通信的节点数量是多少?
- 选话题(Topic):一对多广播。
- 例子:一个里程计节点发布位置,同时给"导航模块"、"UI显示模块"、"日志记录模块"用。
- 选服务(Service):严格的一对一请求。
- 例子:主控板专门向"传感器驱动板"请求一次标定数据。
🌟 总结你的"奥运五环"项目
其实你刚才画的奥运五环,是一个**"话题 + 服务"完美结合**的经典案例:
- 服务(Service)负责"搭台":调用 /spawn 和 /set_pen 把 5 只海龟按位置放好,换好颜色。
- 话题(Topic)负责"唱戏":在 5 只海龟就位后,通过 /cmd_vel 话题持续发布速度,让它们同时画圆。
掌握了这个选型逻辑,你以后在做复杂的机器人项目(比如让小车一边建图、一边导航、一边识别物体)时,就能游刃有余地设计系统架构了!
3.4目录结构总览
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ROS2_WS1/ ├── build/ # 编译产物目录(自动生成) │ └── 存放各功能包的中间编译文件、构建配置 ├── install/ # 安装目录(自动生成) │ └── 存放编译后可执行文件、库、启动文件等运行时资源 ├── log/ # 日志目录(自动生成) │ └── 存放 ROS2 运行时日志、调试信息 └── src/ # 源代码根目录(手动开发) ├── interfaces_exercise/ # 自定义接口功能包(ament_cmake 类型) │ ├── action/ │ │ └── Move.action # 自定义动作接口:定义多边形绘制的 Goal/Result/Feedback │ ├── include/ # 自动生成的 C++ 接口头文件 │ ├── src/ # 自动生成的 C++ 接口实现文件 │ ├── CMakeLists.txt # CMake 构建配置:编译动作/消息/服务接口 │ ├── LICENSE # 许可证文件 │ └── package.xml # 包元数据:声明依赖、导出接口 └── turtle_exercise/ # 业务功能包(ament_python 类型) ├── launch/ # 启动文件目录 │ ├── spawn/ # 子目录:存放 spawn 相关启动文件 │ ├── action_move_launch.py # 动作案例启动文件:启动 turtlesim + 动作服务端 + 发送目标 │ ├── spawn_launch.py # 多海龟生成启动文件:启动 turtlesim + 生成奥运五环海龟 │ └── spiral_launch.py # 螺旋/图形绘制启动文件:启动 turtlesim + 绘制节点 ├── resource/ # 资源目录 │ └── turtle_exercise # 包标识文件(ament_python 必需) ├── test/ # 自动化测试目录 │ ├── test_copyright.py # 版权检查测试 │ ├── test_flake8.py # 代码风格检查(flake8) │ └── test_pep257.py # 文档风格检查(PEP257) ├── turtle_exercise/ # Python 模块根目录(存放所有节点代码) │ ├── init.py # Python 包初始化文件 │ ├── action_move.py # 动作服务端节点:接收动作目标,控制海龟绘制多边形 │ ├── draw_spiral.py # 绘制节点:控制海龟绘制螺旋/正方形/五角星等图形 │ ├── draw_spiral01.py # 绘制节点变体:备用/改进版图形绘制 │ └── spawn.py # 服务调用节点:调用 /spawn 服务生成多只海龟 ├── LICENSE # 许可证文件 ├── package.xml # 包元数据:声明依赖、导出 Python 节点 ├── setup.cfg # Python 包配置(测试、代码风格) └── setup.py # Python 包构建配置:注册节点、安装启动文件等 |