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.
references/*.md file for detailed guidance.User is doing...ReadCreating a workspace, package, or build configreferences/workspace-build.mdWriting nodes, executors, callback groupsreferences/nodes-executors.mdTopics, services, actions, custom interfaces, QoSreferences/communication.mdLifecycle nodes, component loading, compositionreferences/lifecycle-components.mdLaunch files, conditional logic, event handlersreferences/launch-system.mdtf2, URDF, xacro, robot_state_publisherreferences/tf2-urdf.mdros2_control, hardware interfaces, controllersreferences/hardware-interface.mdReal-time constraints, PREEMPT_RT, memory, jitterreferences/realtime.mdNav2, SLAM, costmaps, behavior treesreferences/navigation.mdMoveIt 2, planning scene, grasp pipelinesreferences/manipulation.mdCamera, LiDAR, PCL, cv_bridge, depth processingreferences/perception.mdUnit tests, integration tests, launch_testing, CIreferences/testing.mdros2 doctor, tracing, profiling, rosbag2references/debugging.mdDocker, cross-compile, fleet deployment, OTAreferences/deployment.mdGazebo, Isaac Sim, sim-to-real, use_sim_timereferences/simulation.mdSROS2, DDS security, certificates, supply chainreferences/security.mdmicro-ROS, MCU/RTOS, XRCE-DDS, rclcreferences/micro-ros.mdMulti-robot fleet, Open-RMF, DDS discovery scalereferences/multi-robot.mdMessage types, units, covariance, frame conventionsreferences/message-types.mdROS 1 migration, ros1_bridge, hybrid operationreferences/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.
These apply to every ROS 2 artifact you produce, regardless of domain.
Always ask which ROS 2 distribution the user targets. Key differences:
FeatureFoxy ( EOL)Humble (LTS)Jazzy (LTS)Kilted (non-LTS)RollingEOLJun 2023 ( ended)May 2027May 2029Nov 2025RollingUbuntu20.0422.0424.0424.04LatestDefault DDSFast DDSFast DDSFast DDSFast DDSFast DDSZenoh support———Tier 1Tier 1Type description supportNoNoYesYesYesService introspectionNoNoYesYesYesEventsExecutorNoNoExperimentalStable (+ rclpy)Stable (+ rclpy)Default bag formatsqlite3sqlite3MCAPMCAPMCAPros2_control interfaceN/A (separate)2.x4.x4.xLatestCMake recommendationament_target_depsament_target_depseithertarget_link_libstarget_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.
Choose the language based on the node's role, not personal preference.
Use rclcpp (C++) when:
Use rclpy (Python) when:
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.
Every package should follow this layout. Consistency across a workspace reduces onboarding time and makes CI scripts portable.
text
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.
ParameterDescriptor with FloatingPointRange or IntegerRange
for numeric bounds. The parameter server rejects out-of-range values at set time.controller.kp, controller.ki, controller.kd.config/params.yaml; allow launch-time overrides.set_parameters_callback and
validate new values atomically before accepting.FINALIZED and alert the operator).Start from these profiles and adjust per use case:
Use caseReliabilityDurabilityHistoryDepthDeadlineLifespanSensor streamBEST_EFFORTVOLATILEKEEP_LAST5——Command velocityRELIABLEVOLATILEKEEP_LAST1100 ms200 msMap (latched)RELIABLETRANSIENT_LOCALKEEP_LAST1——DiagnosticsRELIABLEVOLATILEKEEP_LAST10——Parameter eventsRELIABLEVOLATILEKEEP_LAST1000——Action feedbackRELIABLEVOLATILEKEEP_LAST1——Safety heartbeatRELIABLEVOLATILEKEEP_LAST1500 ms1 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.
EntityConventionExamplePackagesnake_case``arm_controllerNodesnake_case``joint_state_broadcasterTopic/snake_case with ns/arm/joint_statesService/snake_case``/arm/set_modeAction/snake_case``/arm/follow_joint_trajectoryParametersnake_case with dot nscontroller.publish_rateFramesnake_case``base_link, camera_opticalInterfacePascalCase.msg/srv/action``JointState.msg
MutuallyExclusiveCallbackGroup serializes its callbacks — safe for
shared state without locks, but limits throughput.ReentrantCallbackGroup allows parallel execution — you must protect
shared state with std::mutex (C++) or threading.Lock (Python).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.sleep) inside a
timer or subscription callback on the default executor. Offload to a
dedicated thread or use a MultiThreadedExecutor with a reentrant group.std::shared_ptr<const MessageT> in subscription
callbacks to avoid unnecessary copies and enable zero-copy intra-process.Default to lifecycle (managed) nodes for anything that owns resources: hardware drivers, sensor pipelines, planners, controllers.
text
┌──────────────┐
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.
colcon build --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo for
development; Release for deployment.-Wall -Wextra -Wpedantic and treat warnings as errors in CI.colcon test with --event-handlers console_cohesion+ so test
output groups by package.rosdep.yaml for reproducible dependency resolution./opt/ros/, .ccache/, and build// install/ in CI to cut build
times by 60–80%.Anti-patternWhy it hurtsFixGlobal variables for node stateBreaks composition, untestableStore state as class membersspin() in main() for multi-node processesStarves other nodesUse MultiThreadedExecutor or component compositionHardcoded topic namesBreaks reuse across robotsUse relative names + namespace remappingKEEP_ALL history with no boundMemory grows unbounded on slow subscribersUse KEEP_LAST with explicit depthUsing time.sleep() / std::this_thread::sleep_forBlocks the executor threadUse create_wall_timer or a dedicated threadMonolithic launch file for everythingUnmanageable past 10 nodesCompose launch files with IncludeLaunchDescriptionSkipping package.xml dependenciesBuilds locally, breaks CI and DockerDeclare every dependency explicitlyPublishing in constructorSubscribers may not be ready, messages lostPublish in on_activate or after a short timerIgnoring QoS compatibilitySilent communication failureMatch publisher/subscriber QoS or check with ros2 topic info -vCreating timers/subs in callbacksResource leak, unpredictable behaviorCreate all entities in constructor or on_configureSynchronous service call in callbackDeadlocks the executor threadUse async_send_request with a callback or dedicated threadService client in same callback group as callerDeadlocks even with async in MultiThreadedExecutorPut service client in a separate MutuallyExclusiveCallbackGroupNo safe command on shutdownMotors hold last velocity after node exitsSend zero-velocity in on_deactivate AND destructor (see references/hardware-interface.md)Dynamic subscriptions with StaticSingleThreadedExecutorNew subs are never picked up after spin()Use SingleThreadedExecutor or MultiThreadedExecutor for dynamic entitiesCPU frequency governor left on powersave/ ondemand10-100 ms latency spikes in RT pathSet performance governor, disable turbo boost (see references/realtime.md)
When upgrading between distributions, check these breaking changes first:
Foxy → Humble:
ros2_control was not bundled in Foxy — must be built separately.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.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.<ros2_control> tag must exist in the URDF.--param-file with spawner.storage_id='mcap'.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):
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.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).ros2 bag play.ROS 1 → ROS 2:
references/migration-ros1.md for a step-by-step strategy.bash
# 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