ROS 2 Navigation with the Clearpath Husky

ROS 2 has been growing significantly in the robotics community, with its newer features, better security, and improved performance. However, the transition from ROS 1 to ROS 2 is not straightforward. Many packages are still not migrated to ROS 2, and the existing packages are not fully adapted to ROS 2. Particularly, those dedicated to the Clearpath Husky, have not completed the migration to ROS 2. Although there are existing GitHub repositories for Clearpath Robots, specifically the Husky, they are not fully adapted to ROS 2, with some even falling into disrepair.

For instance, in the Husky repository, a clear message states that “For ROS 2 Humble, this repository is no longer used. Please visit clearpath_common.” However, the clearpath_common repository is still a work in progress, lacking comprehensive documentation. As a result, the practical solution involves utilizing the ROS 1 packages for the Husky and establishing a bridge to ROS 2.

This tutorial aims to guide you through the process of setting up the ROS 1 navigation stack on the Clearpath Husky and seamlessly connecting it to ROS 2. It assumes a foundational understanding of both ROS 1 and ROS 2.

ROS 1 - ROS 2 Bridge

To configure the Clearpath Husky hardware, we will be using the husky_robot repository. This repository contains the ROS 1 packages for the Husky, including the navigation stack. To connect the ROS 1 packages to ROS 2, we will be using the ros1_bridge package. Detailed instruction on how to setup this is provided in this tutorial on the Robotics Knowledgebase. Once the bridge is established, we can proceed to configure the Husky using the following steps.

# Install Husky Packages
apt-get update && apt install ros-noetic-husky* -y
mkdir -p /home/ros1_ws/src && cd /home/ros1_ws/src
git clone --single-branch --branch noetic-devel https://github.com/husky/husky_robot.git
apt-get update && cd /home/ros1_ws && source /opt/ros/noetic/setup.bash && rosdep install --from-paths src --ignore-src -r -y --rosdistro=noetic
apt update && apt install ros-noetic-roslint ros-noetic-diagnostics -y
cd /home/ros1_ws && source /opt/ros/noetic/setup.bash && catkin_make

NOTE: ros1_ws is the name of the ROS 1 workspace. You can name it anything you want.

We will be using the ROS 2 Nav2 stack for navigation. To install the Nav2 stack, follow the instructions here.

nav2_bringup package provides a launch file to launch the navigation stack for the turtlebot as mentioned in the tutorial. In the Nav2 tutorial, they use amcl for localization, and have a collision checker to check for collisions. However, in this tutorial, we assume that the robot is localized using any ROS package (e.g., robot_localization), and the odometry of the robot is provided in the topic /odom. We also assume that the robot is not moving in a dynamic environment, so we will not be using the collision checker or generating any local costmap, and we will be using the static map provided in the topic /map.

Moreover, to explore further options other than the default DWB Controller, we will be using the Regulated Pure Pursuit Controller, and for the path planning we will be using the SMAC Planner instead of the default NavfnPlanner. The parameters for these plugins are provided in the Nav2 documentation.

We will be creating a new package husky_nav2_bringup, similar to the nav2_bringup package, to launch the navigation stack for the Husky. The package should have the following structure:

husky_nav2_bringup
├── params
│   └── nav2_params.yaml
├── launch
│   └── nav_bringup_launch.py
├── package.xml
└── CMakelists.txt

The nav2_params.yaml file should contain the following parameters:

bt_navigator:
  ros__parameters:
    # use_sim_time: false
    global_frame: map
    robot_base_frame: base_link
    odom_topic: /odom
    bt_loop_duration: 10
    default_server_timeout: 20
    # 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are use defaults:
    # nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml
    # nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
    # They can be set here or via a RewrittenYaml remap from a parent launch file to Nav2.
    # default_nav_to_pose_bt_xml: ""
    plugin_lib_names:
      - nav2_compute_path_to_pose_action_bt_node
      - nav2_compute_path_through_poses_action_bt_node
      - nav2_smooth_path_action_bt_node
      - nav2_follow_path_action_bt_node
      - nav2_spin_action_bt_node
      - nav2_wait_action_bt_node
      - nav2_assisted_teleop_action_bt_node
      - nav2_back_up_action_bt_node
      - nav2_drive_on_heading_bt_node
      - nav2_clear_costmap_service_bt_node
      - nav2_is_stuck_condition_bt_node
      - nav2_goal_reached_condition_bt_node
      - nav2_goal_updated_condition_bt_node
      - nav2_globally_updated_goal_condition_bt_node
      - nav2_is_path_valid_condition_bt_node
      - nav2_initial_pose_received_condition_bt_node
      - nav2_reinitialize_global_localization_service_bt_node
      - nav2_rate_controller_bt_node
      - nav2_distance_controller_bt_node
      - nav2_speed_controller_bt_node
      - nav2_truncate_path_action_bt_node
      - nav2_truncate_path_local_action_bt_node
      - nav2_goal_updater_node_bt_node
      - nav2_recovery_node_bt_node
      - nav2_pipeline_sequence_bt_node
      - nav2_round_robin_node_bt_node
      - nav2_transform_available_condition_bt_node
      - nav2_time_expired_condition_bt_node
      - nav2_path_expiring_timer_condition
      - nav2_distance_traveled_condition_bt_node
      - nav2_single_trigger_bt_node
      - nav2_goal_updated_controller_bt_node
      - nav2_is_battery_low_condition_bt_node
      - nav2_navigate_through_poses_action_bt_node
      - nav2_navigate_to_pose_action_bt_node
      - nav2_remove_passed_goals_action_bt_node
      - nav2_planner_selector_bt_node
      - nav2_controller_selector_bt_node
      - nav2_goal_checker_selector_bt_node
      - nav2_controller_cancel_bt_node
      - nav2_path_longer_on_approach_bt_node
      - nav2_wait_cancel_bt_node
      - nav2_spin_cancel_bt_node
      - nav2_back_up_cancel_bt_node
      - nav2_assisted_teleop_cancel_bt_node
      - nav2_drive_on_heading_cancel_bt_node
      - nav2_is_battery_charging_condition_bt_node

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    controller_plugins: ["FollowPath"]
    progress_checker_plugin: "progress_checker"
    goal_checker_plugins: ["general_goal_checker", "precise_goal_checker"]
    min_x_velocity_threshold: 0.001
    min_y_velocity_threshold: 0.5
    min_theta_velocity_threshold: 0.001
    odom_topic: "odom"

    progress_checker:
      plugin: "nav2_controller::SimpleProgressChecker"
      required_movement_radius: 0.1
      movement_time_allowance: 10.0
    general_goal_checker:
      plugin: "nav2_controller::SimpleGoalChecker"
      xy_goal_tolerance: 0.15 # 15 cm
      yaw_goal_tolerance: 0.25 # 0.25 rad
      stateful: False
    precise_goal_checker:
      plugin: "nav2_controller::SimpleGoalChecker"
      xy_goal_tolerance: 0.05 # 10 cm
      yaw_goal_tolerance: 0.1 # 0.1 rad
      stateful: False
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"
      desired_linear_vel: 0.16
      lookahead_dist: 0.4
      use_velocity_scaled_lookahead_dist: false
      # min_lookahead_dist: 0.3                             # reqd only if use_velocity_scaled_lookahead_dist is true
      # max_lookahead_dist: 0.9                             # reqd only if use_velocity_scaled_lookahead_dist is true
      # lookahead_time: 1.5                                 # reqd only if use_velocity_scaled_lookahead_dist is true
      transform_tolerance: 0.1
      min_approach_linear_velocity: 0.05                    # The minimum velocity (m/s) threshold to apply when approaching the goal to ensure progress. Must be > 0.01.
      approach_velocity_scaling_dist: 0.4                   # The distance (m) left on the path at which to start slowing down. Should be less than the half the costmap width.                  
      use_collision_detection: false
      use_regulated_linear_velocity_scaling: true
      regulated_linear_scaling_min_radius: 1.5              # reqd only if use_regulated_linear_velocity_scaling is true
      regulated_linear_scaling_min_speed: 0.15              # The minimum speed (m/s) for which any of the regulated heuristics can send, to ensure process is still achievable even in high cost spaces with high curvature. Must be > 0.1.
      use_cost_regulated_linear_velocity_scaling: false
      use_fixed_curvature_lookahead: false
      # curvature_lookahead_dist: 0.25                      # reqd only if use_fixed_curvature_lookahead is true
      allow_reversing: true
      use_rotate_to_heading: false                          # either allow_reversing or use_rotate_to_heading must be true
      # rotate_to_heading_min_angle: 0.785                  # reqd only if use_rotate_to_heading is true
      # rotate_to_heading_angular_vel: 1.8                  # reqd only if use_rotate_to_heading is true
      # max_angular_accel: 3.2                              # reqd only if use_rotate_to_heading is true
      max_robot_pose_search_dist: 10.0

global_costmap:
  global_costmap:
    ros__parameters:
      # use_sim_time: false
      footprint_padding: 0.01 # default
      footprint: "[[0.5, 0.35], [0.5, -0.35], [-0.5, -0.35], [-0.5, 0.35]]"     # footprint of Husky
      global_frame: map
      lethal_cost_threshold: 100 # default
      robot_base_frame: base_link
      publish_frequency: 1.0
      update_frequency: 1.0
      # robot_radius: 0.22
      resolution: 0.05
      track_unknown_space: true
      plugins: ["static_layer", "inflation_layer"]
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_subscribe_transient_local: True
        enabled: true
        subscribe_to_updates: true
        transform_tolerance: 0.1
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        enabled: true
        inflation_radius: 0.55
        cost_scaling_factor: 5.0
        inflate_unknown: false
        inflate_around_unknown: true
      always_send_full_costmap: True

planner_server:
  ros__parameters:
    planner_plugins: ["GridBased"]

    GridBased:
      plugin: "nav2_smac_planner/SmacPlannerHybrid"
      downsample_costmap: false               # whether or not to downsample the map
      downsampling_factor: 1                  # multiplier for the resolution of the costmap layer (e.g. 2 on a 5cm costmap would be 10cm)
      tolerance: 0.25                         # dist-to-goal heuristic cost (distance) for valid tolerance endpoints if exact goal cannot be found.
      allow_unknown: true                     # allow traveling in unknown space
      max_iterations: 1000000                 # maximum total iterations to search for before failing (in case unreachable), set to -1 to disable
      max_on_approach_iterations: 1000        # Maximum number of iterations after within tolerances to continue to try to find exact solution
      max_planning_time: 1.0                  # max time in s for planner to plan, smooth
      motion_model_for_search: "REEDS_SHEPP"  # Hybrid-A* Dubin, Redds-Shepp
      angle_quantization_bins: 72             # Number of angle bins for search
      analytic_expansion_ratio: 3.5           # The ratio to attempt analytic expansions during search for final approach.
      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
      minimum_turning_radius: 2.5             # minimum turning radius in m of path / vehicle
      reverse_penalty: 1.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.
      retrospective_penalty: 0.015
      lookup_table_size: 20.0                 # Size of the dubin/reeds-sheep distance window to cache, in meters.
      cache_obstacle_heuristic: false         # Cache the obstacle map dynamic programming distance expansion heuristic between subsiquent replannings of the same goal location. Dramatically speeds up replanning performance (40x) if costmap is largely static.
      debug_visualizations: false             # For Hybrid nodes: Whether to publish expansions on the /expansions topic as an array of poses (the orientation has no meaning) and the path's footprints on the /planned_footprints topic. WARNING: heavy to compute and to display, for debug only as it degrades the performance.
      use_quadratic_cost_penalty: False
      downsample_obstacle_heuristic: True
      allow_primitive_interpolation: False
      smooth_path: True                       # If true, does a simple and quick smoothing post-processing to the path

      smoother:
        max_iterations: 1000
        w_smooth: 0.3
        w_data: 0.2
        tolerance: 1.0e-10
        do_refinement: true
        refinement_num: 2

behavior_server:
  ros__parameters:
    global_costmap_topic: global_costmap/costmap_raw
    global_footprint_topic: global_costmap/published_footprint
    cycle_frequency: 10.0
    behavior_plugins: ["spin", "backup", "drive_on_heading", "wait", "assisted_teleop"]
    spin:
      plugin: "nav2_behaviors/Spin"
    backup:
      plugin: "nav2_behaviors/BackUp"
    drive_on_heading:
      plugin: "nav2_behaviors/DriveOnHeading"
    wait:
      plugin: "nav2_behaviors/Wait"
    assisted_teleop:
      plugin: "nav2_behaviors/AssistedTeleop"
    global_frame: map
    robot_base_frame: base_link
    transform_timeout: 0.1
    simulate_ahead_time: 2.0
    max_rotational_vel: 1.0
    min_rotational_vel: 0.4
    rotational_acc_lim: 3.2

The nav_bringup_launch.py file should contain the following:

import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    ld = LaunchDescription()

    # Nav2 Planner Server and Controller Server
    configured_params = os.path.join(get_package_share_directory('lx_nav2'), 'config', 'params.yaml')
    tf_remappings = [('/tf', 'tf'),
                  ('/tf_static', 'tf_static')]
    tf_and_cmdvel_remappings = [('/tf', 'tf'),
                    ('/tf_static', 'tf_static'),
                     ('cmd_vel', 'cmd_vel_nav')]

    lifecycle_nodes = [
                       'controller_server',
                       'planner_server',
                       'behavior_server',
                       'bt_navigator'
                       ]

    nav2_controller_server = Node(
        package='nav2_controller',
        executable='controller_server',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[configured_params],
        remappings=tf_and_cmdvel_remappings
    )

    nav2_smoother = Node(
        package='nav2_smoother',
        executable='smoother_server',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[configured_params],
        remappings=tf_remappings
    )
    
    nav2_planner_server = Node(
        package='nav2_planner',
        executable='planner_server',
        name='planner_server',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[configured_params],
        remappings=tf_remappings
    )

    nav2_behaviors = Node(
        package='nav2_behaviors',
        executable='behavior_server',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[configured_params],
        remappings=tf_remappings
    )

    nav2_bt_navigator_node = Node(
        package='nav2_bt_navigator',
        executable='bt_navigator',
        name='bt_navigator',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[configured_params],
        remappings=tf_remappings
    )

    nav2_lifecycle_manager = Node(
        package='nav2_lifecycle_manager',
        executable='lifecycle_manager',
        name='lifecycle_manager_navigation',
        output='screen',
        respawn=True,
        respawn_delay=2.0,
        parameters=[{'use_sim_time': False}, {'autostart': True}, {'node_names': lifecycle_nodes}],
        remappings=tf_remappings
    )

    ld.add_action(nav2_controller_server)
    ld.add_action(nav2_smoother)
    ld.add_action(nav2_planner_server)
    ld.add_action(nav2_behaviors)
    ld.add_action(nav2_bt_navigator_node)
    ld.add_action(nav2_lifecycle_manager)

    return ld

Once the package is built, we can launch the husky_nav2_bringup package using the following command:

ros2 launch husky_nav2_bringup nav_bringup_launch.py

To navigate the Husky to the desired pose within the map, we will be usingt the NavigateToPose action server provided by Nav2. The action definition is as follows:

#goal definition
geometry_msgs/PoseStamped pose
string behavior_tree
---
#result definition
std_msgs/Empty result
---
#feedback definition
geometry_msgs/PoseStamped current_pose
builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining

The behavior_tree parameter is used to specify the behavior tree to be used for navigation. The default behavior tree is navigate_to_pose_w_replanning_and_recovery.xml. The behavior tree can be changed by modifying the bt_navigator parameter in the nav2_params.yaml file. For example, to use a behavior tree where the path is replanned every 5 seconds, make a file inside the params folder named navigate_to_pose_w_replanning_and_recovery_5s.xml with the following content:

<root main_tree_to_execute="MainTree">
  <BehaviorTree ID="MainTree">
    <PipelineSequence name="NavigateWithReplanning">
      <RateController hz="0.2">
        <ReactiveSequence>
          <RemovePassedGoals input_goals="{goals}" output_goals="{goals}" radius="0.1"/>
          <ComputePathToPose goals="{goals}" path="{path}" planner_id="GridBased"/>
        </ReactiveSequence>
      </RateController>
      <FollowPath path="{path}" controller_id="FollowPath" goal_checker_id="precise_goal_checker"/>
    </PipelineSequence>
  </BehaviorTree>
</root>

To implement this behavior tree, you can either add the complete path of the XML file to the default_nav_to_pose_bt_xml parameter in bt_navigator section of the nav2_params.yaml file, or you can use the path in the behavior_tree string while calling the NavigateToPose action server.

Summary

This tutorial provides a step-by-step guide to configure the Clearpath Husky for navigation using the ROS 1 packages and seamlessly connecting it to ROS 2. The tutorial also provides a brief overview of the Nav2 stack and how to use it for navigation.

It is recommended to read the Nav2 documentation to understand the Nav2 stack in detail. The Nav2 tutorials are also a good place to start.

See Also:

Further Readings: