ROS 2 Engineering Skills

SkillProductivity

ROS 2 engineering: rclcpp/rclpy, colcon/ament, launch, QoS/DDS, tf2/URDF, ros2_control, Nav2, MoveIt 2, sensors, runtime provenance, and hardware safety. Use for development, review, debugging, and ROS 1 migration to ROS 2. Not for general C++/Python, unrelated middleware, or web/mobile tasks.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the ROS 2 Engineering Skills skill

What this skill tells your AI

The instructions your AI receives, as published by dbwls99706/ros2-engineering-skills in SKILL.md and read by ahel’s review.

Single responsibility: This skill is an API reference & code template guide for ROS 2 development. It tells you how to use ROS 2 APIs correctly and what mistakes to avoid. It does NOT do CI/CD orchestration, incident response, data analysis, or deployment automation — those are separate skill categories.

A progressive-disclosure skill for ROS 2 development — from first workspace to production fleet deployment. Detailed patterns and code templates live in references/; read the relevant file before writing code.

How to use this skill

This always-loaded file carries routing, core principles, pitfalls, and anti-patterns — enough for quick questions and architectural decisions. For implementation work, use the Decision Router below to load the reference file(s) matching the task; the AI pitfalls table lists mistakes worth re-checking before generating code. scripts/ are tools to run (scaffolding, QoS checking, launch validation), not reading material. When domains intersect (e.g. Nav2 + ros2_control) and recommendations conflict, favor safety > determinism > simplicity.

Execution log (opt-in): When the Stop hook runs (Claude Code only) and the SKILL_RUNS_LOG environment variable is set, a session summary is appended to .skill-runs.log. If that file exists in the workspace, read the last few lines to avoid repeating past mistakes. Without the opt-in — and on platforms without hooks — the file is never created, so a read-only session leaves the working tree untouched.

Platform support: SKILL.md and references/ are platform-neutral knowledge documents. scripts/ can be run manually on any platform whose environment has Python and the repository dependencies. The hook wiring in hooks/hooks.json and .skill-runs.log are Claude Code-specific; on other platforms run the validators manually from the skill root: SKILL_WORKSPACE=<dir> python3 scripts/skill_stop_hook.py and python3 scripts/skill_validate_hook.py --file <src> / --command '<cmd>' (the command string is inspected only, never executed; without those flags the validate hook expects a Claude Code PreToolUse payload and checks nothing on its own).

Decision router

User is doing...Read
Creating a workspace, package, or build configreferences/workspace-build.md
Writing nodes, executors, callback groupsreferences/nodes-executors.md
Topics, services, actions, custom interfaces, QoSreferences/communication.md
Lifecycle nodes, component loading, compositionreferences/lifecycle-components.md
Launch files, conditional logic, event handlersreferences/launch-system.md
tf2, URDF, xacro, robot_state_publisherreferences/tf2-urdf.md
ros2_control, hardware interfaces, controllersreferences/hardware-interface.md
Real-time constraints, PREEMPT_RT, memory, jitterreferences/realtime.md
Nav2, SLAM, costmaps, behavior treesreferences/navigation.md
MoveIt 2, planning scene, grasp pipelinesreferences/manipulation.md
Camera, LiDAR, PCL, cv_bridge, depth processingreferences/perception.md
Sensor drivers, clock sync, LiDAR-camera extrinsicsreferences/sensor-integration.md
Unit tests, integration tests, launch_testing, CIreferences/testing.md
ros2 doctor, tracing, profiling, rosbag2, CLI cheat sheetreferences/debugging.md
"Which install/config/publisher is actually running?" auditsreferences/runtime-provenance.md
Faults crossing ROS and non-ROS layers (link, bridge, driver)references/system-diagnostics.md
Docker, cross-compile, fleet deployment, OTAreferences/deployment.md
System bringup, udev rules, boot sequence, watchdogsreferences/system-bringup.md
Gazebo, Isaac Sim, sim-to-real, use_sim_timereferences/simulation.md
SROS2, DDS security, certificates, supply chainreferences/security.md
E-stop, safety chains, command arbitrationreferences/safety-estop.md
micro-ROS, MCU/RTOS, XRCE-DDS, rclcreferences/micro-ros.md
Multi-robot fleet, Open-RMF, DDS discovery scalereferences/multi-robot.md
Message types, units, covariance, frame conventionsreferences/message-types.md
ROS 1 migration, ros1_bridge, hybrid operationreferences/migration-ros1.md

Cross-cutting concerns: Security, error handling, and QoS are not isolated to single reference files — use your judgment and apply them whenever the data path crosses a trust boundary, a node owns hardware, or communication reliability matters.

Core engineering principles

These apply to every ROS 2 artifact you produce, regardless of domain.

1. Distro awareness

Staleness warning: The table below was last verified on 2026-07-15. If the current date is more than 6 months past that, re-verify EOL dates and feature support against https://docs.ros.org/en/rolling/Releases.html before relying on this table. When you update it, change both LAST_UPDATED and NEXT_REVIEW comments above.

Detect the distro before generating code — do not ask first, and do not assume the newest release. Work down this ladder and stop at the first answer:

  1. Active shell: echo $ROS_DISTRO — the distro currently sourced. ls /opt/ros/ is inventory evidence (what is installed), never an automatic selection.
  2. Workspace pin: Dockerfile FROM ros:<distro>, CI matrix, .repos branch names — what the workspace intends to build and deploy against. (package.xml usually shows dependencies without naming a distro.)
  3. Installed versions: ros2 pkg xml <pkg>, dpkg-query -W 'ros-*' (Principle 11) — this also settles behavior the distro label does not.
  4. Ask the user when the workspace holds no evidence.
  5. Greenfield only: default to the latest LTS.

Conflict rule. When active-shell evidence disagrees with the workspace pin, report both and select neither silently. Prefer the workspace's explicit build/deployment pin for guidance about the repository, and treat the shell mismatch as an environment defect to resolve. Never resolve an existing workspace to the newest LTS: that pulls API the installed stack does not have. Key differences:

FeatureHumble (LTS)Jazzy (LTS)Kilted (non-LTS)Lyrical (LTS)Rolling
EOLMay 2027May 2029Dec 2026May 2031Rolling
Ubuntu22.0424.0424.0426.04Latest
Default DDSFast DDSFast DDSFast DDSFast DDSFast DDS
Zenoh supportTier 1Tier 1Tier 1
Type description supportNoYesYesYesYes
Service introspectionNoYesYesYesYes
EventsExecutorNoExperimentalExperimental (+ rclpy port)EventsCBGExecutor (non-experimental, rclcpp)Verify installed rclcpp
Default bag formatsqlite3MCAPMCAPMCAPMCAP
ros2_control interface2.x4.x5.x6.x (verify installed)Latest
CMake recommendationament_target_depseithertarget_link_libstarget_link_libstarget_link_libs

Foxy (EOL June 2023, Ubuntu 20.04, ros2_control not bundled) is a migration reference only — see the migration notes below. The pre-Lyrical EventsExecutor lives in the rclcpp::experimental namespace on every release that ships it; Lyrical adds the separate, non-experimental rclcpp::executors::EventsCBGExecutor.

For a greenfield project with no constraint, the latest LTS is Lyrical Luth (Ubuntu 26.04); use Jazzy when the target platform is Ubuntu 24.04. Pin the exact distro in Dockerfile, CI, and documentation so builds are reproducible.

2. C++ vs Python decision

Choose the language from measured latency, jitter, allocation, library, and operational requirements — not from a frequency threshold alone. rclcpp (C++) is generally appropriate for hardware drivers, controller plugins, allocation-sensitive paths, and tight latency or jitter budgets. rclpy (Python) is generally appropriate for orchestration, monitoring, parameter management, rapid prototyping, and Python-native frameworks. Measure the target workload before treating either language as mandatory.

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 and communicate over intra-host DDS — not zero-overhead by default: the standard inter-process transport pays serialization, copies, and transport bandwidth, and splitting work into another process does not by itself remove encoding costs. Copy avoidance has three distinct mechanisms with different preconditions: (1) the rclcpp intra-process path (use_intra_process_comms(true), same process) avoids copies only depending on publish ownership (unique_ptr), callback type, subscriber count, and QoS; (2) loaned messages / vendor shared memory (SHM/PSMX) are RMW- and vendor-dependent and can avoid some or all copies when their preconditions hold; (3) separate processes on the standard DDS transport get no copy avoidance — crossing processes without copies requires the vendor mechanisms in (2). Details: references/nodes-executors.md.

3. Package structure conventions

Follow the standard layout — package.xml (format 3, explicit dependency tags), config/params.yaml, launch/*.launch.py, src/ + include/<pkg>/ for C++ or <pkg>/ for Python, and test/. A dedicated *_interfaces package is usually preferable when interfaces are shared by multiple packages or must remain independent of implementation dependencies. Defining and using interfaces in one ament_cmake package is supported when that coupling is intentional. Full annotated layout: references/workspace-build.md.

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 caseReliabilityDurabilityHistoryDepthDeadlineLifespan
Sensor streamBEST_EFFORTVOLATILEKEEP_LAST5
Command velocityRELIABLEVOLATILEKEEP_LAST1100 ms200 ms
Map (latched)RELIABLETRANSIENT_LOCALKEEP_LAST1
DiagnosticsRELIABLEVOLATILEKEEP_LAST10
Parameter eventsRELIABLEVOLATILEKEEP_LAST1000
Action feedbackRELIABLEVOLATILEKEEP_LAST1
Safety heartbeatRELIABLEVOLATILEKEEP_LAST1500 ms1 s

These rows are starting points, not verdicts. The sensor row matches rmw_qos_profile_sensor_data (BEST_EFFORT, depth 5), which fits a high-rate stream whose consumer only wants the newest sample — but depth follows the consumer's tolerance for staleness and its processing time, and a sensor whose loss the system cannot detect (a safety-relevant scan, a one-shot calibration) belongs on RELIABLE. Decide per data path, then record why.

QoS mismatch is one common cause of "I published but nobody receives." Inspect the actual endpoints with ros2 topic info <topic> -v before changing either side. Matching QoS is necessary for communication, but compatibility alone does not prove that the delivered data is timely, semantically valid, or safe to act on (Principle 13).

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

EntityConventionExample
Packagesnake_casearm_controller
Nodesnake_casejoint_state_broadcaster
Topic/snake_case with ns/arm/joint_states
Service/snake_case/arm/set_mode
Action/snake_case/arm/follow_joint_trajectory
Parametersnake_case with dot nscontroller.publish_rate
Framesnake_casebase_link, camera_optical
InterfacePascalCase.msg/srv/actionJointState.msg

8. Thread safety and callbacks

  • A MutuallyExclusiveCallbackGroup serializes callbacks in that same group. Shared state is lock-free only when every access is confined to the group and no other callback group, executor, or ordinary thread can touch it.
  • A ReentrantCallbackGroup permits overlapping execution. Actual parallelism also requires an executor with enough worker threads; protect shared state whenever callbacks or external threads can overlap.
  • Calling a service from a callback: If the callback registers the request asynchronously — rclcpp: async_send_request(request, response_callback); rclpy: future = client.call_async(request) then future.add_done_callback(...) — and returns without waiting for the result, the same MutuallyExclusiveCallbackGroup does not deadlock. Deadlock comes from waiting synchronously inside the callback — rclcpp: calling get()/wait()/wait_for() on a not-yet-complete future from the initiating callback, or spin_until_future_complete (inside the response callback the future is already complete, so get() there is safe — the examples use exactly that); rclpy: synchronous Client.call(), spin_until_future_complete, or a loop that blocks until future.done(). (rclpy's future.result() by itself does not block — it immediately returns whatever result is currently stored, which may be unset.) A synchronous wait needs the client in a different callback group or a ReentrantCallbackGroup, plus a matching executor configuration (e.g. MultiThreadedExecutor). Do not assume plain-executor async def callback patterns are safe until tested with your executor; Lyrical's rclpy.experimental.AsyncNode is a separate execution model that officially supports await client.call(...) inside callbacks.
  • 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; whether intra-process delivery is actually copy-free additionally depends on publish ownership, subscriber count, and QoS (Principle 2).

9. Lifecycle-first design

Default to lifecycle (managed) nodes for anything that owns resources: hardware drivers, sensor pipelines, planners, controllers. The managed state machine (unconfigured → inactive → active, with cleanup, shutdown, and error transitions) gives the system manager explicit control over when resources are allocated, when processing starts, and how shutdown proceeds — and makes error recovery predictable. Configure-only transitions also enable hardware-safe config validation (references/testing.md section 4). Full state diagram and callbacks: references/lifecycle-components.md.

A plain node is the right call when nothing external is at stake or the managed state machine cannot be honored: leaf compute nodes that own no device, file handle, or actuator; nodes whose start/stop is already sequenced by an outer supervisor; third-party nodes you do not control; and rclc/micro-ROS targets with limited lifecycle support. Lifecycle is not free — every managed node needs something to manage it, and adds transition-failure states the system must handle.

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.
  • Treat rosdep keys as platform mappings, not dependency locks. Reproducible builds also need pinned source revisions, base images or repositories, and recorded dependency resolution inputs.
  • Prefer compiler caches and dependency/container layers that have explicit, reproducible invalidation inputs.ccache/, apt/rosdep, base images. They are not automatically safe either: an apt layer goes stale with the distro, repository state, dependency declarations, and package-index time, so key it on those.
  • Do not cache build//install/ by default. Opt in only with an exact key covering toolchain, ROS distro, dependency resolution, build options, and the complete relevant source tree — and never partially restore them (restore-keys falls back to an older prefix match, which is exactly the failure). A partially restored install space keeps artifacts of files no longer in the source, so CI links and tests stale code and reports it green — the same stale-overlay failure that bites on robots (references/runtime-provenance.md).

11. Source-first behavior verification

Distro labels are not enough when exact behavior matters — patch releases change parameter names, plugin behavior, and defaults. Before asserting how an installed stack behaves, identify the installed version (ros2 pkg xml, dpkg-query -W) and read what ships with it: reference configs, headers, and the source tag matching that version. Worked Nav2 procedure: references/navigation.md section 6.

12. Motion-safety defaults

Never generate configs that can move an unvalidated robot. Motion recoveries (Spin/BackUp) stay opt-in until robot geometry, locomotion response, and clearance are validated — actuation-free recovery comes first. Velocity limits come from the safe operational ceiling, never the SDK/API maximum. For hardware checks, prefer configure-only lifecycle validation with hardware isolation (references/testing.md section 4). Details: references/navigation.md sections 7 and 10.

A stop command is verified end-to-end, not on a topic. Zero velocity visible on /cmd_vel proves a message was published — not that the robot stopped. Verify all four links: command ownership, driver translation, local submission plus any available remote-acceptance evidence, and measured hardware response (references/safety-estop.md section 3).

13. Verification levels

Say which level a result came from, every time. Each level answers a different question, and a claim never inherits the confidence of a level it did not reach.

LevelWhat ranWhat it proves
L0Static reviewThe code/config reads correctly; nothing was executed
L1Unit testsIsolated logic, no ROS graph, no real time
L2Build + launch smokeIt compiles, nodes start, plugins/params load
L3Runtime, robot disconnectedGraph, QoS, TF and rates on sim or mock hardware
L4Hardware powered, no actuationReal provenance, params, TF and driver state — motors disabled/isolated
L5Bench motion / fault injectionCommanded motion and failsafes on a restrained platform, operator present
L6Supervised field operationThe behavior in its real duty cycle

Never write an L0–L2 result in L4+ language. "Tests pass" and "safe to drive" may not share a sentence. When a level was skipped, say which one and why. Level definitions and required evidence: references/testing.md section 11.

Common anti-patterns

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
165
Forks
17
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ros2-engineering-skills
Source
github.com/dbwls99706/ros2-engineering-skills