ROS 2 Engineering Skills
A progressive-disclosure skill for ROS 2 development — from first workspace to
production fleet deployment. Each section below gives you the essential decision
framework; detailed patterns, code templates, and anti-patterns live in the
references/ directory. Read the relevant reference file before writing code.
How to use this skill
- Identify what the user is building (see Decision Router below).
- Read the matching
references/*.md file for detailed guidance.
- Apply the Core Engineering Principles in every piece of code you generate.
- When multiple domains intersect (e.g. Nav2 + ros2_control), read both files.
Decision router
| User is doing... |
Read |
| Creating a workspace, package, or build config |
references/workspace-build.md |
| Writing nodes, executors, callback groups |
references/nodes-executors.md |
| Topics, services, actions, custom interfaces, QoS |
references/communication.md |
| Lifecycle nodes, component loading, composition |
references/lifecycle-components.md |
| Launch files, conditional logic, event handlers |
references/launch-system.md |
| tf2, URDF, xacro, robot_state_publisher |
references/tf2-urdf.md |
| ros2_control, hardware interfaces, controllers |
references/hardware-interface.md |
| Real-time constraints, PREEMPT_RT, memory, jitter |
references/realtime.md |
| Nav2, SLAM, costmaps, behavior trees |
references/navigation.md |
| MoveIt 2, planning scene, grasp pipelines |
references/manipulation.md |
| Camera, LiDAR, PCL, cv_bridge, depth processing |
references/perception.md |
| Unit tests, integration tests, launch_testing, CI |
references/testing.md |
| ros2 doctor, tracing, profiling, rosbag2 |
references/debugging.md |
| Docker, cross-compile, fleet deployment, OTA |
references/deployment.md |
| Gazebo, Isaac Sim, sim-to-real, use_sim_time |
references/simulation.md |
| SROS2, DDS security, certificates, supply chain |
references/security.md |
| micro-ROS, MCU/RTOS, XRCE-DDS, rclc |
references/micro-ros.md |
| Multi-robot fleet, Open-RMF, DDS discovery scale |
references/multi-robot.md |
| Message types, units, covariance, frame conventions |
references/message-types.md |
| ROS 1 migration, ros1_bridge, hybrid operation |
references/migration-ros1.md |
When a task spans multiple domains, read all relevant files and reconcile
conflicting recommendations by favoring safety, then determinism, then simplicity.
Cross-cutting concern — Security: Security is not isolated to references/security.md.
Every domain should consider its security implications: hardware interfaces need safe
shutdown on auth failure, DDS topics may need encryption, deployment images need supply
chain verification, and fleet communication must use TLS. When reviewing code in any
domain, check whether the data path crosses a trust boundary.
Core engineering principles
These apply to every ROS 2 artifact you produce, regardless of domain.
1. Distro awareness
Always ask which ROS 2 distribution the user targets. Key differences:
| Feature |
Foxy (EOL) |
Humble (LTS) |
Jazzy (LTS) |
Kilted (non-LTS) |
Rolling |
| EOL |
Jun 2023 (ended) |
May 2027 |
May 2029 |
Nov 2025 |
Rolling |
| Ubuntu |
20.04 |
22.04 |
24.04 |
24.04 |
Latest |
| Default DDS |
Fast DDS |
Fast DDS |
Fast DDS |
Fast DDS |
Fast DDS |
| Zenoh support |
— |
— |
— |
Tier 1 |
Tier 1 |
| Type description support |
No |
No |
Yes |
Yes |
Yes |
| Service introspection |
No |
No |
Yes |
Yes |
Yes |
| EventsExecutor |
No |
No |
Experimental |
Stable (+ rclpy) |
Stable (+ rclpy) |
| Default bag format |
sqlite3 |
sqlite3 |
MCAP |
MCAP |
MCAP |
| ros2_control interface |
N/A (separate) |
2.x |
4.x |
4.x |
Latest |
| CMake recommendation |
ament_target_deps |
ament_target_deps |
either |
target_link_libs |
target_link_libs |
When the user does not specify, default to the latest LTS (Jazzy).
Pin the exact distro in Dockerfile, CI, and documentation so builds are reproducible.
2. C++ vs Python decision
Choose the language based on the node's role, not personal preference.
Use rclcpp (C++) when:
- The node sits in a control loop running ≥100 Hz
- Deterministic memory allocation matters (real-time path)
- The node is a hardware driver or controller plugin
- Intra-process zero-copy communication is required
Use rclpy (Python) when:
- The node is orchestration, monitoring, or parameter management
- Rapid prototyping with frequent iteration
- Heavy use of ML frameworks (PyTorch, TensorFlow) that are Python-native
- The node does not sit in a latency-critical path
Mixed stacks are normal. A typical robot has C++ drivers/controllers and Python
orchestration/monitoring. Note: component_container (composition) only loads
C++ components via pluginlib. Python nodes run as separate processes, but can
share a launch file and communicate via zero-overhead intra-host DDS.
Intra-process communication works for any nodes sharing a process — not only
composable components. Any nodes instantiated in the same process with
use_intra_process_comms(true) can use zero-copy transfer.
3. Package structure conventions
Every package should follow this layout. Consistency across a workspace reduces
onboarding time and makes CI scripts portable.
my_package/
├── CMakeLists.txt # or setup.py for pure Python
├── package.xml # format 3, with <depend> tags
├── config/
│ └── params.yaml # default parameters
├── launch/
│ └── bringup.launch.py # Python launch file
├── include/my_package/ # C++ public headers (if library)
├── src/ # C++ source files
├── my_package/ # Python modules (if ament_python or mixed)
├── test/ # gtest, pytest, launch_testing
├── urdf/ # URDF/xacro (if applicable)
├── msg/ srv/ action/ # custom interfaces (dedicated _interfaces package preferred)
└── README.md
Separate interface definitions into a *_interfaces package so downstream
packages can depend on interfaces without pulling in implementation.
4. Parameter discipline
- Declare every parameter with a type, description, range, and default
in the node constructor — never use undeclared parameters.
- Use
ParameterDescriptor with FloatingPointRange or IntegerRange
for numeric bounds. The parameter server rejects out-of-range values at set time.
- Group related parameters under a namespace prefix:
controller.kp, controller.ki, controller.kd.
- Load defaults from a
config/params.yaml; allow launch-time overrides.
- For dynamic reconfiguration, register a
set_parameters_callback and
validate new values atomically before accepting.
5. Error handling philosophy
- Nodes must not silently swallow errors. Log at the appropriate severity,
then take a safe action (stop motion, request help, transition to error state).
- Prefer lifecycle node error transitions over ad-hoc boolean flags.
- When calling a service, always handle the "service not available" and
"future timed out" cases explicitly.
- For hardware drivers, distinguish transient errors (retry with backoff)
from fatal errors (transition to
FINALIZED and alert the operator).
6. Quality of Service defaults
Start from these profiles and adjust per use case:
| Use case |
Reliability |
Durability |
History |
Depth |
Deadline |
Lifespan |
| Sensor stream |
BEST_EFFORT |
VOLATILE |
KEEP_LAST |
5 |
— |
— |
| Command velocity |
RELIABLE |
VOLATILE |
KEEP_LAST |
1 |
100 ms |
200 ms |
| Map (latched) |
RELIABLE |
TRANSIENT_LOCAL |
KEEP_LAST |
1 |
— |
— |
| Diagnostics |
RELIABLE |
VOLATILE |
KEEP_LAST |
10 |
— |
— |
| Parameter events |
RELIABLE |
VOLATILE |
KEEP_LAST |
1000 |
— |
— |
| Action feedback |
RELIABLE |
VOLATILE |
KEEP_LAST |
1 |
— |
— |
| Safety heartbeat |
RELIABLE |
VOLATILE |
KEEP_LAST |
1 |
500 ms |
1 s |
QoS mismatches are the #1 cause of "I published but nobody receives."
Always check compatibility with ros2 topic info -v when debugging.
DEADLINE and LIFESPAN are critical for safety-critical systems. DEADLINE fires an
event when no message arrives within the specified period (detect stale data). LIFESPAN
discards messages older than the specified duration before delivery (prevent acting on
stale data). See references/communication.md section 9 for full API and examples.
7. Naming conventions
| Entity |
Convention |
Example |
| Package |
snake_case |
arm_controller |
| Node |
snake_case |
joint_state_broadcaster |
| Topic |
/snake_case with ns |
/arm/joint_states |
| Service |
/snake_case |
/arm/set_mode |
| Action |
/snake_case |
/arm/follow_joint_trajectory |
| Parameter |
snake_case with dot ns |
controller.publish_rate |
| Frame |
snake_case |
base_link, camera_optical |
| Interface |
PascalCase.msg/srv/action |
JointState.msg |
8. Thread safety and callbacks
- A
MutuallyExclusiveCallbackGroup serializes its callbacks — safe for
shared state without locks, but limits throughput.
- A
ReentrantCallbackGroup allows parallel execution — you must protect
shared state with std::mutex (C++) or threading.Lock (Python).
- Calling a service from a callback: The service client must be in a
separate
MutuallyExclusiveCallbackGroup from the calling callback. Otherwise
the executor deadlocks — the callback waits for the response while the executor
cannot deliver it. Always use async_send_request with a response callback;
never use spin_until_future_complete inside an executor callback.
- Never do blocking work (file I/O, long computation,
sleep) inside a
timer or subscription callback on the default executor. Offload to a
dedicated thread or use a MultiThreadedExecutor with a reentrant group.
- In rclcpp, prefer
std::shared_ptr<const MessageT> in subscription
callbacks to avoid unnecessary copies and enable zero-copy intra-process.
9. Lifecycle-first design
Default to lifecycle (managed) nodes for anything that owns resources:
hardware drivers, sensor pipelines, planners, controllers.
┌──────────────┐
create() ──► │ Unconfigured │
└──────┬───────┘
on_configure │
┌──────▼───────┐
│ Inactive │
└──────┬───────┘
on_activate │
┌──────▼───────┐
│ Active │
└──────┬───────┘
on_deactivate │
┌──────▼───────┐
│ Inactive │
└──────┬───────┘
on_cleanup │
┌──────▼───────┐
│ Unconfigured │
└──────┬───────┘
on_shutdown │
┌──────▼───────┐
│ Finalized │
└───────────────┘
This gives the system manager (launch file, orchestrator, or operator) explicit
control over when resources are allocated, when the node starts processing,
and how it shuts down. It also makes error recovery predictable.
10. Build and CI hygiene
- Use
colcon build --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo for
development; Release for deployment.
- Enable
-Wall -Wextra -Wpedantic and treat warnings as errors in CI.
- Run
colcon test with --event-handlers console_cohesion+ so test
output groups by package.
- Pin rosdep keys in
rosdep.yaml for reproducible dependency resolution.
- Cache
/opt/ros/, .ccache/, and build//install/ in CI to cut build
times by 60–80%.
Common anti-patterns
| Anti-pattern |
Why it hurts |
Fix |
| Global variables for node state |
Breaks composition, untestable |
Store state as class members |
spin() in main() for multi-node processes |
Starves other nodes |
Use MultiThreadedExecutor or component composition |
| Hardcoded topic names |
Breaks reuse across robots |
Use relative names + namespace remapping |
KEEP_ALL history with no bound |
Memory grows unbounded on slow subscribers |
Use KEEP_LAST with explicit depth |
Using time.sleep() / std::this_thread::sleep_for |
Blocks the executor thread |
Use create_wall_timer or a dedicated thread |
| Monolithic launch file for everything |
Unmanageable past 10 nodes |
Compose launch files with IncludeLaunchDescription |
Skipping package.xml dependencies |
Builds locally, breaks CI and Docker |
Declare every dependency explicitly |
| Publishing in constructor |
Subscribers may not be ready, messages lost |
Publish in on_activate or after a short timer |
| Ignoring QoS compatibility |
Silent communication failure |
Match publisher/subscriber QoS or check with ros2 topic info -v |
| Creating timers/subs in callbacks |
Resource leak, unpredictable behavior |
Create all entities in constructor or on_configure |
| Synchronous service call in callback |
Deadlocks the executor thread |
Use async_send_request with a callback or dedicated thread |
| Service client in same callback group as caller |
Deadlocks even with async in MultiThreadedExecutor |
Put service client in a separate MutuallyExclusiveCallbackGroup |
| No safe command on shutdown |
Motors hold last velocity after node exits |
Send zero-velocity in on_deactivate AND destructor (see references/hardware-interface.md) |
Dynamic subscriptions with StaticSingleThreadedExecutor |
New subs are never picked up after spin() |
Use SingleThreadedExecutor or MultiThreadedExecutor for dynamic entities |
CPU frequency governor left on powersave/ondemand |
10-100 ms latency spikes in RT path |
Set performance governor, disable turbo boost (see references/realtime.md) |
Distro-specific migration notes
When upgrading between distributions, check these breaking changes first:
Foxy → Humble:
- Complete API overhaul. Foxy packages require significant rework.
ros2_control was not bundled in Foxy — must be built separately.
- Lifecycle node API stabilized in Humble.
- Action server/client API changed significantly.
Humble → Jazzy:
ros2_control API changed from 2.x to 4.x — export_state_interfaces() and
export_command_interfaces() are now auto-generated by the framework. Manual
overrides use on_export_state_interfaces(). See references/hardware-interface.md.
- Handle
get_value() deprecated → use get_optional<T>() on LoanedStateInterface /
LoanedCommandInterface (controller side). Hardware interfaces use set_state() /
get_state() / set_command() / get_command() helpers with fully qualified names.
- All joints in
<ros2_control> tag must exist in the URDF.
- Controller parameter loading changed — use
--param-file with spawner.
- Default bag format changed from sqlite3 to MCAP. Use
storage_id='mcap'.
- Default middleware changed internal config paths. Regenerate DDS profiles.
nav2_params.yaml schema changes — recoveries_server renamed to behavior_server.
ROS_AUTOMATIC_DISCOVERY_RANGE replaces ROS_LOCALHOST_ONLY (values: LOCALHOST,
SUBNET, OFF, SYSTEM_DEFAULT).
launch_ros actions have new parameter handling — test launch files explicitly.
Jazzy → Kilted (non-LTS):
- Zenoh promoted to Tier 1 middleware —
rmw_zenoh is production-ready.
Install: sudo apt install ros-kilted-rmw-zenoh-cpp, set
RMW_IMPLEMENTATION=rmw_zenoh_cpp. Supports router/peer/client modes.
- EventsExecutor graduated from experimental — available in
rclcpp::executors
(no experimental namespace). Also ported to rclpy.
ament_target_dependencies() deprecated — use target_link_libraries() with
modern CMake targets (e.g. rclcpp::rclcpp, std_msgs::std_msgs__rosidl_typesupport_cpp).
- Multi-bag replay support in
ros2 bag play.
- Gazebo Ionic is the paired simulator (Harmonic was Jazzy; Ionic is the Kilted pairing).
ROS 1 → ROS 2:
- See
references/migration-ros1.md for a step-by-step strategy.
Quick reference — ros2 CLI
# Workspace
colcon build --symlink-install --packages-select my_pkg
colcon test --packages-select my_pkg
colcon graph --dot # dependency graph (DOT format)
source install/setup.bash
# Introspection
ros2 node list
ros2 topic list -t
ros2 topic info /topic_name -v # shows QoS details
ros2 topic hz /topic_name
ros2 topic bw /topic_name
ros2 service list -t
ros2 action list -t
ros2 param list /node_name
ros2 param describe /node_name param
ros2 interface show std_msgs/msg/String
# ros2_control
ros2 control list_controllers
ros2 control list_hardware_interfaces
ros2 control list_hardware_components
# Debugging
ros2 doctor --report # alias: ros2 wtf
ros2 run tf2_tools view_frames
ros2 bag record -a -o my_bag
ros2 bag info my_bag
ros2 bag play my_bag --clock
# Lifecycle
ros2 lifecycle list /node_name
ros2 lifecycle set /node_name configure
ros2 lifecycle set /node_name activate
1---2name: ros2-engineering-skills3description: Comprehensive ROS 2 engineering guide covering workspace setup, node architecture, communication patterns (topics/services/actions with QoS), lifecycle and component nodes, launch composition, tf2/URDF, ros2_control hardware interfaces, real-time constraints, Nav2, MoveIt 2, perception pipelines, simulation (Gazebo/Isaac Sim), security (SROS2/DDS), micro-ROS (MCU/RTOS), multi-robot systems (fleet management/Open-RMF), testing, debugging, deployment, and ROS 1 migration. Trigger whenever the user works on ROS 2 code, packages, launch files, URDF/xacro, DDS configuration, ros2_control, Nav2, MoveIt 2, or any robotics middleware task involving rclcpp, rclpy, colcon, ament, rosbag2, ros2 CLI tools, Gazebo/Isaac Sim, micro-ROS, SROS2, or multi-robot coordination. Also trigger for ROS 1 to ROS 2 migration, cross-compilation, Docker-based ROS 2 workflows, and CI/CD for robotics.4---56# ROS 2 Engineering Skills78A progressive-disclosure skill for ROS 2 development — from first workspace to9production fleet deployment. Each section below gives you the essential decision10framework; detailed patterns, code templates, and anti-patterns live in the11`references/` directory. Read the relevant reference file before writing code.1213## How to use this skill14151. Identify what the user is building (see Decision Router below).162. Read the matching `references/*.md` file for detailed guidance.173. Apply the Core Engineering Principles in every piece of code you generate.184. When multiple domains intersect (e.g. Nav2 + ros2_control), read both files.1920## Decision router2122| User is doing... | Read |23|---------------------------------------------------|-----------------------------------|24| Creating a workspace, package, or build config | `references/workspace-build.md` |25| Writing nodes, executors, callback groups | `references/nodes-executors.md` |26| Topics, services, actions, custom interfaces, QoS | `references/communication.md` |27| Lifecycle nodes, component loading, composition | `references/lifecycle-components.md` |28| Launch files, conditional logic, event handlers | `references/launch-system.md` |29| tf2, URDF, xacro, robot_state_publisher | `references/tf2-urdf.md` |30| ros2_control, hardware interfaces, controllers | `references/hardware-interface.md` |31| Real-time constraints, PREEMPT_RT, memory, jitter | `references/realtime.md` |32| Nav2, SLAM, costmaps, behavior trees | `references/navigation.md` |33| MoveIt 2, planning scene, grasp pipelines | `references/manipulation.md` |34| Camera, LiDAR, PCL, cv_bridge, depth processing | `references/perception.md` |35| Unit tests, integration tests, launch_testing, CI | `references/testing.md` |36| ros2 doctor, tracing, profiling, rosbag2 | `references/debugging.md` |37| Docker, cross-compile, fleet deployment, OTA | `references/deployment.md` |38| Gazebo, Isaac Sim, sim-to-real, use_sim_time | `references/simulation.md` |39| SROS2, DDS security, certificates, supply chain | `references/security.md` |40| micro-ROS, MCU/RTOS, XRCE-DDS, rclc | `references/micro-ros.md` |41| Multi-robot fleet, Open-RMF, DDS discovery scale | `references/multi-robot.md` |42| Message types, units, covariance, frame conventions | `references/message-types.md` |43| ROS 1 migration, ros1_bridge, hybrid operation | `references/migration-ros1.md` |4445When a task spans multiple domains, read all relevant files and reconcile46conflicting recommendations by favoring safety, then determinism, then simplicity.4748**Cross-cutting concern — Security:** Security is not isolated to `references/security.md`.49Every domain should consider its security implications: hardware interfaces need safe50shutdown on auth failure, DDS topics may need encryption, deployment images need supply51chain verification, and fleet communication must use TLS. When reviewing code in any52domain, check whether the data path crosses a trust boundary.5354## Core engineering principles5556These apply to every ROS 2 artifact you produce, regardless of domain.5758### 1. Distro awareness5960Always ask which ROS 2 distribution the user targets. Key differences:6162| Feature | Foxy (**EOL**) | Humble (LTS) | Jazzy (LTS) | Kilted (non-LTS) | Rolling |63|---------------------------|----------------------|--------------------|--------------------|--------------------|--------------------|64| EOL | Jun 2023 (**ended**) | May 2027 | May 2029 | Nov 2025 | Rolling |65| Ubuntu | 20.04 | 22.04 | 24.04 | 24.04 | Latest |66| Default DDS | Fast DDS | Fast DDS | Fast DDS | Fast DDS | Fast DDS |67| Zenoh support | — | — | — | Tier 1 | Tier 1 |68| Type description support | No | No | Yes | Yes | Yes |69| Service introspection | No | No | Yes | Yes | Yes |70| EventsExecutor | No | No | Experimental | Stable (+ rclpy) | Stable (+ rclpy) |71| Default bag format | sqlite3 | sqlite3 | MCAP | MCAP | MCAP |72| ros2_control interface | N/A (separate) | 2.x | 4.x | 4.x | Latest |73| CMake recommendation | ament_target_deps | ament_target_deps | either | target_link_libs | target_link_libs |7475When the user does not specify, default to the latest LTS (Jazzy).76Pin the exact distro in Dockerfile, CI, and documentation so builds are reproducible.7778### 2. C++ vs Python decision7980Choose the language based on the node's role, not personal preference.8182**Use rclcpp (C++) when:**83- The node sits in a control loop running ≥100 Hz84- Deterministic memory allocation matters (real-time path)85- The node is a hardware driver or controller plugin86- Intra-process zero-copy communication is required8788**Use rclpy (Python) when:**89- The node is orchestration, monitoring, or parameter management90- Rapid prototyping with frequent iteration91- Heavy use of ML frameworks (PyTorch, TensorFlow) that are Python-native92- The node does not sit in a latency-critical path9394**Mixed stacks are normal.** A typical robot has C++ drivers/controllers and Python95orchestration/monitoring. Note: `component_container` (composition) only loads96C++ components via pluginlib. Python nodes run as separate processes, but can97share a launch file and communicate via zero-overhead intra-host DDS.9899**Intra-process communication** works for any nodes sharing a process — not only100composable components. Any nodes instantiated in the same process with101`use_intra_process_comms(true)` can use zero-copy transfer.102103### 3. Package structure conventions104105Every package should follow this layout. Consistency across a workspace reduces106onboarding time and makes CI scripts portable.107108```109my_package/110├── CMakeLists.txt # or setup.py for pure Python111├── package.xml # format 3, with <depend> tags112├── config/113│ └── params.yaml # default parameters114├── launch/115│ └── bringup.launch.py # Python launch file116├── include/my_package/ # C++ public headers (if library)117├── src/ # C++ source files118├── my_package/ # Python modules (if ament_python or mixed)119├── test/ # gtest, pytest, launch_testing120├── urdf/ # URDF/xacro (if applicable)121├── msg/ srv/ action/ # custom interfaces (dedicated _interfaces package preferred)122└── README.md123```124125Separate interface definitions into a `*_interfaces` package so downstream126packages can depend on interfaces without pulling in implementation.127128### 4. Parameter discipline129130- Declare every parameter with a type, description, range, and default131 in the node constructor — never use undeclared parameters.132- Use `ParameterDescriptor` with `FloatingPointRange` or `IntegerRange`133 for numeric bounds. The parameter server rejects out-of-range values at set time.134- Group related parameters under a namespace prefix:135 `controller.kp`, `controller.ki`, `controller.kd`.136- Load defaults from a `config/params.yaml`; allow launch-time overrides.137- For dynamic reconfiguration, register a `set_parameters_callback` and138 validate new values atomically before accepting.139140### 5. Error handling philosophy141142- Nodes must not silently swallow errors. Log at the appropriate severity,143 then take a safe action (stop motion, request help, transition to error state).144- Prefer lifecycle node error transitions over ad-hoc boolean flags.145- When calling a service, always handle the "service not available" and146 "future timed out" cases explicitly.147- For hardware drivers, distinguish transient errors (retry with backoff)148 from fatal errors (transition to `FINALIZED` and alert the operator).149150### 6. Quality of Service defaults151152Start from these profiles and adjust per use case:153154| Use case | Reliability | Durability | History | Depth | Deadline | Lifespan |155|-----------------------|---------------|------------------|---------|-------|-------------|-------------|156| Sensor stream | BEST_EFFORT | VOLATILE | KEEP_LAST | 5 | — | — |157| Command velocity | RELIABLE | VOLATILE | KEEP_LAST | 1 | 100 ms | 200 ms |158| Map (latched) | RELIABLE | TRANSIENT_LOCAL | KEEP_LAST | 1 | — | — |159| Diagnostics | RELIABLE | VOLATILE | KEEP_LAST | 10 | — | — |160| Parameter events | RELIABLE | VOLATILE | KEEP_LAST | 1000| — | — |161| Action feedback | RELIABLE | VOLATILE | KEEP_LAST | 1 | — | — |162| Safety heartbeat | RELIABLE | VOLATILE | KEEP_LAST | 1 | 500 ms | 1 s |163164QoS mismatches are the #1 cause of "I published but nobody receives."165Always check compatibility with `ros2 topic info -v` when debugging.166167**DEADLINE and LIFESPAN** are critical for safety-critical systems. DEADLINE fires an168event when no message arrives within the specified period (detect stale data). LIFESPAN169discards messages older than the specified duration before delivery (prevent acting on170stale data). See `references/communication.md` section 9 for full API and examples.171172### 7. Naming conventions173174| Entity | Convention | Example |175|-------------|-----------------------------|--------------------------------|176| Package | `snake_case` | `arm_controller` |177| Node | `snake_case` | `joint_state_broadcaster` |178| Topic | `/snake_case` with ns | `/arm/joint_states` |179| Service | `/snake_case` | `/arm/set_mode` |180| Action | `/snake_case` | `/arm/follow_joint_trajectory` |181| Parameter | `snake_case` with dot ns | `controller.publish_rate` |182| Frame | `snake_case` | `base_link`, `camera_optical` |183| Interface | `PascalCase.msg/srv/action` | `JointState.msg` |184185### 8. Thread safety and callbacks186187- A `MutuallyExclusiveCallbackGroup` serializes its callbacks — safe for188 shared state without locks, but limits throughput.189- A `ReentrantCallbackGroup` allows parallel execution — you must protect190 shared state with `std::mutex` (C++) or `threading.Lock` (Python).191- **Calling a service from a callback:** The service client **must** be in a192 separate `MutuallyExclusiveCallbackGroup` from the calling callback. Otherwise193 the executor deadlocks — the callback waits for the response while the executor194 cannot deliver it. Always use `async_send_request` with a response callback;195 never use `spin_until_future_complete` inside an executor callback.196- Never do blocking work (file I/O, long computation, `sleep`) inside a197 timer or subscription callback on the default executor. Offload to a198 dedicated thread or use a `MultiThreadedExecutor` with a reentrant group.199- In rclcpp, prefer `std::shared_ptr<const MessageT>` in subscription200 callbacks to avoid unnecessary copies and enable zero-copy intra-process.201202### 9. Lifecycle-first design203204Default to lifecycle (managed) nodes for anything that owns resources:205hardware drivers, sensor pipelines, planners, controllers.206207```208 ┌──────────────┐209 create() ──► │ Unconfigured │210 └──────┬───────┘211 on_configure │212 ┌──────▼───────┐213 │ Inactive │214 └──────┬───────┘215 on_activate │216 ┌──────▼───────┐217 │ Active │218 └──────┬───────┘219 on_deactivate │220 ┌──────▼───────┐221 │ Inactive │222 └──────┬───────┘223 on_cleanup │224 ┌──────▼───────┐225 │ Unconfigured │226 └──────┬───────┘227 on_shutdown │228 ┌──────▼───────┐229 │ Finalized │230 └───────────────┘231```232233This gives the system manager (launch file, orchestrator, or operator) explicit234control over when resources are allocated, when the node starts processing,235and how it shuts down. It also makes error recovery predictable.236237### 10. Build and CI hygiene238239- Use `colcon build --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo` for240 development; `Release` for deployment.241- Enable `-Wall -Wextra -Wpedantic` and treat warnings as errors in CI.242- Run `colcon test` with `--event-handlers console_cohesion+` so test243 output groups by package.244- Pin rosdep keys in `rosdep.yaml` for reproducible dependency resolution.245- Cache `/opt/ros/`, `.ccache/`, and `build/`/`install/` in CI to cut build246 times by 60–80%.247248## Common anti-patterns249250| Anti-pattern | Why it hurts | Fix |251|---|---|---|252| Global variables for node state | Breaks composition, untestable | Store state as class members |253| `spin()` in `main()` for multi-node processes | Starves other nodes | Use `MultiThreadedExecutor` or component composition |254| Hardcoded topic names | Breaks reuse across robots | Use relative names + namespace remapping |255| `KEEP_ALL` history with no bound | Memory grows unbounded on slow subscribers | Use `KEEP_LAST` with explicit depth |256| Using `time.sleep()` / `std::this_thread::sleep_for` | Blocks the executor thread | Use `create_wall_timer` or a dedicated thread |257| Monolithic launch file for everything | Unmanageable past 10 nodes | Compose launch files with `IncludeLaunchDescription` |258| Skipping `package.xml` dependencies | Builds locally, breaks CI and Docker | Declare every dependency explicitly |259| Publishing in constructor | Subscribers may not be ready, messages lost | Publish in `on_activate` or after a short timer |260| Ignoring QoS compatibility | Silent communication failure | Match publisher/subscriber QoS or check with `ros2 topic info -v` |261| Creating timers/subs in callbacks | Resource leak, unpredictable behavior | Create all entities in constructor or `on_configure` |262| Synchronous service call in callback | Deadlocks the executor thread | Use `async_send_request` with a callback or dedicated thread |263| Service client in same callback group as caller | Deadlocks even with async in `MultiThreadedExecutor` | Put service client in a separate `MutuallyExclusiveCallbackGroup` |264| No safe command on shutdown | Motors hold last velocity after node exits | Send zero-velocity in `on_deactivate` AND destructor (see `references/hardware-interface.md`) |265| Dynamic subscriptions with `StaticSingleThreadedExecutor` | New subs are never picked up after `spin()` | Use `SingleThreadedExecutor` or `MultiThreadedExecutor` for dynamic entities |266| CPU frequency governor left on `powersave`/`ondemand` | 10-100 ms latency spikes in RT path | Set `performance` governor, disable turbo boost (see `references/realtime.md`) |267268## Distro-specific migration notes269270When upgrading between distributions, check these breaking changes first:271272**Foxy → Humble:**273- Complete API overhaul. Foxy packages require significant rework.274- `ros2_control` was not bundled in Foxy — must be built separately.275- Lifecycle node API stabilized in Humble.276- Action server/client API changed significantly.277278**Humble → Jazzy:**279- `ros2_control` API changed from 2.x to 4.x — `export_state_interfaces()` and280 `export_command_interfaces()` are now auto-generated by the framework. Manual281 overrides use `on_export_state_interfaces()`. See `references/hardware-interface.md`.282- Handle `get_value()` deprecated → use `get_optional<T>()` on `LoanedStateInterface` /283 `LoanedCommandInterface` (controller side). Hardware interfaces use `set_state()` /284 `get_state()` / `set_command()` / `get_command()` helpers with fully qualified names.285- All joints in `<ros2_control>` tag must exist in the URDF.286- Controller parameter loading changed — use `--param-file` with spawner.287- Default bag format changed from sqlite3 to **MCAP**. Use `storage_id='mcap'`.288- Default middleware changed internal config paths. Regenerate DDS profiles.289- `nav2_params.yaml` schema changes — `recoveries_server` renamed to `behavior_server`.290- `ROS_AUTOMATIC_DISCOVERY_RANGE` replaces `ROS_LOCALHOST_ONLY` (values: `LOCALHOST`,291 `SUBNET`, `OFF`, `SYSTEM_DEFAULT`).292- `launch_ros` actions have new parameter handling — test launch files explicitly.293294**Jazzy → Kilted (non-LTS):**295- **Zenoh promoted to Tier 1 middleware** — `rmw_zenoh` is production-ready.296 Install: `sudo apt install ros-kilted-rmw-zenoh-cpp`, set297 `RMW_IMPLEMENTATION=rmw_zenoh_cpp`. Supports router/peer/client modes.298- **EventsExecutor graduated from experimental** — available in `rclcpp::executors`299 (no `experimental` namespace). Also ported to rclpy.300- **`ament_target_dependencies()` deprecated** — use `target_link_libraries()` with301 modern CMake targets (e.g. `rclcpp::rclcpp`, `std_msgs::std_msgs__rosidl_typesupport_cpp`).302- Multi-bag replay support in `ros2 bag play`.303- Gazebo **Ionic** is the paired simulator (Harmonic was Jazzy; Ionic is the Kilted pairing).304305**ROS 1 → ROS 2:**306- See `references/migration-ros1.md` for a step-by-step strategy.307308## Quick reference — ros2 CLI309310```bash311# Workspace312colcon build --symlink-install --packages-select my_pkg313colcon test --packages-select my_pkg314colcon graph --dot # dependency graph (DOT format)315source install/setup.bash316317# Introspection318ros2 node list319ros2 topic list -t320ros2 topic info /topic_name -v # shows QoS details321ros2 topic hz /topic_name322ros2 topic bw /topic_name323ros2 service list -t324ros2 action list -t325ros2 param list /node_name326ros2 param describe /node_name param327ros2 interface show std_msgs/msg/String328329# ros2_control330ros2 control list_controllers331ros2 control list_hardware_interfaces332ros2 control list_hardware_components333334# Debugging335ros2 doctor --report # alias: ros2 wtf336ros2 run tf2_tools view_frames337ros2 bag record -a -o my_bag338ros2 bag info my_bag339ros2 bag play my_bag --clock340341# Lifecycle342ros2 lifecycle list /node_name343ros2 lifecycle set /node_name configure344ros2 lifecycle set /node_name activate345```