You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

612 lines
25 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist, PoseStamped, TransformStamped
from nav_msgs.msg import Path
from visualization_msgs.msg import Marker
from tf2_ros import TransformBroadcaster
import math
class TShapeController(Node):
"""T字形轨迹控制器 - PID角度控制 + 智能动态距离计算版本"""
def __init__(self):
super().__init__('t_shape_controller')
# 声明ROS参数 - 可通过Launch文件或命令行设置
self.declare_parameter('t_width', 3.0) # T字横杠宽度
self.declare_parameter('t_height', 2.0) # T字竖杠高度
self.declare_parameter('linear_speed', 0.3) # 直线速度(米/秒)
self.declare_parameter('angular_speed', 0.5) # 最大角速度(弧度/秒)
self.declare_parameter('loop_enabled', True) # 是否循环运动
# PID参数声明角度控制
self.declare_parameter('pid_kp', 1.5) # 比例系数
self.declare_parameter('pid_ki', 0.01) # 积分系数
self.declare_parameter('pid_kd', 0.15) # 微分系数
self.declare_parameter('angle_tolerance', 0.01) # 角度容差弧度约0.57度)
# 线性运动角度校正参数
self.declare_parameter('linear_angle_tolerance', 0.05) # 线性运动角度容差弧度约2.9度)
self.declare_parameter('angle_correction_speed', 0.3) # 角度校正速度(弧度/秒)
# 获取参数值
self.t_width = self.get_parameter('t_width').value
self.t_height = self.get_parameter('t_height').value
self.linear_speed = self.get_parameter('linear_speed').value
self.angular_speed = self.get_parameter('angular_speed').value
self.loop_enabled = self.get_parameter('loop_enabled').value
# 获取PID参数
self.pid_kp = self.get_parameter('pid_kp').value
self.pid_ki = self.get_parameter('pid_ki').value
self.pid_kd = self.get_parameter('pid_kd').value
self.angle_tolerance = self.get_parameter('angle_tolerance').value
# 获取线性运动角度校正参数
self.linear_angle_tolerance = self.get_parameter('linear_angle_tolerance').value
self.angle_correction_speed = self.get_parameter('angle_correction_speed').value
# 创建发布器
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10) # 速度指令
self.path_pub = self.create_publisher(Path, '/robot_path', 10) # 运动路径
self.marker_pub = self.create_publisher(Marker, '/t_shape_marker', 10) # 参考轨迹
# TF广播器 - 发布odom->base_link变换
self.tf_broadcaster = TransformBroadcaster(self)
# 机器人状态变量
self.x = 0.0 # X坐标
self.y = 0.0 # Y坐标
self.theta = 0.0 # 航向角(弧度)
self.current_segment = 0 # 当前运动段0-9共10个阶段
self.segment_start_time = None # 当前段开始时间
# 关键点坐标记录(智能动态距离计算)
self.t_center_x = None # T字中心点X坐标
self.t_center_y = None # T字中心点Y坐标
self.start_x = None # 起点X坐标
self.start_y = None # 起点Y坐标
self.right_end_x = None # 右端点X坐标
self.right_end_y = None # 右端点Y坐标
# 每个阶段起点坐标(用于距离判断)
self.segment_start_x = 0.0
self.segment_start_y = 0.0
# PID角度控制状态变量
self.target_angle = 0.0 # 目标角度(弧度)
self.error_sum = 0.0 # 误差积分累积
self.last_error = 0.0 # 上一次误差(用于微分)
self.pid_active = False # PID是否激活
# 线性运动角度校正状态
self.linear_target_angle = 0.0 # 线性运动目标角度
self.angle_correction_active = False # 角度校正是否激活
# 路径记录消息
self.path_msg = Path()
self.path_msg.header.frame_id = 'odom'
# 创建定时器控制循环50Hz
self.control_timer = self.create_timer(0.02, self.control_loop)
# 创建定时器发布路径和TF10Hz
self.publish_timer = self.create_timer(0.1, self.publish_status)
# 发布T字形轨迹参考标记
self.publish_t_shape_marker()
self.get_logger().info('T字形控制器已启动PID角度控制 + 智能距离计算 + 线性角度校正)')
self.get_logger().info(f'轨迹参数: 宽度={self.t_width}m, 高度={self.t_height}m, 速度={self.linear_speed}m/s')
self.get_logger().info(f'PID参数: Kp={self.pid_kp}, Ki={self.pid_ki}, Kd={self.pid_kd}')
self.get_logger().info(f'线性角度校正: 容差={self.linear_angle_tolerance:.3f}rad, 校正速度={self.angle_correction_speed}rad/s')
def control_loop(self):
"""主控制循环 - 50Hz频率执行"""
current_time = self.get_clock().now()
# 初始化段开始时间
if self.segment_start_time is None:
self.segment_start_time = current_time
twist = Twist()
# T字形运动分为10个阶段PID角度控制 + 智能距离计算 + 线性角度校正):
# 0: 向前移动到T字中心记录中心点坐标 + 角度校正)
# 1: 左转90度PID控制
# 2: 向左移动到左端(角度校正)
# 3: 右转180度PID控制
# 4: 向右移动到右端(记录右端点坐标 + 角度校正)
# 5: 右转180度PID控制
# 6: 向左移动回中心(动态计算距离 + 角度校正)✅
# 7: 左转90度PID控制
# 8: 向下移动回起点(动态计算距离 + 角度校正)✅
# 9: 左转180度并重置PID控制
if self.current_segment == 0:
# 阶段0: 向前移动T字高度的距离竖杠+ 角度校正
target_distance = self.t_height
target_angle = 0.0 # 保持朝前0度
# 计算从起点移动的实际距离
moved_distance = math.sqrt((self.x - 0.0)**2 + (self.y - 0.0)**2)
if moved_distance < target_distance:
twist.linear.x = self.linear_speed
# 添加角度校正
twist.angular.z = self.linear_angle_correction(target_angle)
self.update_position(twist, 0.02)
else:
# 记录T字中心点坐标第一次到达
if self.t_center_x is None:
self.t_center_x = self.x
self.t_center_y = self.y
self.get_logger().info(f'记录T字中心点: ({self.x:.4f}, {self.y:.4f})')
# 进入下一阶段
self.current_segment += 1
self.segment_start_time = current_time
elif self.current_segment == 1:
# 阶段1: 左转90度PID控制
if not self.pid_active:
# 初始化PID控制
self.target_angle = math.pi / 2
self.reset_pid()
self.pid_active = True
# 计算角度误差
error = self.target_angle - self.theta
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 检查是否到达目标角度
if abs(error) < self.angle_tolerance:
# 到达目标停止PID
self.pid_active = False
# 记录阶段2起点
self.segment_start_x = self.x
self.segment_start_y = self.y
self.current_segment += 1
self.segment_start_time = current_time
else:
# 使用PID计算角速度
twist.angular.z = self.pid_angle_control()
self.update_position(twist, 0.02)
elif self.current_segment == 2:
# 阶段2: 向左移动T字宽度的一半从中心到左端+ 角度校正
target_distance = self.t_width / 2
target_angle = math.pi / 2 # 保持朝左90度
moved_distance = math.sqrt((self.x - self.segment_start_x)**2 + (self.y - self.segment_start_y)**2)
if moved_distance < target_distance:
twist.linear.x = self.linear_speed
# 添加角度校正
twist.angular.z = self.linear_angle_correction(target_angle)
self.update_position(twist, 0.02)
else:
self.current_segment += 1
self.segment_start_time = current_time
elif self.current_segment == 3:
# 阶段3: 右转180度PID控制
if not self.pid_active:
# 初始化PID控制
self.target_angle = -math.pi / 2
self.reset_pid()
self.pid_active = True
# 计算角度误差
error = self.target_angle - self.theta
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 检查是否到达目标角度
if abs(error) < self.angle_tolerance:
# 到达目标停止PID
self.pid_active = False
# 记录阶段4起点
self.segment_start_x = self.x
self.segment_start_y = self.y
self.current_segment += 1
self.segment_start_time = current_time
else:
# 使用PID计算角速度
twist.angular.z = self.pid_angle_control()
self.update_position(twist, 0.02)
elif self.current_segment == 4:
# 阶段4: 向右移动整个T字宽度横杠完整长度+ 角度校正
target_distance = self.t_width
target_angle = -math.pi / 2 # 保持朝右(-90度
moved_distance = math.sqrt((self.x - self.segment_start_x)**2 + (self.y - self.segment_start_y)**2)
if moved_distance < target_distance:
twist.linear.x = self.linear_speed
# 添加角度校正
twist.angular.z = self.linear_angle_correction(target_angle)
self.update_position(twist, 0.02)
else:
# 记录右端点坐标,用于动态计算返回距离
self.right_end_x = self.x
self.right_end_y = self.y
self.current_segment += 1
self.segment_start_time = current_time
elif self.current_segment == 5:
# 阶段5: 右转180度PID控制
if not self.pid_active:
# 初始化PID控制
self.target_angle = math.pi / 2
self.reset_pid()
self.pid_active = True
# 计算角度误差
error = self.target_angle - self.theta
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 检查是否到达目标角度
if abs(error) < self.angle_tolerance:
# 到达目标停止PID
self.pid_active = False
# 记录阶段6起点
self.segment_start_x = self.x
self.segment_start_y = self.y
self.current_segment += 1
self.segment_start_time = current_time
else:
# 使用PID计算角速度
twist.angular.z = self.pid_angle_control()
self.update_position(twist, 0.02)
elif self.current_segment == 6:
# 阶段6: 向左移动回到中心(智能动态距离计算 + 角度校正)✅
# 动态计算到T字中心的实际距离
if self.t_center_x is not None and self.right_end_x is not None:
dx = self.t_center_x - self.right_end_x
dy = self.t_center_y - self.right_end_y
target_distance = math.sqrt(dx*dx + dy*dy)
else:
# 备用:使用理论距离
target_distance = self.t_width / 2
target_angle = math.pi / 2 # 保持朝左90度
# 基于实际移动距离判断
moved_distance = math.sqrt((self.x - self.segment_start_x)**2 + (self.y - self.segment_start_y)**2)
if moved_distance < target_distance:
twist.linear.x = self.linear_speed
# 添加角度校正
twist.angular.z = self.linear_angle_correction(target_angle)
self.update_position(twist, 0.02)
else:
# 位置自然到达,进入下一阶段
self.current_segment += 1
self.segment_start_time = current_time
elif self.current_segment == 7:
# 阶段7: 左转90度PID控制
if not self.pid_active:
# 初始化PID控制
self.target_angle = math.pi
self.reset_pid()
self.pid_active = True
# 计算角度误差
error = self.target_angle - self.theta
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 检查是否到达目标角度
if abs(error) < self.angle_tolerance:
# 到达目标停止PID
self.pid_active = False
# 记录阶段8起点
self.segment_start_x = self.x
self.segment_start_y = self.y
self.current_segment += 1
self.segment_start_time = current_time
else:
# 使用PID计算角速度
twist.angular.z = self.pid_angle_control()
self.update_position(twist, 0.02)
elif self.current_segment == 8:
# 阶段8: 向下移动回到起点(智能动态距离计算 + 角度校正)✅
# 初始化起点坐标
if self.start_x is None:
self.start_x = 0.0
self.start_y = 0.0
# 动态计算到起点的实际距离
if self.t_center_x is not None:
dx = self.start_x - self.t_center_x
dy = self.start_y - self.t_center_y
target_distance = math.sqrt(dx*dx + dy*dy)
else:
target_distance = self.t_height
target_angle = math.pi # 保持朝下180度
# 基于实际移动距离判断
moved_distance = math.sqrt((self.x - self.segment_start_x)**2 + (self.y - self.segment_start_y)**2)
if moved_distance < target_distance:
twist.linear.x = self.linear_speed
# 添加角度校正
twist.angular.z = self.linear_angle_correction(target_angle)
self.update_position(twist, 0.02)
else:
# 位置自然到达,进入下一阶段
self.current_segment += 1
self.segment_start_time = current_time
elif self.current_segment == 9:
# 阶段9: 左转180度回到初始朝向PID控制
if not self.pid_active:
# 初始化PID控制
self.target_angle = 0.0
self.reset_pid()
self.pid_active = True
# 计算角度误差
error = self.target_angle - self.theta
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 检查是否到达目标角度
if abs(error) < self.angle_tolerance:
# 到达目标停止PID
self.pid_active = False
# 循环结束:重置到精确起点,确保每轮一致
self.x = 0.0
self.y = 0.0
self.theta = 0.0
# ⚠️ 只重置右端点T字中心点保持固定第一次记录后永久使用
self.right_end_x = None
self.right_end_y = None
if self.loop_enabled:
self.current_segment = 0
self.segment_start_time = current_time
self.get_logger().info('完成一轮T字形轨迹开始新循环')
else:
self.current_segment = 10
self.get_logger().info('T字形轨迹完成')
else:
# 使用PID计算角速度
twist.angular.z = self.pid_angle_control()
self.update_position(twist, 0.02)
# 发布速度指令到/cmd_vel话题
self.cmd_vel_pub.publish(twist)
def pid_angle_control(self):
"""PID角度控制算法
Returns:
float: 计算出的角速度(弧度/秒)
"""
# 计算角度误差
error = self.target_angle - self.theta
# 归一化误差到[-π, π]范围(处理角度跳变)
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# P: 比例项 - 误差越大,输出越大
p_output = self.pid_kp * error
# I: 积分项 - 消除长期稳态误差
self.error_sum += error
# 限制积分累积,防止积分饱和
max_integral = 10.0
if self.error_sum > max_integral:
self.error_sum = max_integral
elif self.error_sum < -max_integral:
self.error_sum = -max_integral
i_output = self.pid_ki * self.error_sum
# D: 微分项 - 预测趋势,减少超调
error_rate = error - self.last_error
d_output = self.pid_kd * error_rate
# PID总输出
angular_velocity = p_output + i_output + d_output
# 限制最大角速度
if angular_velocity > self.angular_speed:
angular_velocity = self.angular_speed
elif angular_velocity < -self.angular_speed:
angular_velocity = -self.angular_speed
# 保存当前误差用于下次微分计算
self.last_error = error
return angular_velocity
def reset_pid(self):
"""重置PID状态"""
self.error_sum = 0.0
self.last_error = 0.0
def linear_angle_correction(self, target_angle):
"""线性运动角度校正
Args:
target_angle: 目标角度(弧度)
Returns:
float: 校正角速度(弧度/秒)
"""
# 计算角度误差
error = target_angle - self.theta
# 归一化误差到[-π, π]范围
while error > math.pi:
error -= 2 * math.pi
while error < -math.pi:
error += 2 * math.pi
# 如果误差在容差范围内,不需要校正
if abs(error) < self.linear_angle_tolerance:
return 0.0
# 计算校正角速度(比例控制)
correction_velocity = self.angle_correction_speed * (error / abs(error))
# 限制最大校正速度
if correction_velocity > self.angle_correction_speed:
correction_velocity = self.angle_correction_speed
elif correction_velocity < -self.angle_correction_speed:
correction_velocity = -self.angle_correction_speed
return correction_velocity
def update_position(self, twist, dt):
"""更新机器人位置(差速驱动运动学模型)
Args:
twist: 速度指令(线速度和角速度)
dt: 时间步长(秒)
"""
# 根据当前速度和朝向更新位置
self.x += twist.linear.x * math.cos(self.theta) * dt
self.y += twist.linear.x * math.sin(self.theta) * dt
self.theta += twist.angular.z * dt
# 归一化角度到[-π, π]范围
self.theta = math.atan2(math.sin(self.theta), math.cos(self.theta))
def publish_status(self):
"""发布TF变换和路径信息 - 10Hz频率"""
current_time = self.get_clock().now()
# 发布TF变换: odom坐标系 -> base_link坐标系
t = TransformStamped()
t.header.stamp = current_time.to_msg()
t.header.frame_id = 'odom'
t.child_frame_id = 'base_link'
t.transform.translation.x = self.x
t.transform.translation.y = self.y
t.transform.translation.z = 0.0
# 将欧拉角转换为四元数ROS使用四元数表示旋转
q = self.euler_to_quaternion(0, 0, self.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
# 广播TF变换
self.tf_broadcaster.sendTransform(t)
# 添加当前位姿到路径
pose = PoseStamped()
pose.header.stamp = current_time.to_msg()
pose.header.frame_id = 'odom'
pose.pose.position.x = self.x
pose.pose.position.y = self.y
pose.pose.position.z = 0.0
pose.pose.orientation.x = q[0]
pose.pose.orientation.y = q[1]
pose.pose.orientation.z = q[2]
pose.pose.orientation.w = q[3]
self.path_msg.poses.append(pose)
self.path_msg.header.stamp = current_time.to_msg()
# 限制路径长度避免内存无限增长保留最近2000个点
if len(self.path_msg.poses) > 2000:
self.path_msg.poses.pop(0)
# 发布路径到/robot_path话题RViz中显示为绿色轨迹
self.path_pub.publish(self.path_msg)
def publish_t_shape_marker(self):
"""发布T字形轨迹标记参考线"""
marker = Marker()
marker.header.frame_id = 'odom'
marker.header.stamp = self.get_clock().now().to_msg()
marker.ns = 't_shape'
marker.id = 0
marker.type = Marker.LINE_STRIP
marker.action = Marker.ADD
marker.scale.x = 0.05
marker.color.r = 1.0
marker.color.g = 0.0
marker.color.b = 0.0
marker.color.a = 0.5
# 定义T字形路径点
from geometry_msgs.msg import Point
p1 = Point()
p1.x, p1.y, p1.z = 0.0, 0.0, 0.0
p2 = Point()
p2.x, p2.y, p2.z = self.t_height, 0.0, 0.0
p3 = Point()
p3.x, p3.y, p3.z = self.t_height, self.t_width / 2, 0.0
p4 = Point()
p4.x, p4.y, p4.z = self.t_height, 0.0, 0.0
p5 = Point()
p5.x, p5.y, p5.z = self.t_height, -self.t_width / 2, 0.0
p6 = Point()
p6.x, p6.y, p6.z = self.t_height, 0.0, 0.0
p7 = Point()
p7.x, p7.y, p7.z = 0.0, 0.0, 0.0
marker.points = [p1, p2, p3, p4, p5, p6, p7]
self.marker_pub.publish(marker)
self.create_timer(5.0, lambda: self.marker_pub.publish(marker))
def euler_to_quaternion(self, roll, pitch, yaw):
"""将欧拉角转换为四元数"""
qx = math.sin(roll/2) * math.cos(pitch/2) * math.cos(yaw/2) - math.cos(roll/2) * math.sin(pitch/2) * math.sin(yaw/2)
qy = math.cos(roll/2) * math.sin(pitch/2) * math.cos(yaw/2) + math.sin(roll/2) * math.cos(pitch/2) * math.sin(yaw/2)
qz = math.cos(roll/2) * math.cos(pitch/2) * math.sin(yaw/2) - math.sin(roll/2) * math.sin(pitch/2) * math.cos(yaw/2)
qw = math.cos(roll/2) * math.cos(pitch/2) * math.cos(yaw/2) + math.sin(roll/2) * math.sin(pitch/2) * math.sin(yaw/2)
return [qx, qy, qz, qw]
def main(args=None):
rclpy.init(args=args)
node = TShapeController()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()