第十三章:进阶项目——控制小海龟运动¶
用代码控制小海龟画图,这个比较有意思,也很实用。
通过这个项目,你会学到:
- 如何发布 Twist 消息控制机器人运动
- 如何调用服务
- 如何订阅位姿信息
- 如何实现更复杂的运动控制
13.1 了解小海龟的话题¶
先启动小海龟:
ros2 run turtlesim turtlesim_node
会弹出一个窗口,里面有只小海龟。
查看话题列表¶
ros2 topic list
你会看到这些话题:
/turtle1/cmd_vel # 速度控制
/turtle1/color_sensor # 颜色传感器
/turtle1/pose # 位姿信息
/parameter_events
/rosout
最重要的话题: /turtle1/cmd_vel¶
这个话题用来控制小海龟移动。
查看消息类型:
ros2 topic info /turtle1/cmd_vel
输出:
Type: geometry_msgs/msg/Twist
看看这个消息的结构:
ros2 interface show geometry_msgs/msg/Twist
输出:
Vector3 linear
float64 x
float64 y
float64 z
Vector3 angular
float64 x
float64 y
float64 z
理解 Twist 消息
- linear: 线速度(直线运动)
- angular: 角速度(旋转运动)
对于2D的小海龟:
- linear.x: 控制前进/后退
- angular.z: 控制左转/右转
- 其他字段忽略
手动测试一下¶
试试用命令行发布消息:
ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.8}}"
观察结果
小海龟会开始画圆!
linear.x: 2.0让它向前走angular.z: 1.8让它同时转弯
按 Ctrl+C 停止。
13.2 项目一:画圆¶
现在用代码实现刚才的效果。
创建功能包¶
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python turtle_controller --dependencies rclpy geometry_msgs
编写画圆代码¶
创建 turtle_controller/turtle_controller/draw_circle.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class DrawCircle(Node):
def __init__(self):
super().__init__('draw_circle')
# 创建发布者
self.publisher_ = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
# 创建定时器,每0.1秒调用一次
self.timer = self.create_timer(0.1, self.timer_callback)
self.get_logger().info('开始画圆')
def timer_callback(self):
msg = Twist()
msg.linear.x = 1.0 # 向前
msg.angular.z = 1.0 # 同时转弯,就会画圆
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = DrawCircle()
try:
rclpy.spin(node)
except KeyboardInterrupt:
# 停止时让海龟停下来
stop_msg = Twist()
node.publisher_.publish(stop_msg)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
配置 setup.py¶
编辑 setup.py,在 entry_points 里添加:
entry_points={
'console_scripts': [
'draw_circle = turtle_controller.draw_circle:main',
],
},
编译并运行¶
cd ~/ros2_ws
colcon build --packages-select turtle_controller
source install/setup.bash
开两个终端:
终端1: 启动小海龟
ros2 run turtlesim turtlesim_node
终端2: 运行画圆程序
ros2 run turtle_controller draw_circle
效果
小海龟开始画圆了!
想清除轨迹可以调用服务:
ros2 service call /clear std_srvs/srv/Empty
13.3 项目二:画正方形¶
现在我们做个更复杂的:让小海龟画正方形。
分析思路¶
画正方形需要:
- 直走一段距离
- 停下
- 转90度
- 重复4次
我们需要知道小海龟当前的位置,所以要订阅 /turtle1/pose 话题。
查看位姿消息¶
ros2 interface show turtlesim/msg/Pose
输出:
float32 x
float32 y
float32 theta # 朝向角度
float32 linear_velocity
float32 angular_velocity
编写画正方形代码¶
创建 turtle_controller/turtle_controller/draw_square.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.msg import Pose
import math
class DrawSquare(Node):
def __init__(self):
super().__init__('draw_square')
# 发布者
self.publisher_ = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
# 订阅者
self.subscription = self.create_subscription(
Pose, '/turtle1/pose', self.pose_callback, 10)
# 状态变量
self.current_pose = None
self.state = 'move' # 状态: move(直行), turn(转弯)
self.target_distance = 2.0 # 每条边长度
self.start_x = None
self.start_y = None
self.start_theta = None
self.side_count = 0 # 已完成的边数
# 定时器
self.timer = self.create_timer(0.1, self.control_loop)
self.get_logger().info('开始画正方形')
def pose_callback(self, msg):
"""接收位姿信息"""
self.current_pose = msg
def control_loop(self):
"""主控制循环"""
if self.current_pose is None:
return
if self.side_count >= 4:
# 画完了,停止
self.stop()
self.get_logger().info('正方形画完了!')
self.timer.cancel()
return
if self.state == 'move':
self.move_forward()
elif self.state == 'turn':
self.turn_90_degrees()
def move_forward(self):
"""直线前进"""
if self.start_x is None:
# 记录起始位置
self.start_x = self.current_pose.x
self.start_y = self.current_pose.y
# 计算已走距离
distance = math.sqrt(
(self.current_pose.x - self.start_x)**2 +
(self.current_pose.y - self.start_y)**2
)
if distance < self.target_distance:
# 继续前进
msg = Twist()
msg.linear.x = 1.0
self.publisher_.publish(msg)
else:
# 到达目标距离,切换到转弯状态
self.stop()
self.state = 'turn'
self.start_x = None
self.start_y = None
self.start_theta = None
self.get_logger().info(f'完成第 {self.side_count + 1} 条边')
def turn_90_degrees(self):
"""转90度"""
if self.start_theta is None:
# 记录起始角度
self.start_theta = self.current_pose.theta
# 计算已转角度
angle_diff = abs(self.current_pose.theta - self.start_theta)
# 处理角度跳变(-π到π)
if angle_diff > math.pi:
angle_diff = 2 * math.pi - angle_diff
if angle_diff < math.pi / 2 - 0.05: # 90度,留点误差
# 继续转
msg = Twist()
msg.angular.z = 0.5 # 转速
self.publisher_.publish(msg)
else:
# 转完了,切换到前进状态
self.stop()
self.state = 'move'
self.side_count += 1
self.start_theta = None
def stop(self):
"""停止运动"""
msg = Twist()
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = DrawSquare()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
配置 setup.py¶
entry_points={
'console_scripts': [
'draw_circle = turtle_controller.draw_circle:main',
'draw_square = turtle_controller.draw_square:main',
],
},
编译并运行¶
cd ~/ros2_ws
colcon build --packages-select turtle_controller
source install/setup.bash
# 终端1
ros2 run turtlesim turtlesim_node
# 终端2
ros2 run turtle_controller draw_square
效果
小海龟会画一个正方形!
13.4 项目三:键盘控制¶
再来个实用的:用键盘控制小海龟,类似游戏手柄。
安装依赖¶
需要用到 pynput 库来捕获键盘输入:
pip install pynput --break-system-packages
编写代码¶
创建 turtle_controller/turtle_controller/keyboard_control.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from pynput import keyboard
class KeyboardControl(Node):
def __init__(self):
super().__init__('keyboard_control')
self.publisher_ = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
# 当前速度
self.linear_speed = 0.0
self.angular_speed = 0.0
# 速度增量
self.speed_step = 0.5
self.turn_step = 0.5
# 定时发布速度
self.timer = self.create_timer(0.1, self.publish_velocity)
# 键盘监听器
self.listener = keyboard.Listener(on_press=self.on_key_press)
self.listener.start()
self.get_logger().info('键盘控制已启动!')
self.get_logger().info('使用方向键控制, 空格停止, ESC退出')
def on_key_press(self, key):
"""按键处理"""
try:
if key == keyboard.Key.up:
self.linear_speed += self.speed_step
self.get_logger().info(f'加速: {self.linear_speed:.1f}')
elif key == keyboard.Key.down:
self.linear_speed -= self.speed_step
self.get_logger().info(f'减速: {self.linear_speed:.1f}')
elif key == keyboard.Key.left:
self.angular_speed += self.turn_step
self.get_logger().info(f'左转: {self.angular_speed:.1f}')
elif key == keyboard.Key.right:
self.angular_speed -= self.turn_step
self.get_logger().info(f'右转: {self.angular_speed:.1f}')
elif key == keyboard.Key.space:
self.linear_speed = 0.0
self.angular_speed = 0.0
self.get_logger().info('停止')
elif key == keyboard.Key.esc:
self.get_logger().info('退出')
rclpy.shutdown()
except AttributeError:
pass
def publish_velocity(self):
"""定时发布速度"""
msg = Twist()
msg.linear.x = self.linear_speed
msg.angular.z = self.angular_speed
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = KeyboardControl()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
# 停止海龟
stop_msg = Twist()
node.publisher_.publish(stop_msg)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
配置并运行¶
entry_points={
'console_scripts': [
'draw_circle = turtle_controller.draw_circle:main',
'draw_square = turtle_controller.draw_square:main',
'keyboard_control = turtle_controller.keyboard_control:main',
],
},
colcon build --packages-select turtle_controller
source install/setup.bash
ros2 run turtle_controller keyboard_control
操作说明
- ↑ 加速
- ↓ 减速
- ← 左转
- → 右转
- 空格 停止
- ESC 退出
13.5 进阶挑战¶
如果你已经掌握了上面的内容,可以试试这些:
挑战1: 画五角星¶
提示:
- 每次转144度(360/5 * 2)
- 需要5条边
挑战2: 跟随鼠标¶
提示:
- 订阅
/turtle1/pose获取当前位置 - 计算到目标点的距离和角度
- 根据距离调整速度,根据角度调整转向
挑战3: 多只海龟协同¶
提示:
- 使用服务
/spawn生成新海龟 - 每只海龟有独立的话题:
/turtle2/cmd_vel - 让两只海龟追逐或避开
13.6 常用服务调用¶
小海龟还提供了一些实用的服务:
清除轨迹¶
ros2 service call /clear std_srvs/srv/Empty
重置海龟¶
ros2 service call /reset std_srvs/srv/Empty
生成新海龟¶
ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: 'turtle2'}"
杀死海龟¶
ros2 service call /kill turtlesim/srv/Kill "{name: 'turtle2'}"
设置画笔¶
ros2 service call /turtle1/set_pen turtlesim/srv/SetPen "{r: 255, g: 0, b: 0, width: 5, 'off': 0}"
参数说明:
r, g, b: RGB颜色值(0-255)width: 线宽off: 1表示抬笔(不画线),0表示落笔
13.7 调试技巧¶
实时查看速度命令¶
ros2 topic echo /turtle1/cmd_vel
实时查看位姿¶
ros2 topic echo /turtle1/pose
查看节点关系图¶
ros2 run rqt_graph rqt_graph
查看话题发布频率¶
ros2 topic hz /turtle1/cmd_vel
本章小结¶
通过这一章的学习,你应该掌握了:
- ✅ 如何发布 Twist 消息控制运动
- ✅ 如何订阅位姿信息
- ✅ 如何实现状态机控制复杂运动
- ✅ 如何调用服务
- ✅ 如何处理键盘输入
重要收获
这些技能不仅适用于小海龟,也是控制真实机器人的基础。
- Twist 消息是 ROS 中最常用的速度控制消息
- 状态机是机器人控制的常用模式
- 订阅传感器数据 + 发布控制命令 = 机器人的基本工作流程
下一章预告¶
下一章我们会总结学习过程中最常遇到的问题和解决方案。
把这些坑都记下来,能帮你少走很多弯路!