Full Nav2 navigation stack + Simple Commander warehouse application for an RB1 mobile robot. The robot builds a map with Cartographer SLAM, localizes itself with AMCL, autonomously navigates to goals through a DWB local planner, a NavfnPlanner global planner and a custom behavior tree with replanning/recovery, and finally runs a complete pick-attach-ship-return warehouse workflow through the Nav2 Simple Commander API. Supports both simulation and real robot environments via a single parameterized launch command.
Part of the ROS & ROS 2 Developer Master Class certification (Phase 2).
cartographer_nodesubscribes to/scanand/odom, builds a 2D occupancy gridoccupancy_grid_nodepublishes the evolving map on/mapat 1 Hz- User teleops the robot around the warehouse to generate the full map
- Map saved as
warehouse_map_sim.yaml/.pgm(andwarehouse_map_real.*for the real robot) map_serverpackage reloads the saved map for downstream nodes
map_serverserves the saved warehouse mapamclinitializes a particle cloud (2000-80000 particles) and estimates robot pose on the map- Laser scans align with the map as the robot moves — particles converge to the true pose
lifecycle_manager_localizationsequencesmap_server→amclactivation- The
/attach_service_serveris also launched here (reused from Checkpoint 9/10 for shelf attachment)
planner_server(NavfnPlanner / Dijkstra) computes global paths on the global costmapcontroller_server(DWB local planner) tracks the path while avoiding obstacles on the local costmapbt_navigatororchestrates navigation with a custom behavior tree (navigate_w_replanning_and_recovery.xml):- Replans global path at 5 Hz
- On failure: clears costmaps → spins → waits → retries (up to 6 retries)
behavior_serverexecutes recovery primitives:spin,backup,waitcostmap_filter_info_server+filter_mask_serverapply keepout zones to exclude forbidden areas from planninglifecycle_manager_pathplanneractivates all Nav2 nodes in the correct order
Full end-to-end warehouse pipeline in a single Python script (nav2_apps/scripts/move_shelf_to_ship.py) driven by the BasicNavigator class of the Nav2 Simple Commander API. One script handles initial pose, goal dispatch, feedback, task-result handling and recovery across four named waypoints.
- Initialization —
BasicNavigatorboots,setInitialPose()publishes the knowninitpose,waitUntilNav2Active()blocks until the full Nav2 stack is lifecycle-active. - Navigate to
loading_position—goToPose()+ feedback loop pollinggetFeedback().estimated_time_remaining. Retries up to 5 attempts onTaskResult.FAILED. - Attach shelf —
ClientAsynccalls/attach_service(from Checkpoint 9/10). On success:ElevatorPublisherpublishes on/elevator_upto lift the shelfPolygonPublisherswaps the robot footprint from 0.25 m square → 0.45 m cart footprint on both/global_costmap/footprintand/local_costmap/footprintRobotMoverdrives 10 s backwards at -0.2 m/s to clear the loading bay
- Navigate to
shipping_position— samegoToPose+ feedback pattern with the larger cart footprint active, so the planner re-inflates around the robot+cart. - Drop shelf — at shipping,
publish_polygon('robot')restores the small footprint,ElevatorPublisher.drop()publishes/elevator_down,RobotMoverreverses away from the cart. - Return to
init— finalgoToPose()back to the staging point to complete the loop.
| Pose | x | y | qz | qw | Role |
|---|---|---|---|---|---|
init |
0.00 | 0.00 | 0.00 | 1.00 | Staging point (sim) |
loading_position |
5.70 | 0.00 | -0.70 | 0.72 | Under the shelf (sim) |
shipping_position |
2.45 | 1.42 | 0.72 | 0.72 | Drop-off bay (sim) |
loading_position (real) |
4.30 | 0.00 | -0.70 | 0.70 | Real lab |
shipping_position (real) |
1.62 | 1.30 | 0.70 | 0.70 | Real lab |
ClientAsync— service client to/attach_service(sim) //approach_service(real). Async call, spin-once untilfuture.done().ElevatorPublisher—std_msgs/Stringpublisher to/elevator_up//elevator_downwith the real-robot variant publishing 5x for reliability.PolygonPublisher— republishesgeometry_msgs/Polygonto/global_costmap/footprintand/local_costmap/footprint; toggles between robot (0.25 m) and cart (0.45 m) polygons at runtime.RobotMover— 10 s open-loop back-up on/diffbot_base_controller/cmd_vel_unstamped(sim) //cmd_vel(real), stops with a zero-Twist.
- Created
cartographer_slampackage withcartographer.launch.pylaunchingcartographer_node+occupancy_grid_node+ RViz cartographer_sim.lua/cartographer_real.luaconfiguration files (tracking_frame, published_frame, odom_frame, 2D trajectory builder, scan matching)mapping.rvizwith Map, TF, LaserScan displaysuse_sim_timelaunch argument switches config between sim and real- Saved maps:
warehouse_map_sim.yaml/.pgmandwarehouse_map_real.yaml/.pgm - Created
map_serverpackage withmap_server.launch.pylaunchingnav2_map_server::map_server+lifecycle_manager+ RViz map_filelaunch argument selects which map YAML to loadmap_display.rvizfor standalone map visualization
- Created
localization_serverpackage withlocalization.launch.pylaunchingmap_server+amcl+lifecycle_manager_localization+ RViz amcl_config_sim.yaml/amcl_config_real.yamlparameters (likelihood_field laser model, differential motion model, 60 max beams, 2000-80000 particles)- Auto-selects sim/real AMCL config based on
map_filesuffix - TF frames:
map→odom→robot_base_footprint localization_config.rvizwith ParticleCloud displayattach_service_servernode also launched (from CP9/10 shelf attachment)
- Created
path_planner_serverpackage withpathplanner.launch.pylaunching:controller_server(DWB local planner)planner_server(NavfnPlanner)behavior_server(spin, backup, wait recoveries)bt_navigator(behavior tree executor)filter_mask_server+costmap_filter_info_server(keepout zones)lifecycle_manager_pathplannerrviz2
controller_sim.yaml/controller_real.yaml- DWB params + local costmap (voxel + inflation + keepout layers, 3x3 m rolling window, 0.05 m resolution, 0.25 m footprint)planner_sim.yaml/planner_real.yaml- NavfnPlanner + global costmap (static + obstacle + inflation + keepout layers, 0.02 m resolution)recoveries_sim.yaml/recoveries_real.yaml- recovery behavior paramsbt_navigator_sim.yaml/bt_navigator_real.yaml- BT plugin library listfilters_sim.yaml/filters_real.yaml- keepout mask topic and YAML pathnavigate_w_replanning_and_recovery.xml- custom behavior tree with replanning pipeline and recovery sequencepathplanning.rviz- full Nav2 visualization (robot model, TF, laser, odom, particles, global/local costmaps, global/local paths, global/local footprints)use_sim_timelaunch argument switches all configs and remaps/cmd_velbetween sim (/diffbot_base_controller/cmd_vel_unstamped) and real (/cmd_vel)
- Created
nav2_appsPython package with two scripts:move_shelf_to_ship.py- simulation targets,/diffbot_base_controller/cmd_vel_unstamped,/attach_servicemove_shelf_to_ship_real.py- real-lab targets,/cmd_vel,/approach_service, 5x-publish on elevator for reliability
BasicNavigatorwrapper for full lifecycle:setInitialPose,waitUntilNav2Active,goToPose,getFeedback,getResult,TaskResultenum- 5-attempt retry policy around each
goToPosecall withTaskResult.FAILEDhandling - Runtime footprint swap via
Polygonrepublish (robot ↔ cart) keeps global+local costmaps consistent while carrying the shelf - Decomposed into single-responsibility
rclpy.Nodehelpers (ClientAsync,ElevatorPublisher,PolygonPublisher,RobotMover)
Every package supports both environments through a single launch argument. The launch files use PythonExpression substitutions to dynamically select the correct config YAML and topic remapping at launch time.
| Environment | Map file | cmd_vel topic |
Attach service | Controller freq |
|---|---|---|---|---|
| Simulation | warehouse_map_sim.yaml |
/diffbot_base_controller/cmd_vel_unstamped |
/attach_service |
5.0 Hz |
| Real Robot | warehouse_map_real.yaml |
/cmd_vel |
/approach_service |
5.0 Hz |
| Name | Type | Description |
|---|---|---|
/scan |
sensor_msgs/LaserScan (sub) |
Laser scanner data |
/odom |
nav_msgs/Odometry (sub) |
Robot odometry |
/cmd_vel |
geometry_msgs/Twist (pub) |
Velocity commands (remapped per environment) |
/map |
nav_msgs/OccupancyGrid (pub) |
Map served by map_server |
/amcl_pose |
geometry_msgs/PoseWithCovarianceStamped (pub) |
Localized pose estimate |
/particle_cloud |
nav2_msgs/ParticleCloud (pub) |
AMCL particle distribution |
/global_costmap/costmap |
nav_msgs/OccupancyGrid (pub) |
Global costmap (map + inflation + keepout) |
/local_costmap/costmap |
nav_msgs/OccupancyGrid (pub) |
Local costmap (voxel + inflation + keepout) |
/global_costmap/footprint |
geometry_msgs/Polygon (pub) |
Global footprint — swapped at runtime (robot ↔ cart) |
/local_costmap/footprint |
geometry_msgs/Polygon (pub) |
Local footprint — swapped at runtime (robot ↔ cart) |
/plan |
nav_msgs/Path (pub) |
Global path from planner_server |
/local_plan |
nav_msgs/Path (pub) |
Local path from controller_server |
/navigate_to_pose |
nav2_msgs/NavigateToPose (action) |
Send a nav goal to the BT navigator |
/attach_service |
GoToLoading (service) |
Triggers shelf detection and final approach |
/elevator_up |
std_msgs/String (pub) |
Elevator lift command |
/elevator_down |
std_msgs/String (pub) |
Elevator drop command |
map → odom → robot_base_footprint |
TF tree | Full navigation TF chain |
warehouse_project/
├── cartographer_slam/ # CP11 Task 1a: SLAM mapping
│ ├── launch/cartographer.launch.py
│ ├── config/
│ │ ├── cartographer_sim.lua
│ │ ├── cartographer_real.lua
│ │ ├── warehouse_map_sim.{yaml,pgm}
│ │ └── warehouse_map_real.{yaml,pgm}
│ └── rviz/mapping.rviz
├── map_server/ # CP11 Task 1b: Map serving
│ ├── launch/map_server.launch.py
│ ├── config/
│ │ ├── warehouse_map_{sim,real}.{yaml,pgm}
│ │ └── warehouse_map_keepout_{sim,real}.{yaml,pgm}
│ └── rviz/map_display.rviz
├── localization_server/ # CP11 Task 2: AMCL localization
│ ├── launch/localization.launch.py
│ ├── config/
│ │ ├── amcl_config_sim.yaml
│ │ └── amcl_config_real.yaml
│ └── rviz/localization_config.rviz
├── path_planner_server/ # CP11 Task 3: Nav2 navigation
│ ├── launch/pathplanner.launch.py
│ ├── config/
│ │ ├── controller_{sim,real}.yaml # DWB local planner
│ │ ├── planner_{sim,real}.yaml # NavfnPlanner global planner
│ │ ├── bt_navigator_{sim,real}.yaml # Behavior tree navigator
│ │ ├── recoveries_{sim,real}.yaml # spin/backup/wait behaviors
│ │ ├── filters_{sim,real}.yaml # Keepout zone filters
│ │ └── navigate_w_replanning_and_recovery.xml
│ └── rviz/pathplanning.rviz
├── attach_service/ # Shelf attach service (reused from CP9/10)
│ ├── src/attach_service_server.cpp
│ └── srv/GoToLoading.srv
├── nav2_apps/ # CP12: Simple Commander warehouse app
│ └── scripts/
│ ├── move_shelf_to_ship.py # Simulation target
│ └── move_shelf_to_ship_real.py # Real-robot target
└── media/
- ROS 2 Humble
- Gazebo Classic 11
cartographer_ros,nav2_*,nav2_simple_commanderpackages- RB1 robot simulation (
warehouse_rb1.launch.xmlfromthe_construct_office_gazebo)
cd ~/ros2_ws
colcon build
source install/setup.bash# Launch simulation
ros2 launch the_construct_office_gazebo warehouse_rb1.launch.xml
# Simulation
ros2 launch cartographer_slam cartographer.launch.py use_sim_time:=True
# Real robot
ros2 launch cartographer_slam cartographer.launch.py use_sim_time:=False
# Teleop to build the map
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/diffbot_base_controller/cmd_vel_unstamped
# Save the map
ros2 run nav2_map_server map_saver_cli -f ~/warehouse_map_sim
# Load the saved map
ros2 launch map_server map_server.launch.py map_file:=warehouse_map_sim.yaml# Simulation
ros2 launch localization_server localization.launch.py map_file:=warehouse_map_sim.yaml
# Real robot
ros2 launch localization_server localization.launch.py map_file:=warehouse_map_real.yaml# Terminal 1 - Localization
ros2 launch localization_server localization.launch.py map_file:=warehouse_map_sim.yaml
# Terminal 2 - Navigation stack
ros2 launch path_planner_server pathplanner.launch.py use_sim_time:=True
# In RViz, click "2D Nav Goal" and pick a point on the map.
# The robot plans + navigates autonomously with recovery on failure.# Terminal 1 - Localization (also launches /attach_service)
ros2 launch localization_server localization.launch.py map_file:=warehouse_map_sim.yaml
# Terminal 2 - Navigation stack
ros2 launch path_planner_server pathplanner.launch.py use_sim_time:=True
# Terminal 3 - Warehouse application
# Simulation
python3 ~/ros2_ws/src/warehouse_project/nav2_apps/scripts/move_shelf_to_ship.py
# Real robot
python3 ~/ros2_ws/src/warehouse_project/nav2_apps/scripts/move_shelf_to_ship_real.py| Branch | Checkpoint | Description |
|---|---|---|
main |
All | Latest working code (default) |
task_1 |
CP11 Task 1 | Mapping with Cartographer + map_server |
task_2 |
CP11 Task 2 | AMCL localization |
- Cartographer SLAM: 2D trajectory builder, online scan matching, pose graph optimization
- AMCL localization: likelihood-field laser model, differential motion model, particle filter convergence
- Nav2 stack: planner server, controller server, behavior server, bt_navigator, lifecycle manager
- Global planner: NavfnPlanner (Dijkstra) on global costmap with keepout zones
- Local planner: DWB (Dynamic Window Approach) with trajectory critics (PathAlign, GoalAlign, ObstacleFootprint, RotateToGoal)
- Costmaps: static layer, obstacle layer, voxel layer, inflation layer, keepout filter
- Behavior trees: custom XML with PipelineSequence, RecoveryNode, RateController, ClearEntireCostmap
- Recovery behaviors: spin, backup, wait primitives triggered on planning/following failure
- Lifecycle management:
lifecycle_managersequencing Nav2 node activation - Keepout zones: costmap filter with mask topic excluding forbidden areas from planning
- Sim/real parity: parameterized launch files with
PythonExpressionconfig selection andcmd_velremapping
- Nav2 Simple Commander API:
BasicNavigator,setInitialPose,waitUntilNav2Active,goToPose,getFeedback,getResult,TaskResult - Asynchronous service calls:
call_async+spin_oncepolling withfuture.done()gating - Runtime footprint swap: republishing
/global_costmap/footprintand/local_costmap/footprintto inflate costmaps around the carried shelf - Application-level recovery: 5-attempt retry pattern around each
goToPose,TaskResult.CANCELED→ return to staging - Multi-node Python app: single process, multiple
rclpy.Nodeinstances per responsibility (client, publisher, mover) - Real vs sim differences in one repo: separate script variants for lab topics/services/poses without touching the Nav2 stack
- ROS 2 Humble
- Nav2 (planner_server, controller_server, bt_navigator, behavior_server, lifecycle_manager)
- Nav2 Simple Commander (
nav2_simple_commander.robot_navigator.BasicNavigator) - Cartographer ROS
- AMCL (
nav2_amcl) - Gazebo Classic 11
- BehaviorTree.CPP (XML trees)
- C++ 17 / Python 3


