#!/usr/bin/env python3 """ publish_sine_path.py — 基于机器人当前位姿发布一次正弦路径到 /plan 用法: python3 publish_sine_path.py [amplitude] [wavelength] [length] 默认: amplitude=0.8m wavelength=3.0m length=8.0m 路径在机器人前方展开: local_x 沿机器人朝向正前方 local_y = A * sin(2pi * local_x / wavelength) 再旋转变换到 odom 坐标系 依赖: /odom 话题 """ import sys import math import rclpy from rclpy.node import Node from nav_msgs.msg import Path, Odometry from geometry_msgs.msg import PoseStamped class SinePathPublisher(Node): def __init__(self, amplitude, wavelength, length): super().__init__("sine_path_publisher") self.A = amplitude self.wavelength = wavelength self.length = length self.step = 0.05 self.robot_x = 0.0 self.robot_y = 0.0 self.robot_yaw = 0.0 self.has_odom = False self.pub = self.create_publisher(Path, "/plan", 10) self.odom_sub = self.create_subscription( Odometry, "/odom", self.odom_cb, 10) def odom_cb(self, msg): self.robot_x = msg.pose.pose.position.x self.robot_y = msg.pose.pose.position.y qx = msg.pose.pose.orientation.x qy = msg.pose.pose.orientation.y qz = msg.pose.pose.orientation.z qw = msg.pose.pose.orientation.w self.robot_yaw = math.atan2( 2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz)) self.has_odom = True def build_and_publish(self): cos_yaw = math.cos(self.robot_yaw) sin_yaw = math.sin(self.robot_yaw) path = Path() path.header.frame_id = "odom" path.header.stamp = self.get_clock().now().to_msg() lx = 0.0 while lx <= self.length: ly = self.A * math.sin(2.0 * math.pi * lx / self.wavelength) dy_dx = self.A * (2.0 * math.pi / self.wavelength) * \ math.cos(2.0 * math.pi * lx / self.wavelength) local_yaw = math.atan2(dy_dx, 1.0) wx = self.robot_x + lx * cos_yaw - ly * sin_yaw wy = self.robot_y + lx * sin_yaw + ly * cos_yaw world_yaw = self.robot_yaw + local_yaw pose = PoseStamped() pose.header.frame_id = "odom" pose.pose.position.x = wx pose.pose.position.y = wy pose.pose.position.z = 0.0 pose.pose.orientation.z = math.sin(world_yaw / 2.0) pose.pose.orientation.w = math.cos(world_yaw / 2.0) path.poses.append(pose) lx += self.step self.pub.publish(path) self.get_logger().info( f"Published sine path: {len(path.poses)} waypoints " f"from ({self.robot_x:.2f}, {self.robot_y:.2f}, {math.degrees(self.robot_yaw):.0f}°)" ) def main(): rclpy.init() A = float(sys.argv[1]) if len(sys.argv) > 1 else 0.8 wavelength = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0 length = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0 node = SinePathPublisher(A, wavelength, length) # 等待 odom (最多 3 秒) timeout = node.get_clock().now() + rclpy.duration.Duration(seconds=3) while not node.has_odom and node.get_clock().now() < timeout: rclpy.spin_once(node, timeout_sec=0.05) if not node.has_odom: node.get_logger().error("No /odom received within 3s — aborting") node.destroy_node() rclpy.shutdown() sys.exit(1) # 额外 spin 一下让其他订阅就绪 rclpy.spin_once(node, timeout_sec=0.5) node.build_and_publish() node.destroy_node() rclpy.shutdown() if __name__ == "__main__": main()