
Ros2 Robotics
- 241 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Implement ROS2 nodes, topics, services, and actions for robot perception, navigation, and hardware control in autonomous systems.
About
Guides building production ROS2 robotics software: package layout, node lifecycle, pub/sub messaging, hardware integration, and common patterns for perception, planning, and control on real or simulated robots.
- ROS2 nodes and launch files
- Topics, services, and actions
- Sensor and actuator drivers
- Distributed robot middleware
- Autonomous navigation patterns
Ros2 Robotics by the numbers
- 241 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #537 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill ros2-roboticsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 241 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Implement ROS2 nodes, topics, services, and actions for robot perception, navigation, and hardware control in autonomous systems.
Files
Ros2 Robotics
Identity
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
ROS2 Robotics
Patterns
Node Structure
Name
ROS2 Node Architecture
Description
Proper node structure with lifecycle management
When
Creating a new ROS2 node
Pattern
#!/usr/bin/env python3 import rclpy from rclpy.node import Node from rclpy.lifecycle import LifecycleNode, State, TransitionCallbackReturn from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy from std_msgs.msg import String from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist from typing import Optional import threading
class RobotController(LifecycleNode): """ Lifecycle-managed robot controller node.
Lifecycle states: unconfigured -> inactive -> active -> finalized Transitions: configure, activate, deactivate, cleanup, shutdown
Use lifecycle nodes for:
- Controlled startup/shutdown sequences
- Resource management
- Coordinated system bringup
"""
def __init__(self): super().__init__('robot_controller')
Declare parameters with defaults
self.declare_parameter('max_speed', 1.0) self.declare_parameter('sensor_topic', '/scan') self.declare_parameter('cmd_topic', '/cmd_vel')
These will be created in on_configure
self._laser_sub = None self._cmd_pub = None self._timer = None
self.get_logger().info('Node created (unconfigured)')
def on_configure(self, state: State) -> TransitionCallbackReturn: """Configure node resources.""" try:
Get parameters
self._max_speed = self.get_parameter('max_speed').value sensor_topic = self.get_parameter('sensor_topic').value cmd_topic = self.get_parameter('cmd_topic').value
Create QoS profile for sensors
sensor_qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=10 )
Create subscribers and publishers
self._laser_sub = self.create_subscription( LaserScan, sensor_topic, self._laser_callback, sensor_qos )
self._cmd_pub = self.create_publisher( Twist, cmd_topic, 10 )
self.get_logger().info('Configured successfully') return TransitionCallbackReturn.SUCCESS
except Exception as e: self.get_logger().error(f'Configuration failed: {e}') return TransitionCallbackReturn.FAILURE
def on_activate(self, state: State) -> TransitionCallbackReturn: """Activate node - start processing.""" self._timer = self.create_timer(0.1, self._control_loop) self.get_logger().info('Activated') return TransitionCallbackReturn.SUCCESS
def on_deactivate(self, state: State) -> TransitionCallbackReturn: """Deactivate node - stop processing.""" if self._timer: self._timer.cancel() self._timer = None
Stop robot
self._publish_stop() self.get_logger().info('Deactivated') return TransitionCallbackReturn.SUCCESS
def on_cleanup(self, state: State) -> TransitionCallbackReturn: """Clean up resources.""" self._laser_sub = None self._cmd_pub = None self.get_logger().info('Cleaned up') return TransitionCallbackReturn.SUCCESS
def on_shutdown(self, state: State) -> TransitionCallbackReturn: """Final shutdown.""" self._publish_stop() self.get_logger().info('Shutting down') return TransitionCallbackReturn.SUCCESS
def _laser_callback(self, msg: LaserScan): """Process laser scan data.""" self._latest_scan = msg
def _control_loop(self): """Main control loop - called by timer."""
Implement control logic
pass
def _publish_stop(self): """Publish zero velocity to stop robot.""" if self._cmd_pub: stop_cmd = Twist() self._cmd_pub.publish(stop_cmd)
def main(args=None): rclpy.init(args=args) node = RobotController()
Use multi-threaded executor for callbacks
executor = rclpy.executors.MultiThreadedExecutor() executor.add_node(node)
try: executor.spin() except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()
Why
Lifecycle nodes provide controlled startup and resource management
Topic Service Action
Name
Communication Patterns
Description
Topics, services, and actions for different use cases
Pattern
import rclpy from rclpy.node import Node from rclpy.action import ActionServer, ActionClient, GoalResponse, CancelResponse from rclpy.action.server import ServerGoalHandle from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from std_srvs.srv import SetBool from nav2_msgs.action import NavigateToPose
""" WHEN TO USE EACH:
TOPICS (Publisher/Subscriber):
- Continuous data streams
- Sensor data, state, transforms
- No response needed
- Examples: /cmd_vel, /odom, /scan
SERVICES (Request/Response):
- Quick, one-shot operations
- Configuration changes
- Queries
- Examples: /set_mode, /get_state
ACTIONS (Long-running with feedback):
- Tasks that take time
- Preemptable operations
- Need progress feedback
- Examples: /navigate_to_pose, /follow_path
"""
class CommunicationPatterns(Node):
def __init__(self): super().__init__('comm_patterns')
Callback groups for parallel execution
self._sensor_group = MutuallyExclusiveCallbackGroup() self._service_group = ReentrantCallbackGroup() self._action_group = ReentrantCallbackGroup()
Service server
self._enable_service = self.create_service( SetBool, 'enable_robot', self._enable_callback, callback_group=self._service_group )
Action server
self._nav_action = ActionServer( self, NavigateToPose, 'navigate_to_pose', execute_callback=self._navigate_execute, goal_callback=self._navigate_goal_callback, cancel_callback=self._navigate_cancel_callback, callback_group=self._action_group )
Action client
self._nav_client = ActionClient( self, NavigateToPose, 'navigate_to_pose' )
def _enable_callback(self, request, response): """Service callback - quick operation.""" if request.data: self._enabled = True response.success = True response.message = 'Robot enabled' else: self._enabled = False response.success = True response.message = 'Robot disabled' return response
async def _navigate_execute(self, goal_handle: ServerGoalHandle): """Action execute callback - long-running task.""" self.get_logger().info('Executing navigation goal')
feedback = NavigateToPose.Feedback()
while not self._goal_reached():
Check if canceled
if goal_handle.is_cancel_requested: goal_handle.canceled() return NavigateToPose.Result()
Update feedback
feedback.current_pose = self._get_current_pose() feedback.distance_remaining = self._distance_to_goal() goal_handle.publish_feedback(feedback)
await asyncio.sleep(0.1)
goal_handle.succeed() result = NavigateToPose.Result() return result
def _navigate_goal_callback(self, goal_request): """Accept or reject goal.""" if self._is_valid_goal(goal_request): return GoalResponse.ACCEPT return GoalResponse.REJECT
def _navigate_cancel_callback(self, goal_handle): """Accept or reject cancel request.""" return CancelResponse.ACCEPT
Why
Choosing the right communication pattern prevents design problems
Launch System
Name
Launch File Best Practices
Description
Python launch files for system bringup
Pattern
launch/robot_bringup.launch.py
from launch import LaunchDescription from launch.actions import ( DeclareLaunchArgument, IncludeLaunchDescription, GroupAction, TimerAction, OpaqueFunction, LogInfo ) from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.substitutions import ( LaunchConfiguration, PathJoinSubstitution, PythonExpression ) from launch.conditions import IfCondition, UnlessCondition from launch_ros.actions import Node, LifecycleNode, PushRosNamespace from launch_ros.substitutions import FindPackageShare from ament_index_python.packages import get_package_share_directory import os
def generate_launch_description():
Declare arguments
use_sim = DeclareLaunchArgument( 'use_sim', default_value='false', description='Use simulation instead of hardware' )
robot_name = DeclareLaunchArgument( 'robot_name', default_value='robot1', description='Unique robot name for namespacing' )
config_file = DeclareLaunchArgument( 'config_file', default_value=PathJoinSubstitution([ FindPackageShare('my_robot'), 'config', 'params.yaml' ]), description='Path to configuration file' )
Get package paths
pkg_share = get_package_share_directory('my_robot')
Define nodes
robot_state_publisher = Node( package='robot_state_publisher', executable='robot_state_publisher', name='robot_state_publisher', namespace=LaunchConfiguration('robot_name'), parameters=[{ 'robot_description': open( os.path.join(pkg_share, 'urdf', 'robot.urdf') ).read() }], output='screen' )
Lifecycle node with automatic configuration
controller_node = LifecycleNode( package='my_robot', executable='controller_node', name='controller', namespace=LaunchConfiguration('robot_name'), parameters=[LaunchConfiguration('config_file')], output='screen' )
Conditional node (only in simulation)
sim_clock = Node( package='gazebo_ros', executable='gazebo', condition=IfCondition(LaunchConfiguration('use_sim')), output='screen' )
Hardware driver (only on real robot)
hardware_driver = Node( package='my_robot_driver', executable='driver_node', condition=UnlessCondition(LaunchConfiguration('use_sim')), parameters=[LaunchConfiguration('config_file')], output='screen' )
Delayed start (wait for other nodes)
delayed_planner = TimerAction( period=5.0, # Wait 5 seconds actions=[ Node( package='nav2_planner', executable='planner_server', name='planner_server', namespace=LaunchConfiguration('robot_name'), parameters=[LaunchConfiguration('config_file')], output='screen' ) ] )
Include another launch file
sensors_launch = IncludeLaunchDescription( PythonLaunchDescriptionSource([ PathJoinSubstitution([ FindPackageShare('my_robot'), 'launch', 'sensors.launch.py' ]) ]), launch_arguments={ 'robot_name': LaunchConfiguration('robot_name') }.items() )
return LaunchDescription([ use_sim, robot_name, config_file, robot_state_publisher, controller_node, sim_clock, hardware_driver, delayed_planner, sensors_launch ])
Why
Proper launch files enable modular, configurable robot bringup
Qos Configuration
Name
QoS Configuration
Description
Quality of Service settings for reliability
Critical
Pattern
from rclpy.qos import ( QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy, QoSDurabilityPolicy, QoSLivelinessPolicy ) from rclpy.duration import Duration
""" QoS PROFILES FOR DIFFERENT USE CASES:
SENSOR DATA:
- Best effort reliability (drop if slow)
- Keep last N messages
- Volatile durability
COMMANDS:
- Reliable delivery (retry)
- Keep last 1
- Volatile durability
PARAMETERS/CONFIG:
- Reliable delivery
- Transient local (late subscribers get last)
- Keep last 1
"""
Sensor data (high frequency, tolerates drops)
SENSOR_QOS = QoSProfile( reliability=QoSReliabilityPolicy.BEST_EFFORT, history=QoSHistoryPolicy.KEEP_LAST, depth=5, durability=QoSDurabilityPolicy.VOLATILE )
Commands (must arrive)
COMMAND_QOS = QoSProfile( reliability=QoSReliabilityPolicy.RELIABLE, history=QoSHistoryPolicy.KEEP_LAST, depth=1, durability=QoSDurabilityPolicy.VOLATILE )
Parameters/state (late subscribers need last value)
PARAMETER_QOS = QoSProfile( reliability=QoSReliabilityPolicy.RELIABLE, history=QoSHistoryPolicy.KEEP_LAST, depth=1, durability=QoSDurabilityPolicy.TRANSIENT_LOCAL )
Map data (large, infrequent)
MAP_QOS = QoSProfile( reliability=QoSReliabilityPolicy.RELIABLE, history=QoSHistoryPolicy.KEEP_LAST, depth=1, durability=QoSDurabilityPolicy.TRANSIENT_LOCAL )
CRITICAL: QoS must match between publisher and subscriber
Mismatched QoS = silent connection failure!
def check_qos_compatibility(pub_qos, sub_qos): """Check if publisher and subscriber QoS are compatible."""
Reliability: publisher must be >= subscriber
if (sub_qos.reliability == QoSReliabilityPolicy.RELIABLE and pub_qos.reliability == QoSReliabilityPolicy.BEST_EFFORT): return False, "Reliability mismatch"
Durability: publisher must be >= subscriber
if (sub_qos.durability == QoSDurabilityPolicy.TRANSIENT_LOCAL and pub_qos.durability == QoSDurabilityPolicy.VOLATILE): return False, "Durability mismatch"
return True, "Compatible"
Why
QoS mismatches cause silent communication failures
Transforms
Name
TF2 Transform Management
Description
Coordinate frame transforms
Pattern
import rclpy from rclpy.node import Node from tf2_ros import TransformBroadcaster, StaticTransformBroadcaster from tf2_ros import TransformListener, Buffer from tf2_ros import TransformException from geometry_msgs.msg import TransformStamped import tf_transformations import numpy as np
class TransformManager(Node): """ TF2 Transform Best Practices:
1. Use standard frame names (base_link, odom, map) 2. Static transforms: Use static broadcaster 3. Dynamic transforms: Publish at sensor rate 4. Always check transform availability before use 5. Use lookup_transform with timeout """
def __init__(self): super().__init__('transform_manager')
Static broadcaster for fixed transforms
self._static_broadcaster = StaticTransformBroadcaster(self)
Dynamic broadcaster for moving frames
self._broadcaster = TransformBroadcaster(self)
Transform listener with buffer
self._tf_buffer = Buffer() self._tf_listener = TransformListener(self._tf_buffer, self)
Publish static transforms once
self._publish_static_transforms()
def _publish_static_transforms(self): """Publish transforms that never change."""
Sensor mount offset
t = TransformStamped() t.header.stamp = self.get_clock().now().to_msg() t.header.frame_id = 'base_link' t.child_frame_id = 'laser_link' t.transform.translation.x = 0.1 t.transform.translation.y = 0.0 t.transform.translation.z = 0.2 t.transform.rotation.w = 1.0
self._static_broadcaster.sendTransform(t)
def publish_odom_transform(self, x, y, theta): """Publish odometry transform.""" t = TransformStamped() t.header.stamp = self.get_clock().now().to_msg() t.header.frame_id = 'odom' t.child_frame_id = 'base_link'
t.transform.translation.x = x t.transform.translation.y = y t.transform.translation.z = 0.0
q = tf_transformations.quaternion_from_euler(0, 0, theta) t.transform.rotation.x = q[0] t.transform.rotation.y = q[1] t.transform.rotation.z = q[2] t.transform.rotation.w = q[3]
self._broadcaster.sendTransform(t)
def lookup_transform_safe( self, target_frame: str, source_frame: str, timeout_sec: float = 1.0 ): """Safely look up transform with error handling.""" try: transform = self._tf_buffer.lookup_transform( target_frame, source_frame, rclpy.time.Time(), timeout=rclpy.duration.Duration(seconds=timeout_sec) ) return transform except TransformException as e: self.get_logger().warning( f'Could not get transform {source_frame} -> {target_frame}: {e}' ) return None
def transform_point(self, point, source_frame, target_frame): """Transform a point between frames.""" from geometry_msgs.msg import PointStamped
point_stamped = PointStamped() point_stamped.header.frame_id = source_frame point_stamped.header.stamp = self.get_clock().now().to_msg() point_stamped.point = point
try: transformed = self._tf_buffer.transform( point_stamped, target_frame ) return transformed.point except TransformException as e: self.get_logger().error(f'Transform failed: {e}') return None
Why
Proper TF management prevents coordinate frame errors
Anti-Patterns
Topic Name Typo
Name
Typo in Topic Names
Problem
Mistyped topic name causes silent failure - no error
Solution
Use constants, remapping, and check with ros2 topic list
Blocking Callbacks
Name
Blocking in Callbacks
Problem
Long operations in callback block executor
Solution
Use async callbacks or separate threads
Qos Mismatch
Name
QoS Mismatch Between Publisher and Subscriber
Problem
Incompatible QoS causes no connection, no error
Solution
Check compatibility, use ros2 topic info --verbose
Ros2 Robotics - Sharp Edges
Topic Name Typo Causes Silent Failure
Id
topic-name-typo
Severity
critical
Summary
Mistyped topic name creates no connection and no error
Symptoms
- Publisher publishes but subscriber receives nothing
- Node appears to work but no data flows
- ros2 topic list shows separate topics
Why
ROS2 doesn't validate topic names at compile or launch time. If you type '/odom' in one place and '\odom' in another, you get two separate topics with no warning.
Common typos:
- Missing leading slash: 'cmd_vel' vs '/cmd_vel'
- Underscore vs no underscore: 'laser_scan' vs 'laserscan'
- Namespace issues: '/robot1/cmd_vel' vs '/cmd_vel'
This is the #1 debugging issue for ROS beginners.
Gotcha
Publisher uses one name
self.pub = self.create_publisher(Twist, '/cmd_vel', 10)
Subscriber has typo (no error!)
self.sub = self.create_subscription(Twist, 'cmd_vel', callback, 10)
Missing leading '/' - creates separate topic!
Both nodes run fine, but no data flows
Solution
Use constants for topic names
class Topics: CMD_VEL = '/cmd_vel' ODOM = '/odom' SCAN = '/scan'
self.pub = self.create_publisher(Twist, Topics.CMD_VEL, 10) self.sub = self.create_subscription(Twist, Topics.CMD_VEL, callback, 10)
Use remapping in launch files for flexibility
Verify with: ros2 topic list
Check connections: ros2 topic info /topic_name --verbose
QoS Mismatch Causes Silent Connection Failure
Id
qos-mismatch
Severity
critical
Summary
Publisher and subscriber QoS incompatibility prevents connection
Symptoms
- ros2 topic list shows topic exists
- ros2 topic info shows both pub and sub
- No data flows, no error messages
Why
ROS2 enforces QoS compatibility at connection time. If publisher offers less than subscriber requires, no connection.
Most common mismatch:
- Publisher: BEST_EFFORT (sensor default)
- Subscriber: RELIABLE (default for many packages)
Result: Silent failure, no error message.
Gotcha
Sensor driver publishes with BEST_EFFORT
sensor_qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, ...) self.pub = self.create_publisher(LaserScan, '/scan', sensor_qos)
Your node subscribes with default (RELIABLE)
self.sub = self.create_subscription(LaserScan, '/scan', callback, 10)
Connection fails silently!
Solution
Check QoS with verbose info
ros2 topic info /scan --verbose
Match publisher's QoS
from rclpy.qos import qos_profile_sensor_data self.sub = self.create_subscription( LaserScan, '/scan', callback, qos_profile_sensor_data # Matches sensor publishers )
Or explicitly set compatible QoS
sensor_qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=10 )
Blocking Operations in Callbacks
Id
blocking-callback
Severity
high
Summary
Long operations block all other callbacks
Symptoms
- Timer callbacks become irregular
- Subscribers stop receiving messages
- Node becomes unresponsive
Why
ROS2 executors are single-threaded by default. If one callback takes 1 second, ALL other callbacks wait.
This means:
- 100Hz control loop becomes 1Hz
- Watchdogs trigger
- Robot stops responding
Gotcha
def callback(self, msg):
This blocks the entire executor
result = self.expensive_computation(msg) # Takes 500ms
All other callbacks wait
def timer_callback(self):
Expected: 100Hz
Actual: Whenever expensive_computation finishes
self.publish_command()
Solution
Option 1: Use MultiThreadedExecutor
executor = MultiThreadedExecutor(num_threads=4)
With separate callback groups
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
self._sensor_group = MutuallyExclusiveCallbackGroup() self._compute_group = MutuallyExclusiveCallbackGroup()
self.sub = self.create_subscription( ..., callback_group=self._sensor_group ) self.timer = self.create_timer( 0.01, self.control_callback, callback_group=self._compute_group )
Option 2: Async callbacks (ROS2 Humble+)
async def callback(self, msg): result = await asyncio.to_thread(self.expensive_computation, msg)
Transform Lookup Without Timeout
Id
tf-timeout
Severity
high
Summary
Missing TF transform blocks forever
Symptoms
- Node hangs waiting for transform
- No error until Ctrl+C
- Works sometimes, hangs other times
Why
lookup_transform with default timeout blocks forever if the transform doesn't exist. This can happen:
- During startup (TF not published yet)
- If publishing node dies
- If frame name is wrong
No error is raised until you interrupt.
Gotcha
This blocks forever if transform doesn't exist
transform = self.tf_buffer.lookup_transform( 'map', 'base_link', rclpy.time.Time() # Now )
Solution
Always use timeout
try: transform = self.tf_buffer.lookup_transform( 'map', 'base_link', rclpy.time.Time(), timeout=rclpy.duration.Duration(seconds=1.0) ) except TransformException as e: self.get_logger().warning(f'Transform failed: {e}') return # Handle gracefully
Check if transform exists before lookup
if self.tf_buffer.can_transform('map', 'base_link', rclpy.time.Time()): transform = self.tf_buffer.lookup_transform(...)
Using Parameters Without Declaration
Id
parameter-not-declared
Severity
medium
Summary
get_parameter fails if parameter not declared first
Symptoms
- ParameterNotDeclaredException on startup
- Works in some nodes, fails in others
- Parameter files seem ignored
Why
ROS2 requires parameters to be declared before use. This is different from ROS1's dynamic parameters.
If you try to get an undeclared parameter, you get an exception, not a default value.
Gotcha
ROS1 style (doesn't work in ROS2)
speed = self.get_parameter('max_speed').value
Throws: ParameterNotDeclaredException
Even if max_speed is in your YAML file!
Solution
Declare parameter first
self.declare_parameter('max_speed', 1.0) # With default speed = self.get_parameter('max_speed').value
Or declare without default (requires YAML)
self.declare_parameter('max_speed')
Bulk declaration
self.declare_parameters('', [ ('max_speed', 1.0), ('min_speed', 0.1), ('topic_name', '/cmd_vel') ])
Transform Lookup at Time Zero
Id
time-zero-transform
Severity
high
Summary
Requesting latest transform can get stale data
Symptoms
- Transform is slightly behind
- Sensor data doesn't align with transform
- Inconsistent behavior with sim vs real
Why
Time(0) means "give me the latest available transform." But if you're processing a sensor message from 100ms ago, the latest TF might be 100ms newer than your data.
This causes sensor data to be in the wrong place.
Solution
Use message timestamp for transform lookup
def sensor_callback(self, msg): try:
Use sensor message timestamp
transform = self.tf_buffer.lookup_transform( 'map', msg.header.frame_id, msg.header.stamp, # Sensor timestamp, not Time(0) timeout=Duration(seconds=0.1) ) except TransformException as e: self.get_logger().warning(f'TF lookup failed: {e}')
Ros2 Robotics - Validations
Parameter Usage Without Declaration
Id
undeclared-parameter
Severity
warning
Type
regex
Pattern
- get_parameter\(['"][^'"]+['"]\)(?![\s\S]{0,200}declare_parameter)
Message
Declare parameters before using them to avoid ParameterNotDeclaredException.
Fix Action
Add self.declare_parameter('name', default_value) in __init__
Applies To
- */.py
Transform Lookup Without Timeout
Id
tf-lookup-no-timeout
Severity
warning
Type
regex
Pattern
- lookup_transform\([^)]*\)(?![\s\S]{0,50}timeout)
Message
Add timeout to lookup_transform to avoid blocking forever.
Fix Action
Add timeout=Duration(seconds=1.0) parameter
Applies To
- */.py
Potential Blocking Operation in Callback
Id
blocking-in-callback
Severity
info
Type
regex
Pattern
- def.callback.:\s[^}]time\.sleep
- def.callback.:\s[^}]requests\.
- def.callback.:\s[^}]input\(
Message
Avoid blocking operations in callbacks. Use async or separate threads.
Applies To
- */.py
Hardcoded Topic Name (No Constant)
Id
hardcoded-topic-name
Severity
info
Type
regex
Pattern
- create_publisher\([^,]+,\s*['"]/
- create_subscription\([^,]+,\s*['"]/
Message
Consider using constants or parameters for topic names.
Applies To
- */.py
Default QoS Used for Sensor Subscription
Id
default-qos-for-sensors
Severity
info
Type
regex
Pattern
- create_subscription\(.Scan.,\s['"][^'"]+['"],\s\w+,\s*\d+\)
- create_subscription\(.Image.,\s['"][^'"]+['"],\s\w+,\s*\d+\)
Message
Use sensor QoS profile for sensor topics (BEST_EFFORT reliability).
Fix Action
Use qos_profile_sensor_data or custom BEST_EFFORT profile
Applies To
- */.py
Transform Operations Without Exception Handling
Id
no-exception-handling-tf
Severity
warning
Type
regex
Pattern
- lookup_transform(?![\s\S]{0,100}try:|except)
- tf_buffer\.transform(?![\s\S]{0,100}try:|except)
Message
Wrap TF operations in try/except TransformException.
Applies To
- */.py
Lifecycle Node Missing Transition Callbacks
Id
missing-lifecycle-transitions
Severity
info
Type
regex
Pattern
- class.*LifecycleNode(?![\s\S]{0,500}on_configure)
Message
Implement lifecycle transition callbacks (on_configure, on_activate, etc.).
Applies To
- */.py
Heavy Compute with Single-Threaded Executor
Id
single-threaded-executor-compute
Severity
info
Type
regex
Pattern
- rclpy\.spin\(node\)(?![\s\S]{0,200}MultiThreadedExecutor)
Message
Consider MultiThreadedExecutor for nodes with heavy computation.
Applies To
- */.py
Missing Shutdown Handler
Id
no-shutdown-handler
Severity
info
Type
regex
Pattern
- def main.*rclpy\.spin(?![\s\S]{0,200}finally:|shutdown)
Message
Add finally block with rclpy.shutdown() for clean exit.
Applies To
- */.py