MPPI + fence-line loc + waypoint nav with fail skip

This commit is contained in:
cyy
2026-07-03 00:17:08 +00:00
parent 5156a8d766
commit 50ff61a16b
14 changed files with 555 additions and 115 deletions

148
scripts/waypoint_nav.py Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Sequential waypoint navigation for car_nav_lite.
Publishes /goal_pose waypoints one by one. Waits for the car to reach
each waypoint before publishing the next.
Usage:
python3 waypoint_nav.py # default waypoints
python3 waypoint_nav.py --waypoints waypoints.json # from JSON file
python3 waypoint_nav.py --loop # loop back to start
JSON format:
[{"x": 1.0, "y": -1.5, "yaw": 0.0}, ...]
"""
import argparse
import json
import math
import time
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
from nav_msgs.msg import Odometry
from std_msgs.msg import String
# Default waypoints for 5m×5m arena (car spawns near -2.0, -2.3)
DEFAULT_WAYPOINTS = [
{"x": -0.5, "y": -1.5, "yaw": 0.0, "desc": "mid-left"},
{"x": 1.0, "y": -1.5, "yaw": 0.0, "desc": "right"},
{"x": 1.5, "y": 0.0, "yaw": 0.0, "desc": "top-right"},
{"x": 0.0, "y": 1.5, "yaw": 0.0, "desc": "top-center"},
{"x": -1.5, "y": 1.0, "yaw": 0.0, "desc": "top-left"},
{"x": -1.5, "y": -0.5, "yaw": 0.0, "desc": "left"},
]
class WaypointNav(Node):
def __init__(self, waypoints, loop=False, threshold=0.25):
super().__init__("waypoint_nav")
self.waypoints = waypoints
self.loop = loop
self.threshold = threshold
self.idx = 0
self.reached = False
self.fail_gen = 0
self.skip_gen = -1
self.skip_streak = 0 # consecutive skips
self.pose_x = self.pose_y = self.pose_yaw = 0.0
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy
qos = QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.TRANSIENT_LOCAL)
self.pub_goal = self.create_publisher(PoseStamped, "/goal_pose", qos)
self.sub_odom = self.create_subscription(
Odometry, "/odom", self.odom_cb, 10)
self.sub_status = self.create_subscription(
String, "/nav_status", self.status_cb, 10)
self.timer = self.create_timer(0.2, self.check) # 5Hz
def odom_cb(self, msg):
self.pose_x = msg.pose.pose.position.x
self.pose_y = msg.pose.pose.position.y
q = msg.pose.pose.orientation
self.pose_yaw = math.atan2(2*(q.w*q.z + q.x*q.y), 1 - 2*(q.y*q.y + q.z*q.z))
def status_cb(self, msg):
if msg.data == "fail" and self.reached and self.fail_gen != self.skip_gen:
self.skip_streak += 1
if self.skip_streak > 3:
self.get_logger().error("⏭ 3+ consecutive skips — stopping")
return
self.get_logger().warn(f"⏭ WP {self.idx+1} plan failed — skip #{self.skip_streak}")
self.skip_gen = self.fail_gen
self.idx += 1
self.reached = False
def send_goal(self, wp):
msg = PoseStamped()
msg.header.frame_id = "map"
msg.header.stamp = self.get_clock().now().to_msg()
msg.pose.position.x = float(wp["x"])
msg.pose.position.y = float(wp["y"])
yaw_deg = float(wp.get("yaw", 0.0))
yaw = math.radians(yaw_deg) # JSON stores degrees
msg.pose.orientation.z = math.sin(yaw / 2.0)
msg.pose.orientation.w = math.cos(yaw / 2.0)
self.pub_goal.publish(msg)
desc = wp.get("desc", "")
self.get_logger().info(
f"→ WP {self.idx+1}/{len(self.waypoints)}: "
f"({wp['x']:.2f},{wp['y']:.2f}) {desc}"
)
def check(self):
if self.idx >= len(self.waypoints):
if self.loop:
self.idx = 0
self.reached = False
else:
return
wp = self.waypoints[self.idx]
if not self.reached:
self.fail_gen += 1 # new generation for this wp
self.send_goal(wp)
self.reached = True
dist = math.hypot(self.pose_x - wp["x"], self.pose_y - wp["y"])
if dist < self.threshold:
self.get_logger().info(
f"✓ WP {self.idx+1} reached (dist={dist:.2f}m)")
self.idx += 1
self.reached = False
self.skip_streak = 0
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--waypoints", type=str, default=None,
help="JSON file with waypoints")
parser.add_argument("--loop", action="store_true",
help="Loop back to first waypoint after last")
parser.add_argument("--threshold", type=float, default=0.3,
help="Arrival distance threshold (m)")
args = parser.parse_args()
if args.waypoints:
with open(args.waypoints) as f:
waypoints = json.load(f)
print(f"Loaded {len(waypoints)} waypoints from {args.waypoints}")
else:
waypoints = DEFAULT_WAYPOINTS
print(f"Using {len(waypoints)} default waypoints")
rclpy.init()
node = WaypointNav(waypoints, args.loop, args.threshold)
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()