Custom Dynamic Window Approach Implementation for Nav
Introduction to DWA Planner
Overview
This project implements a custom Dynamic Window Approach (DWA) local planner for ROS 2 Humble and demonstrates it on TurtleBot3 in Gazebo. The implementation is compact and pragmatic: it samples feasible (linear, angular) velocity pairs inside a dynamic window, simulates short-rollout trajectories, scores each rollout with a small cost function, and executes the velocity pair with the lowest cost. The code and launch instructions are in the repository linked above.
*Figure 1 — Sampled rollouts (grey) and the selected trajectory (bright). Frame from demo video (approx. 00:00–00:15).*
The image above shows sampled rollouts and the chosen trajectory over the local occupancy field. Below I walk through the practical algorithm, the decisions made while implementing it, and the trade-offs I used when tuning parameters for TurtleBot3.
*Figure 1 — Sampled rollouts (grey) and the selected trajectory (bright). Extracted frame: frame_002.jpeg.*
Motivation
The goal was to build a readable, tuneable DWA planner that integrates with standard ROS 2 stacks while keeping the rollout and cost logic explicit for easy experimentation. This is useful for coursework and for quickly testing changes to sampling, cost terms, or obstacle handling without diving into larger navigation stacks.
Key ideas and workflow
- Service-driven goal interface: a simple ROS 2 service (
togoal) accepts a 2D goal (x, y) and drives the robot toward it. The service node (nav_goal_dwa) repeatedly queries the DWA planner for the best command, publishes visualization markers for rollouts, and sendscmd_veluntil the robot is close enough to the goal. - Planner core:
DWAPlannerCustomperforms the main work. It:- computes the dynamic window from current velocity and acceleration limits,
- samples linear velocity
vand angular velocitywusing configured resolutions (v_res_,w_res_), - forward-simulates each (v, w) pair over a short horizon (
del_T, stepdt_) to produce a trajectory, - evaluates trajectories using a weighted cost: heading (distance to goal), obstacle proximity, and a velocity term that prefers higher forward speed,
- returns the best
(v, w)and the set of sampled trajectories for visualization.
Algorithmic details
- Dynamic window: the planner constrains
vandwusing current velocitiescurr_vx_,curr_w_and acceleration boundsacc_v,acc_w, clipped to configuredv_min_..v_max_andw_min_..w_max_. - Sampling: linear velocity increments are
v_res_(e.g. 0.08 m/s) and angular increments arew_res_(e.g. 0.2 rad/s). These control exploration density vs compute cost. - Rollouts: each (v, w) pair is simulated for
del_T(default 2 s) with integration stepdt_(default 0.05 s). The planner records the sequence of (x, y) points for each rollout. - Cost function: the implementation uses three terms with tunable weights (ALPHA, BETA, GAMMA):
- Heading cost: Euclidean distance from rollout endpoint to goal (encourages progress toward target).
- Obstacle cost: aggregated penalty where trajectory points fall within an
inflation_radof known obstacles (derived fromLaserScantransformed into global obstacle points). - Velocity cost: a small cost that biases selection toward higher forward velocities (favors faster trajectories when safe).
The planner selects the rollout with minimum total cost and publishes the corresponding command.
Pseudocode (concise)
loop:
read odom, scan
compute dynamic_window from curr_v, curr_w, acc limits
for v in linspace(min_v, max_v, v_res):
for w in linspace(min_w, max_w, w_res):
traj = simulate(v, w, dt, del_T)
if any point of traj too close to obstacle: obstacle_cost += penalty
heading_cost = distance(traj.end, goal)
velocity_cost = v_max - v
total = ALPHA*heading_cost + BETA*obstacle_cost + GAMMA*velocity_cost
remember traj and total
choose traj with min total
publish cmd_vel for its (v, w)
publish markers for visualization
stop when distance(goal, last_point) < threshold
Why these choices
- Sampling resolution (
v_res_,w_res_) trades CPU for pathway quality. For TurtleBot3 I used relatively coarse angular steps (0.2 rad/s) and tighter linear steps (0.08 m/s) because small forward-speed changes matter more for steady progress in narrow corridors. - Rollout horizon (
del_T) of 2 s withdt=0.05 s gives enough lookahead to avoid near obstacles while keeping per-cycle simulation count manageable. - The obstacle penalty multiplies how close a trajectory point is to an obstacle by an inflation constant — this is a simple, robust local safety cue that behaved well with noisy LiDAR in simulation.
Visualization and validation
I publish every sampled rollout as a visualization_msgs::msg::MarkerArray and draw the best trajectory thicker and brighter. This visual feedback is invaluable for tuning: you can change a weight and immediately see how the planner’s preferences shift.
*Figure 2 — Candidate rollouts demonstrating obstacle avoidance and trajectory pruning. Frame from demo video (approx. 00:20–00:30).*
*Figure 2 — Candidate rollouts demonstrating obstacle avoidance and trajectory pruning. Extracted frame: frame_006.jpeg.*
If you want higher quality stills from the video for the post, extract frames locally with youtube-dl / yt-dlp and ffmpeg:
1
2
yt-dlp -f bestvideo https://youtu.be/y8iu5mr0jKo -o demo.mp4
ffmpeg -ss 00:00:10 -i demo.mp4 -frames:v 1 frame_10.jpg
Below is an additional frame taken from the storyboard extraction that highlights a close-obstacle scenario used to tune the inflation_rad parameter:
*Figure 3 — Close-obstacle rollback case used during parameter tuning. Extracted frame: frame_010.jpeg.`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
## ROS 2 integration and differences from common navigation workflows
- The project deliberately avoids a full `nav2` integration and implements a focused local planner instead. This keeps the control loop transparent for tuning and learning.
- The goal interface is a short-lived service that spawns a `DWAPlannerCustom` instance and runs an iterative control loop until the goal is reached. The repository notes that converting the service to an action server would be a robust next step (actions provide feedback and preemption semantics better suited for long-running moves).
- The planner uses `rclcpp::spin_some(dwa)` inside the service loop to let subscriptions (odom, scan) update the planner state. This is a simple approach for the assignment, but in production an action-based loop or keeping the planner node alive and communicating with it via topics/services is preferable.
## Implementation notes and practical tips
- Obstacle mapping: `scanCallback` converts `LaserScan` ranges into global (x, y) points using the current pose and yaw. The planner stores obstacles as a vector of coordinate pairs used to compute `obstacle_cost` for each point on a rollout.
- Parameter tuning: weights (ALPHA, BETA, GAMMA), `inflation_rad`, sampling resolutions, and `del_T`/`dt_` are exposed as constants in the planner header. Moving these parameters to a `params.yaml` (as suggested in the README) makes tuning cleaner and avoids recompiles.
- Visualization: `nav_goal_dwa` publishes a `visualization_msgs::msg::MarkerArray` for all sampled rollouts and highlights the chosen trajectory. This is helpful when tuning because you can see the candidate motions in `rviz2` while changing parameters.
- Safety: the planner stops by publishing zero velocities once the target is reached or on exit. The README suggests adding IMU-based flip detection and handling the goal-inside-obstacle case more gracefully.
## How to run
Follow the repository instructions (condensed):
1. Create a workspace and clone the repo into `src`.
2. Install dependencies and `colcon build` the workspace.
3. Launch the TurtleBot3 Gazebo world and run the `nav_goal_dwa` node:
```bash
source ~/10x_av_ws/install/setup.bash
ros2 run dwa_custom_planner nav_goal_dwa
- Call the
togoalservice with goal coordinates (example):
1
ros2 service call /togoal dwa_custom_planner/srv/ToGoal "{goal_x: 1.2, goal_y: 2.0}"
- Open
rviz2with the included config to observe rollouts and the selected trajectory.
If you’d like, I can:
- add a
params.yamland replace the hard-coded constants with ROS 2 parameters (recommended), - convert the
togoalservice into an action server so you get feedback and preemption, - extract a small set of annotated frames from the demo video and add them into
assets/imagesso the post contains local images instead of external links.
Tell me which next step you want and I’ll implement it.
Lessons and next steps
- Move parameters to a YAML file and load them via ROS 2 parameter APIs for live tuning.
- Convert the service into an action server to support preemption and richer feedback.
- Improve obstacle handling: skip goals inside obstacles, incorporate dynamic obstacle prediction, and add a fail-safe when no safe rollout exists.
- Consider reducing compute by using an early-exit heuristic for rollouts that quickly violate safety (e.g., stop rollout simulation once a point falls inside inflation radius).
References
- Github
- Example video demo linked in the repository README.
If you want, I can convert the hard-coded parameters into a params.yaml and wire them into DWAPlannerCustom for runtime tuning. I can also change the togoal service into an action server in a follow-up commit.

