Use when configuring Navigation 2 (costmaps, behavior trees, controller/planner servers) or MoveIt 2 (SRDF, kinematics, planning pipelines, ros2_control integration) for a mobile or manipulator robot.
Scanned 9/19/2026
Install to Claude Code
npx -y skills add enesbirlik/claude-code-robotics --skill nav2-moveit2-config --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Nav2 Moveit2 Config?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/enesbirlik-nav2-moveit2-config)More formats (shields.io, HTML) on the badges page.
---
name: nav2-moveit2-config
description: Use when configuring Navigation 2 (costmaps, behavior trees, controller/planner servers) or MoveIt 2 (SRDF, kinematics, planning pipelines, ros2_control integration) for a mobile or manipulator robot.
---
# Nav2 / MoveIt 2 Configuration Architecture
Standard, consistency-checked configuration layouts for Navigation 2 and MoveIt 2. Both
frameworks fail in the same way when misconfigured: nodes start cleanly but planning/control
silently produces useless or dangerous output, so the rules below emphasize cross-file
consistency, not just per-file syntax.
## 1. Nav2 package layout
```
my_robot_navigation/
├── config/
│ ├── nav2_params.yaml # every lifecycle-managed node's parameters, one file
│ └── behavior_tree/
│ └── navigate_w_recovery.xml
├── maps/
│ └── my_map.yaml
└── launch/
└── navigation.launch.py # includes nav2_bringup/launch/bringup_launch.py
```
## 2. `nav2_params.yaml` skeleton
```yaml
amcl:
ros__parameters:
use_sim_time: true
alpha1: 0.2
alpha2: 0.2
alpha3: 0.2
alpha4: 0.2
scan_topic: scan
robot_model_type: "nav2_amcl::DifferentialMotionModel"
controller_server:
ros__parameters:
use_sim_time: true
controller_frequency: 20.0
min_x_velocity_threshold: 0.001
failure_tolerance: 0.3
controller_plugins: ["FollowPath"]
FollowPath:
plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"
desired_linear_vel: 0.4
max_angular_accel: 2.0
use_collision_detection: true
planner_server:
ros__parameters:
use_sim_time: true
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner::NavfnPlanner"
tolerance: 0.3
behavior_server:
ros__parameters:
use_sim_time: true
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors::Spin"
backup:
plugin: "nav2_behaviors::BackUp"
wait:
plugin: "nav2_behaviors::Wait"
bt_navigator:
ros__parameters:
use_sim_time: true
default_nav_to_pose_bt_xml: "$(find-pkg-share my_robot_navigation)/config/behavior_tree/navigate_w_recovery.xml"
global_costmap:
global_costmap:
ros__parameters:
use_sim_time: true
global_frame: map
robot_base_frame: base_link
resolution: 0.05
robot_radius: 0.22
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
observation_sources: scan
scan:
topic: /scan
data_type: "LaserScan"
marking: true
clearing: true
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
inflation_radius: 0.55
local_costmap:
local_costmap:
ros__parameters:
use_sim_time: true
global_frame: odom
robot_base_frame: base_link
rolling_window: true
width: 3
height: 3
resolution: 0.05
robot_radius: 0.22
plugins: ["obstacle_layer", "inflation_layer"]
lifecycle_manager:
ros__parameters:
use_sim_time: true
autostart: true
node_names:
- controller_server
- planner_server
- behavior_server
- bt_navigator
- amcl
```
Consistency rules (these break navigation silently, not loudly):
- `robot_radius` must match on `global_costmap` and `local_costmap`, and must be `>=` the
robot's actual footprint radius from the URDF — use a `footprint` polygon instead of
`robot_radius` for a non-circular base, applied identically in both costmaps.
- `inflation_radius` should exceed `robot_radius` by a margin that reflects real localization
and control error, not zero — too small and the planner routes paths that graze obstacles.
- `controller_server`'s `controller_frequency` should not exceed the rate at which
`local_costmap` actually updates (`update_frequency`, default layer-dependent) — a controller
running faster than its costmap updates is reacting to stale data.
- `global_costmap`'s `global_frame` is `map` (requires AMCL/SLAM providing `map -> odom`);
`local_costmap`'s `global_frame` is `odom` (works even without localization, from
odometry alone) — swapping these is a common copy-paste mistake that breaks local avoidance
the instant localization lags.
- `use_sim_time: true` must be set identically across every node when running in Gazebo, and
`false` on real hardware — a mismatch here causes TF extrapolation errors that look like a
transform tree bug but are actually a clock source bug.
- `lifecycle_manager.node_names` must list every lifecycle node actually brought up in the
launch file, in an order where dependencies (e.g. `amcl` before nodes that consume `/map` ->
`/odom`) are activated sensibly — a node left off this list never leaves the `unconfigured`
state and bringup will hang waiting on it if anything depends on it.
## 3. Behavior tree basics
```xml
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
</RateController>
<FollowPath path="{path}" controller_id="FollowPath"/>
</PipelineSequence>
</BehaviorTree>
</root>
```
Rules:
- Reference `planner_id`/`controller_id` strings that exactly match the plugin keys registered
in `nav2_params.yaml` (`GridBased`, `FollowPath` above) — a mismatched ID fails at runtime
with an error that's easy to miss in a long bringup log.
- Wrap replanning nodes in a `RateController` so the planner isn't invoked every tick — an
un-throttled `ComputePathToPose` inside a tight BT loop can peg a CPU core.
- Add `RecoveryNode` wrapping the main navigation subtree with fallback behaviors (`spin`,
`backup`, `wait`) for production robots — a bare `PipelineSequence` with no recovery path
means any transient planner failure aborts the whole navigation goal.
## 4. MoveIt 2 package layout
```
my_robot_moveit_config/
├── config/
│ ├── my_robot.srdf # planning groups, disabled collision pairs, named states
│ ├── kinematics.yaml # IK solver selection + params, per planning group
│ ├── joint_limits.yaml # velocity/acceleration overrides for planning
│ ├── moveit_controllers.yaml # maps planning groups to ros2_control action interfaces
│ └── ompl_planning.yaml # planner IDs + projection evaluators, per group
└── launch/
└── move_group.launch.py
```
## 5. SRDF, kinematics, and controller mapping
```xml
<!-- my_robot.srdf -->
<group name="arm">
<chain base_link="base_link" tip_link="tool0"/>
</group>
<group_state name="home" group="arm">
<joint name="joint_1" value="0"/>
<joint name="joint_2" value="0"/>
</group_state>
<disable_collisions link1="link_1" link2="link_2" reason="Adjacent"/>
```
```yaml
# kinematics.yaml
arm:
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
kinematics_solver_search_resolution: 0.005
kinematics_solver_timeout: 0.05
kinematics_solver_attempts: 3
```
```yaml
# moveit_controllers.yaml
moveit_simple_controller_manager:
controller_names: [arm_controller]
arm_controller:
type: FollowJointTrajectory
action_ns: follow_joint_trajectory
default: true
joints: [joint_1, joint_2, joint_3, joint_4, joint_5, joint_6]
```
Rules:
- Every `<group>` chain's `base_link`/`tip_link` must exist in the URDF exactly as spelled —
MoveIt Setup Assistant generates this correctly from a loaded URDF, so prefer regenerating
over hand-editing when the URDF's kinematic chain changes.
- `<disable_collisions>` pairs come from Setup Assistant's self-collision sampling
(`"Adjacent"`, `"Default"`, `"Never"` reasons) — never hand-add a `disable_collisions` entry
to silence a real self-collision warning; fix the geometry or joint limits instead. A
disabled pair that can actually collide is a real-world collision the planner will never
avoid.
- `kinematics_solver_timeout` and `_attempts` should be tuned so IK failures surface as a
planning failure (safe) rather than the solver returning after an excessively long search
that stalls a real-time-adjacent planning call.
- `moveit_controllers.yaml`'s `joints:` list must match, in the URDF's actual joint order, the
joints in the SRDF `<group>` chain and the `ros2_control` hardware interface — a mismatched
or reordered joint list sends trajectory points to the wrong physical joints.
- Cross-check `joint_limits.yaml` velocity/acceleration overrides against the URDF's `<limit>`
values on each joint (see the `urdf-xacro-builder` skill): MoveIt should never plan a
trajectory that violates the URDF's own hard limits — if `joint_limits.yaml` is looser than
the URDF, the executed trajectory can be rejected or, on some hardware interfaces, faulted at
runtime.
## 6. Planning pipeline (OMPL) and using `MoveGroupInterface`
```yaml
# ompl_planning.yaml
arm:
planner_configs: [RRTConnectkConfigDefault]
projection_evaluator: joints(joint_1,joint_2)
longest_valid_segment_fraction: 0.005
```
```python
from moveit.planning import MoveItPy
moveit = MoveItPy(node_name="moveit_py_client")
arm = moveit.get_planning_component("arm")
arm.set_start_state_to_current_state()
arm.set_goal_state(configuration_name="home")
plan_result = arm.plan()
if plan_result:
moveit.execute(plan_result.trajectory, controllers=[])
```
Rules:
- Set `longest_valid_segment_fraction` small enough that collision checking along a planned
segment can't "tunnel" through a thin obstacle between sampled states — the OMPL default is
often too coarse for a robot operating near cluttered fixtures.
- Always check `plan_result` truthiness (or the equivalent success field in the C++ API) before
calling `execute` — MoveIt returns a failed-but-non-throwing result on an infeasible goal, and
executing an unchecked failed plan is undefined behavior.
- Bring up `move_group` with the same `use_sim_time` convention as the rest of the stack, and
make sure the `ros2_control` controller referenced in `moveit_controllers.yaml` is already
active (via the controller manager) before the first `execute()` call, or the action goal
will be rejected.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!