From 32df066e44f30b60222f489cdaa5a612df2f6ce4 Mon Sep 17 00:00:00 2001 From: Erik Caldwell Date: Tue, 28 Apr 2026 14:48:38 +0000 Subject: [PATCH] Winter Autonomy Work --- .../config/aruco_board_params.yaml | 2 +- src/Nav/gps/launch/rover.launch.py | 2 +- .../config/ekf/gps_ekf_global.yaml | 6 +- .../config/ekf/gps_ekf_local.yaml | 6 +- .../ekf/indoor_aruco_boards_ekf_global.yaml | 28 +- .../ekf/indoor_aruco_boards_ekf_local.yaml | 28 +- .../localization/republish_odometry.py | 2 +- .../nav_commanders/nav_then_search.py | 426 ++++++++++++++++++ .../nav_commanders/nav_to_gps_aruco.py | 20 +- .../nav_commanders/search_pattern_viz.py | 95 ++++ src/Nav/nav_commanders/setup.py | 2 + .../bt_swerve_dynamic_replanning.xml | 2 +- .../postprocessing_traversability.yaml | 2 +- src/Nav/navigation/nav_launch.md | 11 + src/Nav/navigation/params/nav2.yaml | 73 ++- src/Nav/navigation/rviz/costmap_trav.rviz | 421 +++++++++++++++++ src/Nav/navigation/scripts/record_nav_bag.sh | 26 ++ src/Nav/navigation/scripts/record_zed_svo.sh | 51 +++ 18 files changed, 1154 insertions(+), 49 deletions(-) create mode 100644 src/Nav/nav_commanders/nav_commanders/nav_then_search.py create mode 100644 src/Nav/nav_commanders/nav_commanders/search_pattern_viz.py create mode 100644 src/Nav/navigation/nav_launch.md create mode 100644 src/Nav/navigation/rviz/costmap_trav.rviz create mode 100755 src/Nav/navigation/scripts/record_nav_bag.sh create mode 100755 src/Nav/navigation/scripts/record_zed_svo.sh diff --git a/src/Nav/aruco_localizer/config/aruco_board_params.yaml b/src/Nav/aruco_localizer/config/aruco_board_params.yaml index 9cf1c66a..e090b16f 100644 --- a/src/Nav/aruco_localizer/config/aruco_board_params.yaml +++ b/src/Nav/aruco_localizer/config/aruco_board_params.yaml @@ -12,7 +12,7 @@ aruco_board_detector_node: # DICT_7X7_50, DICT_7X7_100, DICT_7X7_250, DICT_7X7_1000, # DICT_ARUCO_ORIGINAL, DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, # DICT_APRILTAG_36h10, DICT_APRILTAG_36h11 - aruco_dictionary_id: "DICT_6X6_250" + aruco_dictionary_id: "DICT_4X4_50" # Directory containing board configuration YAML files # This will be set by the launch file diff --git a/src/Nav/gps/launch/rover.launch.py b/src/Nav/gps/launch/rover.launch.py index 514fb8f7..90611321 100644 --- a/src/Nav/gps/launch/rover.launch.py +++ b/src/Nav/gps/launch/rover.launch.py @@ -3,7 +3,7 @@ import launch import launch_ros.actions from ament_index_python.packages import get_package_share_directory -from launch.actions import IncludeLaunchDescription +from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.actions import ExecuteProcess, DeclareLaunchArgument from launch.conditions import IfCondition diff --git a/src/Nav/localization/config/ekf/gps_ekf_global.yaml b/src/Nav/localization/config/ekf/gps_ekf_global.yaml index 173ab1c2..3a6ddc15 100644 --- a/src/Nav/localization/config/ekf/gps_ekf_global.yaml +++ b/src/Nav/localization/config/ekf/gps_ekf_global.yaml @@ -62,16 +62,16 @@ global_ekf: # odom0_pose_rejection_threshold: 1000000.0 # odom0_twist_rejection_threshold: 1.0 - odom1: /drive/odom + odom1: /chassis_controller/wheel_odom odom1_config: [ # position false, false, false, # orientation false, false, false, # lineal vel - true, false, false, + true, true, false, # angular vel - false, false, false, + false, false, true, # lineal acc false, false, false, ] diff --git a/src/Nav/localization/config/ekf/gps_ekf_local.yaml b/src/Nav/localization/config/ekf/gps_ekf_local.yaml index 4b859927..ac265202 100644 --- a/src/Nav/localization/config/ekf/gps_ekf_local.yaml +++ b/src/Nav/localization/config/ekf/gps_ekf_local.yaml @@ -62,16 +62,16 @@ local_ekf: # odom0_pose_rejection_threshold: 1000000.0 # odom0_twist_rejection_threshold: 1.0 - odom1: /drive/odom + odom1: /chassis_controller/wheel_odom odom1_config: [ # position false, false, false, # orientation false, false, false, # lineal vel - true, false, false, + true, true, false, # angular vel - false, false, false, + false, false, true, # lineal acc false, false, false, ] diff --git a/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_global.yaml b/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_global.yaml index a6e02bb7..c01f7a5f 100644 --- a/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_global.yaml +++ b/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_global.yaml @@ -4,7 +4,7 @@ global_ekf: # sensor_timeout: 20.0 use_sim_time: false - two_d_mode: true + two_d_mode: false # transform_time_offset: 0.25 # transform_timeout: 3.0 @@ -49,16 +49,36 @@ global_ekf: pose0_rejection_threshold: 50.0 # High threshold to accept bootstrap poses with high covariance # pose0_pose_rejection_threshold: 5.0 # Old parameter name (deprecated) - odom0: /drive/odom + pose1: /zed/zed_node/pose_with_covariance + pose1_config: [ + # position + true, true, true, + # orientation + false, false, false, + # lineal vel + false, false, false, + # angular vel + false, false, false, + # lineal acc + false, false, false, + ] + # pose1_queue_size: 2 + # pose1_nodelay: false + pose1_differential: true + pose1_relative: false + pose1_pose_rejection_threshold: 5.0 # Mahalanobis Distance + # pose1_twist_rejection_threshold: 1.0 + + odom0: /chassis_controller/wheel_odom odom0_config: [ # position false, false, false, # orientation false, false, false, # lineal vel - true, false, false, + true, true, false, # angular vel - false, false, false, + false, false, true, # lineal acc false, false, false, ] diff --git a/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_local.yaml b/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_local.yaml index 084620f2..63e86aa0 100644 --- a/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_local.yaml +++ b/src/Nav/localization/config/ekf/indoor_aruco_boards_ekf_local.yaml @@ -4,7 +4,7 @@ local_ekf: # sensor_timeout: 20.0 use_sim_time: false - two_d_mode: true + two_d_mode: false # transform_time_offset: 0.25 # transform_timeout: 3.0 @@ -49,16 +49,36 @@ local_ekf: pose0_rejection_threshold: 50.0 # High threshold to accept bootstrap poses with high covariance # pose0_pose_rejection_threshold: 5.0 # Old parameter name (deprecated) - odom0: /drive/odom + pose1: /zed/zed_node/pose_with_covariance + pose1_config: [ + # position + true, true, false, + # orientation + false, false, false, + # lineal vel + false, false, false, + # angular vel + false, false, false, + # lineal acc + false, false, false, + ] + # pose1_queue_size: 2 + # pose1_nodelay: false + pose1_differential: true + pose1_relative: false + pose1_pose_rejection_threshold: 5.0 # Mahalanobis Distance + # pose1_twist_rejection_threshold: 1.0 + + odom0: /chassis_controller/wheel_odom odom0_config: [ # position false, false, false, # orientation false, false, false, # lineal vel - true, false, false, + true, true, false, # angular vel - false, false, false, + false, false, true, # lineal acc false, false, false, ] diff --git a/src/Nav/localization/localization/republish_odometry.py b/src/Nav/localization/localization/republish_odometry.py index 0f301ffc..67315743 100644 --- a/src/Nav/localization/localization/republish_odometry.py +++ b/src/Nav/localization/localization/republish_odometry.py @@ -76,7 +76,7 @@ def main(args=None): rclpy.init(args=args) # Set the frequency (in Hz) at which to republish the odometry - frequency = 10 # Change to your desired frequency (Hz) + frequency = 1.0 # Change to your desired frequency (Hz) # Create the node and start spinning node = OdometryRepublisher(frequency) diff --git a/src/Nav/nav_commanders/nav_commanders/nav_then_search.py b/src/Nav/nav_commanders/nav_commanders/nav_then_search.py new file mode 100644 index 00000000..52d5d0d3 --- /dev/null +++ b/src/Nav/nav_commanders/nav_commanders/nav_then_search.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 + +import math +from enum import Enum, auto +from threading import Event +from collections import Counter + +import rclpy +from rclpy.node import Node +from rclpy.executors import MultiThreadedExecutor +from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup +from rclpy.qos import ( + QoSProfile, + ReliabilityPolicy, + DurabilityPolicy, + qos_profile_sensor_data, +) + +from geometry_msgs.msg import PoseStamped, Quaternion +from nav_msgs.msg import Odometry, Path +from std_msgs.msg import Int8, Int32MultiArray +from std_srvs.srv import Trigger + +from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult +from robot_localization.srv import FromLL +from interfaces.srv import NavToGPSGeopose + + +class MissionState(Enum): + DO_NOTHING = auto() + NAV_TO_START = auto() + EXECUTE_SEARCH = auto() + FOUND_TARGET = auto() + NAV_CANCELLED = auto() + + +class SearchPatternCommander(Node): + def __init__(self): + super().__init__("search_pattern_commander") + self.navigator = BasicNavigator("search_pattern_navigator") + self.mission_state = MissionState.DO_NOTHING + + # --- Parameters --- + self.declare_parameter("object_topic", "/aruco_detections") + self.object_topic = ( + self.get_parameter("object_topic").get_parameter_value().string_value + ) + + self.declare_parameter("search_size_meters", 20.0) + self.search_size = ( + self.get_parameter("search_size_meters").get_parameter_value().double_value + ) + + self.declare_parameter("lane_spacing_meters", 4.0) + self.lane_spacing = ( + self.get_parameter("lane_spacing_meters").get_parameter_value().double_value + ) + + self.declare_parameter("frequency", 1 / 5.0) + self.frequency = ( + self.get_parameter("frequency").get_parameter_value().double_value + ) + + self.declare_parameter("robot_pose_topic", "odometry/filtered/global") + self.robot_pose_topic = ( + self.get_parameter("robot_pose_topic").get_parameter_value().string_value + ) + + self.declare_parameter("detection_window_sec", 5.0) + self.detection_window_sec = ( + self.get_parameter("detection_window_sec") + .get_parameter_value() + .double_value + ) + + self.declare_parameter("min_detections", 5) + self.min_detections = ( + self.get_parameter("min_detections").get_parameter_value().integer_value + ) + + self.qos_profile = QoSProfile(reliability=ReliabilityPolicy.RELIABLE, depth=10) + + # Transient Local QoS for the Path so it stays visible in RViz + self.path_qos = QoSProfile( + depth=1, + durability=DurabilityPolicy.TRANSIENT_LOCAL, + reliability=ReliabilityPolicy.RELIABLE, + ) + + # --- Topic Names and Service Names --- + self.nav_fix_service_name = "fromLL" + self.geopose_service_name = "commander/nav_to_gps_geopose" + self.cancel_nav_service_name = "commander/cancel_nav" + self.lights_topic = "/light" + self.intended_path_topic = "/intended_search_path" + + self.nav_activate_light_code = 1 + self.nav_completed_light_code = 3 + self.nav_cancelled_light_code = 2 + + # --- Callback Groups --- + self.localizer_callback_group = MutuallyExclusiveCallbackGroup() + self.pose_callback_group = ReentrantCallbackGroup() + self.goal_callback_group = MutuallyExclusiveCallbackGroup() + self.timer_group = MutuallyExclusiveCallbackGroup() + self.lights_callback_group = MutuallyExclusiveCallbackGroup() + self.detection_callback_group = ReentrantCallbackGroup() + self.cancel_service_callback_group = MutuallyExclusiveCallbackGroup() + + # --- Clients, Services, Subscriptions, Publishers --- + self.localizer_client = self.create_client( + FromLL, + self.nav_fix_service_name, + callback_group=self.localizer_callback_group, + ) + self.geopose_service = self.create_service( + NavToGPSGeopose, + self.geopose_service_name, + self.geopose_server, + callback_group=self.goal_callback_group, + ) + self.cancel_nav_service = self.create_service( + Trigger, + self.cancel_nav_service_name, + self.cancel_nav_server, + callback_group=self.cancel_service_callback_group, + ) + self.robot_pose_subscription = self.create_subscription( + Odometry, + self.robot_pose_topic, + self.robot_pose_callback, + qos_profile_sensor_data, + callback_group=self.pose_callback_group, + ) + self.lights_publisher = self.create_publisher( + Int8, + self.lights_topic, + qos_profile=self.qos_profile, + callback_group=self.lights_callback_group, + ) + + # New Publisher for RViz full path visualization + self.path_publisher = self.create_publisher( + Path, self.intended_path_topic, qos_profile=self.path_qos + ) + + # --- Object Detection --- + self.detections_buffer = [] + self.detection_subscription = self.create_subscription( + Int32MultiArray, + self.object_topic, + self.detection_callback, + qos_profile_sensor_data, + callback_group=self.detection_callback_group, + ) + + # --- State Variables --- + self.final_lat_lon = None + self.current_robot_pose = None + self.goal_handle = None + self.stop_triggered_id = None + self.search_poses = [] + + # --- Timer --- + self.timer = self.create_timer( + 1.0 / self.frequency, self.timer_callback, callback_group=self.timer_group + ) + + # --- Initialization Checks --- + self.get_logger().info( + f"Waiting for {self.nav_fix_service_name} to be active..." + ) + while not self.localizer_client.wait_for_service(timeout_sec=2.0): + self.get_logger().info("Waiting...") + self.get_logger().info(f"{self.nav_fix_service_name} is active") + + self.get_logger().info("Waiting for Nav2 to be active...") + self.navigator.waitUntilNav2Active(localizer="controller_server") + self.get_logger().info("Nav2 is active. Ready to generate search patterns.") + + def robot_pose_callback(self, msg: Odometry): + self.current_robot_pose = msg.pose + + def detection_callback(self, msg: Int32MultiArray): + if self.mission_state not in [ + MissionState.NAV_TO_START, + MissionState.EXECUTE_SEARCH, + ]: + return + + current_time = self.get_clock().now().nanoseconds / 1e9 + self.detections_buffer = [ + (ts, tag_id) + for ts, tag_id in self.detections_buffer + if (current_time - ts) <= self.detection_window_sec + ] + + for detected_id in msg.data: + self.detections_buffer.append((current_time, detected_id)) + + tag_ids_in_window = [tag_id for ts, tag_id in self.detections_buffer] + tag_counts = Counter(tag_ids_in_window) + + for tag_id, count in tag_counts.items(): + if count >= self.min_detections: + self.get_logger().info(f"Object ID {tag_id} detected! Stopping search.") + self.mission_state = MissionState.FOUND_TARGET + self.stop_triggered_id = tag_id + self.navigator.cancelTask() + self.reset_state_variables() + + light_msg = Int8() + light_msg.data = self.nav_completed_light_code + self.lights_publisher.publish(light_msg) + return + + def geopose_server( + self, request: NavToGPSGeopose.Request, response: NavToGPSGeopose.Response + ) -> NavToGPSGeopose.Response: + self.get_logger().info(f"Received new GPS search zone: {request.goal}") + self.reset_state_variables() + self.final_lat_lon = ( + request.goal.position.latitude, + request.goal.position.longitude, + request.goal.position.altitude, + request.goal.orientation, + ) + self.mission_state = MissionState.NAV_TO_START + response.success = True + self.timer.reset() + + msg = Int8() + msg.data = self.nav_activate_light_code + self.lights_publisher.publish(msg) + return response + + def cancel_nav_server( + self, request: Trigger.Request, response: Trigger.Response + ) -> Trigger.Response: + self.get_logger().info("Received request to cancel search.") + self.navigator.cancelTask() + self.reset_state_variables() + self.mission_state = MissionState.NAV_CANCELLED + + msg = Int8() + msg.data = self.nav_cancelled_light_code + self.lights_publisher.publish(msg) + + response.success = True + response.message = "Search cancelled by user." + return response + + def convert_lat_lon_to_pose(self, latitude, longitude, altitude, orientation): + req = FromLL.Request() + req.ll_point.longitude = longitude + req.ll_point.latitude = latitude + req.ll_point.altitude = altitude + + try: + event = Event() + + def done_callback(future): + nonlocal event + event.set() + + future = self.localizer_client.call_async(req) + future.add_done_callback(done_callback) + event.wait(timeout=5.0) + + if not future.done(): + self.get_logger().error("fromLL service timeout.") + return None + + result = future.result() + target_pose = PoseStamped() + target_pose.header.frame_id = "map" + target_pose.header.stamp = self.get_clock().now().to_msg() + target_pose.pose.position = result.map_point + target_pose.pose.orientation = orientation + return target_pose + except Exception as e: + self.get_logger().error(f"Error during lat/lon conversion: {e}") + return None + + def yaw_to_quaternion(self, yaw): + q = Quaternion() + q.x, q.y = 0.0, 0.0 + q.z = math.sin(yaw / 2.0) + q.w = math.cos(yaw / 2.0) + return q + + def publish_intended_path(self, poses): + """Publishes the full array of search poses to RViz.""" + path_msg = Path() + path_msg.header.frame_id = "map" + path_msg.header.stamp = self.get_clock().now().to_msg() + path_msg.poses = poses + self.path_publisher.publish(path_msg) + self.get_logger().info( + f"Published intended search path to {self.intended_path_topic}" + ) + + def generate_search_pattern(self, center_pose: PoseStamped): + poses = [] + c_x = center_pose.pose.position.x + c_y = center_pose.pose.position.y + half_size = self.search_size / 2.0 + + start_x = c_x - half_size + start_y = c_y - half_size + num_lanes = int(self.search_size / self.lane_spacing) + 1 + + for i in range(num_lanes): + current_y = start_y + (i * self.lane_spacing) + if i % 2 == 0: + x_start, x_end, yaw = start_x, start_x + self.search_size, 0.0 + else: + x_start, x_end, yaw = start_x + self.search_size, start_x, math.pi + + pose_start = PoseStamped() + pose_start.header.frame_id = "map" + pose_start.pose.position.x = x_start + pose_start.pose.position.y = current_y + pose_start.pose.orientation = self.yaw_to_quaternion(yaw) + poses.append(pose_start) + + pose_end = PoseStamped() + pose_end.header.frame_id = "map" + pose_end.pose.position.x = x_end + pose_end.pose.position.y = current_y + pose_end.pose.orientation = self.yaw_to_quaternion(yaw) + poses.append(pose_end) + + self.get_logger().info( + f"Generated {len(poses)} waypoints for {self.search_size}x{self.search_size}m search area." + ) + + # Immediately publish to RViz so it's clear what the rover is attempting + self.publish_intended_path(poses) + return poses + + def reset_state_variables(self): + self.goal_handle = None + self.final_lat_lon = None + self.detections_buffer = [] + self.stop_triggered_id = None + self.search_poses = [] + + # Clear the RViz path when resetting + empty_path = Path() + empty_path.header.frame_id = "map" + self.path_publisher.publish(empty_path) + + def handle_task_result(self, success_state): + result = self.navigator.getResult() + if result == TaskResult.SUCCEEDED: + if success_state == MissionState.EXECUTE_SEARCH: + self.get_logger().info( + "Arrived at start point. Beginning search pattern!" + ) + self.mission_state = MissionState.EXECUTE_SEARCH + self.goal_handle = None + else: + self.get_logger().info( + "Search pattern fully completed without finding object." + ) + self.mission_state = MissionState.DO_NOTHING + self.reset_state_variables() + msg = Int8() + msg.data = self.nav_completed_light_code + self.lights_publisher.publish(msg) + + elif result == TaskResult.CANCELED: + if self.stop_triggered_id is not None: + self.get_logger().info( + f"Mission cancelled by target detection (ID: {self.stop_triggered_id})." + ) + else: + self.get_logger().info("Mission canceled by user.") + elif result == TaskResult.FAILED: + self.get_logger().error("Navigation task failed!") + self.mission_state = MissionState.DO_NOTHING + self.reset_state_variables() + + def timer_callback(self): + if self.mission_state == MissionState.NAV_TO_START: + if not self.search_poses: + center_pose = self.convert_lat_lon_to_pose(*self.final_lat_lon) + if not center_pose: + return + + self.search_poses = self.generate_search_pattern(center_pose) + + self.get_logger().info("Navigating to the search start point...") + self.goal_handle = self.navigator.goToPose(self.search_poses[0]) + return + + if self.navigator.isTaskComplete(): + self.handle_task_result(success_state=MissionState.EXECUTE_SEARCH) + + elif self.mission_state == MissionState.EXECUTE_SEARCH: + if self.goal_handle is None: + self.get_logger().info("Executing goThroughPoses for search grid...") + self.goal_handle = self.navigator.goThroughPoses(self.search_poses[1:]) + return + + if self.navigator.isTaskComplete(): + self.handle_task_result(success_state=MissionState.DO_NOTHING) + + +def main(args=None): + rclpy.init(args=args) + node = SearchPatternCommander() + executor = MultiThreadedExecutor() + executor.add_node(node) + try: + executor.spin() + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/Nav/nav_commanders/nav_commanders/nav_to_gps_aruco.py b/src/Nav/nav_commanders/nav_commanders/nav_to_gps_aruco.py index 479b7f27..2684bf14 100644 --- a/src/Nav/nav_commanders/nav_commanders/nav_to_gps_aruco.py +++ b/src/Nav/nav_commanders/nav_commanders/nav_to_gps_aruco.py @@ -11,7 +11,7 @@ from geometry_msgs.msg import PoseStamped from nav_msgs.msg import Odometry -from std_msgs.msg import Int8, Int32MultiArray +from std_msgs.msg import Int8, Int32 from std_srvs.srv import Trigger from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult @@ -42,7 +42,7 @@ def __init__(self): self.mission_state = MissionState.DO_NOTHING # --- Parameters --- - self.declare_parameter("aruco_topic", "/aruco_detections") + self.declare_parameter("aruco_topic", "/marker_detected") self.aruco_topic = ( self.get_parameter("aruco_topic").get_parameter_value().string_value ) @@ -148,7 +148,7 @@ def __init__(self): # Stores (timestamp, detected_id) tuples for all detected tags within the defined window. self.aruco_detections_buffer = [] self.aruco_subscription = self.create_subscription( - Int32MultiArray, + Int32, self.aruco_topic, self.aruco_callback, qos_profile_sensor_data, @@ -191,7 +191,7 @@ def robot_pose_callback(self, msg: Odometry): """Callback to update the current robot pose from odometry.""" self.current_robot_pose = msg.pose - def aruco_callback(self, msg: Int32MultiArray): + def aruco_callback(self, msg: Int32): """ Callback for Aruco tag detections. Tracks the frequency of all detected tags within a configurable sliding window. @@ -212,12 +212,12 @@ def aruco_callback(self, msg: Int32MultiArray): if (current_time - ts) <= self.aruco_detection_window_sec ] - # Add new detections from the current message to the buffer - for detected_id in msg.data: - self.aruco_detections_buffer.append((current_time, detected_id)) - self.get_logger().debug( - f"Aruco ID {detected_id} detected. Buffer size: {len(self.aruco_detections_buffer)}" - ) + # Add new detection from the current message to the buffer (single marker ID) + detected_id = msg.data + self.aruco_detections_buffer.append((current_time, detected_id)) + self.get_logger().debug( + f"Aruco ID {detected_id} detected. Buffer size: {len(self.aruco_detections_buffer)}" + ) # Count occurrences of each unique tag ID in the current detection window tag_ids_in_window = [tag_id for ts, tag_id in self.aruco_detections_buffer] diff --git a/src/Nav/nav_commanders/nav_commanders/search_pattern_viz.py b/src/Nav/nav_commanders/nav_commanders/search_pattern_viz.py new file mode 100644 index 00000000..f39bed24 --- /dev/null +++ b/src/Nav/nav_commanders/nav_commanders/search_pattern_viz.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import math +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import PoseStamped, Quaternion +from nav_msgs.msg import Path + + +class SearchPatternVisualizer(Node): + """ + A simple ROS2 node that generates a lawnmower search pattern + and continuously publishes it as a Path message for RViz visualization. + """ + + def __init__(self): + super().__init__("search_pattern_visualizer") + + # Publisher for the path topic + self.path_pub = self.create_publisher(Path, "/search_pattern_path", 10) + + # Grid parameters (Modify these to instantly see changes in RViz) + self.search_size = 20.0 # 20x20 meter grid + self.lane_spacing = 4.0 # 4 meters between lanes + + # Publish at 1Hz so it remains persistent in RViz + self.timer = self.create_timer(1.0, self.publish_path) + self.get_logger().info( + "Publishing search pattern to '/search_pattern_path' at 1Hz..." + ) + + def yaw_to_quaternion(self, yaw): + """Converts a yaw angle (in radians) to a geometry_msgs Quaternion.""" + q = Quaternion() + q.x, q.y = 0.0, 0.0 + q.z = math.sin(yaw / 2.0) + q.w = math.cos(yaw / 2.0) + return q + + def publish_path(self): + path_msg = Path() + path_msg.header.frame_id = "map" + path_msg.header.stamp = self.get_clock().now().to_msg() + + # Center of the search area (0,0 for visualization purposes) + center_x, center_y = 0.0, 0.0 + half_size = self.search_size / 2.0 + + start_x = center_x - half_size + start_y = center_y - half_size + num_lanes = int(self.search_size / self.lane_spacing) + 1 + + for i in range(num_lanes): + current_y = start_y + (i * self.lane_spacing) + + # Alternate directions for the boustrophedon sweep + if i % 2 == 0: + x_start, x_end, yaw = start_x, start_x + self.search_size, 0.0 + else: + x_start, x_end, yaw = start_x + self.search_size, start_x, math.pi + + # Entry point of the current lane + pose_start = PoseStamped() + pose_start.header = path_msg.header + pose_start.pose.position.x = x_start + pose_start.pose.position.y = current_y + pose_start.pose.orientation = self.yaw_to_quaternion(yaw) + path_msg.poses.append(pose_start) + + # Exit point of the current lane + pose_end = PoseStamped() + pose_end.header = path_msg.header + pose_end.pose.position.x = x_end + pose_end.pose.position.y = current_y + pose_end.pose.orientation = self.yaw_to_quaternion(yaw) + path_msg.poses.append(pose_end) + + # Send to RViz + self.path_pub.publish(path_msg) + + +def main(args=None): + rclpy.init(args=args) + node = SearchPatternVisualizer() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/Nav/nav_commanders/setup.py b/src/Nav/nav_commanders/setup.py index 10cff416..91073a74 100644 --- a/src/Nav/nav_commanders/setup.py +++ b/src/Nav/nav_commanders/setup.py @@ -25,6 +25,8 @@ "gps_commander_node = nav_commanders.nav_to_gps_coords:main", "incremental_gps_commander_node = nav_commanders.incremental_gps_commander:main", "aruco_gps_commander_node = nav_commanders.nav_to_gps_aruco:main", + "nav_then_search_commander_node = nav_commanders.nav_then_search:main", + "search_pattern_viz_node = nav_commanders.search_pattern_viz:main", ], }, ) diff --git a/src/Nav/navigation/behavior_trees/bt_swerve_dynamic_replanning.xml b/src/Nav/navigation/behavior_trees/bt_swerve_dynamic_replanning.xml index e5be3123..a04b3a34 100644 --- a/src/Nav/navigation/behavior_trees/bt_swerve_dynamic_replanning.xml +++ b/src/Nav/navigation/behavior_trees/bt_swerve_dynamic_replanning.xml @@ -44,7 +44,7 @@ - + diff --git a/src/Nav/navigation/config/elevation_mapping/postprocessing_traversability.yaml b/src/Nav/navigation/config/elevation_mapping/postprocessing_traversability.yaml index bc2e4020..91c74dcc 100644 --- a/src/Nav/navigation/config/elevation_mapping/postprocessing_traversability.yaml +++ b/src/Nav/navigation/config/elevation_mapping/postprocessing_traversability.yaml @@ -69,7 +69,7 @@ elevation_mapping: type: gridMapFilters/MathExpressionFilter params: output_layer: traversability - expression: 0.13 * (slope / 0.6) + 0.24 * (roughness / 0.1) # roughness: 0.3 + expression: 0.7 * (slope / 0.6) + 0.45 * (roughness / 0.1) # roughness: 0.3 # Set lower threshold on traversability. filter7: diff --git a/src/Nav/navigation/nav_launch.md b/src/Nav/navigation/nav_launch.md new file mode 100644 index 00000000..26683fe0 --- /dev/null +++ b/src/Nav/navigation/nav_launch.md @@ -0,0 +1,11 @@ +ros2 launch navigation slam.launch.py > slam_log.txt 2>&1 & +ros2 run localization repub_odom > rpub_odom.txt 2>&1 & +ros2 launch navigation nav2.launch.py > nav2.log 2>&1 & +ros2 launch video_streaming video_streaming.launch.py > video.log 2>&1 & + +ros2 launch bringup control.launch.py + +python3 send_gps_command.py --file p7_feb22_2026_aruco.yaml +ros2 launch nav_commanders incremental_gps_commander.launch.py + +ros2 param set /detect_node detection_type "ARUCO" \ No newline at end of file diff --git a/src/Nav/navigation/params/nav2.yaml b/src/Nav/navigation/params/nav2.yaml index 8023985e..93c3bff1 100644 --- a/src/Nav/navigation/params/nav2.yaml +++ b/src/Nav/navigation/params/nav2.yaml @@ -90,10 +90,11 @@ global_costmap: global_costmap: ros__parameters: update_frequency: 10.0 - publish_frequency: 0.5 + publish_frequency: 2.0 global_frame: map robot_base_frame: base_link rolling_window: true + transform_tolerance: 0.3 height: 100 width: 100 origin_x: -50.0 @@ -145,11 +146,11 @@ planner_server: analytic_expansion_max_length: 3.0 # For Hybrid/Lattice nodes: The maximum length of the analytic expansion to be considered valid to prevent unsafe shortcutting analytic_expansion_max_cost: 200.0 # The maximum single cost for any part of an analytic expansion to contain and be valid, except when necessary on approach to goal analytic_expansion_max_cost_override: false # Whether or not to override the maximum cost setting if within critical distance to goal (ie probably required) - minimum_turning_radius: 0.40 # minimum turning radius in m of path / vehicle (Swerve can turn tighter) + minimum_turning_radius: 0.3 # minimum turning radius in m of path / vehicle (Swerve can turn tighter) reverse_penalty: 2.0 # Penalty to apply if motion is reversing, must be => 1 change_penalty: 0.0 # Penalty to apply if motion is changing directions (L to R), must be >= 0 non_straight_penalty: 1.2 # Penalty to apply if motion is non-straight, must be => 1 - cost_penalty: 2.0 # Penalty to apply to higher cost areas when adding into the obstacle map dynamic programming distance expansion heuristic. This drives the robot more towards the center of passages. A value between 1.3 - 3.5 is reasonable. + cost_penalty: 1.5 # Penalty to apply to higher cost areas when adding into the obstacle map dynamic programming distance expansion heuristic. This drives the robot more towards the center of passages. A value between 1.3 - 3.5 is reasonable. retrospective_penalty: 0.015 lookup_table_size: 40.0 # Size of the dubin/reeds-sheep distance window to cache, in meters. cache_obstacle_heuristic: true # Cache the obstacle map dynamic programming distance expansion heuristic between subsequent replannings of the same goal location. Dramatically speeds up replanning performance (40x) if costmap is largely static. @@ -161,21 +162,51 @@ planner_server: smoother: max_iterations: 1000 - w_smooth: 0.3 + w_smooth: 0.6 # Increased from 0.3 to produce smoother initial paths w_data: 0.2 tolerance: 1.0e-10 do_refinement: true - refinement_num: 2 + refinement_num: 4 # Increased from 2 for better initial smoothing smoother_server: ros__parameters: use_sim_time: True - smoother_plugins: [simple_smoother] + # smoother_plugins: [simple_smoother] + smoother_plugins: [constrained_smoother] simple_smoother: plugin: nav2_smoother::SimpleSmoother tolerance: 1.0e-10 max_its: 1000 do_refinement: True + constrained_smoother: + plugin: "nav2_constrained_smoother/ConstrainedSmoother" + reversing_enabled: false # whether to detect forward/reverse direction and cusps. Should be set to false for paths without orientations assigned + path_downsampling_factor: 5 # every n-th node of the path is taken (increased from 3 to speed up smoothing) + path_upsampling_factor: 3 # 0 - path remains downsampled, 1 - path is upsampled back to original granularity using cubic bezier, 2... - more upsampling + keep_start_orientation: true # whether to prevent the start orientation from being smoothed + keep_goal_orientation: true # whether to prevent the gpal orientation from being smoothed + minimum_turning_radius: 0.1 # minimum turning radius the robot can perform. Can be set to 0.0 (or w_curve can be set to 0.0 with the same effect) for diff-drive/holonomic robots + w_curve: 150.0 # weight to enforce minimum_turning_radius (increased to strongly prefer larger ~0.8m arcs but allow 0.1m when obstacles require) + w_dist: 0.0 # weight to bind path to original as optional replacement for cost weight + w_smooth: 2000000.0 # weight to maximize smoothness of path + w_cost: 0.5 # weight to steer robot away from collision and cost (HIGH value to strongly respect elevation/traversability costmap and force tighter turns in rough terrain) + + # Parameters used to improve obstacle avoidance near cusps (forward/reverse movement changes) + # See the [docs page](https://navigation.ros.org/configuration/packages/configuring-constrained-smoother) for further clarification + w_cost_cusp_multiplier: 1.0 # option to have higher weight during forward/reverse direction change which is often accompanied with dangerous rotations (set to 1.0 since reversing disabled) + cusp_zone_length: 2.5 # length of the section around cusp in which nodes use w_cost_cusp_multiplier (w_cost rises gradually inside the zone towards the cusp point, whose costmap weight equals w_cost*w_cost_cusp_multiplier) + + # Points in robot frame to grab costmap values from. Format: [x1, y1, weight1, x2, y2, weight2, ...] + # IMPORTANT: Requires much higher number of iterations to actually improve the path. Uncomment only if you really need it (highly elongated/asymmetric robots) + # See the [docs page](https://navigation.ros.org/configuration/packages/configuring-constrained-smoother) for further clarification + # cost_check_points: [-0.185, 0.0, 1.0] + + optimizer: + max_iterations: 50 # max iterations of smoother (reduced from 70 for faster convergence) + debug_optimizer: false # print debug info + gradient_tol: 0.01 # Relaxed from 0.005 for faster convergence + fn_tol: 1.0e-10 # Relaxed from 1.0e-15 for faster convergence + param_tol: 1.0e-15 # Relaxed from 1.0e-20 for faster convergence controller_server: ros__parameters: @@ -197,18 +228,20 @@ controller_server: general_goal_checker: stateful: True plugin: nav2_controller::SimpleGoalChecker - xy_goal_tolerance: 0.5 + xy_goal_tolerance: 0.6 yaw_goal_tolerance: 0.2 # ~11 degrees FollowPath: - plugin: nav2_rotation_shim_controller::RotationShimController - primary_controller: nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController + # plugin: nav2_rotation_shim_controller::RotationShimController + # primary_controller: nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController + plugin: nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController + # Rotation Shim Parameters (Swerve Logic - Point Turn Before Driving) - angular_dist_threshold: 0.5 # If path angle > 30deg, rotate in place first + angular_dist_threshold: 0.3 # If path angle > 30deg, rotate in place first forward_sampling_distance: 0.5 # How far ahead to check path orientation rotate_to_heading_angular_vel: 1.0 # Angular velocity during point turn - max_angular_accel: 2.0 # Max angular acceleration + max_angular_accel: 0.8 # Max angular acceleration simulate_ahead_time: 1.0 # Simulation time for collision checking use_rotate_to_heading: false # Disable since using rotation shim controller @@ -216,7 +249,7 @@ controller_server: # Regulated Pure Pursuit Parameters (Driving Logic) transform_tolerance: 1.0 - desired_linear_vel: 0.3 # Increase to 1.0 for outdoor swerve agility + desired_linear_vel: 0.35 # Increase to 1.0 for outdoor swerve agility max_robot_pose_search_dist: 10.0 lookahead_dist: 1.2 @@ -247,13 +280,13 @@ behavior_server: cycle_frequency: 0.4 behavior_plugins: [spin, backup, drive_on_heading, wait] spin: - plugin: nav2_behaviors::Spin + plugin: nav2_behaviors/Spin backup: - plugin: nav2_behaviors::BackUp + plugin: nav2_behaviors/BackUp drive_on_heading: - plugin: nav2_behaviors::DriveOnHeading + plugin: nav2_behaviors/DriveOnHeading wait: - plugin: nav2_behaviors::Wait + plugin: nav2_behaviors/Wait global_frame: odom robot_base_frame: base_link transform_tolerance: 0.1 @@ -295,8 +328,8 @@ velocity_smoother: # max_decel: [-0.5, 0.0, -1.5] # INDOOR TESTING # Safe limits for initial testing in confined spaces - max_velocity: [0.3, 0.0, 0.5] # [x, y, theta] in m/s, m/s, rad/s - min_velocity: [-0.3, 0.0, -0.5] - max_accel: [0.2, 0.0, 0.5] # [x, y, theta] in m/s², m/s², rad/s² - max_decel: [-0.2, 0.0, -0.5] + max_velocity: [0.6, 0.0, 0.6] # [x, y, theta] in m/s, m/s, rad/s + min_velocity: [-0.6, 0.0, -0.6] + max_accel: [0.3, 0.0, 0.5] # [x, y, theta] in m/s², m/s², rad/s² + max_decel: [-0.3, 0.0, -0.5] diff --git a/src/Nav/navigation/rviz/costmap_trav.rviz b/src/Nav/navigation/rviz/costmap_trav.rviz new file mode 100644 index 00000000..50a693cf --- /dev/null +++ b/src/Nav/navigation/rviz/costmap_trav.rviz @@ -0,0 +1,421 @@ +Panels: + - Class: rviz_common/Displays + Help Height: 78 + Name: Displays + Property Tree Widget: + Expanded: + - /Global Options1 + - /Status1 + - /TF1/Frames1 + - /TF1/Tree1 + - /TraversibilityMap1 + - /Path2 + Splitter Ratio: 0.5423280596733093 + Tree Height: 725 + - Class: rviz_common/Selection + Name: Selection + - Class: rviz_common/Tool Properties + Expanded: + - /2D Goal Pose1 + - /Publish Point1 + Name: Tool Properties + Splitter Ratio: 0.5886790156364441 + - Class: rviz_common/Views + Expanded: + - /Current View1 + Name: Views + Splitter Ratio: 0.5 + - Class: rviz_common/Time + Experimental: false + Name: Time + SyncMode: 0 + SyncSource: "" +Visualization Manager: + Class: "" + Displays: + - Alpha: 0.5 + Cell Size: 1 + Class: rviz_default_plugins/Grid + Color: 160; 160; 164 + Enabled: false + Line Style: + Line Width: 0.029999999329447746 + Value: Lines + Name: Grid + Normal Cell Count: 0 + Offset: + X: 0 + Y: 0 + Z: 0 + Plane: XY + Plane Cell Count: 10 + Reference Frame: + Value: false + - Class: rviz_default_plugins/TF + Enabled: true + Frame Timeout: 15 + Frames: + All Enabled: false + Antenna_Back: + Value: false + Antenna_Front: + Value: false + EndEffector: + Value: false + Frame: + Value: false + GPS_Left: + Value: false + GPS_Right: + Value: false + Link_1: + Value: false + Link_2: + Value: false + Link_3: + Value: false + Link_4: + Value: false + Link_5: + Value: false + Link_6: + Value: false + Rocker_Left: + Value: false + Rocker_Right: + Value: false + Sus_Arm_Back_Left: + Value: false + Sus_Arm_Back_Right: + Value: false + Sus_Arm_Front_Left: + Value: false + Sus_Arm_Front_Right: + Value: false + Wheel_Arm_Back_Left: + Value: false + Wheel_Arm_Back_Right: + Value: false + Wheel_Arm_Front_Left: + Value: false + Wheel_Arm_Front_Right: + Value: false + Wheel_Back_Left: + Value: false + Wheel_Back_Right: + Value: false + Wheel_Front_Left: + Value: false + Wheel_Front_Right: + Value: false + Zed: + Value: false + base_link: + Value: true + gps_link: + Value: false + imu_link: + Value: false + lidar_link: + Value: false + map: + Value: false + odom: + Value: false + zed_camera_center: + Value: true + zed_camera_link: + Value: false + zed_imu_link: + Value: false + zed_left_camera_frame: + Value: false + zed_left_camera_optical_frame: + Value: false + zed_right_camera_frame: + Value: false + zed_right_camera_optical_frame: + Value: false + Marker Scale: 1 + Name: TF + Show Arrows: true + Show Axes: true + Show Names: false + Tree: + map: + odom: + base_link: + Frame: + Antenna_Back: + {} + Antenna_Front: + {} + GPS_Left: + {} + GPS_Right: + {} + Rocker_Left: + Sus_Arm_Back_Left: + Wheel_Arm_Back_Left: + Wheel_Back_Left: + {} + Sus_Arm_Front_Left: + Wheel_Arm_Front_Left: + Wheel_Front_Left: + {} + Rocker_Right: + Sus_Arm_Back_Right: + Wheel_Arm_Back_Right: + Wheel_Back_Right: + {} + Sus_Arm_Front_Right: + Wheel_Arm_Front_Right: + Wheel_Front_Right: + {} + Zed: + {} + Link_1: + Link_2: + Link_3: + Link_4: + Link_5: + Link_6: + EndEffector: + {} + gps_link: + {} + imu_link: + {} + lidar_link: + {} + zed_camera_link: + zed_camera_center: + zed_left_camera_frame: + zed_imu_link: + {} + zed_left_camera_optical_frame: + {} + zed_right_camera_frame: + zed_right_camera_optical_frame: + {} + Update Interval: 0 + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Class: grid_map_rviz_plugin/GridMap + Color: 200; 200; 200 + Color Layer: traversability + Color Transformer: IntensityLayer + Enabled: true + Height Layer: elevation + Height Transformer: Layer + History Length: 1 + Invert Rainbow: false + Max Color: 0; 255; 0 + Max Intensity: 1 + Min Color: 170; 0; 0 + Min Intensity: 0 + Name: TraversibilityMap + Show Grid Lines: true + Topic: + Depth: 5 + Durability Policy: Volatile + Filter size: 10 + History Policy: Keep Last + Reliability Policy: Reliable + Value: /traversability_map + Use Rainbow: true + Value: true + - Alpha: 0.699999988079071 + Class: rviz_default_plugins/Map + Color Scheme: map + Draw Behind: false + Enabled: true + Name: GlobalMap + Topic: + Depth: 5 + Durability Policy: Volatile + Filter size: 10 + History Policy: Keep Last + Reliability Policy: Reliable + Value: /global_costmap/costmap + Update Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /global_costmap/costmap_updates + Use Timestamp: false + Value: true + - Alpha: 1 + Buffer Length: 1 + Class: rviz_default_plugins/Path + Color: 25; 255; 0 + Enabled: true + Head Diameter: 0.30000001192092896 + Head Length: 0.20000000298023224 + Length: 0.30000001192092896 + Line Style: Lines + Line Width: 0.029999999329447746 + Name: Path + Offset: + X: 0 + Y: 0 + Z: 0 + Pose Color: 255; 85; 255 + Pose Style: None + Radius: 0.029999999329447746 + Shaft Diameter: 0.10000000149011612 + Shaft Length: 0.10000000149011612 + Topic: + Depth: 5 + Durability Policy: Volatile + Filter size: 10 + History Policy: Keep Last + Reliability Policy: Reliable + Value: /plan + Value: true + - Class: rviz_default_plugins/MarkerArray + Enabled: true + Name: MarkerArray + Namespaces: + {} + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /aruco_markers_viz + Value: true + - Alpha: 1 + Buffer Length: 1 + Class: rviz_default_plugins/Path + Color: 255; 0; 0 + Enabled: true + Head Diameter: 0.30000001192092896 + Head Length: 0.20000000298023224 + Length: 0.30000001192092896 + Line Style: Lines + Line Width: 0.029999999329447746 + Name: Path + Offset: + X: 0 + Y: 0 + Z: 0 + Pose Color: 255; 85; 255 + Pose Style: None + Radius: 0.029999999329447746 + Shaft Diameter: 0.10000000149011612 + Shaft Length: 0.10000000149011612 + Topic: + Depth: 5 + Durability Policy: Volatile + Filter size: 10 + History Policy: Keep Last + Reliability Policy: Reliable + Value: /unsmoothed_plan + Value: true + - Alpha: 1 + Axes Length: 1 + Axes Radius: 0.10000000149011612 + Class: rviz_default_plugins/Pose + Color: 255; 25; 0 + Enabled: true + Head Length: 0.30000001192092896 + Head Radius: 0.10000000149011612 + Name: Pose + Shaft Length: 1 + Shaft Radius: 0.05000000074505806 + Shape: Arrow + Topic: + Depth: 5 + Durability Policy: Volatile + Filter size: 10 + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + Value: true + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Fixed Frame: map + Frame Rate: 30 + Name: root + Tools: + - Class: rviz_default_plugins/Interact + Hide Inactive Objects: true + - Class: rviz_default_plugins/MoveCamera + - Class: rviz_default_plugins/Select + - Class: rviz_default_plugins/FocusCamera + - Class: rviz_default_plugins/Measure + Line color: 128; 128; 0 + - Class: rviz_default_plugins/SetInitialPose + Covariance x: 0.25 + Covariance y: 0.25 + Covariance yaw: 0.06853891909122467 + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /initialpose + - Class: rviz_default_plugins/SetGoal + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + - Class: rviz_default_plugins/PublishPoint + Single click: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /clicked_point + Transformation: + Current: + Class: rviz_default_plugins/TF + Value: true + Views: + Current: + Class: rviz_default_plugins/Orbit + Distance: 23.05002212524414 + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Focal Point: + X: 3.423001289367676 + Y: 2.539494276046753 + Z: 0.7042691707611084 + Focal Shape Fixed Size: false + Focal Shape Size: 0.05000000074505806 + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Pitch: 1.399796485900879 + Target Frame: + Value: Orbit (rviz) + Yaw: 3.1303207874298096 + Saved: ~ +Window Geometry: + Displays: + collapsed: true + Height: 1016 + Hide Left Dock: true + Hide Right Dock: false + QMainWindow State: 000000ff00000000fd00000004000000000000016d0000035efc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073000000003b0000035e000000c700fffffffb0000000a0049006d0061006700650000000298000001010000000000000000fb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c0000026100000001000001750000035efc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b0000035e000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000003ea0000003efc0100000002fb0000000800540069006d00650100000000000003ea0000025300fffffffb0000000800540069006d00650100000000000004500000000000000000000003ea0000035e00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Time: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: false + Width: 1002 + X: 70 + Y: 27 diff --git a/src/Nav/navigation/scripts/record_nav_bag.sh b/src/Nav/navigation/scripts/record_nav_bag.sh new file mode 100755 index 00000000..7caa301d --- /dev/null +++ b/src/Nav/navigation/scripts/record_nav_bag.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Define the bag name with a timestamp so you don't overwrite previous bags +BAG_NAME="autonomy_debug_bag_$(date +%Y_%m_%d_%H_%M_%S)" + +echo "Starting ROS 2 bag recording: $BAG_NAME" +echo "Press Ctrl+C to stop recording." + +# Run the ros2 bag record command +ros2 bag record -o "$BAG_NAME" \ + /odometry/filtered/global \ + /global_costmap/costmap \ + /global_costmap/costmap_updates \ + /traversability_map \ + /plan \ + /unsmoothed_plan \ + /goal_pose \ + /goal \ + /zed/zed_node/left/image_rect_color/compressed \ + /zed/zed_node/left/camera_info \ + /tf \ + /tf_static \ + /marker_detected \ + /intended_search_path + +# Note: /tf and /tf_static were added (see explanation below) \ No newline at end of file diff --git a/src/Nav/navigation/scripts/record_zed_svo.sh b/src/Nav/navigation/scripts/record_zed_svo.sh new file mode 100755 index 00000000..562fa3ef --- /dev/null +++ b/src/Nav/navigation/scripts/record_zed_svo.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Default recording directory (matches your example) +REC_DIR="/usr/local/zed/recordings" + +# Help Menu +if [[ "$1" == "--help" || "$1" == "-h" ]]; then + echo "Usage: $0 " + echo "" + echo "Description:" + echo " Starts a ZED camera SVO recording via ROS2 service call." + echo " Files are saved to: $REC_DIR" + echo "" + echo "Arguments:" + echo " filename The name of the output file (e.g., 'mission_test')." + echo " The script automatically appends '.svo2' if missing." + echo "" + echo "Options:" + echo " -h, --help Show this help message and exit." + exit 0 +fi + +# Check for argument +if [ -z "$1" ]; then + echo "Error: No filename provided." + echo "Try '$0 --help' for usage." + exit 1 +fi + +# Construct filename +FILENAME="$1" +# Append extension if user didn't provide it +if [[ "$FILENAME" != *.svo2 ]]; then + FILENAME="${FILENAME}.svo2" +fi + +# Ensure full path +FULL_PATH="${REC_DIR}/${FILENAME}" + +echo "Starting ZED recording..." +echo "Saving to: $FULL_PATH" + +# Execute Service Call +# compression_mode: 2 (H.265/HEVC - High compression, good quality) +ros2 service call /zed/zed_node/start_svo_rec zed_msgs/srv/StartSvoRec "{ + svo_filename: '$FULL_PATH', + compression_mode: 2, + bitrate: 0, + target_framerate: 0, + input_transcode: false +}" \ No newline at end of file