ros2-skill

Controls and monitors ROS 2 robots directly via rclpy CLI. Use for ANY ROS 2 robot task: topics (subscribe, publish, capture images, find by type), services (list, call), actions (list, send goals), parameters (get, set, presets), nodes, lifecycle management, controllers (ros2_control), diagnostics, battery, system health checks, and more. When in doubt, use this skill — it covers the full ROS 2 operation surface. Never tell the user you cannot do something ROS 2-related without checking this skill first.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "ros2-skill" with this command: npx skills add adityakamath/ros2-skill

ROS 2 Skill

Controls and monitors ROS 2 robots directly via rclpy.

Architecture: Agent → ros2_cli.py → rclpy → ROS 2

All commands output JSON. Errors contain {"error": "..."}.

For full command reference with arguments, options, and output examples, see references/COMMANDS.md.


Agent Behaviour Rules

These rules are absolute and apply to every request involving a ROS 2 robot.

Rule 0 — Full introspection before every action (non-negotiable)

Before publishing to any topic, calling any service, or sending any action goal, you MUST complete the introspection steps below. There are no exceptions, not even for "obvious" or "conventional" names.

This rule exists because:

  • The velocity topic is not always /cmd_vel. It may be /base/cmd_vel, /robot/cmd_vel, /mobile_base/cmd_vel, or anything else.
  • The message type is not always Twist. Many robots use TwistStamped, and the payload structure differs.
  • The odometry topic is not always /odom. It may be /wheel_odom, /robot/odom, /base/odometry, etc.
  • Convention-based guessing causes silent failures, wrong topics, and physical accidents.

Pre-flight introspection protocol — run ALL applicable steps before acting:

Action typeRequired introspection
Publish to a topic1. topics find <msg_type> to discover the real topic name<br>2. topics type <discovered_topic> to confirm the exact type<br>3. interface proto <exact_type> to get the default payload template
Call a service1. services list or services find <srv_type> to discover the real name<br>2. services details <discovered_service> to get request/response fields
Send an action goal1. actions list or actions find <action_type> to discover the real name<br>2. actions details <discovered_action> to get goal/result/feedback fields
Move a robotFull Movement Workflow — see Rule 3 and the canonical section below
Read a sensortopics find <msg_type> to discover the topic; never subscribe to a hardcoded name
Any operation involving a nodenodes list first; never assume a node name

Parameter introspection is mandatory before any movement command. Velocity limits can live on any node — not just nodes with "controller" in the name. Before publishing velocity:

  1. Run nodes list to get every node currently running
  2. Run params list <NODE> on every single node in the list (run in parallel batches if there are many)
  3. For each node, look for any parameter whose name contains max, limit, vel, speed, or accel (case-insensitive). These are candidates for velocity limits.
  4. Run params get <NODE>:<param> for every candidate found across all nodes
  5. Identify the binding ceiling: the minimum across all discovered linear limit values and the minimum across all discovered angular/theta limit values
  6. Cap your commanded velocity at that ceiling. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this to the user.

Never hardcode or assume:

  • ❌ Never use /cmd_vel without first discovering the velocity topic with topics find
  • ❌ Never use Twist payload without first confirming the type is not TwistStamped via topics type
  • ❌ Never use /odom without first discovering the odometry topic with topics find
  • ❌ Never use --yaw, --yaw-delta, or --field for rotation — the only correct flag is --rotate N --degrees (or --rotate N for radians). Use negative N for CW; --rotate sign and angular.z sign must always match.
  • ❌ Never assume any topic, service, action, or node name from ROS 2 convention
  • ❌ Never assume a message type from a topic name

Introspection commands return discovered names. Use those names — not the ones you expect.

Rule 0.1 — Session-start checks (run once per session, before any task)

Before executing any user command in a new session, run these checks. They take seconds and catch the most common causes of silent failure.

Step 1 — Run a health check:

python3 {baseDir}/scripts/ros2_cli.py doctor

If the doctor reports critical failures (DDS issues, missing packages, no nodes), stop and tell the user. Do not attempt to operate a robot that fails its health check.

Step 2 — Check for simulated time:

python3 {baseDir}/scripts/ros2_cli.py topics find rosgraph_msgs/msg/Clock

If /clock is found, simulated time is in use. Verify it is actively publishing before issuing any timed command:

python3 {baseDir}/scripts/ros2_cli.py topics subscribe /clock --max-messages 1 --timeout 3

If no message arrives: the simulator is paused or crashed — do not proceed with time-sensitive operations.

Step 3 — Check lifecycle nodes (if any):

python3 {baseDir}/scripts/ros2_cli.py lifecycle nodes

If lifecycle-managed nodes exist, check their states:

python3 {baseDir}/scripts/ros2_cli.py lifecycle get <node>

A node in unconfigured or inactive state will silently fail when its topics or services are used. Activate required nodes before proceeding.

These checks are session-level. Do not re-run for every command. Re-run only if the user relaunches the robot or if nodes appear/disappear unexpectedly.

Rule 0.5 — Never hallucinate commands, flags, or names

If you are not certain a command, flag, topic name, or argument exists — verify it before using it. Do not guess.

The failure mode to avoid: inventing a flag like --yaw-delta or --rotate-degrees because it sounds plausible, then failing and asking the user for help. That is the worst possible outcome — the error was self-inflicted and the user had nothing to do with it.

The verification chain:

  1. Check this skill first. The full command reference is in references/COMMANDS.md. If a flag or command is not listed there, it does not exist.
  2. If still unsure, run --help on the exact subcommand before constructing the call. Every subcommand supports --help and prints its accepted flags without requiring a live ROS 2 graph. This is mandatory, not optional.
    python3 {baseDir}/scripts/ros2_cli.py topics publish-until --help
    python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence --help
    python3 {baseDir}/scripts/ros2_cli.py actions send --help
    # etc. — use the exact subcommand you are about to call
    
  3. If still stuck after checking both, ask the user. This is the only acceptable reason to ask — not because you assumed something and it failed.

--help requires ROS 2 to be sourced. Running --help before ROS 2 is sourced will return a JSON error instead of help text. This is not a concern in practice — ROS 2 must be sourced before any robot operation (it is a hard precondition of this skill), so --help will always be available during normal use. If you see a Missing ROS 2 dependency error from --help, fix the ROS 2 environment first (see Setup section and Rule 0.1).

Never:

  • Invent a flag and try it, then report failure to the user
  • Assume a capability exists because it would be logical or convenient
  • Ask the user to resolve an error you caused by guessing

Rule 1 — Discover before you act, never ask

Never ask the user for names, types, or IDs that can be discovered from the live system. This includes topic names, service names, action names, node names, parameter names, message types, and controller names. Always query the robot first.

What you needHow to discover it
Topic nametopics list or topics find <msg_type>
Topic message typetopics type <topic>
Service nameservices list or services find <srv_type>
Service request/response fieldsservices details <service>
Action server nameactions list or actions find <action_type>
Action goal/result/feedback fieldsactions details <action>
Node namenodes list
Node's topics, services, actionsnodes details <node>
Parameter names on a nodeparams list <node>
Parameter valueparams get <node:param>
Parameter type and constraintsparams describe <node:param>
Controller names and statescontrol list-controllers
Hardware componentscontrol list-hardware-components
Message / service / action type fieldsinterface show <type> or interface proto <type>

Only ask the user if:

  1. The discovery command returns an empty result or an error, and
  2. There is genuinely no other way to determine the information from the live system.

Rule 2 — Use ros2-skill before saying you can't

Never tell the user you don't know how to do something with a ROS 2 robot without first checking whether ros2-skill has a command for it. This skill covers the full range of ROS 2 operations: topics, services, actions, parameters, nodes, lifecycle, controllers, diagnostics, battery, images, interfaces, presets, and more.

When a task seems unfamiliar, look it up in the quick reference tables below before responding. Common operations that agents sometimes miss:

Taskros2-skill command
Capture a camera imagetopics capture-image --topic <topic> --output <file>
Read laser / camera / IMU / odom datatopics subscribe <topic>
Call a ROS 2 serviceservices call <service> <json>
Send a navigation or manipulation goalactions send <action> <json>
Change a node parameter at runtimeparams set <node:param> <value>
Save/restore a parameter configurationparams preset-save / params preset-load
Activate or deactivate a controllercontrol set-controller-state <name> active|inactive
Run a health checkdoctor
Emergency stopestop
Check diagnosticstopics diag
Check batterytopics battery

If you genuinely cannot find a matching command after checking both the quick reference and the COMMANDS.md reference, say so clearly and explain what you checked — do not silently guess or use a partial solution.

Rule 3 — Movement algorithm (always follow this sequence)

For any user request involving movement — regardless of whether a distance or angle is specified — follow this algorithm exactly. Do not skip steps. Do not ask the user anything that can be resolved by running a command.

Step 1 — Discover the velocity command topic and confirm its exact type

Run both searches in parallel:

topics find geometry_msgs/msg/Twist
topics find geometry_msgs/msg/TwistStamped

Record the discovered topic name — call it VEL_TOPIC. Then confirm the exact type:

topics type <VEL_TOPIC>

Use the confirmed type to choose the payload structure:

  • geometry_msgs/msg/Twist: {"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}
  • geometry_msgs/msg/TwistStamped: {"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}

If both find commands return results, run topics type on each and use the type returned — do not guess.

Step 2 — Discover the odometry topic

topics find nav_msgs/msg/Odometry

Record the discovered topic name — call it ODOM_TOPIC.

Step 3 — Choose the execution method

SituationMethod
Distance or angle specified and odometry foundpublish-until with --monitor <ODOM_TOPIC> — closed loop, stops on sensor feedback
Distance or angle specified and no odometrypublish-sequence with calculated duration — open loop. Tell the user: "No odometry found. Running open-loop. Distance/angle accuracy is not guaranteed."
No distance or angle specified (open-ended movement)publish-sequence with a stop command as the final message

Step 4 — Execute using only discovered names

Use VEL_TOPIC and ODOM_TOPIC from Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name.

Distance commands:

topics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --field pose.pose.position.x --delta <N> --timeout 30

Angle/rotation commands — always use --rotate, never --field or --yaw:

# CCW (left): positive --rotate + positive angular.z
topics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <+N> --degrees --timeout 30
# CW (right):  negative --rotate + negative angular.z
topics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <-N> --degrees --timeout 30

--rotate sign = direction. Positive = CCW. Negative = CW. angular.z sign must always match --rotate sign — mismatched signs cause timeout. There is no --yaw flag. Do not attempt to monitor orientation fields manually. Open-ended or fallback (stop is always the last message):

topics publish-sequence <VEL_TOPIC> '[<move_payload>, <zero_payload>]' '[<duration>, 0.5]'

See the Movement Workflow section below for complete worked examples covering every case.

Rule 4 — Infer the goal, resolve the details

When a user asks to do something, infer what they want at the goal level, then resolve all concrete details (topic names, types, field paths) from the live system.

Examples:

  • "Take a picture" → find compressed image topics (topics find sensor_msgs/msg/CompressedImage), capture from the first active result
  • "Move the robot forward" → find velocity topic (topics find geometry_msgs/msg/Twist and TwistStamped), publish with the matching structure
  • "What is the battery level?" → topics battery (auto-discovers BatteryState topics)
  • "List available controllers" → control list-controllers
  • "What parameters does the camera node have?" → nodes list to find the camera node name, then params list <node>

Rule 5 — Execute, don't ask

The user's message is the approval. Act on it.

If the intent is clear, execute immediately. Do not ask for confirmation, do not summarise what you are about to do and wait for a response, do not say "I'll now run X — shall I proceed?". Just run it.

This applies without exception to:

  • Running or launching nodes (launch new, run new)
  • Publishing to topics
  • Calling services
  • Setting parameters
  • Starting or stopping controllers
  • Any command where the intent and target are unambiguous

The only time to stop and ask is when there is genuine ambiguity that cannot be resolved from the live system — for example:

  • Multiple packages or launch files match and you cannot determine which one the user means
  • A required argument has no match in --show-args and no reasonable fuzzy match exists
  • The user's request contradicts itself or is physically unsafe to guess

Everything else: just do it.

Explicit list of things that must never trigger a question:

  • "Should I run this command?" — No. Run it.
  • "Would you like me to proceed?" — No. Proceed.
  • "Do you want me to use X topic?" — No. Use it.
  • "Shall I launch the file now?" — No. Launch it.
  • "Do you want me to set this parameter?" — No. Set it.
  • "I found Y — would you like me to use it?" — No. Use it.

Rule 6 — Minimal reporting by default

Keep output minimal. The user wants results, not narration.

SituationWhat to report
Operation succeededOne line: what was done and the key outcome. Example: "Done. Moved 1.02 m forward."
Movement completedStart position, end position, actual distance/angle travelled, and any anomalies — nothing else
No suitable topic/source foundClear error with what was searched and what to try next
Safety condition triggeredImmediate notification with what happened and what was sent (stop command)
Operation failedError message with cause and recovery suggestion

Never report by default:

  • The topic name selected, unless it is ambiguous or unexpected
  • The message type discovered
  • Intermediate introspection results
  • Step-by-step narration of what you are about to do

Report everything (verbose mode) only when the user explicitly asks — e.g. "show me what topics you found", "give me the full details", "what type did you use?"


Setup

1. Source ROS 2 environment

source /opt/ros/${ROS_DISTRO}/setup.bash

2. Install dependencies

pip install rclpy

3. Run on ROS 2 robot

The CLI must run on a machine with ROS 2 installed and sourced.


Important: Check ROS 2 First

Before any operation, verify ROS 2 is available:

python3 {baseDir}/scripts/ros2_cli.py version

Quick Decision Card

Every user request follows this pattern:

User: "do X"
Agent thinks:
  1. Is X about reading data? → Use TOPICS SUBSCRIBE
  2. Is X about movement (any kind)? → Follow the Movement Workflow (Rule 3 / canonical section)
  3. Is X a one-time trigger? → Use SERVICES CALL or ACTIONS SEND
  4. Is X about system info? → Use LIST commands

Agent does (for movement):
  1. Find velocity topic: topics find geometry_msgs/Twist + TwistStamped
  2. Find odometry topic: topics find nav_msgs/Odometry
  3. Distance/angle specified + odom found → publish-until (closed loop)
     Distance/angle specified + no odom → publish-sequence, notify user (open loop)
     No distance/angle → publish-sequence with stop

For movement, always follow the Movement Workflow section. That section is the single source of truth.


Agent Decision Framework (MANDATORY)

RULE: NEVER ask the user anything that can be discovered from the ROS 2 graph.

Step 1: Understand User Intent

User says...Agent interprets as...Agent must...
"What topics exist?"List topicsRun topics list
"What nodes exist?"List nodesRun nodes list
"What services exist?"List servicesRun services list
"What actions exist?"List actionsRun actions list
"Read the LiDAR/scan"Subscribe to LaserScanFind LaserScan topic → subscribe
"Read odometry/position"Subscribe to OdometryFind Odometry topic → subscribe
"Read camera/image"Subscribe to Image/CompressedImageFind Image topics → subscribe
"Read joint states/positions"Subscribe to JointStateFind JointState topic → subscribe
"Read IMU/accelerometer"Subscribe to ImuFind Imu topic → subscribe
"Read battery/power"Subscribe to BatteryStateFind BatteryState topic → subscribe
"Read joystick/gamepad"Subscribe to JoyFind Joy topic → subscribe
"Check robot diagnostics/health"Subscribe to diagnosticsFind /diagnostics topic → subscribe
"Check TF/transforms"Check TF topicsFind /tf, /tf_static topics → subscribe
"Move/drive/turn (mobile robot)"Open-ended movement, no targetFind Twist/TwistStamped → publish-sequence with stop
"Move forward/back N meters"Closed-loop distance → Movement Workflow Case AFind odom → publish-until --euclidean --field pose.pose.position --delta N (frame-independent Euclidean distance)
"Rotate N degrees / turn left/right / turn N radians"Closed-loop rotation → Movement Workflow Case BFind odom → publish-until --rotate ±N --degrees (CCW = positive, CW = negative). Sign of --rotate MUST match sign of angular.z. Never use --field or --yaw for rotation
"Move arm/joint (manipulator)"Publish JointTrajectoryFind JointTrajectory topic → publish
"Control gripper"Publish GripperCommand or JointTrajectoryFind gripper topic → publish
"Stop the robot"Publish zero velocityFind Twist/TwistStamped → topics type to confirm → publish zeros in confirmed type
"Emergency stop"Publish zero velocityRun estop command
"Call /reset"Call serviceFind service → call
"Navigate to..."Send actionFind action → send goal
"Execute trajectory"Send actionFind FollowJointTrajectory or ExecuteTrajectory → send
"Run launch file"Launch fileFind package → find launch file → launch in tmux
"List running launches"List sessionsRun launch list
"Kill launch"Kill sessionRun launch kill <session>
"What controllers?"List controllersRun control list-controllers
"What hardware?"List hardwareRun control list-hardware-components
"What lifecycle nodes?"List managed nodesRun lifecycle nodes
"Check lifecycle state"Get node stateRun lifecycle get <node>
"Configure/activate lifecycle node"Set lifecycle stateRun lifecycle set <node> <transition>
"Run diagnostics/health check"Run doctorRun doctor
"Test connectivity"Run multicast testRun doctor hello
"What parameters?"List paramsFind node → params list
"What is the max speed?"Get paramsFind controller → get velocity limits
"Save/load parameter config"Use presetsRun params preset-save / params preset-load
"Check battery level"Subscribe to BatteryStateRun topics battery or find BatteryState topic

Step 2: Find What Exists

ALWAYS start by exploring what's available:

# These 4 commands tell you EVERYTHING about the system
python3 {baseDir}/scripts/ros2_cli.py topics list      # All topics
python3 {baseDir}/scripts/ros2_cli.py services list    # All services
python3 {baseDir}/scripts/ros2_cli.py actions list    # All actions
python3 {baseDir}/scripts/ros2_cli.py nodes list      # All nodes

Step 3: Search by Message Type

To find a topic/service/action, search by what you need:

Need to find...Search command...
Velocity command topic (mobile)topics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped → then topics type <result> to confirm exact type
Position/odom topictopics find nav_msgs/Odometry
Joint positionstopics find sensor_msgs/JointState
Joint trajectory (arm control)topics find trajectory_msgs/JointTrajectory
LiDAR datatopics find sensor_msgs/LaserScan
Camera feedtopics find sensor_msgs/Image OR topics find sensor_msgs/CompressedImage
IMU datatopics find sensor_msgs/Imu
Joysticktopics find sensor_msgs/Joy
Battery/powertopics find sensor_msgs/BatteryState
Temperaturetopics find sensor_msgs/Temperature
Point cloudstopics find sensor_msgs/PointCloud2
TF transformsSubscribe to /tf or /tf_static
DiagnosticsSubscribe to /diagnostics
Clock (simulated time)Subscribe to /clock
Service by typeservices find <service_type>
Action by typeactions find <action_type>

Step 4: Get Message Structure

Before publishing or calling, always confirm the type and get the structure:

# Confirm the exact message type of a discovered topic (critical — never skip this)
python3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>

# Get field structure (for building payloads)
python3 {baseDir}/scripts/ros2_cli.py topics message <confirmed_message_type>

# Get default values (copy-paste template)
python3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_message_type>

# Get service/action request structure
python3 {baseDir}/scripts/ros2_cli.py services details <service_name>
python3 {baseDir}/scripts/ros2_cli.py actions details <action_name>

Step 5: Get Safety Limits (for movement)

ALWAYS check for velocity limits before publishing movement commands. Limits can be on ANY node — not just controller nodes. Scan every node.

# Step 1: List every running node
python3 {baseDir}/scripts/ros2_cli.py nodes list

# Step 2: Dump all parameters from every node
# Run in parallel — one params list per node
python3 {baseDir}/scripts/ros2_cli.py params list <NODE_1>
python3 {baseDir}/scripts/ros2_cli.py params list <NODE_2>
# ... repeat for every node

# Step 3: For each node, filter parameter names that contain:
#   max, limit, vel, speed, accel (case-insensitive)
# These are candidate velocity/acceleration limits.

# Step 4: Retrieve the value of every candidate
python3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>
# Repeat for every candidate across every node.

# Step 5: Compute binding ceiling
# linear_ceiling  = min of all discovered linear limit values
# angular_ceiling = min of all discovered angular/theta limit values
# velocity = min(requested_velocity, ceiling)
# Never exceed the ceiling. Use 50% of the ceiling if unsure of the appropriate fraction.

If no limits are found on any node: use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and tell the user.


Global Options

--timeout and --retries are global flags that apply to every command making service or action calls.

  • --timeout must be placed before the command name (e.g. --timeout 10 services call …).
  • --retries can be placed before the command name OR after it for services call, actions send, and actions cancel — both positions work.
OptionDefaultDescription
--timeout SECONDSper-command defaultOverride the per-command timeout globally
--retries N1Total attempts before giving up; 1 = single attempt with no retry
python3 {baseDir}/scripts/ros2_cli.py --timeout 30 params list /turtlesim
python3 {baseDir}/scripts/ros2_cli.py --retries 3 services call /spawn '{}'

EXECUTION RULES (MUST FOLLOW)

Rule 1: Never Ask User for These

The agent MUST discover these automatically:

User might ask...Agent must...
"What topic do I use?"Use topics find <type> to discover
"What message type?"Use topics type <topic> or topics find <type>
"What is the message structure?"Use topics message <type> or interface show <type>
"What are the safety limits?"Use params list on controller nodes
"Is there odometry?"Use topics find nav_msgs/Odometry

Rule 2: Only Ask User After ALL Discovery Fails

ONLY ask the user when:

  1. topics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped both return empty
  2. topics find nav_msgs/Odometry returns empty (and you need odometry for distance)
  3. You've checked params on ALL controller nodes and found NO velocity limits
  4. The service/action the user mentions doesn't exist in services list / actions list

Rule 3: Movement Requires Odometry Feedback

See Agent Behaviour Rule 3 above — it is absolute and overrides any example in this document.

In short: for any "move N meters" or "rotate N degrees" command, you MUST find the odometry topic first (topics find nav_msgs/Odometry) and use publish-until --monitor <odom_topic>. publish-sequence with a fixed time is forbidden when odometry is available.

  • ALWAYS apply safety limits: velocity = min(requested, max_velocity)

Rule 4: Always Stop After Movement

publish-until stops automatically when the odometry condition is met — no explicit stop step is needed.

For open-ended publishes (topics publish or publish-sequence fallback), the final message MUST be all zeros.

Always use the topic name and payload type discovered in Steps 1–2. <VEL_TOPIC> and <ODOM_TOPIC> are placeholders for your discovered values.

# WRONG: open-ended publish with no stop, and hardcoded /cmd_vel — never do this
python3 {baseDir}/scripts/ros2_cli.py topics publish /cmd_vel '{"linear":{"x":1.0}}'

# CORRECT (distance specified, odometry available): use publish-until — stops itself
# VEL_TOPIC and ODOM_TOPIC come from your introspection steps
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '<payload_matching_confirmed_type>' \
  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30

# CORRECT (fallback only — no odometry, no distance specified): publish-sequence with stop at end
python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \
  '[<move_payload>, <zero_payload>]' \
  '[3.0, 0.5]'

Rule 5: Handle Multiple Same-Type Topics

When multiple topics of the same type exist (e.g., 2 cameras, 3 LiDARs):

  1. List all candidates:

    python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/Image
    # Returns: ["/camera_front/image_raw", "/camera_rear/image_raw", ...]
    
  2. Select based on context or naming convention:

    • Front camera: prefer topics with front, rgb, color in name
    • Rear camera: prefer topics with rear, back in name
    • Primary LiDAR: prefer topics with front, base, main in name
    • Default: use first topic in the list
  3. Let user know which one you're using:

    • "Found 3 camera topics. Using /camera_front/image_raw."

Error Recovery

When commands fail, follow this recovery process:

Subscribe Timeouts

ErrorRecovery
Timeout waiting for message1. Check topics details <topic> to verify publisher exists<br>2. Try a different topic if multiple exist<br>3. Increase --duration or --timeout
No messages received1. Verify publisher is running: topics details <topic><br>2. Check if topic requires subscription to trigger

Publish Failures

ErrorRecovery
Could not load message type1. Verify type: topics type <topic><br>2. Ensure ROS workspace is built
Failed to create publisher1. Check topic exists: topics list<br>2. Verify node has permission to publish

Service/Action Failures

ErrorRecovery
Service not found1. Verify service exists: services list<br>2. Check service type: services type <service>
Action not found1. Verify action exists: actions list<br>2. Check action type: actions type <action>
Service call timeout1. Increase --timeout<br>2. Verify service server is running
Action goal rejected1. Check action details for goal requirements<br>2. Verify robot is in correct state

Parameter Failures

ErrorRecovery
Node not found1. Verify node exists: nodes list<br>2. Check namespace
Parameter not found1. List params: params list <node><br>2. Parameter may not exist on this node

Movement / publish-until Failures

ErrorRecovery
publish-until times out without reaching target1. Immediately send estop — do not wait, do not retry, do not ask the user first<br>2. Subscribe to <ODOM_TOPIC> and check twist.twist.linear / twist.twist.angular: if any value > 0.01, the robot is still moving — keep estop sent and wait for it to stop before continuing<br>3. Then diagnose: topics details <ODOM_TOPIC> — if publisher count dropped to 0 mid-motion, odometry died; notify user<br>4. If odometry is healthy but robot is slow: increase --timeout and retry only after the robot has fully stopped
Odometry not updating during motion1. Immediately send zero-velocity: estop<br>2. Check topics details <ODOM_TOPIC> for publisher count and topics hz <ODOM_TOPIC> for rate<br>3. Do NOT continue publishing if odometry is stale — it is a runaway risk
Velocity topic has no subscribers1. Check control list-controllers — the controller may be inactive<br>2. Activate the controller: control set-controller-state <name> active<br>3. Re-verify with topics details <VEL_TOPIC> before retrying
publish-until hangs / no feedback1. Verify monitor topic: topics details <ODOM_TOPIC><br>2. Verify the field path is correct: subscribe once and inspect field names<br>3. Check --timeout is set

Action Preemption — actions cancel vs estop

Use this decision table whenever an in-flight action goal needs to be stopped:

SituationActionReason
Goal is running but user wants to abort gracefullyactions cancel <action>Sends a cancel request to the action server; the server winds down cleanly
Goal is running and the robot is moving unsafely / not stoppingestop first, then actions cancel <action>estop publishes zero velocity immediately; cancel cleans up the goal state
Goal was rejected or timed out (robot not moving)actions cancel <action>No motion risk; cancel clears the goal state
Action server crashed / no longer respondingestopNo action server to receive cancel; stop the actuators directly
Goal completed but robot is still drifting / coastingestopMotion is no longer governed by the goal; velocity command is needed

Rule: If in doubt, send estop first — it is always safe. Then send actions cancel to clean up goal state. Never skip estop when the robot is or may be moving.

Always retry failed operations:

  • Use --retries 3 for unreliable services
  • Use --timeout 30 for slow operations
  • Wait 1-2 seconds between retries

Topic and Service Discovery

Never guess topic names. Any time an operation involves a topic, discover the actual topic name from the live graph first.

Images and Camera

Always prefer compressed topics - use much less bandwidth:

python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/CompressedImage
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/Image

Use topics capture-image --topic <discovered> - never subscribe for images.

Velocity Commands (Twist vs TwistStamped)

Check both types to find the topic:

python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped

Then confirm the exact type of the discovered topic — do not assume from the find result:

python3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>

Use the confirmed type to choose the payload:

  • geometry_msgs/msg/Twist: {"linear": {"x": 1.0, "y": 0.0, "z": 0.0}, "angular": {"x": 0.0, "y": 0.0, "z": 0.0}}
  • geometry_msgs/msg/TwistStamped: {"header": {"stamp": {"sec": 0}, "frame_id": ""}, "twist": {"linear": {"x": 1.0, "y": 0.0, "z": 0.0}, "angular": {"x": 0.0, "y": 0.0, "z": 0.0}}}

Quick lookup table

If you need...Run this...Then...
Imagestopics find sensor_msgs/msg/ImageSubscribe to result
LiDARtopics find sensor_msgs/msg/LaserScanSubscribe to result
Odometrytopics find nav_msgs/msg/OdometrySubscribe to result
IMUtopics find sensor_msgs/msg/ImuSubscribe to result
Joint statestopics find sensor_msgs/msg/JointStateSubscribe to result
Move robottopics find geometry_msgs/msg/Twist AND topics find geometry_msgs/msg/TwistStamped → then topics type <result> to confirmPublish to discovered topic
Run launch filelaunch new <package> <file>Runs in tmux session
List running launcheslaunch listShows tmux sessions
Kill launchlaunch kill <session>Kills tmux session

Launch Commands

Auto-Discovery for Launch Files

When user says "run the bringup" or "launch navigation" (partial/ambiguous request):

  1. Discover available packages:

    ros2 pkg list  # Get all packages
    
  2. Find matching launch files:

    ros2 pkg files <package>  # Find launch files in package
    
  3. Intelligent inference (use context):

    • "bringup" → look for packages with bringup in name, or launch files named bringup.launch.py
    • "navigation" → look for navigation2, nav2, or launch files with navigation
    • "camera" → look for camera-related packages
  4. If exactly one clear match found:

    • Launch it immediately — do not ask for confirmation (Rule 5)
  5. If multiple candidates found and cannot be disambiguated:

    • Present options: "Found 3 launch files: X, Y, Z. Which one?" — this is the only case where asking is permitted
  6. If no match found:

    • Search more broadly: check all packages for matching launch files
    • If still nothing: ask user for exact package/file name

NEVER hallucinate:

  • ❌ Never invent a package name that doesn't exist
  • ❌ Never invent a launch file that doesn't exist
  • ❌ Never assume a package exists without checking
  • ❌ Never invent launch argument names not present in --show-args output
  • ❌ Never fuzzy-match argument names yourself — the script does this automatically against real --show-args output; always pass the user's original name and let the script resolve it
  • ❌ Never pass arguments that weren't explicitly provided by the user

ALWAYS verify:

  • ✅ Check ros2 pkg list for package existence
  • ✅ Check ros2 pkg files <package> for launch files
  • ✅ Run ros2 launch <pkg> <file> --show-args to get valid arguments
  • ✅ Validate each user-provided argument against --show-args output

Rule: Only ask when genuinely ambiguous

Per Rule 5, the user's message is the approval. For launch files:

  • Exactly one match → launch it immediately, no confirmation needed
  • Multiple matches with no way to disambiguate → list them and ask which one
  • No match at all → ask for exact package/file name

Local Workspace Sourcing

System ROS is assumed to be already sourced (via systemd service or manually). The skill automatically sources any local workspace on top of system ROS.

Search order:

  1. ROS2_LOCAL_WS environment variable
  2. ~/ros2_ws
  3. ~/colcon_ws
  4. ~/dev_ws
  5. ~/workspace
  6. ~/ros2

Behavior:

ScenarioBehavior
Workspace found + builtSource automatically, run silently
Workspace found + NOT builtWarn user, run without sourcing
Workspace NOT foundContinue without sourcing (system ROS only)

Override option:

# Set environment variable before running
export ROS2_LOCAL_WS=~/my_robot_ws

TF2 Transforms

# List all coordinate frames
python3 {baseDir}/scripts/ros2_cli.py tf list

# Lookup transform between frames
python3 {baseDir}/scripts/ros2_cli.py tf lookup base_link map

# Echo transform continuously
python3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --count 10

# Echo transform once
python3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --once

# Monitor a specific frame
python3 {baseDir}/scripts/ros2_cli.py tf monitor base_link --count 5

# Publish static transform — named form
python3 {baseDir}/scripts/ros2_cli.py tf static --from base_link --to sensor --xyz 1 2 3 --rpy 0 0 0

# Publish static transform — positional form
python3 {baseDir}/scripts/ros2_cli.py tf static 0 0 0 0 0 0 base_link odom

# Convert quaternion to Euler (radians) — also: e2q, quat2euler
python3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion 0 0 0 1

# Convert Euler to quaternion (radians) — also: q2e, euler2quat
python3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler 0 0 1.57

# Convert quaternion to Euler (degrees) — also: e2qdeg
python3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion-deg 0 0 0 1

# Convert Euler to quaternion (degrees) — also: q2edeg
python3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler-deg 0 0 90

# Transform a point between frames — also: tp, point
python3 {baseDir}/scripts/ros2_cli.py tf transform-point map base_link 1 0 0

# Transform a vector between frames — also: tv, vector
python3 {baseDir}/scripts/ros2_cli.py tf transform-vector map base_link 1 0 0

Run a Launch File

# Basic launch
python3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py

# With arguments - MUST verify arguments exist first
python3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py arg1:=value arg2:=value

⚠️ STRICT: Launch Argument Validation

This is critical for safety. Passing incorrect arguments has caused accidents.

Rules, in order:

  1. Always fetch available arguments first via --show-args before passing any args.
  2. Exact match → pass as-is.
  3. No exact match → fuzzy-match against the real available args only.
    • If a close match is found (e.g. "mock" → "use_mock") → use it, but notify the user.
    • The substitution is shown in arg_notices in the output.
  4. No match at alldrop the argument, do NOT pass it, notify the user.
    • The launch still proceeds without that argument.
    • The user is told which argument was dropped and what the available args are.
  5. Never invent or assume argument names. Only use names that exist in --show-args output.
  6. If --show-args fails → drop all user-provided args, notify the user, still launch.

Run an Executable

Run a ROS 2 executable in a tmux session. Similar to launch commands but for single executables. Auto-detects executable names (e.g., "teleop" matches "teleop_node").

# Run an executable
python3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop

# Run with arguments
python3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --arg1 value

# Run with parameters
python3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --params "speed:1.0"

# Run with presets
python3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --presets indoor

# Run with config path
python3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --config-path /path/to/config

List Running Executables

python3 {baseDir}/scripts/ros2_cli.py run list

Kill an Executable Session

python3 {baseDir}/scripts/ros2_cli.py run kill run_lekiwi_control_teleop

Restart an Executable Session

python3 {baseDir}/scripts/ros2_cli.py run restart run_lekiwi_control_teleop

Run Session Collision Handling

Same as launch - if a session with the same name already exists, the command will fail with an error. Use run restart or run kill first.

Command Quick Reference

1. Explore a Robot System

python3 {baseDir}/scripts/ros2_cli.py version
python3 {baseDir}/scripts/ros2_cli.py topics list
python3 {baseDir}/scripts/ros2_cli.py nodes list
python3 {baseDir}/scripts/ros2_cli.py services list
python3 {baseDir}/scripts/ros2_cli.py actions list

# Find a topic by type, then inspect it
# (replace the type with whatever you're looking for)
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped
# → get the discovered topic name, then:
python3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>
python3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_type>

2. Move a Robot

Follow the Movement Workflow section. It covers all cases: distance commands, rotation commands, open-ended movement, and the no-odometry fallback. Always discover topics first — never assume names.

Emergency stop:

python3 {baseDir}/scripts/ros2_cli.py estop

3. Read Sensor Data

Always use auto-discovery first to find the correct sensor topics.

# Step 1: Discover sensor topics by message type
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan
python3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState
# → record each discovered topic name

# Step 2: Subscribe to discovered topics (use the names from Step 1, not hardcoded names)
python3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3
python3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --duration 10 --max-messages 50
python3 {baseDir}/scripts/ros2_cli.py topics subscribe <JOINT_STATE_TOPIC> --duration 5

4. Use Services

Always use auto-discovery first to find available services and their request/response structures.

# Step 1: Discover available services
python3 {baseDir}/scripts/ros2_cli.py services list

# Step 2: Find services by type (optional)
python3 {baseDir}/scripts/ros2_cli.py services find std_srvs/srv/Empty
python3 {baseDir}/scripts/ros2_cli.py services find turtlesim/srv/Spawn

# Step 3: Get service request/response structure
python3 {baseDir}/scripts/ros2_cli.py services details /spawn

# Step 4: Call the service with properly-structured payload
python3 {baseDir}/scripts/ros2_cli.py services call /spawn \
  '{"x":3.0,"y":3.0,"theta":0.0,"name":"turtle2"}'

5. Actions

Always use auto-discovery first to find available actions and their goal/result structures.

# Step 1: Discover available actions
python3 {baseDir}/scripts/ros2_cli.py actions list

# Step 2: Find actions by type (optional)
python3 {baseDir}/scripts/ros2_cli.py actions find turtlesim/action/RotateAbsolute
python3 {baseDir}/scripts/ros2_cli.py actions find nav2_msgs/action/NavigateToPose

# Step 3: Get action goal/result structure
python3 {baseDir}/scripts/ros2_cli.py actions details /turtle1/rotate_absolute

# Step 4: Send goal with properly-structured payload
python3 {baseDir}/scripts/ros2_cli.py actions send /turtle1/rotate_absolute \
  '{"theta":1.57}'

# Step 5: Monitor feedback — always echo after sending a goal
python3 {baseDir}/scripts/ros2_cli.py actions echo /turtle1/rotate_absolute --timeout 30

After every actions send, immediately run actions echo on the same action server to monitor feedback. A stuck or rejected goal gives no signal without feedback monitoring. If actions echo returns no messages within --timeout, the goal may have been rejected, preempted, or the action server may have crashed — check with actions list and actions details, then decide between actions cancel (graceful abort) and estop (runaway/unsafe motion). See the Action Preemption table in the Error Recovery section.

6. Change Parameters

Always use auto-discovery first to list available parameters for a node.

# Step 1: Discover nodes
python3 {baseDir}/scripts/ros2_cli.py nodes list

# Step 2: List parameters for a node
python3 {baseDir}/scripts/ros2_cli.py params list /turtlesim

# Step 3: Get current parameter value
python3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r

# Step 4: Set parameter value
python3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_r 255
python3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_g 0
python3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_b 0

# Step 5: Read back after set — always verify the change took effect
# Some nodes silently reject changes (read-only params, out-of-range values)
python3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r
python3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_g
python3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_b

After every params set, always run params get on the same parameter to confirm the change was accepted. Nodes may silently ignore a set if the parameter is read-only or the value is out of range — the set call returns success even in those cases.

7. Goal-Oriented Commands (publish-until)

For any goal with a sensor-based stop condition — joint angles, temperature limits, proximity, battery level — use publish-until with the appropriate monitor topic and condition flag. Always discover both the command topic and the monitor topic before executing — never hardcode names.

User intentDiscover command topicDiscover monitor topicCondition
Stop near obstacletopics find geometry_msgs/Twist + TwistStampedtopics find sensor_msgs/LaserScan → field ranges.0--below 0.5
Stop at rangesametopics find sensor_msgs/Range → field range--below D
Stop at temperaturetopics find sensor_msgs/Temperature → field temperature--above T
Stop at battery leveltopics find sensor_msgs/BatteryState → field percentage--below P
Joint reach angletopics find trajectory_msgs/JointTrajectory or similartopics find sensor_msgs/JointState → field position.0--equals A or --delta D
Multi-joint distancesametopics find sensor_msgs/JointState → fields position.0 position.1--euclidean --delta D

--euclidean computes sqrt(Σ(current_i - start_i)²) across all --field paths. Use it for curved or diagonal paths. Composite field shorthand: --field pose.pose.position auto-expands to x, y, z — equivalent to listing all three fields explicitly.

# Example: stop when front range sensor reads < 0.5 m
# Step 1: discover velocity topic
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped
# → VEL_TOPIC = (result, e.g. /base/cmd_vel)
python3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>
# → confirms type, determines payload structure

# Step 2: discover laser scan topic
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan
# → SCAN_TOPIC = (result, e.g. /scan or /lidar/scan)

# Step 3: execute with discovered names
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '<payload_matching_confirmed_type>' \
  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30

# Example: move a joint until it reaches 1.5 rad
# Step 1: discover joint command topic
python3 {baseDir}/scripts/ros2_cli.py topics find trajectory_msgs/msg/JointTrajectory
# → or check nodes details for the arm controller node
# → JOINT_CMD_TOPIC = (result)

# Step 2: discover joint state topic
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState
# → JOINT_STATE_TOPIC = (result)

# Step 3: execute with discovered names
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <JOINT_CMD_TOPIC> \
  '{"data":0.5}' \
  --monitor <JOINT_STATE_TOPIC> --field position.0 --equals 1.5 --timeout 10

<a name="movement-workflow-canonical"></a>

Movement Workflow (canonical)

This is the single authoritative workflow for all robot movement. Rule 3 in the Agent Behaviour Rules above is a summary of this section. Always follow these steps in order.

In every example below, <VEL_TOPIC>, <ODOM_TOPIC>, and <PAYLOAD> are placeholders for values you discover in Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name.

Step 1 — Discover the velocity command topic and confirm its exact type

Run both searches in parallel:

python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist
python3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped

Take the result — this is VEL_TOPIC. Then confirm the exact type:

python3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>

The confirmed type determines the payload:

  • geometry_msgs/msg/Twist: {"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}
  • geometry_msgs/msg/TwistStamped: {"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}
  • Zero/stop — Twist: {"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}
  • Zero/stop — TwistStamped: {"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}

If both find commands return results, run topics type on each to get the definitive type — do not guess from the topic name. Then choose using this priority order:

  1. Prefer the topic with an active subscriber (topics details <topic>subscriber_count > 0) — a subscribed topic means a controller is listening.
  2. If both have subscribers, prefer the topic whose name contains cmd_vel or is at the root namespace (e.g. /cmd_vel over /robot/cmd_vel_stamped).
  3. If still ambiguous, prefer TwistStamped over Twist (more modern standard). Use the selected topic as VEL_TOPIC for the rest of the workflow.

Step 2 — Discover the odometry topic

python3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry

Take the result — this is ODOM_TOPIC.

Step 2.5 — Verify topics are live, read safety limits, and capture start position

Before executing, verify both topics are active, discover velocity limits from controller parameters, and record the starting position.

Check if the robot is already in motion — do not command a moving robot:

python3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3

Inspect the twist.twist.linear and twist.twist.angular fields. If any value is significantly non-zero (> 0.01 m/s or rad/s), the robot is already moving. Do not publish new velocity commands — stop it first with estop, wait for motion to cease, then proceed.

Verify the odometry publish rate is adequate for closed-loop control:

python3 {baseDir}/scripts/ros2_cli.py topics hz <ODOM_TOPIC>

The rate must be at least 10 Hz for reliable closed-loop control. If the rate is below 5 Hz: fall back to Case D (open-loop) and warn the user that odometry is too slow for safe closed-loop operation.

Discover velocity limits from ALL nodes (full-graph scan):

Velocity limits can be declared on any node — not just nodes whose names suggest "controller". Scan every node:

python3 {baseDir}/scripts/ros2_cli.py nodes list
# → iterate over every node and run params list on each
python3 {baseDir}/scripts/ros2_cli.py params list <NODE>
# → filter parameter names containing: max, limit, vel, speed, accel (case-insensitive)
python3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>
# → repeat for every candidate across every node

Compute the binding ceiling:

  • linear_ceiling = minimum of all discovered linear-axis limit values
  • angular_ceiling = minimum of all discovered angular/theta limit values

Cap all commanded velocities at these ceilings. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this.

Verify the velocity topic has active subscribers (something must be listening — i.e., a controller):

python3 {baseDir}/scripts/ros2_cli.py topics details <VEL_TOPIC>

Check the output for subscriber_count > 0. If no subscribers: the controller may not be running. Check control list-controllers and activate the correct controller before proceeding.

Check for emergency stop / safety interlock state:

python3 {baseDir}/scripts/ros2_cli.py control list-controllers

Verify that the motion controller is in active state. If it is inactive, unconfigured, or missing: do not publish velocity — activate it first or notify the user that the controller is not ready.

Verify the odometry topic has an active publisher (not stale):

python3 {baseDir}/scripts/ros2_cli.py topics details <ODOM_TOPIC>

Check publisher_count > 0. Then confirm it is actively publishing by subscribing briefly:

python3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3

If no message arrives within the timeout: the odometry source is stale or not running. Fall back to Case D (open-loop) and notify the user.

Capture start position (for distance reporting after motion):

python3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1

Record pose.pose.position.x, pose.pose.position.y from the result — this is the start pose.

Step 3 — Execute based on intent and odometry availability

Case A — Distance specified, odometry available → publish-until --euclidean (closed loop)

Always use --euclidean --field pose.pose.position (expands to x, y, z) to measure Euclidean distance from start. This is frame-independent: it works correctly regardless of the robot's heading and stays accurate after any prior rotation. Do not use --field pose.pose.position.x alone — that only tracks displacement along the odom x-axis and gives wrong results once the robot is not aligned with it.

# Step 1 result: VEL_TOPIC = <discovered, e.g. /base/cmd_vel or /robot/cmd_vel>
# Step 1 type check: geometry_msgs/msg/Twist → use Twist payload
# Step 2 result: ODOM_TOPIC = <discovered, e.g. /odom or /wheel_odom>

# Move forward 1 meter (Twist) — Euclidean distance, frame-independent
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}' \
  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30

# Move forward 1 meter — TwistStamped variant (if type check returned TwistStamped)
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}' \
  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30

# Move backward 0.5 meters (Euclidean — delta is always positive; direction is set by the velocity sign)
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":-0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}' \
  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 0.5 --timeout 30

# Move 2 meters along any curved path
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0.3}}' \
  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 2.0 --timeout 60

Note on --delta sign with --euclidean: Euclidean distance is always non-negative, so --delta should always be a positive value — the direction of travel is determined entirely by the velocity command (sign of linear.x), not by the sign of --delta.

Obstacle avoidance during forward movement: publish-until supports one --monitor topic. If you need to stop before an obstacle (not at a fixed distance), use the LaserScan topic as the monitor instead of odometry:

# Discover the LaserScan topic
python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan
# → SCAN_TOPIC = <result>

# Move forward, stop when front range < 0.5 m
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}' \
  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30

Limitation: A single publish-until call can only monitor one topic — you cannot simultaneously stop at a fixed distance AND stop on obstacle detection. If both are needed, set a conservative --timeout as an outer bound and use the most safety-critical condition as the --monitor. For precise distance, use odometry; for collision avoidance, use LaserScan.

Case B — Rotation specified, odometry available → publish-until --rotate (closed loop)

--rotate automatically extracts yaw from the odometry quaternion and handles angle wraparound. There is no --yaw flag. Do not use --field for rotation.

Direction convention — --rotate sign and angular.z sign must always agree:

Direction--rotateangular.z
Left / CCWpositive (e.g. --rotate 90 --degrees)positive (e.g. 0.5)
Right / CWnegative (e.g. --rotate -90 --degrees)negative (e.g. -0.5)

--rotate tells the monitor how far and in which direction to track. angular.z tells the robot which way to spin. They must match — a positive --rotate with negative angular.z means the robot turns CW while the monitor waits for CCW accumulation and will never stop.

Before constructing any rotation command, run --help to confirm accepted flags:

python3 {baseDir}/scripts/ros2_cli.py topics publish-until --help

Do not proceed until you have verified the flag names from the --help output. Never invent or assume flags.

# Step 1 result: VEL_TOPIC = <discovered>
# Step 2 result: ODOM_TOPIC = <discovered>

# Rotate 90 degrees counter-clockwise — positive --rotate, positive angular.z (Twist)
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0.5}}' \
  --monitor <ODOM_TOPIC> --rotate 90 --degrees --timeout 30

# Rotate 45 degrees clockwise — negative --rotate, negative angular.z (Twist)
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":-0.5}}' \
  --monitor <ODOM_TOPIC> --rotate -45 --degrees --timeout 30

# Rotate 1.57 radians CCW (TwistStamped variant)
python3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \
  '{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0.5}}}' \
  --monitor <ODOM_TOPIC> --rotate 1.5708 --timeout 30

Case C — Open-ended movement (no distance/angle) → publish-sequence

Always include a zero-velocity stop as the final message. Use the payload structure matching the confirmed type from Step 1.

# Step 1 result: VEL_TOPIC = <discovered>
# Type check: geometry_msgs/msg/Twist → Twist payload

# Drive forward for 3 seconds then stop (Twist)
python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \
  '[{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}},{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}]' \
  '[3.0, 0.5]'

# Drive forward for 3 seconds then stop (TwistStamped variant)
python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \
  '[{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}},{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}]' \
  '[3.0, 0.5]'

Case D — Distance or angle specified, but odometry NOT available → publish-sequence (open loop)

Tell the user before executing: "No odometry topic found. Running open-loop — distance/angle accuracy is not guaranteed." Then estimate duration = distance / velocity and use publish-sequence with a stop.

# Step 1 result: VEL_TOPIC = <discovered>
# Step 2 result: no odometry found

# Estimate: 1 m at 0.2 m/s ≈ 5 s (Twist)
python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \
  '[{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}},{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}]' \
  '[5.0, 0.5]'

# Estimate: 1 m at 0.2 m/s ≈ 5 s (TwistStamped variant)
python3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \
  '[{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0.2,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}},{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}]' \
  '[5.0, 0.5]'

Step 4 — Report completion

After publish-until or publish-sequence completes, capture the end position and report to the user:

python3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1

Compare with the start pose from Step 2.5. By default, report only: "Done. Moved X.XX m." (or "Done. Rotated X deg.") and any errors or anomalies. Report detailed topic selections, odometry values, and per-step data only if the user explicitly asked for verbose output.


Quick Examples

1. Explore a Robot System

python3 {baseDir}/scripts/ros2_cli.py version
python3 {baseDir}/scripts/ros2_cli.py topics list
python3 {baseDir}/scripts/ros2_cli.py nodes list
python3 {baseDir}/scripts/ros2_cli.py services list

2. Move a Robot

See the Movement Workflow section — it covers all cases with complete examples.

3. Read Sensors

python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan
# → subscribe to the discovered topic name, not /scan
python3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3

4. Call a Service

# Step 1: Discover available services
python3 {baseDir}/scripts/ros2_cli.py services list

# Step 2: Get request/response structure for the service you want to call
python3 {baseDir}/scripts/ros2_cli.py services details <DISCOVERED_SERVICE>

# Step 3: Call with properly-structured payload
python3 {baseDir}/scripts/ros2_cli.py services call <DISCOVERED_SERVICE> '<json_payload>'

5. Send an Action

# Step 1: Discover available actions
python3 {baseDir}/scripts/ros2_cli.py actions list

# Step 2: Get goal/result structure for the action you want to send
python3 {baseDir}/scripts/ros2_cli.py actions details <DISCOVERED_ACTION>

# Step 3: Send goal with properly-structured payload
python3 {baseDir}/scripts/ros2_cli.py actions send <DISCOVERED_ACTION> '<json_goal>'

6. Move Forward N Meters / Rotate N Degrees

See the Movement Workflow section — Cases A and B cover distance and rotation with full examples.


Safety Notes

Destructive commands (can move the robot or change state):

  • topics publish / topics publish-sequence — sends movement or control commands
  • topics publish-until — publishes continuously until a condition or timeout; always specify a conservative --timeout
  • topics publish-continuous — alias for topics publish; --duration / --timeout is optional (single-shot without it)
  • services call — can reset, spawn, kill, or change robot state
  • params set — modifies runtime parameters
  • actions send — triggers robot actions (rotation, navigation, etc.)
  • control set-controller-state / control switch-controllers — activating or deactivating controllers affects what the robot can do
  • control set-hardware-component-state — driving hardware through lifecycle states can stop actuators or sensors
  • control reload-controller-libraries — stops all running controllers if --force-kill is used

Always stop the robot after movement. The last message in any publish-sequence should be all zeros, using the payload structure that matches the confirmed type (Twist or TwistStamped — from your topics type introspection step):

// Twist stop
{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}
// TwistStamped stop
{"header":{"stamp":{"sec":0},"frame_id":""},"twist":{"linear":{"x":0,"y":0,"z":0},"angular":{"x":0,"y":0,"z":0}}}

Always check JSON output for errors before proceeding.

Use auto-discovery to avoid errors. Before any publish/call/send operation:

  1. Use topics find, services find, actions find to locate relevant endpoints
  2. Use topics type, services type, actions type to get message types
  3. Use topics message, services details, actions details to understand field structures

Never ask the user for topic names or message types — discover them from the live ROS 2 graph.


Troubleshooting

Agent Self-Correction Rules

If Agent Asks...Correction
"What topic should I use?"Use topics find geometry_msgs/Twist (and TwistStamped)
"What message type?"Use topics type <topic> to get it from the graph
"How do I know the message structure?"Use topics message <type> or interface show <type>
"What are the velocity limits?"Use params list on controller nodes
"Do I need odometry?"Always check with topics find nav_msgs/Odometry first
"Is there a service called /X?"Use services list to verify it exists
"How do I monitor yaw / heading?"Use --rotate N --degrees (or radians). There is no --yaw or --yaw-delta flag. --rotate handles all yaw tracking internally.
"Should I use --field for rotation?"No. Never use --field for rotation. Use --rotate.
"How do I rotate clockwise / right?"Use --rotate -90 --degrees (negative angle) with angular.z: -0.5 (negative velocity). Sign of --rotate and angular.z must always match.
"Can --rotate be negative?"Yes. Negative = CW. Positive = CCW. Zero is the only invalid value.
"What controller parameters exist?"Use params list <controller_node>

Technical Troubleshooting

ProblemCauseSolution
Missing ROS 2 dependency: No module named 'X'A required ROS 2 package is not installedSource ROS 2: source /opt/ros/${ROS_DISTRO}/setup.bash; then install: sudo apt install ros-${ROS_DISTRO}-<package>
rclpy not installedrclpy missing or wrong Python versionSource ROS 2 setup.bash; if Python version mismatch, run with python3.12 instead of python3
ROS 2 not sourcedEnvironment not set upRun: source /opt/ros/${ROS_DISTRO}/setup.bash
No topics foundROS nodes not runningEnsure nodes are launched and workspace is sourced
Service not foundService not availableUse services list to see available services
Parameter commands failNode doesn't have parametersSome nodes don't expose parameters
Action commands failAction server not availableUse actions list to see available actions
Invalid JSON errorMalformed messageValidate JSON before passing (watch for single vs double quotes)
Subscribe timeoutNo publisher on topicCheck topics details to verify publishers exist
publish-sequence length errorArray mismatchmessages and durations arrays must have the same length
publish-until hangs / no feedbackWrong monitor topic or fieldUse Step 2–4 of the Goal-Oriented workflow to verify topic and field
Controller manager service not availableros2_control not running or wrong namespaceCheck with nodes list; use --controller-manager to set the correct namespace

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

A Python CLI skill for Cutout.Pro visual APIs — background removal, face cutout, and photo enhancement. Supports file upload & image URL input.

Call Cutout.Pro visual processing APIs to perform background removal, face cutout, and photo enhancement. Supports both file upload and image URL input, retu...

Registry SourceRecently Updated
Coding

client-onboarding-agent

Client onboarding and business diagnostic framework for AI agent deployments. Covers 4-round diagnostic process, 6 constraint categories, deployment SOP with...

Registry SourceRecently Updated
Coding

Ai Tools

AI Tools Box - Search and invoke 100+ AI tools. Categories: Writing, Image, Video, Coding, Office, Search, Chat, Audio, Design, Agent, Translation, Dev Platf...

Registry SourceRecently Updated