This document explains the key architectural and design decisions made in this project.
Decision: Split the project into 8 separate ROS 2 packages instead of a monolithic package.
Rationale:
- Separation of concerns — each package has a single responsibility
- Independent testing —
smooth_nav_corecan be tested without ROS 2 - Reusability — core algorithms can be used in non-ROS contexts
- Faster incremental builds — changing a launch file doesn't recompile C++
Decision: smooth_nav_core has NO ROS 2 dependencies. Pure C++17 only.
Rationale:
- Unit tests run with plain GTest, no
rclcppspin required - Algorithms are portable to other frameworks (ROS 1, standalone apps)
- Enforced by CMakeLists.txt: only
find_package(GTest), nofind_package(rclcpp)
Decision: All algorithm families (smoothing, trajectory generation, control) use the Strategy pattern with a Factory for instantiation.
Rationale:
- Adding a new smoother = one
.hpp+ one.cpp+ one factory registration - Runtime algorithm selection via YAML parameter (no recompile)
- Easy A/B comparison between algorithms
Decision: Use a ROS 2 Action (not a simple service) for trajectory tracking.
Rationale:
- Trajectory execution is long-running — Actions support feedback and cancellation
- Progress percentage, cross-track error, and heading error reported as feedback
- Graceful cancellation: sends zero velocity on cancel
Decision: Path smoothing and trajectory generation are exposed as ROS 2 Services.
Rationale:
- These are request/response operations (not continuous)
- Services ensure the caller waits for the result before proceeding
- Matches the pipeline: smooth → generate → execute
Decision: Odometry updates in the tracker node are protected by std::mutex.
Rationale:
- The
/odomsubscriber callback and the control-loop timer run on different executor threads - Mutex ensures consistent pose reads during control computation
Decision: All development and testing happens inside Docker containers.
Rationale:
- Reproducible across any host OS (Windows, macOS, Linux)
- No "works on my machine" issues
- CI uses the same Docker image
- TurtleBot3 + Gazebo dependencies are managed in the Dockerfile
Decision: A dedicated safety_watchdog_node sits between the controller (/cmd_vel_raw) and the motor driver (/cmd_vel), rather than embedding safety logic inside the controller.
Rationale:
- Single Responsibility — the controller focuses on trajectory tracking; the watchdog focuses on hardware protection
- Re-usable — the same watchdog works with any controller (Pure Pursuit, nav2, teleop)
- Fail-safe — if the controller node crashes, the watchdog's timeout triggers an emergency stop
- Transparent — services and action server don't need to know about safety limits
Decision: A Python waypoint_client_node sequences the entire Smooth → Generate → Execute pipeline, rather than requiring the user to call each service manually.
Rationale:
- End-to-end demo —
ros2 launch smooth_nav_bringup demo.launch.pyruns everything hands-free - Retry with exponential backoff — handles service startup delays gracefully
- Abort mechanism — publish to
/abort_missionto stop at any point - Visualization — publishes waypoint and trajectory markers for RViz
Decision: All C++ nodes use add_on_set_parameters_callback for live tuning, with input validation.
Rationale:
- Tune PID gains, look-ahead distance, velocity limits via
rqt_reconfigurewhile the robot is running - No restart = faster iteration in simulation and on real hardware
- Validation callbacks reject invalid values (e.g., negative velocities) before they cause crashes
Decision: The trajectory generator enforces
Rationale:
- Without this, the robot attempts sharp turns at full speed → wheel slip, tracking divergence
max_lateral_accelerationis exposed as a ROS parameter so it can be tuned per environment- This matches how industrial AGVs and autonomous cars handle curvature-limited corridors
Decision:
Rationale:
- At low speed: small
$L_d$ → tight tracking through curves - At high speed: large
$L_d$ → smooth, stable pursuit without oscillation - Classic Pure Pursuit uses fixed
$L_d$ and requires careful per-path tuning; adaptive scales automatically