| name | ros2 |
| description | ROS2 Humble development assistant - nodes, topics, services, actions, launch files, colcon builds, QoS, RViz2, Nav2, MoveIt, Gazebo, dev/deploy workflow on Ubuntu 22.04 |
| triggers | ["ros2","ros 2","rclpy","rclcpp","colcon","launch file","publisher","subscriber","topic","service server","action server","nav2","moveit","gazebo","rviz","rviz2","rosbag","tf2","urdf","xacro","qos","quality of service","symlink-install","marker","visualization","robot_description","lifecycle node","callback group","executor","composable node","slam_toolbox","cartographer","ros2_control"] |
| argument-hint | [task or question about ROS2] |
ROS2 Humble ๊ฐ๋ฐ ์คํฌ
ํ๊ฒฝ ์ ๋ณด (ROS2 Humble ๊ธฐ์ค)
- Distro: ROS2 Humble (Ubuntu 22.04 LTS)
- ๊ฒฝ๋ก:
/opt/ros/humble/
- Python: 3.10 / C++: g++ 11.4 / CMake: 3.22
- DDS: FastRTPS (๊ธฐ๋ณธ), CycloneDDS ์ง์
- ์ฃผ์ ์คํ: Nav2, MoveIt2, Gazebo, cartographer, rosbridge, tf2
ํ๊ฒฝ ์์ฑ
source /opt/ros/humble/setup.bash
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
์ํฌ์คํ์ด์ค ์์ฑ
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash
package.xml (Python ํจํค์ง)
<?xml version="1.0"?>
<package format="3">
<name>my_pkg</name>
<version>0.0.1</version>
<description>My ROS2 package</description>
<maintainer email="user@example.com">User</maintainer>
<license>Apache-2.0</license>
<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>geometry_msgs</depend>
<build_depend>ament_cmake</build_depend>
<buildtool_depend>ament_cmake</buildtool_depend>
<exec_depend>ament_cmake_python</exec_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
setup.py (Python ํจํค์ง)
from setuptools import setup
package_name = 'my_pkg'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/my_launch.py']),
],
install_requires=['setuptools'],
zip_safe=True,
entry_points={
'console_scripts': [
'my_node = my_pkg.my_node:main',
],
},
)
Python ๋
ธ๋ ํจํด
Publisher + Timer
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MyPublisher(Node):
def __init__(self):
super().__init__('my_publisher')
self.pub = self.create_publisher(String, 'topic', 10)
self.timer = self.create_timer(0.5, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello {self.i}'
self.pub.publish(msg)
self.get_logger().info(f'Published: {msg.data}')
self.i += 1
def main():
rclpy.init()
node = MyPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
Subscriber
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MySubscriber(Node):
def __init__(self):
super().__init__('my_subscriber')
self.sub = self.create_subscription(String, 'topic', self.callback, 10)
def callback(self, msg):
self.get_logger().info(f'Received: {msg.data}')
def main():
rclpy.init()
node = MySubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
Service Server
from rclpy.node import Node
from std_srvs.srv import SetBool
import rclpy
class MyServiceServer(Node):
def __init__(self):
super().__init__('my_service_server')
self.srv = self.create_service(SetBool, 'my_service', self.handle_request)
def handle_request(self, request, response):
self.get_logger().info(f'Request: {request.data}')
response.success = True
response.message = 'OK'
return response
Service Client (async)
import rclpy
from rclpy.node import Node
from std_srvs.srv import SetBool
class MyServiceClient(Node):
def __init__(self):
super().__init__('my_service_client')
self.client = self.create_client(SetBool, 'my_service')
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().warn('Service not available...')
def send_request(self, value: bool):
req = SetBool.Request()
req.data = value
future = self.client.call_async(req)
rclpy.spin_until_future_complete(self, future)
return future.result()
Action Server
import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer
from action_msgs.msg import GoalStatus
class MyActionServer(Node):
def __init__(self):
super().__init__('my_action_server')
self._action_server = ActionServer(
self, MyAction, 'my_action', self.execute_callback)
async def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
feedback = MyAction.Feedback()
for i in range(10):
feedback.progress = float(i) / 10.0
goal_handle.publish_feedback(feedback)
await asyncio.sleep(0.1)
goal_handle.succeed()
result = MyAction.Result()
result.success = True
return result
Parameter ์ฌ์ฉ
class MyNode(Node):
def __init__(self):
super().__init__('my_node')
self.declare_parameter('my_param', 'default_value')
self.declare_parameter('speed', 1.0)
param = self.get_parameter('my_param').get_parameter_value().string_value
speed = self.get_parameter('speed').get_parameter_value().double_value
self.add_on_set_parameters_callback(self.param_callback)
def param_callback(self, params):
from rcl_interfaces.msg import SetParametersResult
return SetParametersResult(successful=True)
C++ ๋
ธ๋ ํจํด
CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(my_cpp_pkg)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
add_executable(my_node src/my_node.cpp)
ament_target_dependencies(my_node rclcpp std_msgs geometry_msgs)
install(TARGETS my_node DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})
ament_package()
Publisher (C++)
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MyPublisher : public rclcpp::Node {
public:
MyPublisher() : Node("my_publisher"), count_(0) {
pub_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
std::bind(&MyPublisher::timer_callback, this));
}
private:
void timer_callback() {
auto msg = std_msgs::msg::String();
msg.data = "Hello " + std::to_string(count_++);
pub_->publish(msg);
RCLCPP_INFO(this->get_logger(), "Published: '%s'", msg.data.c_str());
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
size_t count_;
};
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MyPublisher>());
rclcpp::shutdown();
return 0;
}
Launch ํ์ผ (Python)
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
use_sim_time = LaunchConfiguration('use_sim_time', default='false')
return LaunchDescription([
DeclareLaunchArgument('use_sim_time', default_value='false'),
Node(
package='my_pkg',
executable='my_node',
name='my_node',
output='screen',
parameters=[{
'use_sim_time': use_sim_time,
'speed': 1.5,
}],
remappings=[('/old_topic', '/new_topic')],
),
IncludeLaunchDescription(
PathJoinSubstitution([
FindPackageShare('other_pkg'), 'launch', 'other.launch.py'
]),
launch_arguments={'param': 'value'}.items(),
),
])
colcon ๋น๋ ์ํฌํ๋ก์ฐ
cd ~/ros2_ws
colcon build --symlink-install
colcon build --symlink-install --packages-select my_pkg
colcon build --symlink-install --packages-up-to my_pkg
colcon build --symlink-install --parallel-workers 4
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
colcon test --packages-select my_pkg
colcon test-result --verbose
๋น๋ ์บ์ ์ ๋ฆฌ
rm -rf build/ install/ log/
colcon build --symlink-install
๋๋ฒ๊น
CLI ๋๊ตฌ
ros2 topic list
ros2 topic echo /topic_name
ros2 topic hz /topic_name
ros2 topic bw /topic_name
ros2 topic info /topic_name
ros2 topic pub /topic std_msgs/msg/String "data: 'hello'"
ros2 node list
ros2 node info /node_name
ros2 service list
ros2 service type /service_name
ros2 service call /my_service std_srvs/srv/SetBool "{data: true}"
ros2 action list
ros2 action info /action_name
ros2 action send_goal /action nav2_msgs/action/NavigateToPose "{}"
ros2 param list
ros2 param get /node_name param_name
ros2 param set /node_name param_name value
ros2 param dump /node_name
ros2 interface show std_msgs/msg/String
ros2 interface show nav_msgs/msg/OccupancyGrid
rqt_graph
rqt
ros2 bag record -a
ros2 bag record /topic1 /topic2
ros2 bag play my_bag/
ros2 bag info my_bag/
TF2 ํจํด
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster, Buffer, TransformListener
from geometry_msgs.msg import TransformStamped
import tf2_ros
import tf2_geometry_msgs
class TFNode(Node):
def __init__(self):
super().__init__('tf_node')
self.br = TransformBroadcaster(self)
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
self.timer = self.create_timer(0.1, self.broadcast_tf)
def broadcast_tf(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = 'robot'
t.transform.translation.x = 1.0
t.transform.translation.y = 0.0
t.transform.translation.z = 0.0
t.transform.rotation.w = 1.0
self.br.sendTransform(t)
def lookup_transform(self, target, source):
try:
return self.tf_buffer.lookup_transform(
target, source, rclpy.time.Time())
except tf2_ros.LookupException as e:
self.get_logger().error(f'TF error: {e}')
return None
QoS ํ๋กํ์ผ
from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy
sensor_qos = QoSProfile(
reliability=QoSReliabilityPolicy.BEST_EFFORT,
history=QoSHistoryPolicy.KEEP_LAST,
depth=1,
durability=QoSDurabilityPolicy.VOLATILE,
)
state_qos = QoSProfile(
reliability=QoSReliabilityPolicy.RELIABLE,
history=QoSHistoryPolicy.KEEP_LAST,
depth=1,
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
)
self.pub = self.create_publisher(Image, '/camera/image', sensor_qos)
Nav2 ํตํฉ
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
from geometry_msgs.msg import PoseStamped
import rclpy
def main():
rclpy.init()
navigator = BasicNavigator()
navigator.waitUntilNav2Active()
goal = PoseStamped()
goal.header.frame_id = 'map'
goal.header.stamp = navigator.get_clock().now().to_msg()
goal.pose.position.x = 2.0
goal.pose.position.y = 1.0
goal.pose.orientation.w = 1.0
navigator.goToPose(goal)
while not navigator.isTaskComplete():
feedback = navigator.getFeedback()
result = navigator.getResult()
if result == TaskResult.SUCCEEDED:
print('๋ชฉํ ๋๋ฌ!')
elif result == TaskResult.FAILED:
print('๋ค๋น๊ฒ์ด์
์คํจ')
Nav2 ์คํ
ros2 launch nav2_bringup tb3_simulation_launch.py
ros2 launch nav2_bringup bringup_launch.py map:=/path/to/map.yaml
Gazebo ์๋ฎฌ๋ ์ด์
ros2 launch gazebo_ros gazebo.launch.py
ros2 run gazebo_ros spawn_entity.py \
-entity my_robot \
-file /path/to/robot.urdf
ros2 launch my_pkg robot_sim.launch.py
URDF/Xacro ๋ก๋ด ๊ธฐ์
<?xml version="1.0"?>
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:property name="base_radius" value="0.2"/>
<link name="base_link">
<visual>
<geometry><cylinder radius="${base_radius}" length="0.1"/></geometry>
</visual>
<collision>
<geometry><cylinder radius="${base_radius}" length="0.1"/></geometry>
</collision>
<inertial>
<mass value="5.0"/>
<inertia ixx="0.1" iyy="0.1" izz="0.1" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<gazebo>
<plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so">
<ros><namespace>/my_robot</namespace></ros>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.4</wheel_separation>
<wheel_diameter>0.1</wheel_diameter>
</plugin>
</gazebo>
</robot>
์ปค์คํ
๋ฉ์์ง/์๋น์ค/์ก์
ํจํค์ง ๊ตฌ์กฐ
my_interfaces/
โโโ msg/
โ โโโ MyMsg.msg
โโโ srv/
โ โโโ MySrv.srv
โโโ action/
โ โโโ MyAction.action
โโโ CMakeLists.txt
โโโ package.xml
๋ฉ์์ง ์ ์
# MyMsg.msg
std_msgs/Header header
float64 value
string label
int32[] data_array
์๋น์ค ์ ์
# MySrv.srv
float64 input_x
float64 input_y
---
float64 result
bool success
string message
์ก์
์ ์
# MyAction.action
# Goal
float64 target
---
# Result
bool success
float64 final_value
---
# Feedback
float32 progress
CMakeLists.txt (์ธํฐํ์ด์ค ํจํค์ง)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MyMsg.msg"
"srv/MySrv.srv"
"action/MyAction.action"
DEPENDENCIES std_msgs
)
๋ผ์ดํ์ฌ์ดํด ๋
ธ๋
import rclpy
from rclpy.lifecycle import LifecycleNode, State, TransitionCallbackReturn
class MyLifecycleNode(LifecycleNode):
def __init__(self):
super().__init__('my_lifecycle_node')
def on_configure(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info('Configuring...')
return TransitionCallbackReturn.SUCCESS
def on_activate(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info('Activating...')
return TransitionCallbackReturn.SUCCESS
def on_deactivate(self, state: State) -> TransitionCallbackReturn:
return TransitionCallbackReturn.SUCCESS
def on_cleanup(self, state: State) -> TransitionCallbackReturn:
return TransitionCallbackReturn.SUCCESS
def on_shutdown(self, state: State) -> TransitionCallbackReturn:
return TransitionCallbackReturn.SUCCESS
ํ
์คํ
import pytest
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
@pytest.fixture(autouse=True)
def ros_init():
rclpy.init()
yield
rclpy.shutdown()
def test_publisher():
node = Node('test_node')
pub = node.create_publisher(String, 'test_topic', 10)
assert pub is not None
msg = String()
msg.data = 'test'
pub.publish(msg)
node.destroy_node()
from launch import LaunchDescription
from launch_ros.actions import Node as LaunchNode
import launch_testing
@pytest.mark.launch_test
def generate_test_description():
return LaunchDescription([
LaunchNode(package='my_pkg', executable='my_node', name='test_node'),
launch_testing.actions.ReadyToTest(),
])
ํ
์คํธ ์คํ
cd ~/ros2_ws
colcon test --packages-select my_pkg
colcon test-result --verbose
python3 -m pytest src/my_pkg/test/ -v
rosdep ์์กด์ฑ ์ค์น
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
์์ฃผ ์ฐ๋ ๋ฉ์์ง ํ์
| ์ฉ๋ | ๋ฉ์์ง ํ์
|
|---|
| ๋ฌธ์์ด | std_msgs/msg/String |
| ์ ์/์ค์ | std_msgs/msg/Int32, Float64 |
| 2D ํฌ์ฆ | geometry_msgs/msg/Pose2D |
| 3D ํฌ์ฆ | geometry_msgs/msg/PoseStamped |
| ์๋ ๋ช
๋ น | geometry_msgs/msg/Twist |
| ์ด๋ฏธ์ง | sensor_msgs/msg/Image |
| ํฌ์ธํธํด๋ผ์ฐ๋ | sensor_msgs/msg/PointCloud2 |
| IMU | sensor_msgs/msg/Imu |
| ๋ ์ด์ ์ค์บ | sensor_msgs/msg/LaserScan |
| ์ง๋ | nav_msgs/msg/OccupancyGrid |
| ๊ฒฝ๋ก | nav_msgs/msg/Path |
| ์ค๋๋ฉํธ๋ฆฌ | nav_msgs/msg/Odometry |
| ์กฐ์ธํธ ์ํ | sensor_msgs/msg/JointState |
| ์ง๋จ | diagnostic_msgs/msg/DiagnosticArray |
DDS ํ๊ฒฝ๋ณ์
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export ROS_LOCALHOST_ONLY=1
export ROS_DOMAIN_ID=42
ํธ๋ฌ๋ธ์ํ
| ์ฆ์ | ์์ธ | ํด๊ฒฐ |
|---|
ros2: command not found | ํ๊ฒฝ ๋ฏธ์์ฑ | source /opt/ros/humble/setup.bash |
| ํ ํฝ ์ ๋ณด์ | ๋๋ฉ์ธ ID ๋ถ์ผ์น | ROS_DOMAIN_ID ํ์ธ |
| ๋น๋ ์คํจ | ์์กด์ฑ ๋๋ฝ | rosdep install --from-paths src |
| QoS ํธํ์ฑ ๊ฒฝ๊ณ | Publisher/Subscriber QoS ๋ถ์ผ์น | Reliability, Durability ๋ง์ถ๊ธฐ |
| TF ์๋ฌ | ํ์์คํฌํ ๋ถ์ผ์น | use_sim_time ํ๋ผ๋ฏธํฐ ํ์ธ |
| ๋
ธ๋ ์ ์ฃฝ์ | Ctrl+C ๋ฏธ์ฒ๋ฆฌ | rclpy.spin() ๊ฐ์ธ๊ธฐ with try/except |
| colcon ๋น๋ ๋๋ฆผ | ๋ณ๋ ฌ ๋น๋ ์๋จ | --parallel-workers N |
๋น ๋ฅธ ์ฐธ์กฐ ๋ช
๋ น
source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
ros2 pkg create --build-type ament_python my_pkg --dependencies rclpy std_msgs
ros2 pkg create --build-type ament_cmake my_cpp_pkg --dependencies rclcpp std_msgs
colcon build --symlink-install --packages-select my_pkg && source install/setup.bash
ros2 run my_pkg my_node
ros2 node list && ros2 topic list && ros2 service list
๊ณ ๊ธ ํจํด (20๊ฐ ์์ด์ ํธ ์ฐ๊ตฌ ๊ฒฐ๊ณผ)
Python ๊ณ ๊ธ ํจํด
ROS2 Humble / Python 3.10. ๊ธฐ๋ณธ ํํ ๋ฆฌ์ผ์ ์๋ ์ค์ ํจํด๋ง ์๋ก.
1. MultiThreadedExecutor + CallbackGroup
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
class ConcurrentNode(Node):
def __init__(self):
super().__init__('concurrent_node')
srv_group = MutuallyExclusiveCallbackGroup()
scan_group = ReentrantCallbackGroup()
ctrl_group = MutuallyExclusiveCallbackGroup()
self.create_service(SetBool, 'enable', self._enable_cb, callback_group=srv_group)
self.create_subscription(LaserScan, '/scan', self._scan_cb, 10, callback_group=scan_group)
self.create_timer(0.02, self._control_loop, callback_group=ctrl_group)
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node); executor.spin()
2. async ์๋น์ค ํธ์ถ (์ฝ๋ฐฑ ๋ด๋ถ)
์ฝ๋ฐฑ ์์์ spin_until_future_complete() โ ๋ฐ๋๋ฝ. async def + await ์ฌ์ฉ.
class AsyncServiceNode(Node):
def __init__(self):
super().__init__('async_svc')
g = ReentrantCallbackGroup()
self.cli = self.create_client(SetBool, 'upstream', callback_group=g)
self.create_service(Trigger, 'do_work', self._handle, callback_group=g)
async def _handle(self, req, res):
future = self.cli.call_async(SetBool.Request(data=True))
await future
res.success = bool(future.result() and future.result().success)
return res
3. ApproximateTimeSynchronizer
header.stamp๊ฐ ์๋ N๊ฐ ํ ํฝ์ ์๊ฐ ์ค์ฐจ ๋ฒ์ ๋ด์์ ๋๊ธฐํ.
import message_filters
from sensor_msgs.msg import Image, LaserScan, Imu
class FusionNode(Node):
def __init__(self):
super().__init__('fusion_node')
subs = [
message_filters.Subscriber(self, Image, '/camera/image_raw'),
message_filters.Subscriber(self, LaserScan, '/scan'),
message_filters.Subscriber(self, Imu, '/imu/data'),
]
self.ts = message_filters.ApproximateTimeSynchronizer(subs, queue_size=20, slop=0.05)
self.ts.registerCallback(self._fused_cb)
def _fused_cb(self, img: Image, scan: LaserScan, imu: Imu):
self.get_logger().info('synced')
4. sim_time ์ฒ๋ฆฌ
from rclpy.parameter import Parameter
class SimAwareNode(Node):
def __init__(self):
super().__init__('sim_node', parameter_overrides=[
Parameter('use_sim_time', Parameter.Type.BOOL, True)
])
self.create_timer(0.5, self._wait_clock)
def _wait_clock(self):
if self.get_clock().now().nanoseconds == 0:
self.get_logger().warn('Waiting for /clock...'); return
self.get_logger().info('Clock ready')
ros2 run my_pkg my_node --ros-args -p use_sim_time:=true
ros2 bag play my.bag --clock
5. Transient Local QoS (ROS1 latched ๋์ฒด)
from rclpy.qos import QoSProfile, DurabilityPolicy, ReliabilityPolicy, HistoryPolicy
def latched_qos(depth=1) -> QoSProfile:
return QoSProfile(depth=depth, durability=DurabilityPolicy.TRANSIENT_LOCAL,
reliability=ReliabilityPolicy.RELIABLE, history=HistoryPolicy.KEEP_LAST)
pub = node.create_publisher(String, '/robot_description', latched_qos())
sub = node.create_subscription(String, '/robot_description', cb, latched_qos())
Quick Reference
| ํจํด | ํต์ฌ | ์ฃผ์ |
|---|
| ๋ณ๋ ฌ ์ฝ๋ฐฑ | MultiThreadedExecutor + ReentrantCallbackGroup | executor ์์ด๋ ์ง๋ ฌ |
| ์ฝ๋ฐฑ ์ง๋ ฌํ | MutuallyExclusiveCallbackGroup | ์๋น์ค/์ํ ๋ณ๊ฒฝ์ฉ |
| async ์๋น์ค ํธ์ถ | async def + await future | SingleThreaded์์ ๋ฐ๋๋ฝ |
| ํ์์คํฌํ ๋๊ธฐํ | ApproximateTimeSynchronizer(subs, queue_size, slop) | header.stamp ํ์ |
| sim_time ํ์ฑํ | Parameter('use_sim_time', ..., True) | /clock ์์ผ๋ฉด ํ์ด๋จธ ์ ์ง |
| rosbag ์๊ณ ์ฌ์ | ros2 bag play --clock | use_sim_time๊ณผ ํจ๊ป ์ฌ์ฉ |
| ๋ณด์กด ํ ํฝ | DurabilityPolicy.TRANSIENT_LOCAL | ์์ชฝ QoS ์ผ์น ํ์ |
C++ ๊ณ ๊ธ ํจํด
1. rclcpp Components โ ๋ฐํ์ ๋ก๋ ๊ฐ๋ฅํ ๋
ธ๋
#include "my_pkg/my_component.hpp"
#include "rclcpp_components/register_node_macro.hpp"
namespace my_pkg {
MyComponent::MyComponent(const rclcpp::NodeOptions & options)
: Node("my_component", options)
{
pub_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
timer_ = this->create_timer(std::chrono::seconds(1), [this]() {
pub_->publish(std_msgs::msg::String().set__data("hello"));
});
}
}
RCLCPP_COMPONENTS_REGISTER_NODE(my_pkg::MyComponent)
ros2 run rclcpp_components component_container &
ros2 component load /ComponentManager my_pkg my_pkg::MyComponent
2. MultiThreadedExecutor + CallbackGroup
reentrant_cb_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);
exclusive_cb_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
rclcpp::SubscriptionOptions opts;
opts.callback_group = reentrant_cb_;
sub_ = this->create_subscription<Msg>("topic", 10, callback, opts);
timer_ = this->create_timer(100ms, timer_cb, exclusive_cb_);
rclcpp::executors::MultiThreadedExecutor exec(rclcpp::ExecutorOptions(), 4);
exec.add_node(node); exec.spin();
3. ParameterEventHandler โ ์๊ฒฉ ํ๋ผ๋ฏธํฐ ๊ฐ์
#include "rclcpp/parameter_event_handler.hpp"
param_handler_ = std::make_shared<rclcpp::ParameterEventHandler>(this);
handle1_ = param_handler_->add_parameter_callback("speed",
[this](const rclcpp::Parameter & p) {
RCLCPP_INFO(get_logger(), "speed = %s", p.value_to_string().c_str());
});
handle2_ = param_handler_->add_parameter_callback("speed", cb, "/driver_node");
4. LifecycleNode C++
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "rclcpp_lifecycle/lifecycle_publisher.hpp"
using CallbackReturn =
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn;
class Driver : public rclcpp_lifecycle::LifecycleNode {
public:
Driver() : rclcpp_lifecycle::LifecycleNode("driver") {}
CallbackReturn on_configure(const rclcpp_lifecycle::State &) override {
pub_ = this->create_publisher<std_msgs::msg::String>("out", 10);
return CallbackReturn::SUCCESS;
}
CallbackReturn on_activate(const rclcpp_lifecycle::State &) override {
pub_->on_activate();
timer_ = this->create_timer(500ms, [this]() {
if (pub_->is_activated()) pub_->publish(std_msgs::msg::String());
});
return CallbackReturn::SUCCESS;
}
CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override {
timer_.reset(); pub_->on_deactivate(); return CallbackReturn::SUCCESS;
}
CallbackReturn on_cleanup(const rclcpp_lifecycle::State &) override {
pub_.reset(); return CallbackReturn::SUCCESS;
}
private:
rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
5. NodeOptions โ ์ ๋ก์นดํผ + ํ๋ผ๋ฏธํฐ ์๋ ์ ์ธ
auto node = std::make_shared<MyComponent>(rclcpp::NodeOptions{}
.use_intra_process_comms(true)
.automatically_declare_parameters_from_overrides(true)
.allow_undeclared_parameters(true));
CMakeLists.txt โ Component ๋น๋
find_package(rclcpp_components REQUIRED)
add_library(my_component SHARED src/my_component.cpp)
ament_target_dependencies(my_component rclcpp rclcpp_components std_msgs)
# ํ๋ฌ๊ทธ์ธ ๋ฑ๋ก + ๋จ๋
์คํ ๋ฐ์ด๋๋ฆฌ(my_component_node) ๋์ ์์ฑ
rclcpp_components_register_node(my_component
PLUGIN "my_pkg::MyComponent"
EXECUTABLE my_component_node)
install(TARGETS my_component my_component_node
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION lib/${PROJECT_NAME})
Service/Action ๊ณ ๊ธ ํจํด
Humble ์ ํ์ฌํญ: configure_introspection() (์๋น์ค ์ธํธ๋ก์คํ์
)์ Iron+์์๋ง ์ง์.
Humble ๋์: ros2 service list -t / ros2 service find <type> CLI ์ฌ์ฉ.
GoalStatus ์์ ๋น ๋ฅธ ์ฐธ์กฐ
| ๊ฐ | ์์ | ์๋ฏธ |
|---|
| 0 | STATUS_UNKNOWN | ์ํ ๋ฏธ์ค์ |
| 1 | STATUS_ACCEPTED | ์๋ฝ, ์คํ ๋๊ธฐ |
| 2 | STATUS_EXECUTING | ์คํ ์ค |
| 3 | STATUS_CANCELING | ์ทจ์ ์๋ฝ, ์ ๋ฆฌ ์ค |
| 4 | STATUS_SUCCEEDED | ์ฑ๊ณต ์๋ฃ |
| 5 | STATUS_CANCELED | ์ธ๋ถ ์์ฒญ์ผ๋ก ์ทจ์๋จ |
| 6 | STATUS_ABORTED | ์๋ฒ ์์ฒด ์ข
๋ฃ |
from action_msgs.msg import GoalStatus
from rclpy.action.server import GoalResponse, CancelResponse
Cancel ์ฒ๋ฆฌ โ execute_callback ํจํด
๋ฃจํ๋ง๋ค is_cancel_requested ํด๋ง. ๋ฐํ ์ ํฐ๋ฏธ๋ ๋ฉ์๋ ๋ฐ๋์ ํธ์ถ.
async def execute_callback(self, goal_handle):
try:
for step in range(goal_handle.request.total_steps):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return MyAction.Result()
goal_handle.publish_feedback(feedback)
await asyncio.sleep(0.01)
goal_handle.succeed()
return result
except Exception:
goal_handle.abort()
return MyAction.Result()
Preempt ํจํด (built-in ์์, ์ง์ ๊ตฌํ)
def handle_accepted_callback(self, goal_handle):
if self._current_goal is not None and self._current_goal.is_active:
self._current_goal.abort()
self._current_goal = goal_handle
goal_handle.execute()
ServerGoalHandle ์์ฑ: is_active, is_cancel_requested, request, goal_id
๋น๋๊ธฐ ์๋น์ค ํธ์ถ
future = self.cli.call_async(SetBool.Request(data=data))
future.add_done_callback(lambda f: self.get_logger().info(
f'success={f.result().success}, msg={f.result().message}'))
๋ฐ๋๋ฝ: SingleThreadedExecutor + ์ฝ๋ฐฑ ๋ด cli.call() = ๋ฐ๋๋ฝ.
๋๊ธฐ ํธ์ถ ํ์ ์ MultiThreadedExecutor + ReentrantCallbackGroup ํ์.
๊ฒ์ฆ๋ ์ธํฐํ์ด์ค:
std_srvs/SetBool: bool data โ bool success, string message
std_srvs/Trigger: (empty) โ bool success, string message
Multi-Goal: ๋์ ์คํ
ReentrantCallbackGroup + MultiThreadedExecutor ํ์. ์์ฐจ ํ๋ handle_accepted_callback์์ ํ์ ํ ์๋ฃ ์ queue[0].execute().
class MultiGoalServer(Node):
def __init__(self):
self._goals, self._lock = {}, threading.Lock()
self._server = ActionServer(
self, MyAction, 'my_action',
execute_callback=self.execute_cb,
goal_callback=lambda req: GoalResponse.REJECT
if len(self._goals) >= 5 else GoalResponse.ACCEPT,
handle_accepted_callback=lambda gh: (
self._goals.__setitem__(str(gh.goal_id), gh) or gh.execute()),
cancel_callback=lambda gh: CancelResponse.ACCEPT,
callback_group=ReentrantCallbackGroup(),
)
async def execute_cb(self, gh):
gid = str(gh.goal_id)
try:
for _ in range(gh.request.steps):
if gh.is_cancel_requested:
gh.canceled(); return MyAction.Result()
await asyncio.sleep(0.1)
gh.succeed()
return MyAction.Result()
finally:
with self._lock: self._goals.pop(gid, None)
Action Client: ๋น๋๊ธฐ ํ๋ฆ
send_f = self._client.send_goal_async(goal_msg, feedback_callback=self.fb_cb)
send_f.add_done_callback(lambda f: (
f.result().get_result_async().add_done_callback(self.result_cb)
if f.result().accepted else None))
def result_cb(self, future):
status = future.result().status
result = future.result().result
self._goal_handle.cancel_goal_async()
Launch ๊ณ ๊ธ ํจํด
OpaqueFunction โ ๋ฐํ์ ๋ถ๊ธฐ
๋์ฒด ํํ์์ผ๋ก ์ฒ๋ฆฌ ๋ถ๊ฐํ ๋ก์ง(๊ฒฝ๋ก ๊ณ์ฐ, ํ๊ฒฝ ๋ณ์ ๋ถ๊ธฐ)์ ์ฌ์ฉ. ์๋ช
func(context, *args, **kwargs), ๋ฐํ๊ฐ์ ๋ฐ๋์ list.
def launch_setup(context, *args, **kwargs):
robot_name = LaunchConfiguration('robot_name').perform(context)
return [Node(
package='robot_state_publisher', executable='robot_state_publisher',
parameters=[{'robot_description': open(f'/robots/{robot_name}/model.urdf').read()}],
)]
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument('robot_name', default_value='my_robot'),
OpaqueFunction(function=launch_setup),
])
ComposableNodeContainer โ ์ ๋ก ๋ณต์ฌ ์ธํธ๋ผ ํ๋ก์ธ์ค
๋์ผ ์ปจํ
์ด๋ ๋ด ๋
ธ๋๋ ์ง๋ ฌํ ์์ด ํฌ์ธํฐ๋ก ๋ฉ์์ง ์ ๋ฌ. ์์ชฝ ๋
ธ๋ ๋ชจ๋ extra_arguments=[{'use_intra_process_comms': True}] ํ์.
ComposableNodeContainer(
name='image_proc_container',
namespace='',
package='rclcpp_components',
executable='component_container_mt',
composable_node_descriptions=[
ComposableNode(
package='image_proc', plugin='image_proc::DebayerNode',
name='debayer', remappings=[('image_raw', 'camera/image_raw')],
extra_arguments=[{'use_intra_process_comms': True}],
),
ComposableNode(
package='image_proc', plugin='image_proc::RectifyColorNode',
name='rectify',
extra_arguments=[{'use_intra_process_comms': True}],
),
],
output='screen',
)
์ด๋ฏธ ์คํ ์ค์ธ ์ปจํ
์ด๋์ ์ถ๊ฐํ ๋๋ LoadComposableNodes(target_container=..., ...) ์ฌ์ฉ.
LaunchConfigurationEquals/NotEquals๋ก ์์ฑ/๋ก๋๋ฅผ ๋ถ๊ธฐํ๋ ๊ฒ์ด ํ์ค ํจํด.
GroupAction + PushRosNamespace
PushRosNamespace๋ GroupAction ๋ฒ์ ๋ด ๋ชจ๋ ๋
ธ๋์ ๋ค์์คํ์ด์ค ์๋ ์ ์ฉ. IfCondition๊ณผ ๊ฒฐํฉํด ์กฐ๊ฑด๋ถ ๊ทธ๋ฃน ๊ตฌ์ฑ.
GroupAction(
condition=IfCondition(LaunchConfiguration('launch_camera')),
actions=[
PushRosNamespace('camera'),
Node(package='usb_cam', executable='usb_cam_node_exe'),
Node(package='image_proc', executable='image_proc'),
],
)
IfCondition / UnlessCondition
'true'/'false'/'1'/'0'์ผ๋ก resolve๋๋ Substitution์ ๋ฐ์. AndSubstitution, OrSubstitution, NotSubstitution์ผ๋ก ๋ณตํฉ ์กฐ๊ฑด ๊ตฌ์ฑ.
Node(package='rviz2', executable='rviz2',
condition=IfCondition(AndSubstitution(
LaunchConfiguration('use_rviz'), LaunchConfiguration('simulation'))))
Node(package='my_pkg', executable='visualizer',
condition=UnlessCondition(LaunchConfiguration('headless')))
TimerAction โ ์ง์ฐ ์คํ
๋
ธ๋ ๊ธฐ๋ ์์ ์์กด์ฑ ํด์. period๋ ์ด ๋จ์ float.
LaunchDescription([
TimerAction(period=0.0, actions=[GroupAction([secondary_include])]),
TimerAction(period=2.0, actions=[GroupAction([primary_include])]),
])
ํ๋ผ๋ฏธํฐ YAML ๋ก๋ฉ
ํ์ผ ๊ฒฝ๋ก์ ์ธ๋ผ์ธ dict๋ฅผ parameters= ํผํฉ ๊ฐ๋ฅ. ๋์ ๊ฒฝ๋ก๋ OpaqueFunction ๋ด perform(context)๋ก resolve.
pkg_share = get_package_share_directory('my_robot_bringup')
Node(
package='my_robot', executable='my_node',
parameters=[
os.path.join(pkg_share, 'config', 'params.yaml'),
{'use_sim_time': True},
],
)
YAML ๊ตฌ์กฐ:
my_node:
ros__parameters:
update_rate: 50.0
sensor_frame: base_link
/**:
ros__parameters:
use_sim_time: true
Nav2 ์ฌ์ธต ํตํฉ
BasicNavigator ์ ์ฒด API
| ๋ฉ์๋ | ์ค๋ช
|
|---|
setInitialPose(pose) | AMCL ์ด๊ธฐ ์์น ์ค์ (PoseStamped) |
waitUntilNav2Active(navigator, localizer) | ์คํ ํ์ฑํ ๋๊ธฐ |
goToPose(goal, behavior_tree=None) | ๋จ์ผ ๋ชฉํ ํ์ |
goThroughPoses(poses, behavior_tree=None) | ์ค๊ฐ ๊ฒฝ์ ์ง ์์ฐจ ํต๊ณผ |
followWaypoints(poses) | ์จ์ดํฌ์ธํธ ๋ฆฌ์คํธ ์์ฐจ ์คํ |
followPath(path, controller_id, goal_checker_id) | ์ฌ์ ๊ณ์ฐ ๊ฒฝ๋ก ์คํ |
spin(spin_dist, time_allowance) | ์ ์๋ฆฌ ํ์ (๋ผ๋์) |
backup(backup_dist, backup_speed, time_allowance) | ํ์ง ์ด๋ |
cancelTask() | ํ์ฌ ์ก์
์ทจ์ |
isTaskComplete() | ์๋ฃ ์ฌ๋ถ ํด๋ง (100ms ํ์์์) |
getFeedback() | ์ต์ ํผ๋๋ฐฑ ๋ฉ์์ง ๋ฐํ ๋๋ None |
getResult() | TaskResult ์ด๊ฑฐํ ๋ฐํ |
getPath(start, goal, planner_id, use_start) | ๊ฒฝ๋ก ๊ณ์ฐ (๋ฏธ์คํ) |
smoothPath(path, smoother_id, max_duration) | ๊ฒฝ๋ก ์ค๋ฌด๋ฉ |
clearAllCostmaps() | ๋ก์ปฌ+๊ธ๋ก๋ฒ costmap ์ด๊ธฐํ |
clearLocalCostmap() / clearGlobalCostmap() | ๊ฐ๋ณ ์ด๊ธฐํ |
getGlobalCostmap() / getLocalCostmap() | Costmap ๋ฐํ |
changeMap(map_filepath) | ์ ์ ์ง๋ ํซ์ค์ |
lifecycleStartup() / lifecycleShutdown() | ์๋ ์์/์ข
๋ฃ |
์ต์ nav2_params.yaml ๊ตฌ์กฐ
lifecycle_manager:
ros__parameters:
use_sim_time: True
autostart: True
node_names: [map_server, amcl, planner_server, controller_server,
bt_navigator, behavior_server, waypoint_follower]
planner_server:
ros__parameters:
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 0.5
use_astar: false
controller_server:
ros__parameters:
controller_frequency: 20.0
controller_plugins: ["FollowPath"]
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
max_vel_x: 0.26
max_vel_theta: 1.0
global_costmap:
global_costmap:
ros__parameters:
resolution: 0.05
robot_radius: 0.22
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
observation_sources: scan
scan: {topic: /scan, data_type: LaserScan, clearing: True, marking: True}
inflation_layer: {plugin: "nav2_costmap_2d::InflationLayer", cost_scaling_factor: 3.0, inflation_radius: 0.55}
local_costmap:
local_costmap:
ros__parameters:
rolling_window: true
width: 3
height: 3
resolution: 0.05
global_frame: odom
plugins: ["voxel_layer", "inflation_layer"]
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
observation_sources: scan
scan: {topic: /scan, data_type: LaserScan, clearing: True, marking: True}
inflation_layer: {plugin: "nav2_costmap_2d::InflationLayer", cost_scaling_factor: 3.0, inflation_radius: 0.55}
๋น์ฉ ๊ฐ: 0=FREE, 1-252=Inflated, 253=LETHAL_OBSTACLE, 255=NO_INFORMATION
์จ์ดํฌ์ธํธ ์์ฐฐ ํจํด
import rclpy
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
from geometry_msgs.msg import PoseStamped
def make_pose(nav, x, y):
p = PoseStamped()
p.header.frame_id = 'map'
p.header.stamp = nav.get_clock().now().to_msg()
p.pose.position.x = x; p.pose.position.y = y
p.pose.orientation.w = 1.0
return p
rclpy.init()
nav = BasicNavigator()
nav.waitUntilNav2Active()
waypoints = [make_pose(nav,x,y) for x,y in [(0,0),(2,0),(2,2),(0,2)]]
while rclpy.ok():
nav.followWaypoints(waypoints)
while not nav.isTaskComplete():
fb = nav.getFeedback()
if fb: print(f'Waypoint {fb.current_waypoint + 1}/{len(waypoints)}')
if nav.getResult() != TaskResult.SUCCEEDED:
nav.clearAllCostmaps()
slam_toolbox ๋ฐ์น ์ค๋ํซ
from launch_ros.actions import Node
slam_node = Node(
package='slam_toolbox',
executable='async_slam_toolbox_node',
name='slam_toolbox',
parameters=['/path/to/mapper_params_online_async.yaml', {'use_sim_time': True}]
)
nav.waitUntilNav2Active(navigator='bt_navigator', localizer='slam_toolbox')
ros2 run nav2_map_server map_saver_cli -f ~/my_map --ros-args -p save_map_timeout:=5.0
MoveIt2 ํตํฉ
MoveIt2๋ ROS2์ ํ์ค ๋ชจ์
ํ๋๋ ํ๋ ์์ํฌ๋ค. MoveGroupInterface๋ก ํ๋๋ยท์คํ์
์ ์ดํ๊ณ , PlanningSceneInterface๋ก ์ถฉ๋ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ๋ค.
์ค์น: sudo apt install ros-humble-moveit ros-humble-moveit-servo
MoveGroupInterface ํต์ฌ ๋ฉ์๋ (C++)
| ๋ฉ์๋ | ์ค๋ช
|
|---|
MoveGroupInterface(node, "group") | ์ด๊ธฐํ (๋ฐฑ๊ทธ๋ผ์ด๋ executor ํ์) |
setPoseTarget(pose) | ๋ชฉํ ํฌ์ฆ ์ง์ |
setNamedTarget("ready") | SRDF ์ด๋ฆ ์ํ๋ก ์ด๋ |
setJointValueTarget(values) | ๊ด์ ๋ชฉํ๊ฐ ์ง์ |
plan(plan) | ๊ฒฝ๋ก ํ๋๋ (๋ฐํ: MoveItErrorCode) |
execute(plan) / move() | ์คํ / ํ๋๋+์คํ ํ ๋ฒ์ |
computeCartesianPath(wps, eef_step, jump, traj) | Cartesian ๊ฒฝ๋ก (๋ฐํ: ๋ฌ์ฑ๋ฅ ) |
getCurrentPose("link") | ํ์ฌ ์๋์ดํํฐ ํฌ์ฆ |
setMaxVelocityScalingFactor(0.5) | ์ต๋ ์๋ ๋น์จ ์ค์ |
setPathConstraints(c) / clearPathConstraints() | ๊ฒฝ๋ก ์ ์ฝ ์ค์ ยทํด์ |
getPlanningFrame() / getEndEffectorLink() | ํ๋ ์ยท๋งํฌ ์ด๋ฆ ์กฐํ |
auto node = std::make_shared<rclcpp::Node>("moveit_node");
auto exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
exec->add_node(node); std::thread([&exec]() { exec->spin(); }).detach();
moveit::planning_interface::MoveGroupInterface mg(node, "panda_arm");
moveit::planning_interface::PlanningSceneInterface psi;
geometry_msgs::msg::Pose target;
target.orientation.w = 1.0; target.position.x = 0.28; target.position.z = 0.5;
mg.setPoseTarget(target);
mg.setMaxVelocityScalingFactor(0.5);
moveit::planning_interface::MoveGroupInterface::Plan plan;
if (mg.plan(plan) == moveit::core::MoveItErrorCode::SUCCESS) mg.execute(plan);
std::vector<geometry_msgs::msg::Pose> wps = {mg.getCurrentPose().pose};
wps.back().position.z += 0.2;
moveit_msgs::msg::RobotTrajectory traj;
if (mg.computeCartesianPath(wps, 0.01, 0.0, traj) > 0.95) mg.execute(traj);
Planning Scene: ์ถ๊ฐยท์ ๊ฑฐยท๋ถ์ฐฉ
moveit_msgs::msg::CollisionObject obj;
obj.header.frame_id = mg.getPlanningFrame(); obj.id = "box1";
shape_msgs::msg::SolidPrimitive prim;
prim.type = prim.BOX; prim.dimensions = {0.1, 0.1, 0.1};
geometry_msgs::msg::Pose p; p.orientation.w = 1.0;
p.position.x = 0.48; p.position.z = 0.25;
obj.primitives.push_back(prim); obj.primitive_poses.push_back(p);
obj.operation = moveit_msgs::msg::CollisionObject::ADD;
psi.addCollisionObjects({obj});
mg.attachObject("box1", "panda_hand", {"panda_leftfinger", "panda_rightfinger"});
mg.detachObject("box1");
psi.removeCollisionObjects({"box1"});
MoveItConfigsBuilder ๋ฐ์น ํจํด
from launch import LaunchDescription
from launch_ros.actions import Node
from moveit_configs_utils import MoveItConfigsBuilder
def generate_launch_description():
moveit_config = (
MoveItConfigsBuilder("panda", package_name="panda_moveit_config")
.robot_description(file_path="config/panda.urdf.xacro")
.robot_description_semantic(file_path="config/panda.srdf")
.trajectory_execution(file_path="config/moveit_controllers.yaml")
.planning_pipelines(pipelines=["ompl", "pilz_industrial_motion_planner"])
.to_moveit_configs()
)
return LaunchDescription([
Node(
package="moveit_ros_move_group",
executable="move_group",
output="screen",
parameters=[moveit_config.to_dict()],
)
])
Servo ์ค์๊ฐ Cartesian ์ ์ด
MoveIt Servo๋ TwistStamped ๋ช
๋ น์ ~100 Hz๋ก ์์ ํด ๊ด์ ๋ช
๋ น์ผ๋ก ๋ณํํ๋ค.
ํน์ด์ ยท๊ด์ ํ๊ณ ์ ๊ทผ ์ ์๋ ๊ฐ์/์ ์งํ๋ค.
| ํ ํฝ / ์๋น์ค | ํ์
| ์ฉ๋ |
|---|
/servo_node/delta_twist_cmds | TwistStamped | Cartesian ์๋ ๋ช
๋ น |
/servo_node/delta_joint_cmds | JointJog | ๊ด์ ์๋ ๋ช
๋ น |
/servo_node/start_servo | Trigger (srv) | Servo ์์ |
/servo_node/stop_servo | Trigger (srv) | Servo ์ ์ง |
from geometry_msgs.msg import TwistStamped
from std_srvs.srv import Trigger
node.create_client(Trigger, '/servo_node/start_servo').call_async(Trigger.Request())
pub = node.create_publisher(TwistStamped, '/servo_node/delta_twist_cmds', 10)
cmd = TwistStamped()
cmd.header.stamp = node.get_clock().now().to_msg()
cmd.header.frame_id = "panda_link0"
cmd.twist.linear.x = 0.1
cmd.twist.angular.z = 0.2
pub.publish(cmd)
node.create_client(Trigger, '/servo_node/stop_servo').call_async(Trigger.Request())
servo_params.yaml ํต์ฌ ํญ๋ชฉ:
moveit_servo:
move_group_name: panda_arm
ee_frame_name: panda_link8
robot_link_command_frame: panda_link0
incoming_command_timeout: 0.1
lower_singularity_threshold: 17.0
hard_stop_singularity_threshold: 30.0
publish_period: 0.034
Gazebo ์๋ฎฌ๋ ์ด์
๊ณ ๊ธ
Gazebo Classic 11 + ROS2 Humble. ํต์ฌ ํจํค์ง: gazebo_ros_pkgs.
์์คํ
ํ๋ฌ๊ทธ์ธ (world SDF ํ์)
<plugin name="gazebo_ros_init" filename="libgazebo_ros_init.so"/>
<plugin name="gazebo_ros_factory" filename="libgazebo_ros_factory.so"/>
<plugin name="gazebo_ros_state" filename="libgazebo_ros_state.so"/>
gazebo_ros.launch.py ์ฌ์ฉ ์ ์๋ ๋ก๋. initโ/clock, factoryโspawn/delete ์๋น์ค, stateโmodel_states.
spawn_entity.py
ros2 run gazebo_ros spawn_entity.py \
-topic /robot_description -entity my_robot -x 0.0 -y 0.0 -z 0.1
ros2 run gazebo_ros spawn_entity.py \
-file robot.sdf -entity robot1 -robot_namespace /robot1 -x 1.0
์ฃผ์ ์ธ์: -topic/-file/-database (์์ค ํ1), -entity (ํ์), -x/-y/-z, -R/-P/-Y, -robot_namespace, -unpause, -timeout.
diff_drive ํ๋ฌ๊ทธ์ธ
<gazebo>
<plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so">
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.3</wheel_separation>
<wheel_diameter>0.1</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<max_wheel_acceleration>1.0</max_wheel_acceleration>
<command_topic>cmd_vel</command_topic>
<odometry_topic>odom</odometry_topic>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_footprint</robot_base_frame>
<odometry_source>0</odometry_source>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<update_rate>50</update_rate>
<ros><namespace>/</namespace></ros>
</plugin>
</gazebo>
์นด๋ฉ๋ผ + IMU ์ผ์ ํ๋ฌ๊ทธ์ธ
<sensor name="camera_sensor" type="camera">
<update_rate>30.0</update_rate>
<camera name="front_camera">
<horizontal_fov>1.3962634</horizontal_fov>
<image><width>640</width><height>480</height><format>R8G8B8</format></image>
<clip><near>0.02</near><far>300</far></clip>
</camera>
<plugin name="camera_plugin" filename="libgazebo_ros_camera.so">
<ros><namespace>/camera</namespace></ros>
<camera_name>front_camera</camera_name>
<frame_name>camera_link_optical</frame_name>
</plugin>
</sensor>
<sensor name="imu_sensor" type="imu">
<update_rate>200</update_rate>
<plugin name="imu_plugin" filename="libgazebo_ros_imu_sensor.so">
<ros><namespace>/</namespace><remapping>~/out:=imu</remapping></ros>
<initial_orientation_as_reference>false</initial_orientation_as_reference>
<frame_name>imu_link</frame_name>
</plugin>
</sensor>
Lidar: libgazebo_ros_ray_sensor.so + <output_type>sensor_msgs/LaserScan</output_type>. ํผ๋ธ๋ฆฌ์: camera/image_raw, imu.
gazebo_ros2_control
<ros2_control name="GazeboSystem" type="system">
<hardware><plugin>gazebo_ros2_control/GazeboSystem</plugin></hardware>
<joint name="left_wheel_joint">
<command_interface name="velocity"><param name="min">-10</param><param name="max">10</param></command_interface>
<state_interface name="position"/><state_interface name="velocity"/>
</joint>
<joint name="right_wheel_joint">
<command_interface name="velocity"><param name="min">-10</param><param name="max">10</param></command_interface>
<state_interface name="position"/><state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<robot_param>robot_description</robot_param>
<robot_param_node>robot_state_publisher</robot_param_node>
<parameters>$(find my_robot)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
controller_manager:
ros__parameters:
update_rate: 100
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.3
wheel_radius: 0.05
odom_frame_id: odom
base_frame_id: base_link
enable_odom_tf: true
cmd_vel_timeout: 0.5
์ปจํธ๋กค๋ฌ ๋ก๋: ros2 run controller_manager spawner joint_state_broadcaster diff_drive_controller
๊ฒ์ฆ๋ ์ฃผ์ ํ๋ฌ๊ทธ์ธ (/opt/ros/humble/lib/)
libgazebo_ros_init.so ยท libgazebo_ros_factory.so ยท libgazebo_ros_state.so (์์คํ
3์ข
ํ์)
libgazebo_ros_diff_drive.so ยท libgazebo_ros2_control.so ยท libgazebo_ros_camera.so
libgazebo_ros_ray_sensor.so ยท libgazebo_ros_imu_sensor.so ยท libgazebo_ros_p3d.so ยท libgazebo_ros_joint_state_publisher.so ยท libgazebo_ros_gps_sensor.so ยท libgazebo_ros_ft_sensor.so
๋ชจ๋ ๋
ธ๋์ use_sim_time:=true ํ์. gazebo_ros_init ๊ฐ /clock ์ ์๋ ํผ๋ธ๋ฆฌ์ํจ.
๋๋ฒ๊น
๊ณ ๊ธ ๋๊ตฌ
๋ณ๋ ์ค์น ํ์: ros2 doctor โ ros-humble-ros2doctor,
rqt_* โ ros-humble-rqt-graph ros-humble-rqt-console ros-humble-rqt-plot ros-humble-rqt-topic ros-humble-rqt-service-caller
rqt GUI ๋๊ตฌ
rqt --standalone rqt_graph
ros2 run rqt_console rqt_console
ros2 run rqt_plot rqt_plot /cmd_vel/linear/x /odom/pose/pose/position/x
ros2 run rqt_topic rqt_topic
ros2 run rqt_service_caller rqt_service_caller
export DISPLAY=:99 && Xvfb :99 -screen 0 1024x768x24 &
ros2 topic / node / service ๊ณ ๊ธ ๋ช
๋ น
ros2 topic find sensor_msgs/msg/LaserScan
ros2 topic find geometry_msgs/msg/Twist
ros2 topic delay /camera/image_raw --window 100
ros2 node info /my_node
ros2 node list --all
ros2 service type /my_service
ros2 service call /set_bool std_srvs/srv/SetBool "{data: true}"
RCUTILS ๋ก๊น
ํ๊ฒฝ ๋ณ์
| ๋ณ์ | ์ฉ๋ |
|---|
RCUTILS_LOGGING_SEVERITY_THRESHOLD | ์ ์ฒด ์ต์ ๋ ๋ฒจ: DEBUG INFO WARN ERROR FATAL |
RCUTILS_CONSOLE_OUTPUT_FORMAT | ํฌ๋งท ํ ํฐ: {severity} {time} {name} {message} {line_number} |
RCUTILS_COLORIZED_OUTPUT | ANSI ์์ (1 = ํ์ฑํ) |
RCUTILS_LOGGING_SEVERITY_THRESHOLD=DEBUG ros2 run my_pkg my_node
ros2 run my_pkg my_node --ros-args --log-level my_node:=DEBUG
rosbag2 ํํฐ๋ง / ์์ถ
ros2 bag record /scan /odom /cmd_vel -o nav_bag
ros2 bag record -a -e "/camera.*" -x "/camera/.*/compressed" -o cam_bag
ros2 bag record -a --compression-mode message --compression-format zstd \
--compression-threads 4 -o compressed_bag
ros2 bag play nav_bag/ -r 0.5 --topics /scan /odom --clock 200
ros2 bag convert -i my_bag/ -o convert_options.yaml
GDB๋ก ROS2 ๋
ธ๋ ๋๋ฒ๊น
ros2 run my_pkg my_node --prefix 'gdb -ex run --args'
ros2 run my_pkg my_node --prefix 'gdb -batch -ex run -ex bt --args'
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug
colcon build --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo
gdb -p $(pgrep my_node)
ํ
์คํ
๊ณ ๊ธ ํจํด
launch_testing ์์ ํจํด
active test (๋
ธ๋ ์คํ ์ค)์ post-shutdown test (์ข
๋ฃ ํ) ๋ ๋จ๊ณ๋ก ๊ตฌ์ฑ๋๋ค.
import unittest, pytest, launch, launch_ros.actions
import launch_testing, launch_testing.actions
@pytest.mark.launch_test
def generate_test_description():
node = launch_ros.actions.Node(
package='my_pkg', executable='my_node', name='my_node')
return launch.LaunchDescription([
node, launch_testing.actions.ReadyToTest(),
]), {'my_node': node}
class TestActive(unittest.TestCase):
def test_startup(self, my_node, proc_output):
proc_output.assertWaitFor('Node ready', process=my_node, timeout=10)
@launch_testing.post_shutdown_test()
class TestAfterShutdown(unittest.TestCase):
def test_exit_codes(self, proc_info):
launch_testing.asserts.assertExitCodes(proc_info)
@launch_testing.markers.keep_alive: ํ๋ก์ธ์ค ์ข
๋ฃ ํ์๋ launch ์ ์ง.
MockPublisherNode ํจํด
from rclpy.node import Node
from std_msgs.msg import Int32
class MockPublisherNode(Node):
def __init__(self, topic='/test'):
super().__init__('mock_publisher')
self.pub = self.create_publisher(Int32, topic, 10)
def publish(self, data):
self.pub.publish(Int32(data=data))
class MockSubscriberNode(Node):
def __init__(self, topic='/test'):
super().__init__('mock_subscriber')
self.received = []
self.create_subscription(Int32, topic, self.received.append, 10)
def test_pub_sub(rclpy_init):
pub, sub = MockPublisherNode(), MockSubscriberNode()
pub.publish(42)
rclpy.spin_once(sub, timeout_sec=1.0)
assert sub.received[0].data == 42
pub.destroy_node(); sub.destroy_node()
colcon test ํต์ฌ ์ต์
colcon test --packages-select my_pkg --return-code-on-test-failure
colcon test --event-handlers console_direct+
colcon test --executor sequential
colcon test --pytest-args -v -k test_my_case
colcon test-result
colcon test-result --all
colcon test-result --verbose
ament ๋ฆฐํ
ament_copyright src/ test/
ament_flake8 src/ test/
ament_cpplint include/ src/
CMakeLists.txt (colcon test ์ lint ์๋ ์คํ):
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies() # copyright, flake8, cpplint ๋ฑ๋ก
endif()
package.xml: <test_depend>ament_lint_auto</test_depend> + <test_depend>ament_lint_common</test_depend>.
GitHub Actions โ ros-tooling/action-ros-ci
name: ROS2 CI
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
test:
runs-on: ubuntu-22.04
env:
ROS_DOMAIN_ID: ${{ github.run_number % 101 + 100 }}
steps:
- uses: actions/checkout@v4
- uses: ros-tooling/setup-ros@v0.7
with:
required-ros-distributions: humble
- uses: ros-tooling/action-ros-ci@v0.3
with:
package-name: my_pkg
target-ros2-distro: humble
colcon-defaults: |
{
"build": {"cmake-args": ["-DCMAKE_BUILD_TYPE=Release"]},
"test": {"pytest-with-coverage": true}
}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: ros_ws/build/*/test_results/**/*.xml
action-ros-ci: rosdep ์ค์น, colcon build/test, ๊ฒฐ๊ณผ ์์ง ์๋ ์ฒ๋ฆฌ. skip-tests: true๋ก ๋น๋ ์ ์ฉ ์คํ ๊ฐ๋ฅ.
TF2 / QoS / ๋ผ์ดํ์ฌ์ดํด ๊ณ ๊ธ
TF2 C++ ์กฐํ ํจํด
#include "tf2_ros/buffer.hpp"
#include "tf2_ros/transform_listener.hpp"
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
tf_buffer_ = std::make_unique<tf2_ros::Buffer>(this->get_clock());
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_, this);
try {
auto tf = tf_buffer_->lookupTransform(
"map", "base_link",
tf2::TimePointZero,
tf2::durationFromSec(1.0));
geometry_msgs::msg::PointStamped out;
tf2::doTransform(point_in, out, tf);
} catch (const tf2::TransformException & ex) {
RCLCPP_WARN(get_logger(), "TF: %s", ex.what());
}
TF2 ์์ธ (tf2::TransformException ๋ถ๋ชจ): LookupException ยท ConnectivityException ยท ExtrapolationException ยท InvalidArgumentException
TF2 Python ์กฐํ ํจํด
import tf2_ros, tf2_geometry_msgs
self.tf_buf = tf2_ros.Buffer()
self.tf_listener = tf2_ros.TransformListener(self.tf_buf, self)
transformed = self.tf_buf.transform(pose_stamped, 'map')
tf = self.tf_buf.lookup_transform('map', 'base_link', rclpy.time.Time())
out = tf2_geometry_msgs.do_transform_pose(pose_stamped, tf)
/tf_static โ TRANSIENT_LOCAL (๋ฆ๊ฒ ์ฐธ์ฌํด๋ ์ ์ฒด ์์ ) /tf โ VOLATILE
QoS ํ๋กํ์ผ ๋น๊ตํ (Humble ๊ฒ์ฆ๊ฐ)
| ํ๋กํ์ผ | History | Depth | Reliability | Durability |
|---|
sensor_data | KEEP_LAST | 5 | BEST_EFFORT | VOLATILE |
parameters | KEEP_LAST | 1000 | RELIABLE | VOLATILE |
parameter_events | KEEP_LAST | 1000 | RELIABLE | VOLATILE |
services_default | KEEP_LAST | 10 | RELIABLE | VOLATILE |
system_default | SYSTEM_DEFAULT | 0 | SYSTEM_DEFAULT | SYSTEM_DEFAULT |
ํธํ์ฑ: ๊ตฌ๋
์ ์ ์ฑ
์ด ๋ฐํ์๋ณด๋ค ์๊ฒฉํ๋ฉด ์ฐ๊ฒฐ ๋ถ๊ฐ (๋ฌด์ ์คํจ).
BEST_EFFORT ๋ฐํ โ RELIABLE ๊ตฌ๋
: ๋ถ๊ฐ / VOLATILE ๋ฐํ โ TRANSIENT_LOCAL ๊ตฌ๋
: ๋ถ๊ฐ.
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSDurabilityPolicy, qos_profile_sensor_data
self.create_subscription(Imu, '/imu', self.cb, qos_profile_sensor_data)
custom = QoSProfile(reliability=QoSReliabilityPolicy.RELIABLE,
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL, depth=10)
rclcpp::SensorDataQoS()
rclcpp::ServicesQoS()
auto qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable().transient_local();
LifecycleNode ์ํ/์ ํ ํ
์ด๋ธ
| ID | Primary State | ์ง์
์ฝ๋ฐฑ | ์ค๋ช
|
|---|
| 1 | UNCONFIGURED | โ | ์ด๊ธฐ ์ํ, ๋ฆฌ์์ค ์์ |
| 2 | INACTIVE | on_configure() | ๋ฆฌ์์ค ํ ๋น, ๋ฏธ์ฒ๋ฆฌ |
| 3 | ACTIVE | on_activate() | ์ ์ ๋์, ํผ๋ธ๋ฆฌ์ |
| 4 | FINALIZED | on_shutdown() | ์ข
๋ฃ ์ง์ |
๋ฐํ๊ฐ: SUCCESS (์ ์) ยท FAILURE (์ด์ ์ํ ๋ณต๊ท) ยท ERROR โ on_error(), ์คํจ ์ FINALIZED
CallbackReturn on_configure(const rclcpp_lifecycle::State &) override {
pub_ = this->create_publisher<Msg>("out", 10);
return CallbackReturn::SUCCESS;
}
CallbackReturn on_activate(const rclcpp_lifecycle::State &) override {
pub_->on_activate();
return CallbackReturn::SUCCESS;
}
CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override {
pub_->on_deactivate();
return CallbackReturn::SUCCESS;
}
CLI:
ros2 lifecycle set /my_node configure
ros2 lifecycle set /my_node activate
ros2 lifecycle set /my_node deactivate
ros2 lifecycle set /my_node cleanup
ros2 lifecycle set /my_node shutdown
LifecycleManager YAML (Nav2)
lifecycle_manager_navigation:
ros__parameters:
autostart: true
node_names:
- map_server
- amcl
- controller_server
- planner_server
- bt_navigator
bond_timeout: 4.0
QoS ์ฌ์ธต ๊ฐ์ด๋ (์ด์ ๋ค๋ฐ ์์ญ)
ROS2 ํ์ฅ์์ ๊ฐ์ฅ ๋ง์ ๋ฌด์ ์คํจ(silent failure)๋ฅผ ์ ๋ฐํ๋ ์์ญ.
ํ ํฝ ์ฐ๊ฒฐ์ ๋๋๋ฐ ๋ฉ์์ง๊ฐ ์ ์ค๋ฉด QoS ๋ถ์ผ์น ๋จผ์ ์์ฌ.
๋น ๋ฅธ ์ง๋จ
ros2 topic info /topic_name --verbose
ํธํ์ฑ ๋งคํธ๋ฆญ์ค (์์ ํ)
๊ตฌ๋
์ ์ ์ฑ
์ด ๋ฐํ์๋ณด๋ค ์๊ฒฉํ๋ฉด ๋ฌด์ ์คํจ (์ฐ๊ฒฐ์ ๋์ง๋ง ๋ฉ์์ง 0๊ฐ).
Reliability
| Publisher | Subscriber | ๊ฒฐ๊ณผ |
|---|
| RELIABLE | RELIABLE | โ
|
| RELIABLE | BEST_EFFORT | โ
(๊ตฌ๋
์๊ฐ ๋ ์๊ฒฉ) |
| BEST_EFFORT | RELIABLE | โ ๋ฌด์ ์คํจ |
| BEST_EFFORT | BEST_EFFORT | โ
|
Durability
| Publisher | Subscriber | ๊ฒฐ๊ณผ |
|---|
| TRANSIENT_LOCAL | TRANSIENT_LOCAL | โ
(๋ฆ๊ฒ ์ฐธ์ฌํด๋ ์์ ) |
| TRANSIENT_LOCAL | VOLATILE | โ
|
| VOLATILE | VOLATILE | โ
|
| VOLATILE | TRANSIENT_LOCAL | โ ๋ฌด์ ์คํจ |
์์ฃผ ๋ฐ์ํ๋ QoS ์ด์ ํจํด
์ด์ 1: ์นด๋ฉ๋ผ/์ผ์ ํ ํฝ ์์ ์ ๋จ
ros2 topic info /camera/image_raw --verbose | grep -A3 "Publisher\|Subscriber"
from rclpy.qos import qos_profile_sensor_data
self.create_subscription(Image, '/camera/image_raw', cb, qos_profile_sensor_data)
์ด์ 2: /robot_description ๋ฆ๊ฒ ์์ํ ๋
ธ๋๊ฐ ๋ชป ๋ฐ์
from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy
latched = QoSProfile(
depth=1,
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
reliability=QoSReliabilityPolicy.RELIABLE,
)
self.create_subscription(String, '/robot_description', cb, latched)
์ด์ 3: Nav2 / ์ง๋ ์๋ฒ ๋ฉ์์ง ๋ชป ๋ฐ์
ros2 topic info /map --verbose
map_qos = QoSProfile(
depth=1,
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
reliability=QoSReliabilityPolicy.RELIABLE,
history=QoSHistoryPolicy.KEEP_LAST,
)
์ด์ 4: rosbag ์ฌ์ ์ ๋ฉ์์ง ์์ ์ ๋จ
ros2 bag play my_bag/ --qos-profile-overrides-path qos_override.yaml
/camera/image_raw:
reliability: best_effort
durability: volatile
history: keep_last
depth: 10
์ด์ 5: ๊ฐ์ ๋จธ์ ์ธ๋ฐ DDS ๋๋ฉ์ธ ๋ถ์ผ์น
echo $ROS_DOMAIN_ID
export ROS_DOMAIN_ID=0
export ROS_LOCALHOST_ONLY=1
๊ณ ๊ธ QoS ์ ์ฑ
Deadline (์ฃผ๊ธฐ ๋ณด์ฅ)
Publisher๊ฐ ์ง์ ์ฃผ๊ธฐ ๋ด ๊ฒ์ ์ ํ๊ฑฐ๋ Subscriber๊ฐ ์ง์ ์ฃผ๊ธฐ ๋ด ๋ชป ๋ฐ์ผ๋ฉด ์ด๋ฒคํธ ๋ฐ์.
from rclpy.qos import QoSProfile
from rclpy.qos_event import PublisherEventCallbacks, SubscriptionEventCallbacks
import rclpy.duration
deadline_qos = QoSProfile(depth=10)
deadline_qos.deadline = rclpy.duration.Duration(seconds=0, nanoseconds=100_000_000)
callbacks = SubscriptionEventCallbacks(
deadline=lambda event: node.get_logger().warn('Deadline missed!')
)
sub = node.create_subscription(Twist, '/cmd_vel', cb, deadline_qos,
event_callbacks=callbacks)
Liveliness (์์กด ํ์ธ)
from rclpy.qos import QoSLivelinessPolicy
import rclpy.duration
qos = QoSProfile(depth=10)
qos.liveliness = QoSLivelinessPolicy.MANUAL_BY_TOPIC
qos.liveliness_lease_duration = rclpy.duration.Duration(seconds=1)
pub = node.create_publisher(String, 'topic', qos)
pub.assert_liveliness()
Lifespan (๋ฉ์์ง ์ ํจ๊ธฐ๊ฐ)
qos = QoSProfile(depth=10)
qos.lifespan = rclpy.duration.Duration(seconds=0, nanoseconds=500_000_000)
QoS ์ด์ ๋๋ฒ๊น
์ฒดํฌ๋ฆฌ์คํธ
โก ros2 topic info /topic --verbose ๋ก ์์ชฝ QoS ํ์ธ
โก Reliability ๋ถ์ผ์น? โ ๊ตฌ๋
์๋ฅผ BEST_EFFORT๋ก ๋ฎ์ถ๊ฑฐ๋ ๋ฐํ์๋ฅผ RELIABLE๋ก ๋์ด๊ธฐ
โก Durability ๋ถ์ผ์น? โ /robot_description, /map ๋ฑ์ TRANSIENT_LOCAL ํ์
โก ROS_DOMAIN_ID ๋์ผํ์ง ํ์ธ
โก ROS_LOCALHOST_ONLY ์ค์ ํ์ธ
โก rosbag ์ฌ์ ์ QoS override ํ์ ์ฌ๋ถ ํ์ธ
โก ๊ฐ์ ํ์
์ธ๋ฐ ์ฐ๊ฒฐ ์ ๋๋ฉด ros2 topic find <type> ๋ก ์ด๋ฆ ์คํ ํ์ธ
โก FastRTPS vs CycloneDDS ํผ์ฉ ์ DDS ๋ ๋ฒจ ๋นํธํ ๊ฐ๋ฅ โ RMW_IMPLEMENTATION ํต์ผ
์ฃผ์ ํ ํฝ๋ณ ๊ธฐ๋ณธ QoS ์ ๋ฆฌ
| ํ ํฝ | Reliability | Durability | ๋น๊ณ |
|---|
/scan, /imu, /camera/* | BEST_EFFORT | VOLATILE | sensor_data |
/map | RELIABLE | TRANSIENT_LOCAL | ๋ฆ๊ฒ ๊ตฌ๋
ํด๋ ์์ |
/robot_description | RELIABLE | TRANSIENT_LOCAL | ๋ฆ๊ฒ ๊ตฌ๋
ํด๋ ์์ |
/tf | RELIABLE | VOLATILE | KEEP_LAST 100 |
/tf_static | RELIABLE | TRANSIENT_LOCAL | ๋ฆ๊ฒ ๊ตฌ๋
ํด๋ ์์ |
/cmd_vel | RELIABLE | VOLATILE | ๊ธฐ๋ณธ๊ฐ |
/odom | RELIABLE | VOLATILE | ๊ธฐ๋ณธ๊ฐ |
| ROS2 ํ๋ผ๋ฏธํฐ | RELIABLE | VOLATILE | parameters QoS |
| ROS2 ์๋น์ค | RELIABLE | VOLATILE | services_default |
RViz2 ์ค์ ๊ฐ์ด๋
RViz2๋ ROS2์ ํ์ค 3D ์๊ฐํ ๋๊ตฌ. ์ค์ ํ์ผ(.rviz)๋ก ๋ ์ด์์ ์ ์ฅ/์ฌ์ฌ์ฉ.
๊ธฐ๋ณธ ์คํ
rviz2
rviz2 -d /path/to/config.rviz
rviz2 -d $(ros2 pkg prefix my_pkg)/share/my_pkg/rviz/default.rviz
rviz2 -d config.rviz --ros-args -p fixed_frame:=map
export DISPLAY=:99 && Xvfb :99 -screen 0 1024x768x24 &
rviz2 -d config.rviz
Launch ํ์ผ์์ RViz2 ์คํ
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
use_rviz = LaunchConfiguration('use_rviz', default='true')
rviz_config = PathJoinSubstitution([
FindPackageShare('my_pkg'), 'rviz', 'default.rviz'
])
return LaunchDescription([
DeclareLaunchArgument('use_rviz', default_value='true'),
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', rviz_config],
parameters=[{'use_sim_time': True}],
output='screen',
condition=IfCondition(use_rviz),
),
])
.rviz ์ค์ ํ์ผ ๊ตฌ์กฐ
Panels:
- Class: rviz_common/Displays
Name: Displays
- Class: rviz_common/Views
Name: Views
Visualization Manager:
Class: ""
Displays:
- Alpha: 0.5
Class: rviz_default_plugins/Grid
Name: Grid
Cell Size: 1
Color: 160; 160; 164
Enabled: true
Plane: XY
- Class: rviz_default_plugins/RobotModel
Name: RobotModel
Enabled: true
Description Topic:
Depth: 5
Durability Policy: Transient Local
Value: /robot_description
Visual Enabled: true
Collision Enabled: false
- Class: rviz_default_plugins/LaserScan
Name: LaserScan
Topic:
Value: /scan
Depth: 5
Reliability Policy: Best Effort
Durability Policy: Volatile
Style: Points
Size (m): 0.03
Color Transformer: AxisColor
Enabled: true
- Class: rviz_default_plugins/PointCloud2
Name: PointCloud2
Topic:
Value: /points
Depth: 5
Reliability Policy: Best Effort
Durability Policy: Volatile
Style: Points
Size (m): 0.01
Color Transformer: RGB8
Enabled: true
- Class: rviz_default_plugins/Image
Name: Camera
Topic:
Value: /camera/image_raw
Depth: 5
Reliability Policy: Best Effort
Durability Policy: Volatile
Enabled: true
- Class: rviz_default_plugins/Map
Name: Map
Topic:
Value: /map
Depth: 1
Reliability Policy: Reliable
Durability Policy: Transient Local
Color Scheme: map
Enabled: true
- Class: rviz_default_plugins/Path
Name: Global Path
Topic:
Value: /plan
Depth: 5
Color: 0; 128; 0
Enabled: true
- Class: rviz_default_plugins/Odometry
Name: Odometry
Topic:
Value: /odom
Depth: 10
Shape: Arrow
Enabled: true
- Class: rviz_default_plugins/Axes
Name: Axes
Enabled: true
Length: 0.5
- Class: rviz_default_plugins/TF
Name: TF
Enabled: false
Marker Scale: 0.5
Show Names: true
Show Axes: true
- Class: rviz_default_plugins/Marker
Name: Markers
Topic:
Value: /visualization_marker
Depth: 100
Enabled: true
- Class: rviz_default_plugins/MarkerArray
Name: MarkerArray
Topic:
Value: /visualization_marker_array
Enabled: true
- Class: rviz_default_plugins/PoseArray
Name: Particles
Topic:
Value: /particlecloud
Color: 255; 25; 0
Enabled: true
Fixed Frame: map
Background Color: 48; 48; 48
Tools:
- Class: rviz_default_plugins/Interact
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
- Class: rviz_default_plugins/FocusCamera
- Class: rviz_default_plugins/Measure
- Class: rviz_default_plugins/SetInitialPose
Topic:
Value: /initialpose
- Class: rviz_default_plugins/SetGoal
Topic:
Value: /goal_pose
Views:
Current:
Class: rviz_default_plugins/Orbit
Target Frame: base_link
Distance: 5.0
Pitch: 0.785398
Yaw: 0.0
Python์์ Marker ๋ฐํ (RViz2 ์๊ฐํ)
from visualization_msgs.msg import Marker, MarkerArray
from geometry_msgs.msg import Point
import rclpy
from rclpy.node import Node
class MarkerNode(Node):
def __init__(self):
super().__init__('marker_node')
self.pub = self.create_publisher(MarkerArray, '/visualization_marker_array', 10)
self.create_timer(1.0, self.publish_markers)
def publish_markers(self):
arr = MarkerArray()
m = Marker()
m.header.frame_id = 'map'
m.header.stamp = self.get_clock().now().to_msg()
m.ns = 'obstacles'; m.id = 0
m.type = Marker.SPHERE
m.action = Marker.ADD
m.pose.position.x = 1.0; m.pose.position.y = 0.5
m.pose.orientation.w = 1.0
m.scale.x = m.scale.y = m.scale.z = 0.3
m.color.r = 1.0; m.color.a = 1.0
m.lifetime.sec = 0
arr.markers.append(m)
line = Marker()
line.header.frame_id = 'map'
line.header.stamp = self.get_clock().now().to_msg()
line.ns = 'path'; line.id = 1
line.type = Marker.LINE_STRIP
line.action = Marker.ADD
line.scale.x = 0.05
line.color.g = 1.0; line.color.a = 1.0
for x, y in [(0,0),(1,0),(1,1),(0,1)]:
p = Point(); p.x = float(x); p.y = float(y)
line.points.append(p)
arr.markers.append(line)
txt = Marker()
txt.header.frame_id = 'map'
txt.header.stamp = self.get_clock().now().to_msg()
txt.ns = 'labels'; txt.id = 2
txt.type = Marker.TEXT_VIEW_FACING
txt.action = Marker.ADD
txt.pose.position.x = 0.5; txt.pose.position.z = 0.5
txt.pose.orientation.w = 1.0
txt.scale.z = 0.3
txt.color.r = txt.color.g = txt.color.b = txt.color.a = 1.0
txt.text = 'Hello RViz2'
arr.markers.append(txt)
del_m = Marker(); del_m.action = Marker.DELETEALL
self.pub.publish(arr)
Marker ํ์
๋น ๋ฅธ ์ฐธ์กฐ
| ํ์
| ์์ | ์ฉ๋ |
|---|
ARROW | 0 | ๋ฐฉํฅ ๋ฒกํฐ |
CUBE | 1 | ๋ฐ์ค |
SPHERE | 2 | ๊ตฌ์ฒด |
CYLINDER | 3 | ์ํต |
LINE_STRIP | 4 | ์ฐ์์ |
LINE_LIST | 5 | ์ ์ |
CUBE_LIST | 6 | ๋ฐ์ค ๋ฐฐ์ด |
SPHERE_LIST | 7 | ๊ตฌ์ฒด ๋ฐฐ์ด |
POINTS | 8 | ์ ๊ตฌ๋ฆ |
TEXT_VIEW_FACING | 9 | ํ๋ฉด ํฅ ํ
์คํธ |
MESH_RESOURCE | 10 | 3D ๋ฉ์ ํ์ผ |
ํจํค์ง์ RViz ์ค์ ํฌํจ์ํค๊ธฐ
my_pkg/
โโโ rviz/
โ โโโ default.rviz
โโโ setup.py โ data_files์ ์ถ๊ฐ ํ์
โโโ package.xml
data_files=[
('share/' + package_name + '/rviz', ['rviz/default.rviz']),
],
RViz2 ํธ๋ฌ๋ธ์ํ
| ์ฆ์ | ์์ธ | ํด๊ฒฐ |
|---|
| Fixed Frame ์๋ฌ | TF์ ํด๋น ํ๋ ์ ์์ | ros2 run tf2_tools view_frames |
| RobotModel ์ ๋ณด์ | /robot_description QoS ๋ถ์ผ์น | Durability: Transient Local ์ค์ |
| LaserScan ์ ๋ณด์ | QoS ๋ถ์ผ์น (BEST_EFFORT ํ์) | Reliability: Best Effort ์ค์ |
| Map ์ ๋ณด์ | /map QoS ๋ถ์ผ์น | Durability: Transient Local ์ค์ |
| TF ํ์ดํ ๋๋ฆผ | TF display ํ์ฑํ ์ ๋ถํ | TF display ๋นํ์ฑ ๋๋ ํํฐ๋ง |
| RViz2 ๋ฉ์ถค | ๋์ฉ๋ PointCloud2 | depthโ, Decimate ํ๋ฌ๊ทธ์ธ ์ฌ์ฉ |
ros2 run tf2_tools view_frames
ros2 run tf2_ros tf2_echo map base_link
๊ฐ๋ฐ vs ๋ฐฐํฌ Launch ์ํฌํ๋ก์ฐ
ํต์ฌ ์์น: ๊ฐ๋ฐ ์ค์๋ src/ ์ง์ ์ฐธ์กฐ, ๋ฐฐํฌ ์์๋ install/ ์ฌ์ฉ.
๊ฐ๋ฐ ํ๊ฒฝ (src ํด๋ ์ง์ ์ฌ์ฉ)
cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash
๊ฐ๋ฐ ํ๊ฒฝ ํ์ผ ์ฐธ์กฐ ๊ตฌ์กฐ:
ros2_ws/
โโโ src/my_pkg/
โ โโโ my_pkg/my_node.py โ ์ค์ ํ์ผ (์ฌ๊ธฐ์ ํธ์ง)
โ โโโ launch/my.launch.py โ ์ค์ ํ์ผ
โ โโโ config/params.yaml โ ์ค์ ํ์ผ
โโโ install/my_pkg/
โโโ lib/my_pkg/my_node โ symlink โ src/my_pkg/my_pkg/my_node.py
โโโ share/my_pkg/launch/ โ symlink โ src/my_pkg/launch/
โโโ share/my_pkg/config/ โ symlink โ src/my_pkg/config/
๊ฐ๋ฐ ์ Launch ํ์ผ์์ src ๊ฒฝ๋ก ์ฐธ์กฐ ๊ธ์ง:
config_path = '/home/user/ros2_ws/src/my_pkg/config/params.yaml'
from ament_index_python.packages import get_package_share_directory
import os
config_path = os.path.join(get_package_share_directory('my_pkg'), 'config', 'params.yaml')
๋ฐฐํฌ ํ๊ฒฝ (install ํด๋ ์ฌ์ฉ)
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
ros2 launch my_pkg my.launch.py
๋ฐฐํฌ ํ๊ฒฝ ํ์ผ ๊ตฌ์กฐ:
install/
โโโ setup.bash โ ๋ฐ๋์ source
โโโ my_pkg/
โ โโโ lib/my_pkg/my_node โ ์ค์ ์คํํ์ผ ๋ณต์ฌ๋ณธ
โ โโโ share/my_pkg/
โ โโโ launch/ โ ์ค์ launch ํ์ผ ๋ณต์ฌ๋ณธ
โ โโโ config/ โ ์ค์ ์ค์ ํ์ผ ๋ณต์ฌ๋ณธ
โ โโโ rviz/ โ ์ค์ rviz ์ค์ ๋ณต์ฌ๋ณธ
โโโ local_setup.bash โ ์ด ํจํค์ง๋ง ์์ฑ
setup.py - ๋ฐฐํฌ ํ์ผ ๋ฑ๋ก (๋ฐ๋์ ํฌํจ)
from setuptools import setup, find_packages
import os
from glob import glob
package_name = 'my_pkg'
setup(
name=package_name,
version='0.0.1',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch',
glob('launch/*.launch.py')),
('share/' + package_name + '/config',
glob('config/*.yaml')),
('share/' + package_name + '/rviz',
glob('rviz/*.rviz')),
('share/' + package_name + '/urdf',
glob('urdf/*.urdf') + glob('urdf/*.xacro')),
('share/' + package_name + '/maps',
glob('maps/*')),
*[('share/' + package_name + '/' + os.path.dirname(f),
[f]) for f in glob('config/**/*.yaml', recursive=True)],
],
install_requires=['setuptools'],
zip_safe=True,
entry_points={
'console_scripts': [
'my_node = my_pkg.my_node:main',
],
},
)
CMakeLists.txt - C++ ํจํค์ง ๋ฐฐํฌ ํ์ผ ๋ฑ๋ก
# launch, config, rviz, urdf ํด๋ install/์ ๋ณต์ฌ
install(
DIRECTORY launch config rviz urdf maps
DESTINATION share/${PROJECT_NAME}
)
# ์คํํ์ผ
install(
TARGETS my_node my_component
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION lib/${PROJECT_NAME}
)
๊ฐ๋ฐโ๋ฐฐํฌ ์ ํ ์ฒดํฌ๋ฆฌ์คํธ
๊ฐ๋ฐ ์:
โก colcon build --symlink-install ์ฌ์ฉ
โก source install/setup.bash
โก Python ์์ : ์ฌ๋น๋ ๋ถํ์ (์ฌ๋งํฌ)
โก C++ ์์ : colcon build --packages-select my_pkg ํ์
โก Launch/config/yaml ์์ : ์ฌ๋น๋ ๋ถํ์ (์ฌ๋งํฌ)
โก ์ ํ์ผ ์ถ๊ฐ: ๋ฐ๋์ ์ฌ๋น๋ (setup.py/CMake์ ๋ฑ๋ก ํ)
๋ฐฐํฌ ์:
โก colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release (--symlink-install ์ ๊ฑฐ)
โก install/ ํด๋๋ง ํ๊ฒ ์๋ฒ์ ๋ณต์ฌ (src/ ๋ถํ์)
โก ํ๊ฒ ์๋ฒ์์ source install/setup.bash
โก setup.py data_files์ ๋ชจ๋ ๋ฆฌ์์ค ๋ฑ๋ก ํ์ธ
โก get_package_share_directory() ์ฌ์ฉ ํ์ธ (ํ๋์ฝ๋ฉ ๊ฒฝ๋ก ์์)
โก rosdep install --from-paths src --ignore-src -r -y (์์กด์ฑ ์ค์น)
์์ฃผ ๋ฐ์ํ๋ ๊ฐ๋ฐ/๋ฐฐํฌ ์ด์
| ์ฆ์ | ์์ธ | ํด๊ฒฐ |
|---|
| ๋ฐฐํฌ ํ๊ฒฝ์์ FileNotFoundError (yaml/launch) | setup.py data_files ๋ฏธ๋ฑ๋ก | data_files์ ํด๋น ํด๋ ์ถ๊ฐ ํ ์ฌ๋น๋ |
| Python ์์ ์ด ๋ฐ์ ์ ๋จ | --symlink-install ์์ด ๋น๋ | colcon build --symlink-install ์ฌ์คํ |
| C++ ์์ ์ด ๋ฐ์ ์ ๋จ | ์ฌ๋น๋ ์ ํจ | colcon build --packages-select pkg |
| src/ ๊ฒฝ๋ก ํ๋์ฝ๋ฉ ์ค๋ฅ | ์ ๋๊ฒฝ๋ก ์ฌ์ฉ | get_package_share_directory() ์ฌ์ฉ |
| install/ ์๋ ํ์ผ ์ฐธ์กฐ | ์ ํ์ผ ์ถ๊ฐ ํ ๋ฏธ๋ฑ๋ก | setup.py ๋ฑ๋ก + ์ฌ๋น๋ |
| ํ๊ฒ ์๋ฒ์์ ํจํค์ง ๋ชป ์ฐพ์ | ROS2 ๋ฏธ์์ฑ | source /opt/ros/humble/setup.bash + source install/setup.bash |
ํ๊ฒฝ๋ณ ์์ฑ ์์
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
source /opt/ros/humble/setup.bash
source ~/base_ws/install/setup.bash
source ~/my_ws/install/setup.bash
source /opt/ros/humble/setup.bash
source /opt/my_robot/install/setup.bash
Launch ํ์ผ ์์ฑ ๊ฐ์ด๋
์ํฉ๋ณ launch ํ์ผ์ ์ฒ์๋ถํฐ ๋ง๋๋ ํ
ํ๋ฆฟ ๋ชจ์.
ํจํค์ง์ launch ํด๋ ์ถ๊ฐ
mkdir -p ~/ros2_ws/src/my_pkg/launch
touch ~/ros2_ws/src/my_pkg/launch/my_robot.launch.py
setup.py data_files์ ๋ฐ๋์ ๋ฑ๋ก:
('share/' + package_name + '/launch', glob('launch/*.launch.py')),
ํ
ํ๋ฆฟ 1: ๋จ์ผ ๋
ธ๋ ์คํ (๊ฐ์ฅ ๊ธฐ๋ณธ)
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument('use_sim_time', default_value='false'),
DeclareLaunchArgument('log_level', default_value='info'),
Node(
package='my_pkg',
executable='my_node',
name='my_node',
output='screen',
emulate_tty=True,
parameters=[{
'use_sim_time': LaunchConfiguration('use_sim_time'),
}],
arguments=['--ros-args', '--log-level',
LaunchConfiguration('log_level')],
),
])
ros2 launch my_pkg my_node.launch.py use_sim_time:=true log_level:=debug
ํ
ํ๋ฆฟ 2: ๋ก๋ด ํ์คํ (robot_state_publisher + ๋
ธ๋๋ค)
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import xacro
def generate_launch_description():
pkg_share = get_package_share_directory('my_pkg')
use_sim_time = LaunchConfiguration('use_sim_time', default='false')
xacro_file = os.path.join(pkg_share, 'urdf', 'robot.urdf.xacro')
robot_description = xacro.process_file(xacro_file).toxml()
return LaunchDescription([
DeclareLaunchArgument('use_sim_time', default_value='false'),
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[{
'robot_description': robot_description,
'use_sim_time': use_sim_time,
}],
),
Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
name='joint_state_publisher_gui',
output='screen',
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('my_pkg'), 'launch', 'sensors.launch.py'
])
]),
launch_arguments={'use_sim_time': use_sim_time}.items(),
),
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', os.path.join(pkg_share, 'rviz', 'robot.rviz')],
parameters=[{'use_sim_time': use_sim_time}],
output='screen',
),
])
ํ
ํ๋ฆฟ 3: Gazebo ์๋ฎฌ๋ ์ด์
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import (DeclareLaunchArgument, IncludeLaunchDescription,