Differential-drive robot simulation for indoor mapping and navigation, with a C++ LiDAR velocity-safety monitor and a Python waypoint mission/evaluation client.
Simulation only: no hardware validation or safety certification is claimed. Target platform: Ubuntu 24.04, ROS 2 Jazzy, Gazebo Harmonic, RViz2, C++17 and Python 3.
- Custom URDF/Xacro robot with physically derived inertias, two driven wheels and a rear spherical support.
- Four launch modes for simulation, SLAM mapping, AMCL localization and Nav2 navigation.
- One stamped command path through a configurable C++ safety monitor, with manual/autonomy selection, stale-data stops and a latched stop service.
- Sequential waypoint actions with timeout/cancellation handling, live mission status and measured CSV output.
- Deterministic reference-map generation, map saving, MCAP recording, runtime graph checks and automated tests.
This project intentionally stays with 2D perception. The simulated IMU is available for inspection and recording; it is not fused into odometry.
- Headless/full and Gazebo GUI/full acceptance passed on the target machine; each completed all 12 route goals.
- Maximum estimated final errors were 0.1574 m position and 0.1485 rad yaw across those two runs; these estimates are not ground-truth accuracy measurements.
- Moving SLAM, saved-map reload, AMCL/Nav2 activation, safety stops, cancellation and stopped-state recovery checks passed.
- Manual verification covered simulation, mapping, localization and navigation, including Gazebo/RViz interaction and keyboard teleoperation. MCAP recording was separately exercised and archived in the validation evidence.
See validation evidence for the exact environment, measurements, limitations and archived records. Public GitHub Actions source checks and ROS 2 Jazzy build/tests pass on Ubuntu 24.04. Graphical Gazebo/Nav2 acceptance remains a target-machine runtime validation.
flowchart TD
G["Gazebo Harmonic"] --> H["gz_ros2_control"]
H --> D["Drive controller and wheel odometry"]
G --> B["Sensor and clock bridge"]
B --> L["SLAM Toolbox or AMCL"]
D --> L
L --> N["Nav2 planning and control"]
N --> S["C++ safety monitor"]
T["Keyboard teleoperation"] --> S
B --> S
S --> D
M["Python waypoint missions"] --> N
Only one localization backend runs in a launch: SLAM Toolbox owns map -> odom during mapping; AMCL owns it during saved-map localization/navigation. Detailed data flow, TF ownership and design decisions explain the interfaces.
| Path | Responsibility |
|---|---|
src/amr_description/ |
Robot Xacro, inertias and RViz views |
src/amr_simulation/ |
SDF room, sensor bridges and ordered controller startup |
src/amr_control/ |
Drivetrain configuration, C++ safety monitor and core tests |
src/amr_navigation/ |
SLAM/AMCL/Nav2 launch and parameters, reference map, 12-goal route |
src/amr_bringup/ |
Single mode-based entry point with a base-readiness gate |
src/amr_tools/ |
Python action client, metrics, live graph checks and C++ TF authority observer |
scripts/ |
Dependency setup, build, map saving, recording and evaluation |
tests/ |
Source, configuration, Xacro, map and integration-contract tests |
docs/ |
Architecture, evaluation, validation and troubleshooting |
media/ |
Actual Gazebo/RViz screenshots and their capture provenance |
results/ |
Ignored runtime outputs; no fabricated benchmark dataset |
.github/workflows/ |
Source checks and ROS Jazzy build/test workflow |
Install ROS 2 Jazzy desktop on Ubuntu 24.04 using the official binary installation instructions. Use the Jazzy vendor integration for Gazebo Harmonic through ros_gz and gz_ros2_control; no Gazebo Classic installation is needed. This is the documented Jazzy integration.
A working OpenGL/EGL graphics stack is required by GPU LiDAR, including when the Gazebo GUI is disabled. A headless run still renders sensors. No robot hardware, CUDA toolkit, pretrained models or external Gazebo model downloads are required.
The repository root is the colcon workspace root. Build in the checkout where you intend to run it: CMake caches and symlink installs are not portable between paths.
From the repository root:
bash scripts/install_deps.sh
bash scripts/build.sh
source /opt/ros/jazzy/setup.bash
source install/setup.bashinstall_deps.sh checks the platform and declared dependencies. If anything is missing, it prints installation guidance for you to review and run manually. It does not request administrator privileges. Build using the system Python associated with ROS, not a development virtual environment.
In every additional terminal, enter this repository and source:
source /opt/ros/jazzy/setup.bash
source install/setup.bashRun only one AMR launch at a time. Stop the previous launch with Ctrl+C and wait for shutdown before changing modes. Do not separately launch the robot-state publisher, SLAM Toolbox or an upstream Nav2 bringup alongside this entry point.
Gazebo transport defaults to loopback (127.0.0.1) for this single-machine project. Override with gz_ip:=<local-interface-address> if needed. An existing GZ_IP environment variable is respected. For separate gz CLI diagnostics, use export GZ_IP=127.0.0.1 in that terminal as well.
Terminal 1:
ros2 launch amr_bringup amr.launch.py mode:=simulationTerminal 2:
ros2 run amr_tools runtime_check --mode simulation --ros-args -p use_sim_time:=true
bash scripts/teleop.shUse i for forward, , for reverse, j/l for rotation and k to stop. Hold a key for repeated input. The script starts at 0.2 m/s and 0.5 rad/s; the safety node and controller impose additional limits. A released key stops generating fresh commands, so the deadman timeout brakes the robot. teleop_twist_keyboard repeat behavior is deliberately left disabled.
To disable both graphical windows:
ros2 launch amr_bringup amr.launch.py mode:=simulation headless:=true rviz:=falseTo inspect only the URDF, with the simulation stopped:
ros2 launch amr_description display.launch.pyStop simulation mode first, then launch mapping:
ros2 launch amr_bringup amr.launch.py mode:=mappingIn a second sourced terminal, run bash scripts/teleop.sh. Drive slowly around the room, observe all walls and both sides of obstacles, then revisit the start area for a loop closure. RViz uses map as its fixed frame in this mode. A brief missing-map status before the first scans are processed is expected.
Stop the robot with k, leave mapping running, and save from another terminal:
bash scripts/save_map.sh results/maps/indoor_slamThe script creates indoor_slam.yaml and indoor_slam.pgm and refuses to overwrite an existing map. This occupancy-map pair is suitable for Nav2/AMCL. It is not a serialized SLAM pose graph for resuming graph optimization.
Stop mapping before starting localization:
ros2 launch amr_bringup amr.launch.py mode:=localization \
map:="$(realpath results/maps/indoor_slam.yaml)"AMCL's configured initial estimate is (0, 0, 0) in map, matching the default spawn and the included reference map. For a map with a different coordinate origin, use RViz 2D Pose Estimate to set the correct position and heading before moving. Laser returns should align with mapped obstacles.
ros2 run amr_tools runtime_check --mode localization --ros-args -p use_sim_time:=trueThe default map, src/amr_navigation/maps/reference.yaml, is generated deterministically from the SDF room geometry. It is explicitly synthetic reference geometry, not a measured SLAM result. It makes an initial saved-map navigation run possible before mapping. Regenerate/check it with:
python3 scripts/generate_reference_map.py --checkStop the previous mode, then launch the default reference-map navigation scenario:
ros2 launch amr_bringup amr.launch.py mode:=navigationFor your own map, add map:="$(realpath results/maps/indoor_slam.yaml)" to that command.
ros2 run amr_tools runtime_check --mode navigation --ros-args -p use_sim_time:=trueWhen checks pass and the LiDAR aligns with the map, select Nav2 Goal in RViz and drag a goal pose onto clear mapped floor. The global path appears in orange. Nav2 plans around mapped obstacles and uses live LiDAR in both costmaps. The local controller checks collisions; the custom safety filter provides a final conservative velocity gate.
Use one goal producer at a time: RViz or the mission runner. Do not send RViz goals while a mission is active; Nav2 may preempt the existing goal.
With navigation running:
ros2 run amr_tools mission_runner \
--mission "$(ros2 pkg prefix --share amr_navigation)/missions/indoor_route.yaml" \
--output results/demo/missions.csv \
--ros-args -p use_sim_time:=trueThe included route contains 12 goals. --limit 1 runs a single goal for a first check; --repeats 1 is the default. Output files must be new: existing CSVs are never silently overwritten. The default stops on failure. --continue-on-failure continues after a confirmed terminal failure, while an unconfirmed cancellation always prevents further dispatch.
ros2 topic echo /mission/status
ros2 topic echo /diagnosticsCtrl+C requests cancellation and waits for a terminal action result. If termination cannot be confirmed, the runner attempts to latch the safety stop and exits unsuccessfully. Inspect the action/launch logs before releasing that stop.
/cmd_vel_teleop and /cmd_vel_nav carry geometry_msgs/msg/TwistStamped. The safety node selects one source and exclusively publishes /diff_drive_controller/cmd_vel, the Jazzy controller's stamped input. Mapping/simulation/localization select manual input; navigation selects Nav2.
The monitor transforms scan points into base_footprint, validates full scan coverage and ranges, computes front/left/rear/right distances, and limits speed using a circular footprint plus a stopping-distance allowance. Invalid/stale scans, stale commands, a paused/reset clock, or the emergency-stop latch cause zero commands. There is also an independent 0.5-second controller command timeout.
# Latch the simulated stop.
ros2 service call /safety_monitor/emergency_stop std_srvs/srv/SetBool '{data: true}'
# After inspecting the cause, clear the latch; fresh valid input is still required.
ros2 service call /safety_monitor/emergency_stop std_srvs/srv/SetBool '{data: false}'Cancel any running navigation action before switching to manual control:
ros2 service call /safety_monitor/set_autonomy std_srvs/srv/SetBool '{data: false}'
bash scripts/teleop.shUse the same service with data: true to select Nav2 again. A mode change clears the stored command. Parameters are loaded from amr_control/config/safety.yaml at startup. See safety assumptions. This is a simulation software safeguard, not a certified physical emergency stop.
In a sourced terminal with the simulation running:
bash scripts/record_bag.sh results/bags/demo_01Ctrl+C finalizes the MCAP bag. It includes clock, TF, sensors, odometry, command streams, mission/action status and diagnostics. Use ros2 bag info results/bags/demo_01 to inspect it. For playback analysis, stop the live simulation, publish /clock only from the bag player and do not run a second command source.
ROS-independent checks, in a separate development terminal:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
bash scripts/check.shROS build and package tests, in a fresh terminal using the system Python:
bash scripts/build.sh
source /opt/ros/jazzy/setup.bash
source install/setup.bash
colcon test --event-handlers console_direct+
colcon test-result --verboseWith all AMR launches stopped, run the integrated Ubuntu acceptance suite:
python3 scripts/acceptance.py --fullIt starts/stops its own simulation processes and checks two clean startups, commanded motion against wheel odometry and Gazebo model pose, deadman and latched stops, moving SLAM, map saving and reload, reference-map localization, Nav2 activation, planning and navigation motion, and confirmed cancellation followed by healthy servers and a stopped robot. --full runs all 12 goals; omit it for a one-goal smoke test. Add --gui to also open Gazebo. RViz is checked separately. A diagnostic subset such as --stage mapping or --stage navigation --full is explicitly recorded as a partial run.
Analyze a mission's actual results:
python3 scripts/analyze_results.py results/demo/missions.csv --output results/demo/analysisThe CSV records action outcome, wall/simulation time, estimated final position/yaw error, wheel-odometry path length, recovery count and minimum observed obstacle distance. Missing measurements remain blank. Evaluation definitions and the repeatable 12-mission protocol explain limitations and success criteria.
Actual captures from the target machine on 2026-09-09:
The SLAM capture shows a partial map from a short maneuver. The navigation capture uses the explicitly synthetic reference map. Capture provenance and optional demo sequence distinguish these workflows.
| Symptom | First check |
|---|---|
Duplicate SLAM nodes or conflicting map -> odom |
Stop earlier launches; use a single amr.launch.py mode |
| RViz fixed-frame error | Simulation uses odom; mapping/localization/navigation use map after the backend starts |
| Robot model or LiDAR missing | Use included RViz config; verify /robot_description durability, /scan best-effort QoS and TF |
| Controller manager timeout | Inspect the first Gazebo/plugin error; verify dependencies and that simulation is running |
| Robot refuses to move | Inspect /diagnostics, selected safety mode, stamped command timestamps and stop latch |
| Nav2 inactive | Run runtime_check --mode navigation; inspect the first lifecycle configure/activate error |
| Headless LiDAR absent | Check OpenGL/EGL support; try --gui acceptance or a regular GUI launch |
The troubleshooting guide includes focused commands and fixes for stale overlays, simulation time, rendering and map alignment.
- Simulation validation covers the included static room and route. It does not establish hardware performance or general navigation reliability across environments.
- Single robot, flat floor, known static reference world and a planar laser model. No multi-robot namespacing or hardware driver is provided.
- Wheel odometry can drift or slip. The IMU is recorded but not fused; estimated goal error is not ground-truth error.
- The circular safety envelope is conservative and can prevent escape from an obstacle already inside the stop zone. It cannot detect cliffs, overhead obstacles outside the scan plane, or a sensor falsely reporting only no returns.
- Watchdogs use both ROS timestamps and wall time. Very low simulation performance may trigger a stop; this should be investigated before changing timeouts.
- Ordered startup and a fixed world support repeatable workflows; physics, sensor noise and navigation are not bitwise deterministic.
- CI specifies build/source checks; graphical and navigation acceptance remains an Ubuntu runtime gate.
After measured validation, sensible extensions are odometry/IMU fusion with explicitly transferred TF ownership, additional worlds, and dynamic-obstacle experiments. 3D perception and docking are intentionally outside this release.
The source is licensed under Apache-2.0. See contributing and the completion changes.


