Problem: Writing ROS 2 Control Logic Takes Hours
You need a behavior tree for obstacle avoidance or a state machine for gripper control, but writing boilerplate ROS 2 nodes, handling service calls, and testing edge cases eats up 3-4 hours per feature.
You'll learn:
- How to prompt LLMs for valid ROS 2 Jazzy code
- Validation workflow to catch AI hallucinations
- When AI-generated control logic is production-ready
Time: 20 min | Level: Intermediate
Why This Works Now
ROS 2 Jazzy (2024+) has stable APIs that LLMs trained on recent data understand well. Unlike ROS 1, the rclpy/rclcpp patterns are consistent enough for GPT-4/Claude to generate functional code.
Common mistakes AI makes:
- Using deprecated lifecycle node syntax
- Wrong QoS settings for real-time control
- Missing executor spin logic in main()
- Incorrect behavior tree XML schema
Key insight: AI excels at boilerplate but needs constraints for robotics-specific requirements (timing, safety, error handling).
Solution
Step 1: Set Up Validation Environment
# Create workspace with test dependencies
mkdir -p ~/ros2_ai_ws/src
cd ~/ros2_ai_ws
colcon build
source install/setup.bash
# Install validation tools
sudo apt install ros-jazzy-behaviortree-cpp ros-jazzy-nav2-bt-navigator
pip install --break-system-packages ruff pytest-ros
Expected: Clean build, pytest-ros available for auto-testing.
Step 2: Create Structured Prompt
Don't just ask "make a behavior tree." Give constraints:
PROMPT TEMPLATE:
Generate a ROS 2 Jazzy behavior tree node in C++ that:
REQUIREMENTS:
- Subscribes to /scan (sensor_msgs/LaserScan)
- Publishes to /cmd_vel (geometry_msgs/Twist)
- Stops robot if obstacle within 0.5m
- Uses BT.CPP 4.x syntax
- Includes proper QoS (SENSOR_DATA for /scan)
- Has timeout handling (500ms max)
CONSTRAINTS:
- No deprecated APIs
- Include all #includes
- Add inline comments explaining BT logic
- Return SUCCESS/FAILURE correctly
OUTPUT FORMAT:
1. Full .cpp file
2. CMakeLists.txt snippet
3. XML tree definition
Why this works: LLMs need explicit ROS 2 version, message types, and failure modes. Generic prompts produce ROS 1-style code.
Step 3: Generate and Validate
# Save AI output to file
cat > src/obstacle_stop_bt.cpp
# (paste generated code)
# Immediate syntax check
ruff check src/obstacle_stop_bt.cpp --select=E9 # Python syntax if using rclpy
# OR for C++:
g++ -fsyntax-only -std=c++17 src/obstacle_stop_bt.cpp \
-I/opt/ros/jazzy/include
Expected: No syntax errors. If it fails, the AI likely used old syntax.
If it fails:
- Error: "rclcpp::Node::Node() no matching constructor": AI used ROS 1 pattern, regenerate with "ROS 2 Jazzy rclcpp::Node constructor with NodeOptions"
- Error: "bt_cpp/tree_node.h not found": Add to prompt: "Use #include <behaviortree_cpp/bt_factory.h>"
Step 4: Semantic Validation
AI code compiles but may have logic errors:
# test_obstacle_logic.py
import pytest
from unittest.mock import MagicMock
import rclpy
from sensor_msgs.msg import LaserScan
def test_stops_on_close_obstacle():
"""Verify robot stops when obstacle detected"""
node = ObstacleStopNode() # AI-generated node
# Simulate close obstacle
scan = LaserScan()
scan.ranges = [0.3] * 360 # 30cm all around
result = node.check_obstacle(scan)
assert result == "FAILURE", "Should fail (stop) on close obstacle"
assert node.last_cmd.linear.x == 0.0, "Should publish zero velocity"
def test_continues_when_clear():
"""Verify robot continues when path clear"""
scan = LaserScan()
scan.ranges = [2.0] * 360 # 2m clearance
result = node.check_obstacle(scan)
assert result == "SUCCESS", "Should succeed when clear"
Run tests:
pytest test_obstacle_logic.py -v
You should see: 2/2 tests passing. If not, the AI misunderstood the logic.
Step 5: Integration Test in Simulation
# Launch Gazebo with test world
ros2 launch my_robot gazebo.launch.py world:=obstacle_course
# Run AI-generated node
ros2 run my_package obstacle_stop_bt
# Manually verify in new Terminal
ros2 topic echo /cmd_vel
# Place obstacle in rviz, should see velocity drop to 0
Expected: Robot stops within 500ms of obstacle appearing at 0.5m.
Real Example: Gripper State Machine
Here's what worked for a parallel gripper controller:
Prompt:
Generate ROS 2 Jazzy action server in Python for gripper control:
- Action: control_msgs/GripperCommand
- States: IDLE, CLOSING, OPEN, ERROR
- Timeout: 3 seconds per action
- Publish /gripper/state (std_msgs/String)
- Use rclpy.executors.MultiThreadedExecutor
AI Output Quality:
- ✅ Correct action server boilerplate
- ✅ Proper state transitions
- ❌ Used
time.sleep()instead ofrclpy.Rate - ❌ Missing error state recovery logic
Fix needed:
# AI generated:
time.sleep(0.1)
# Fixed version:
rate = self.create_rate(10) # 10 Hz
rate.sleep() # Allows executor to process callbacks
Lesson: AI nails structure, needs human review for real-time requirements.
Verification Checklist
After generating any control logic:
Functional:
- Compiles with no warnings (
colcon build --cmake-args -Wall) - All topics/services exist (
ros2 node info /node_name) - Handles disconnection gracefully (kill a dependency node)
Performance:
- Control loop runs at target rate (
ros2 topic hz /cmd_vel) - CPU usage acceptable (
top, should be <30% for single node) - No memory leaks (
valgrindfor C++ nodes)
Safety:
- Stops on timeout (disconnect sensor topic)
- Publishes zero velocity on error
- Logs failures to
/rosout
What You Learned
- LLMs generate 80% correct ROS 2 code with proper prompts
- Always validate with unit tests and sim before hardware
- Biggest AI gaps: real-time constraints, error recovery, QoS tuning
When NOT to use AI:
- Safety-critical paths (autonomous driving, medical robots)
- Novel algorithms (AI regurgitates training data patterns)
- Hardware drivers (too device-specific)
When it excels:
- Standard patterns (service clients, topic bridges)
- Behavior tree scaffolding
- Test harness generation
Advanced: Automated Prompt Refinement
If initial output fails, feed errors back to AI:
# Capture build error
colcon build 2>&1 | tee build_error.txt
# Create refinement prompt
echo "Previous code failed with:
$(cat build_error.txt)
Fix the code to compile with ROS 2 Jazzy. Changes needed:
1. Use correct rclcpp namespaces
2. Fix QoS profile syntax
3. Ensure all headers included"
Pro tip: Keep a "working examples" file to paste into prompts. AI improves dramatically with reference code.
Production Gotchas
1. AI hallucinates package names
# AI might generate:
from robot_control_msgs.msg import GripperState # doesn't exist
# Verify first:
ros2 interface list | grep -i gripper
# Use actual message: control_msgs/GripperCommand
2. Outdated lifecycle patterns AI trained on Foxy-era code may use old managed node syntax. Always specify "Jazzy" in prompts.
3. Missing executor shutdown
// AI often forgets:
rclcpp::shutdown();
executor.cancel(); // Critical for clean exit
Tested on ROS 2 Jazzy Jalisco, Ubuntu 24.04, Nav2 Humble→Jazzy migration verified