ROS 2 Service의 개념을 다시 복습해봅시다.
image from : docs.ros.org
Service 개념 정리
Service의 중요한 특징 한 가지 추가하자면, 하나의 Service Server에 여러 Client가 request 할 수 있지만, Server는 동시에 여러 request를 처리하지 못합니다.
image from : docs.ros.org
$ ros2 interface show gazebo_msgs/srv/SpawnEntity
string name # Name of the entity to be spawned (optional).
string xml # Entity XML description as a string, either URDF or SDF.
string robot_namespace # Spawn robot and all ROS interfaces under this namespace
geometry_msgs/Pose initial_pose # Initial entity pose.
string reference_frame # initial_pose is defined relative to the frame of this entity.
# If left empty or "world" or "map", then gazebo world frame is
# used.
# If non-existent entity is specified, an error is returned
# and the entity is not spawned.
---
bool success # Return true if spawned successfully.
string status_message # Comments if available.
서비스 타입 중간에 보이는 - - - 부분은 request와 response의 구분자라고 생각하시면 됩니다.
# package clone
cd ~/ros2_ws/src
git clone https://github.com/RB2023ROS/gz_ros2_examples.git
# package build & sourcing
cd ~/ros2_ws
cbp rgbd_world && source install/setup.bash
ros2 launch rgbd_world depth_world.launch.py
⇒ 카메라와 벽이 포함된 간단한 환경을 구성하였습니다. 참고로 PC 성능에 따라 Rviz2 화면이 매우 느릴 수 있으니 불편하시다면 point cloud 시각화는 제거해주세요!!
# Demo package build
cd ~/ros2_ws/src
cbp py_service_tutorial
source install/local_setup.bash
# Terminal 1 - gazebo env launch
ros2 launch rgbd_world depth_world.launch.py
# Terminal 2 - Execute Service Client
ros2 run py_service_tutorial spawn_object_wo_gravity
Enter Model name Among Below List
1.arm_part 2.beer 3.biscuits
4.book 5.bowl 6.create
7.disk_part 8.eraser 9.glue
10.hammer 11.plastic_cup 12.snacks
13.soap 14.soap2 15.soda_can
16.sticky_notes
[Type your choice]:
Gazebo는 개발 초기부터 ROS의 사용을 고려한 시스템입니다. Gazebo와 ROS 사이의 인터페이스는 gazebo_ros_pkg가 담당하고 있습니다.
⇒ https://github.com/ros-simulation/gazebo_ros_pkgs
⇒ https://classic.gazebosim.org/tutorials?cat=connect_ros
# Terminal 1
ros2 launch gazebo_ros_pkg gazebo.launch.py
# Terminal 2
ros2 topic list
ros2 servie list
# get info about service
ros2 service type /spawn
# get info about srv
ros2 interface show <sth>
from gazebo_msgs.srv import SpawnEntity
import rclpy
from rclpy.node import Node
class SpawnClientNode(Node):
def __init__(self):
super().__init__('gazebo_model_spawner')
self.client = self.create_client(SpawnEntity, 'spawn_entity')
Parameter | Description |
---|---|
Srv Type | Service에 사용될 데이터 타입 |
Service Name | Request 대상이 되는 Service 이름 |
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().error('service not available, waiting again...')
def main(args=None):
rclpy.init(args=args)
robot_spawn_node = SpawnClientNode()
future = robot_spawn_node.send_req()
rclpy.spin_until_future_complete(robot_spawn_node, future)
if future.done():
try:
response = future.result()
except Exception:
raise RuntimeError(
'exception while calling service: %r' % future.exception()
)
else:
robot_spawn_node.get_logger().info('==== Service Call Done ====')
robot_spawn_node.get_logger().info(f'Status_message : {response.status_message}')
finally:
robot_spawn_node.get_logger().warn('==== Shutting down node. ====')
robot_spawn_node.destroy_node()
rclpy.shutdown()
Future란 rclpy에서 멀티쓰레딩을 위해 사용되며, task의 상태를 담고 있는 객체입니다.
def send_req(self):
...
self.get_logger().debug('==== Sending service request to `/spawn_entity` ====')
self.future = self.client.call_async(self.req)
return self.future
def send_req(self):
...
self.get_logger().debug('==== Sending service request to `/spawn_entity` ====')
self.future = self.client.call(self.req)
return self.future
Code | Description |
---|---|
create_client(, ) | client 생성 |
spin_until_future_complete(,) | Client Request Task가 완료되기까지 비동기 대기 |
future.done(), future.result() | Future의 상태를 다루는 함수들 |
call_async(), call() | 비동기, 동기 service call |
self.physics_client = self.create_client(Empty, 'pause_physics')
...
# physics pause request
pause_future = robot_spawn_node.send_pause_req()
rclpy.spin_until_future_complete(robot_spawn_node, pause_future)
future_pending_logic("Pause",robot_spawn_node, pause_future)
이번에 보여드릴 ROS 2 service 예시는 Gazebo상의 카메라를 통해 사진을 찍는 Service Server입니다.
# Terminal 1 - Execute Gazebo World
ros2 launch rgbd_world depth_world.launch.py
# Terminal 2 - Execute Service Server
ros2 run py_service_tutorial take_picture_server
# Terminal 3 - Execute rqt's service caller
rqt
⇒ data 필드에 True를 전달하면 사진을 찍어 workspace에 저장해줍니다.
코드를 살펴보기 전에, 이를 어떻게 구현할 수 있을지 같이 생각해봅시다.
항상 사진을 찍는 것이 아니라 Service Call이 오는 그 순간에만 사진을 저장해야 합니다.
⇒ 위 조건에 따라 작성한 Node의 생성자는 다음과 같습니다.
class PictureNode(Node):
def __init__(self):
super().__init__('turtle_circle_server')
self.img_topic_name = "/rgb_cam/rgb_cam/image_raw"
self.server = self.create_service(
SetBool, 'take_picture', self.take_picture_callback
)
self.subscriber = self.create_subscription(
Image, self.img_topic_name, self.sub_callback, 10
)
self.br = CvBridge()
self.is_request = False
Parameter | Description |
---|---|
SetBool | Srv Type |
’take_picture' | Service Name |
self.take_picture_callback | Request 시 실행될 Service Callback |
$ ros2 interface show example_interfaces/srv/SetBool
# This is an example of a service to set a boolean value.
# This can be used for testing but a semantically meaningful
# one should be created to be built upon.
bool data # e.g. for hardware enabling / disabling
---
bool success # indicate successful run of triggered service
string message # informational, e.g. for error messages
def take_picture_callback(self, request, response):
if request.data is True:
self.get_logger().info('KimChi~')
self.is_request = True
response.success = True
response.message = "Successfully image written"
return response
def sub_callback(self, data):
if self.is_request:
current_frame = self.br.imgmsg_to_cv2(data, "bgr8")
file_name = str(self.get_clock().now().to_msg().sec) + '.png'
cv2.imwrite(file_name, current_frame)
self.get_logger().info(f'Image saved in {file_name}')
self.is_request = False
# Terminal 1 - gazebo env launch
ros2 launch rgbd_world depth_world.launch.py
# Terminal 2 - Execute Service Client
ros2 run cpp_service_tutorial spawn_object_wo_gravity
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/empty.hpp"
#include "gazebo_msgs/srv/spawn_entity.hpp"
class GZSpawnClient : public rclcpp::Node {
private:
rclcpp::Client<Spawn>::SharedPtr spawn_client;
rclcpp::Client<Empty>::SharedPtr pause_client;
...
public:
GZSpawnClient() : Node("gazebo_model_spawner"){
pause_client = this->create_client<Empty>("pause_physics");
spawn_client = this->create_client<Spawn>("spawn_entity");
Parameter | Description |
---|---|
template argument | Service에 사용될 데이터 타입 |
service name | Request 대상이 되는 Service 이름 |
while (!pause_client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service. Exiting.");
exit(0);
}
RCLCPP_INFO(this->get_logger(), "Pause service not available, waiting again...");
}
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<GZSpawnClient>();
auto pause_result = node->send_pause_request();
if (rclcpp::spin_until_future_complete(node, pause_result) == rclcpp::FutureReturnCode::SUCCESS) {
RCLCPP_INFO(node->get_logger(), "Pause Successfully Done.");
} else {
RCLCPP_ERROR(node->get_logger(), "Failed to pause physics!");
}
std::string model_name;
...
auto spawn_result = node->send_spawn_request(model_name);
// Wait for the spawn_result.
if (rclcpp::spin_until_future_complete(node, spawn_result) == rclcpp::FutureReturnCode::SUCCESS) {
RCLCPP_INFO(node->get_logger(), "Spawn Successfully Done.");
} else {
RCLCPP_ERROR(node->get_logger(), "Failed to spawn model!");
}
rclcpp::shutdown();
return 0;
}
Enumerator |
---|
SUCCESS |
INTERRUPTED |
TIMEOUT |
auto send_pause_request(){
return pause_client->async_send_request(pause_request);
}
auto send_spawn_request(const std::string &model_name){
auto item = model_map.find(model_name);
std::string model_path;
...
spawn_request->xml = ss.str();
...
return spawn_client->async_send_request(spawn_request);
}
service call의 동기, 비동기 실행에 대해서는 이후 Advanced 강의에서 좀 더 자세하게 다루어 볼 예정입니다.
Code | Description |
---|---|
create_client() | client 생성 |
rclcpp::spin_until_future_complete(,) | Client Request Task가 완료되기까지 비동기 대기 |
rclcpp::FutureReturnCode | Future의 상태를 담고 있는 enum type |
async_send_request(std::make_sharedSpawn::Request) | 비동기 service call |
# Terminal 1 - Execute Gazebo World
ros2 launch rgbd_world depth_world.launch.py
# Terminal 2 - Execute Service Server
ros2 run cpp_service_tutorial take_picture_server
# Terminal 3 - Execute rqt's service caller
rqt
지금, 우리에게 필요한 통신 메커니즘은 2가지 입니다.
복습을 해볼 겸, 이 부분을 직접 코딩해보는 것도 좋은 학습이 될 것입니다.
class PictureNode : public rclcpp::Node {
private:
rclcpp::Service<SetBool>::SharedPtr bool_server;
rclcpp::Subscription<Image>::SharedPtr image_subscriber;
...
public:
PictureNode() : Node("take_picture_server"){
image_subscriber = this->create_subscription<Image>(
this->img_topic_name, 10,
std::bind(&PictureNode::img_sub_callback, this, std::placeholders::_1)
);
bool_server = this->create_service<SetBool>(
"take_picture",
std::bind(&PictureNode::server_callback, this, std::placeholders::_1, std::placeholders::_2)
);
client service 먼저 살펴보겠습니다.
Parameter | Description |
---|---|
template argument | Srv Type |
“take_picture” | Service Name |
std::bind(Class, Method) | Request 시 실행될 Service Callback |
void server_callback(const std::shared_ptr<SetBool::Request> request,
const std::shared_ptr<SetBool::Response> response){
if (request->data) {
RCLCPP_INFO(this->get_logger(), "KimChi~");
this->is_request = true;
}
response->success = true;
response->message = "Successfully image written";
}
#include <cv_bridge/cv_bridge.h>
#include <image_transport/image_transport.hpp> // Using image_transport allows us to publish and subscribe to compressed image streams in ROS2
#include <opencv2/opencv.hpp> // We include everything about OpenCV as we don't care much about compilation time at the moment.
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/image.hpp"
#include "example_interfaces/srv/set_bool.hpp"
...
void img_sub_callback(const Image::ConstSharedPtr msg){
if (this->is_request){
cv::Mat img = cv_bridge::toCvShare(msg, "bgr8")->image;
std::string file_name = std::to_string(this->get_clock()->now().seconds()) + ".jpg";
cv::imwrite(file_name, img);
RCLCPP_INFO(this->get_logger(), "Image saved as %s", file_name.c_str());
this->is_request = false;
}
}
find_package(rclcpp REQUIRED)
find_package(std_srvs REQUIRED)
find_package(turtlesim REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(gazebo_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(custom_interfaces REQUIRED)
find_package(example_interfaces REQUIRED)
find_package(OpenCV REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(image_transport REQUIRED)
add_executable(take_picture_server src/take_picture_server.cpp)
ament_target_dependencies(take_picture_server rclcpp example_interfaces std_msgs sensor_msgs cv_bridge image_transport OpenCV)