ros2
The foundation tool skill for the robium ROS vertical. Everything that touches
ROS 2 itself — workspaces, colcon, package anatomy, nodes, topics/services/
actions, QoS, launch files, parameters, TF2, rosdep, and gluing third-party ROS
packages together — lives here. nav2, gazebo, and rviz2 all assume this
skill's content and cross-reference it rather than re-explaining ROS 2 basics;
load this skill alongside any of them. This skill is ROS 2 only — ROS 1 reached
end-of-life with Noetic Ninjemys and is out of scope everywhere in robium.
When to use this skill
- Any ROS 2 development or debugging task: creating a package, building a
workspace, writing nodes, wiring topics/services/actions, writing launch
files, setting parameters, working with TF2, resolving dependencies.
- The trigger phrases in the description: 'ros2', 'colcon', 'launch file',
'package.xml', 'QoS mismatch', 'TF', 'node not receiving messages', 'rosdep'.
- A ROS 2 node isn't receiving messages it should be — start here (QoS is the
first suspect), not in application logic.
- Cross-references — go to the sibling skill instead when the question is:
- Autonomous navigation specifics (costmaps, planners, controllers,
behavior trees, AMCL/localization) →
nav2. This skill covers the ROS 2
substrate Nav2 runs on, not navigation algorithms.
- Simulating a ROS 2 robot →
gazebo.
- Visualizing ROS 2 data →
rviz2 (local display) or foxglove (remote/
headless).
- Choosing uv vs Docker, or macOS/GPU environment setup →
environments.
- Module boundaries, non-ROS transports at a system boundary, Dockerfiles/
compose for a multi-module app →
integration. This skill's "bridge two
third-party packages" pattern below is intra-system (still ROS 2 topics);
crossing to a non-ROS peer is integration's call.
- The whole-stack decision this feeds into →
architect (routes here).
Key directives
- Delegation posture: embed. No good upstream "how do I use ROS 2" skill
exists to point to — this content lives here, in depth, not as a thin
pointer to docs.ros.org. Link out only for exact version/tag facts that
change release-to-release.
- Always
rosdep install before building. A workspace that hasn't had
rosdep install --from-paths src -y --ignore-src run against it is not
ready to build — missing system/package dependencies produce confusing
colcon failures that look like source bugs. Run rosdep first, every time a
new package or a fresh clone enters the workspace, not just on first setup.
See references/workspace-and-packages.md.
- QoS compatibility is the first suspect for silent topic failures. A
node that runs cleanly, discovers its peer, and still exchanges zero
messages is almost always a QoS mismatch (e.g. one side
RELIABLE, the
other BEST_EFFORT), not a code bug — DDS drops the connection silently
with no error on either side. Check ros2 topic info -v before debugging
anything else. See references/interfaces-and-qos.md and
references/debugging.md.
- Prefer workspace overlays over editing third-party package source.
When a third-party ROS package needs different behavior, put your changes
in an overlay package (a new package, or a
COLCON_IGNOREd fork built on
top) rather than hand-editing files inside an installed or vendored
package. Overlays survive rosdep update/reinstalls and keep the diff
visible; in-place edits to third-party source silently rot. See
references/workspace-and-packages.md.
- Never write distro, tag, or API-surface facts from memory. ROS 2's
distro cadence, package availability, and even some API idioms (e.g. the
rclpy.init() context-manager form) change release to release. Verify
before repeating a claim in a real project — every example in this skill is
marked status: unverified for exactly this reason.
Quick start
1. Confirm the workspace exists and rosdep is current:
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws
source /opt/ros/lyrical/setup.bash
rosdep update
rosdep install --from-paths src -y --ignore-src
2. Build and source the overlay:
colcon build --symlink-install
source install/setup.bash
3. Run something. The examples/package-ament-python/ directory in this
skill is a minimal, internally-consistent ament_python package (one
parameterized publisher node, one launch file) — copy it into src/, rebuild,
and run with ros2 run ros2_example_pkg talker or ros2 launch ros2_example_pkg talker.launch.py.
For any step beyond this, see the matching usage pattern below and the
references it points to.
Usage patterns
Create a package → build → run.
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name <node> <package> scaffolds package.xml, setup.py, setup.cfg, the
resource marker, and a starter node. Fill in package.xml's
<description>/<maintainer>, add real dependencies, colcon build --symlink-install from the workspace root, source install/setup.bash, then
ros2 run <package> <executable>. See references/workspace-and-packages.md
and examples/package-ament-python/.
Add a dependency. Declare it in package.xml as <exec_depend> (runtime)
or <build_depend> (build-time C++), then re-run rosdep install --from-paths src -y --ignore-src before rebuilding — adding the tag alone does not install
the underlying apt/pip package. See references/workspace-and-packages.md.
Write a launch file. Python launch files are the default choice (XML/YAML
exist but are thinner and less composable): generate_launch_description()
returning a LaunchDescription of Node actions, with
DeclareLaunchArgument/LaunchConfiguration for anything that should be
overridable from the command line, and the launch directory registered in
setup.py's data_files plus <exec_depend>launch</exec_depend> /
<exec_depend>launch_ros</exec_depend> in package.xml. See
references/launch-patterns.md and
examples/package-ament-python/launch/talker.launch.py.
Bridge two third-party packages (remap + relay). When two existing
packages almost line up but use different topic names or message shapes,
prefer wiring them at the launch/CLI layer over patching either package's
source: remappings=[('from_topic', 'to_topic')] on a Node action (or
<remap> in XML) for a straight rename, and ros2 run topic_tools relay <in> <out> (or relay_field for a field-level republish) when the fix
needs to live as its own running node rather than a launch-time rename. This
stays inside one ROS 2 system — crossing to a non-ROS peer is integration's
call, not this pattern. See references/launch-patterns.md.
Parameterize a node. Call self.declare_parameter('name', default) in
the node's __init__, read it with self.get_parameter('name').value (or
the typed .get_parameter_value() accessors), and feed it from a launch
file's parameters=[{...}] list, a YAML params file, or --ros-args -p name:=value on the CLI — don't hardcode values a launch file should own. See
references/interfaces-and-qos.md and
examples/package-ament-python/ros2_example_pkg/talker_node.py.
Platform gotchas
- macOS has no native ROS 2 — Docker only. There is no supported native
ROS 2 install on macOS/Apple Silicon; every ROS 2 workflow on a Mac dev
machine runs inside Docker, even for local iteration. Don't try to
pip install/homebrew a native ROS 2 as a shortcut. See the environments
skill's Docker patterns and its ROS 2 base-image guidance.
ROS_DOMAIN_ID collisions are silent. All ROS 2 nodes default to
domain ID 0; two unrelated ROS 2 systems on the same network segment with
the same domain ID will discover and cross-talk with each other with no
error. Export a project-unique ROS_DOMAIN_ID (export ROS_DOMAIN_ID=<n>) in every shell/container that runs this project's
nodes, the same way you'd pick a non-default port.
- Shell sourcing order matters. Source the underlay (
/opt/ros/<distro>/ setup.bash) before the workspace overlay (install/setup.bash) — each
setup.bash only extends the environment the previous one built, so
sourcing the overlay alone (or in the wrong order) silently drops the
underlay's paths. A fresh shell that skips sourcing entirely is the most
common "package not found" / "command not found: ros2" report — check this
before anything else.
set -u before sourcing a ROS setup script kills the script. ROS's
own setup.bash reads unset variables, so a strict-mode wrapper
(set -euo pipefail at the top, then source /opt/ros/<distro>/ setup.bash) aborts with an unbound-variable error from inside ROS's
script — an alarming failure that has nothing to do with your code. Order
it the other way: source first, then set -u. Verified 2026-07-11
(nav-trial).
ros2 launch as container PID 1 ignores SIGTERM. The kernel drops
unhandled signals to PID 1, and launch installs no SIGTERM handler — so
the normal teardown path (docker stop, or an in-container
os.kill(1, SIGTERM)) is a silent no-op and the container sits there
until the 10 s timeout escalates to SIGKILL, taking the sim down hard.
SIGINT hits launch's real shutdown path (clean exit 0, nodes
shut down in order — verified in-container 2026-07-12, nav-trial demo).
Either send SIGINT, or don't run launch as PID 1: an init shim
(docker run --init, tini) reaps and forwards signals properly and is
the better default for any launch-as-entrypoint image.
- TurtleBot 4: the Create 3 base is invisible to the Pi until you drop the
stock
CYCLONEDDS_URI interface restriction. /etc/turtlebot4/setup.bash
exports CYCLONEDDS_URI=/etc/turtlebot4/cyclonedds_rpi.xml, whose
<Interfaces> block pins CycloneDDS to wlan0, so it never discovers the
base over usb0 — ros2 node list shows no /motion_control, no
/battery_state, and /cmd_vel reaches nothing (the robot won't drive).
/scan still streams because the RPLIDAR is a Pi-local node, which masks
the break completely — seeing the scan proves nothing about the base link.
Fix: comment out that export (CycloneDDS then defaults to all interfaces),
sudo systemctl restart turtlebot4.service, and restart any
foxglove_bridge so it inherits the env. Dead-ends that did NOT work:
uncommenting usb0 in the xml, a Create 3 reboot (POST http://192.168.186.2/api/reboot), ntpd resync, a full Pi reboot, and
matching the Pi/base clocks (the clock is a red herring — the Pi has no RTC
and reboots to a stale date, and the base syncs NTP from the Pi, so don't
manually jump the Pi clock or reboot it). Verified 2026-07-24 (tb4-teleop):
base nodes appeared, /battery_state → 0.99, robot drove.
- Inspecting Create 3 (best-effort) topics from the CLI.
ros2 topic echo
defaults to RELIABLE QoS and silently receives nothing from the base's
best-effort publishers — pass --qos-reliability best_effort. And
ros2 topic pub --once /cmd_vel … HANGS on "Waiting for at least 1 matching
subscription" because the base subscribes in a separate DDS realm invisible
to the publisher (so ros2 topic info /cmd_vel also shows 0 subscribers
even when working) — add -w 0 to publish without waiting. Verified
2026-07-24 (tb4-teleop).
Customization
- Different ROS 2 distro: this skill's commands are written against
Lyrical Luth, the current LTS. Swap
lyrical for another distro name
in /opt/ros/<distro>/setup.bash and in the example package's dependency
versions; re-verify package availability for that distro first. The
ROS 2 + Nav2 + Gazebo navigation vertical currently defaults to Jazzy
Jalisco instead, because Nav2 has not yet shipped binaries for Lyrical —
see nav2's and architect's Platform gotchas for the current status
before picking a distro for that path.
- ament_cmake instead of ament_python: the workspace/colcon/rosdep/launch
mechanics in this skill are build-type-agnostic; only package internals
differ (a
CMakeLists.txt build/install pipeline instead of
setup.py/setup.cfg, with find_package/ament_target_dependencies
instead of Python imports). See references/workspace-and-packages.md for
both anatomies side by side.
- Different node/topic names in the example package:
package.xml's
<name>, setup.py's package_name/console-script entry point, and the
launch file's package=/executable= must all agree — rename all four
places together (see examples/package-ament-python/) rather than
drifting one and hitting a confusing ros2 run/ros2 launch "not found".
References
references/workspace-and-packages.md — workspace layout, colcon build
mechanics, underlay/overlay sourcing, ament_python vs ament_cmake package
anatomy, rosdep workflow, adding dependencies.
references/launch-patterns.md — Python launch files in depth: Node
actions, launch arguments, parameter files, remapping, includes, and the
remap+relay bridging pattern.
references/interfaces-and-qos.md — topics/services/actions overview, the
full QoS policy set, compatibility rules, preset profiles, and TF2 basics.
references/debugging.md — the ros2 doctor/ros2 topic/ros2 node
introspection toolkit, common failure signatures (unsourced shell, domain
ID collision, QoS mismatch, missing rosdep install) and how to tell them
apart.
examples/package-ament-python/ — a minimal, internally-consistent
ament_python package: package.xml, setup.py, setup.cfg, one
parameterized publisher node, one launch file (status: unverified — each
file links its own upstream source).
- Upstream: ROS 2 documentation (blocked for direct
fetch on 2026-07-10 — verified via the
ros2/ros2_documentation GitHub
repo through ctx7 instead; re-check docs.ros.org directly when it's
reachable), colcon documentation,
ros2/launch,
ros-tooling/topic_tools.
Sibling skills: nav2, gazebo, rviz2 (load alongside), environments
(macOS/Docker setup), integration (cross-boundary comms, compose),
architect (routes here).
Changelog
1.2.0 (2026-07-24): tb4-teleop absorption — two TurtleBot 4 / Create 3
gotchas that cost ~7 failed resets: the stock CYCLONEDDS_URI interface
restriction blocks base discovery over usb0 (robot won't drive; /scan
masks it — disable the export), and Create 3 base topics need
--qos-reliability best_effort to echo plus -w 0 to ros2 topic pub
(split-DDS hides the subscriber). Clock-skew ruled out as a red herring.
1.1.0 (2026-07-13): nav-trial absorption — two container/shell gotchas
that cost real debugging time: set -u before sourcing a ROS setup
script aborts on ROS's own unset vars (source first, then set -u), and
ros2 launch as PID 1 ignores SIGTERM (kernel drops unhandled signals to
PID 1) so teardown must use SIGINT or an init shim. Both were previously
captured only in demo-specific notes.
1.0.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.
1---2name: ros2-53description: Core ROS 2 usage: workspaces, colcon builds, packages (ament_python/ament_cmake), nodes, topics/services/actions, QoS, launch files, parameters, TF2, rosdep, and gluing third-party packages together. Use when: any ROS 2 development or debugging; 'ros2', 'colcon', 'launch file', 'package.xml', 'QoS mismatch', 'TF', 'node not receiving messages', 'rosdep'. Foundation skill for the ROS vertical — load alongside nav2, gazebo, rviz2. ROS 2 only; ROS 1 is EOL and out of scope. Not for: navigation specifics (nav2), simulation (gazebo), or visualization (rviz2/foxglove).4---56# ros278The foundation tool skill for the robium ROS vertical. Everything that touches9ROS 2 itself — workspaces, colcon, package anatomy, nodes, topics/services/10actions, QoS, launch files, parameters, TF2, rosdep, and gluing third-party ROS11packages together — lives here. `nav2`, `gazebo`, and `rviz2` all assume this12skill's content and cross-reference it rather than re-explaining ROS 2 basics;13load this skill alongside any of them. This skill is ROS 2 only — ROS 1 reached14end-of-life with Noetic Ninjemys and is out of scope everywhere in robium.1516## When to use this skill1718- Any ROS 2 development or debugging task: creating a package, building a19 workspace, writing nodes, wiring topics/services/actions, writing launch20 files, setting parameters, working with TF2, resolving dependencies.21- The trigger phrases in the description: 'ros2', 'colcon', 'launch file',22 'package.xml', 'QoS mismatch', 'TF', 'node not receiving messages', 'rosdep'.23- A ROS 2 node isn't receiving messages it should be — start here (QoS is the24 first suspect), not in application logic.25- Cross-references — go to the sibling skill instead when the question is:26 - Autonomous navigation specifics (costmaps, planners, controllers,27 behavior trees, AMCL/localization) → `nav2`. This skill covers the ROS 228 substrate Nav2 runs on, not navigation algorithms.29 - Simulating a ROS 2 robot → `gazebo`.30 - Visualizing ROS 2 data → `rviz2` (local display) or `foxglove` (remote/31 headless).32 - Choosing uv vs Docker, or macOS/GPU environment setup → `environments`.33 - Module boundaries, non-ROS transports at a system boundary, Dockerfiles/34 compose for a multi-module app → `integration`. This skill's "bridge two35 third-party packages" pattern below is intra-system (still ROS 2 topics);36 crossing to a non-ROS peer is `integration`'s call.37 - The whole-stack decision this feeds into → `architect` (routes here).3839## Key directives4041- **Delegation posture: embed.** No good upstream "how do I use ROS 2" skill42 exists to point to — this content lives here, in depth, not as a thin43 pointer to docs.ros.org. Link out only for exact version/tag facts that44 change release-to-release.45- **Always `rosdep install` before building.** A workspace that hasn't had46 `rosdep install --from-paths src -y --ignore-src` run against it is not47 ready to build — missing system/package dependencies produce confusing48 colcon failures that look like source bugs. Run rosdep first, every time a49 new package or a fresh clone enters the workspace, not just on first setup.50 See `references/workspace-and-packages.md`.51- **QoS compatibility is the first suspect for silent topic failures.** A52 node that runs cleanly, discovers its peer, and still exchanges zero53 messages is almost always a QoS mismatch (e.g. one side `RELIABLE`, the54 other `BEST_EFFORT`), not a code bug — DDS drops the connection silently55 with no error on either side. Check `ros2 topic info -v` before debugging56 anything else. See `references/interfaces-and-qos.md` and57 `references/debugging.md`.58- **Prefer workspace overlays over editing third-party package source.**59 When a third-party ROS package needs different behavior, put your changes60 in an overlay package (a new package, or a `COLCON_IGNORE`d fork built on61 top) rather than hand-editing files inside an installed or vendored62 package. Overlays survive `rosdep update`/reinstalls and keep the diff63 visible; in-place edits to third-party source silently rot. See64 `references/workspace-and-packages.md`.65- **Never write distro, tag, or API-surface facts from memory.** ROS 2's66 distro cadence, package availability, and even some API idioms (e.g. the67 `rclpy.init()` context-manager form) change release to release. Verify68 before repeating a claim in a real project — every example in this skill is69 marked `status: unverified` for exactly this reason.7071## Quick start7273**1. Confirm the workspace exists and rosdep is current:**7475```bash76mkdir -p ~/ros2_ws/src && cd ~/ros2_ws77source /opt/ros/lyrical/setup.bash78rosdep update79rosdep install --from-paths src -y --ignore-src80```8182**2. Build and source the overlay:**8384```bash85colcon build --symlink-install86source install/setup.bash87```8889**3. Run something.** The `examples/package-ament-python/` directory in this90skill is a minimal, internally-consistent ament_python package (one91parameterized publisher node, one launch file) — copy it into `src/`, rebuild,92and run with `ros2 run ros2_example_pkg talker` or `ros2 launch93ros2_example_pkg talker.launch.py`.9495For any step beyond this, see the matching usage pattern below and the96references it points to.9798## Usage patterns99100**Create a package → build → run.**101`ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name102<node> <package>` scaffolds `package.xml`, `setup.py`, `setup.cfg`, the103resource marker, and a starter node. Fill in `package.xml`'s104`<description>`/`<maintainer>`, add real dependencies, `colcon build105--symlink-install` from the workspace root, `source install/setup.bash`, then106`ros2 run <package> <executable>`. See `references/workspace-and-packages.md`107and `examples/package-ament-python/`.108109**Add a dependency.** Declare it in `package.xml` as `<exec_depend>` (runtime)110or `<build_depend>` (build-time C++), then re-run `rosdep install --from-paths111src -y --ignore-src` before rebuilding — adding the tag alone does not install112the underlying apt/pip package. See `references/workspace-and-packages.md`.113114**Write a launch file.** Python launch files are the default choice (XML/YAML115exist but are thinner and less composable): `generate_launch_description()`116returning a `LaunchDescription` of `Node` actions, with117`DeclareLaunchArgument`/`LaunchConfiguration` for anything that should be118overridable from the command line, and the launch directory registered in119`setup.py`'s `data_files` plus `<exec_depend>launch</exec_depend>` /120`<exec_depend>launch_ros</exec_depend>` in `package.xml`. See121`references/launch-patterns.md` and122`examples/package-ament-python/launch/talker.launch.py`.123124**Bridge two third-party packages (remap + relay).** When two existing125packages almost line up but use different topic names or message shapes,126prefer wiring them at the launch/CLI layer over patching either package's127source: `remappings=[('from_topic', 'to_topic')]` on a `Node` action (or128`<remap>` in XML) for a straight rename, and `ros2 run topic_tools relay129<in> <out>` (or `relay_field` for a field-level republish) when the fix130needs to live as its own running node rather than a launch-time rename. This131stays inside one ROS 2 system — crossing to a non-ROS peer is `integration`'s132call, not this pattern. See `references/launch-patterns.md`.133134**Parameterize a node.** Call `self.declare_parameter('name', default)` in135the node's `__init__`, read it with `self.get_parameter('name').value` (or136the typed `.get_parameter_value()` accessors), and feed it from a launch137file's `parameters=[{...}]` list, a YAML params file, or `--ros-args -p138name:=value` on the CLI — don't hardcode values a launch file should own. See139`references/interfaces-and-qos.md` and140`examples/package-ament-python/ros2_example_pkg/talker_node.py`.141142## Platform gotchas143144- **macOS has no native ROS 2 — Docker only.** There is no supported native145 ROS 2 install on macOS/Apple Silicon; every ROS 2 workflow on a Mac dev146 machine runs inside Docker, even for local iteration. Don't try to `pip147 install`/homebrew a native ROS 2 as a shortcut. See the `environments`148 skill's Docker patterns and its ROS 2 base-image guidance.149- **`ROS_DOMAIN_ID` collisions are silent.** All ROS 2 nodes default to150 domain ID `0`; two unrelated ROS 2 systems on the same network segment with151 the same domain ID will discover and cross-talk with each other with no152 error. Export a project-unique `ROS_DOMAIN_ID` (`export153 ROS_DOMAIN_ID=<n>`) in every shell/container that runs this project's154 nodes, the same way you'd pick a non-default port.155- **Shell sourcing order matters.** Source the underlay (`/opt/ros/<distro>/156 setup.bash`) before the workspace overlay (`install/setup.bash`) — each157 `setup.bash` only extends the environment the previous one built, so158 sourcing the overlay alone (or in the wrong order) silently drops the159 underlay's paths. A fresh shell that skips sourcing entirely is the most160 common "package not found" / "command not found: ros2" report — check this161 before anything else.162- **`set -u` before sourcing a ROS setup script kills the script.** ROS's163 own `setup.bash` reads unset variables, so a strict-mode wrapper164 (`set -euo pipefail` at the top, then `source /opt/ros/<distro>/165 setup.bash`) aborts with an unbound-variable error from inside ROS's166 script — an alarming failure that has nothing to do with your code. Order167 it the other way: source first, *then* `set -u`. Verified 2026-07-11168 (nav-trial).169- **`ros2 launch` as container PID 1 ignores SIGTERM.** The kernel drops170 unhandled signals to PID 1, and launch installs no SIGTERM handler — so171 the normal teardown path (`docker stop`, or an in-container172 `os.kill(1, SIGTERM)`) is a silent no-op and the container sits there173 until the 10 s timeout escalates to SIGKILL, taking the sim down hard.174 **SIGINT** hits launch's real shutdown path (clean exit 0, nodes175 shut down in order — verified in-container 2026-07-12, nav-trial demo).176 Either send SIGINT, or don't run launch as PID 1: an init shim177 (`docker run --init`, `tini`) reaps and forwards signals properly and is178 the better default for any launch-as-entrypoint image.179- **TurtleBot 4: the Create 3 base is invisible to the Pi until you drop the180 stock `CYCLONEDDS_URI` interface restriction.** `/etc/turtlebot4/setup.bash`181 exports `CYCLONEDDS_URI=/etc/turtlebot4/cyclonedds_rpi.xml`, whose182 `<Interfaces>` block pins CycloneDDS to `wlan0`, so it never discovers the183 base over `usb0` — `ros2 node list` shows no `/motion_control`, no184 `/battery_state`, and `/cmd_vel` reaches nothing (the robot won't drive).185 `/scan` still streams because the RPLIDAR is a Pi-local node, which masks186 the break completely — seeing the scan proves nothing about the base link.187 Fix: comment out that export (CycloneDDS then defaults to all interfaces),188 `sudo systemctl restart turtlebot4.service`, and restart any189 `foxglove_bridge` so it inherits the env. Dead-ends that did NOT work:190 uncommenting `usb0` in the xml, a Create 3 reboot (`POST191 http://192.168.186.2/api/reboot`), ntpd resync, a full Pi reboot, and192 matching the Pi/base clocks (the clock is a red herring — the Pi has no RTC193 and reboots to a stale date, and the base syncs NTP *from* the Pi, so don't194 manually jump the Pi clock or reboot it). Verified 2026-07-24 (tb4-teleop):195 base nodes appeared, `/battery_state` → 0.99, robot drove.196- **Inspecting Create 3 (best-effort) topics from the CLI.** `ros2 topic echo`197 defaults to RELIABLE QoS and silently receives nothing from the base's198 best-effort publishers — pass `--qos-reliability best_effort`. And199 `ros2 topic pub --once /cmd_vel …` HANGS on "Waiting for at least 1 matching200 subscription" because the base subscribes in a separate DDS realm invisible201 to the publisher (so `ros2 topic info /cmd_vel` also shows 0 subscribers202 even when working) — add `-w 0` to publish without waiting. Verified203 2026-07-24 (tb4-teleop).204205## Customization206207- **Different ROS 2 distro:** this skill's commands are written against208 **Lyrical Luth**, the current LTS. Swap `lyrical` for another distro name209 in `/opt/ros/<distro>/setup.bash` and in the example package's dependency210 versions; re-verify package availability for that distro first. The211 ROS 2 + Nav2 + Gazebo navigation vertical currently defaults to **Jazzy212 Jalisco** instead, because Nav2 has not yet shipped binaries for Lyrical —213 see `nav2`'s and `architect`'s Platform gotchas for the current status214 before picking a distro for that path.215- **ament_cmake instead of ament_python:** the workspace/colcon/rosdep/launch216 mechanics in this skill are build-type-agnostic; only package internals217 differ (a `CMakeLists.txt` build/install pipeline instead of218 `setup.py`/`setup.cfg`, with `find_package`/`ament_target_dependencies`219 instead of Python imports). See `references/workspace-and-packages.md` for220 both anatomies side by side.221- **Different node/topic names in the example package:** `package.xml`'s222 `<name>`, `setup.py`'s `package_name`/console-script entry point, and the223 launch file's `package=`/`executable=` must all agree — rename all four224 places together (see `examples/package-ament-python/`) rather than225 drifting one and hitting a confusing `ros2 run`/`ros2 launch` "not found".226227## References228229- `references/workspace-and-packages.md` — workspace layout, colcon build230 mechanics, underlay/overlay sourcing, ament_python vs ament_cmake package231 anatomy, rosdep workflow, adding dependencies.232- `references/launch-patterns.md` — Python launch files in depth: Node233 actions, launch arguments, parameter files, remapping, includes, and the234 remap+relay bridging pattern.235- `references/interfaces-and-qos.md` — topics/services/actions overview, the236 full QoS policy set, compatibility rules, preset profiles, and TF2 basics.237- `references/debugging.md` — the `ros2 doctor`/`ros2 topic`/`ros2 node`238 introspection toolkit, common failure signatures (unsourced shell, domain239 ID collision, QoS mismatch, missing rosdep install) and how to tell them240 apart.241- `examples/package-ament-python/` — a minimal, internally-consistent242 ament_python package: `package.xml`, `setup.py`, `setup.cfg`, one243 parameterized publisher node, one launch file (status: unverified — each244 file links its own upstream source).245- Upstream: [ROS 2 documentation](https://docs.ros.org/) (blocked for direct246 fetch on 2026-07-10 — verified via the `ros2/ros2_documentation` GitHub247 repo through ctx7 instead; re-check docs.ros.org directly when it's248 reachable), [colcon documentation](https://colcon.readthedocs.io/),249 [ros2/launch](https://github.com/ros2/launch),250 [ros-tooling/topic_tools](https://github.com/ros-tooling/topic_tools).251 Sibling skills: `nav2`, `gazebo`, `rviz2` (load alongside), `environments`252 (macOS/Docker setup), `integration` (cross-boundary comms, compose),253 `architect` (routes here).254255## Changelog256257<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->258259- 1.2.0 (2026-07-24): tb4-teleop absorption — two TurtleBot 4 / Create 3260 gotchas that cost ~7 failed resets: the stock `CYCLONEDDS_URI` interface261 restriction blocks base discovery over `usb0` (robot won't drive; `/scan`262 masks it — disable the export), and Create 3 base topics need263 `--qos-reliability best_effort` to echo plus `-w 0` to `ros2 topic pub`264 (split-DDS hides the subscriber). Clock-skew ruled out as a red herring.265266- 1.1.0 (2026-07-13): nav-trial absorption — two container/shell gotchas267 that cost real debugging time: `set -u` before sourcing a ROS setup268 script aborts on ROS's own unset vars (source first, then set -u), and269 `ros2 launch` as PID 1 ignores SIGTERM (kernel drops unhandled signals to270 PID 1) so teardown must use SIGINT or an init shim. Both were previously271 captured only in demo-specific notes.272273- 1.0.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.