(ROS2-4 시작) 서비스

ROS2 서비스

  • ROS2 서비스는 클라이언트/서버 시스템입니다.
    • 동기식 또는 비동기식
    • 요청에 대한 하나의 메시지 유형, 응답에 대한 하나의 메시지 유형
    • Python, C++, …의 ROS 노드에서 직접 작성할 수 있습니다.
    • 서비스 서버는 한 번만 존재할 수 있지만 여러 클라이언트를 가질 수 있습니다.


  • 서비스 인터페이스 예
ros2 interface show example_interfaces/srv/AddTwoInts
"
int64 a # request
int64 b # request
---
int64 sum # response
"

섬기는 사람

cd ~/ros2_ws/src/my_py_pkg/my_py_pkg
touch add_two_ints_server.py
chmod +x add_two_ints_server.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
 
from example_interfaces.srv import AddTwoInts
 
class AddTwoIntsServerNode(Node):
    def __init__(self):
        super().__init__("add_two_ints_server")
        self.server_ = self.create_service(
            AddTwoInts, "add_two_ints", self.callback_add_two_ints)
        self.get_logger().info("Add two ints server has been started.")

    def callback_add_two_ints(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info(str(request.a) + " + " + 
                               str(request.b) + " = " + str(response.sum))
        return response
 
def main(args=None):
    rclpy.init(args=args)
    node = AddTwoIntsServerNode()
    rclpy.spin(node)
    rclpy.shutdown()
 
 
if __name__ == "__main__":
    main()
  • 서비스에 대한 명령
ros2 service list # you can see list of services

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 3, b: 10}"


OOP가 없는 클라이언트

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node

from example_interfaces.srv import AddTwoInts

def main(args=None):
    rclpy.init(args=args)
    node = Node("add_two_ints_no_oop")
    
    client = node.create_client(AddTwoInts, "add_two_ints")
    while not client.wait_for_service(1.0):
        node.get_logger().warn("Waiting for Server Add Two Ints...")

    request = AddTwoInts.Request()
    request.a = 3
    request.b = 8

    future = client.call_async(request)
    rclpy.spin_until_future_complete(node, future)

    try:
        response = future.result()
        node.get_logger().info(str(request.a) + " + " + 
                               str(request.b) + " = " + str(response.sum))
    except Exception as e:
        node.get_logger().error("Service call failed %r" % (e,))

    rclpy.shutdown()
 
 
if __name__ == "__main__":
    main()

클라이언트.콜 는 서버가 응답할 때까지 차단되는 동기 호출이지만 경우에 따라 응답이 제공되더라도 영원히 멈출 수 있으므로 권장하지 않습니다.

우리는 항상 사용할 것입니다 client.call_async 사용하면 호출_async 요청을 보내지만 계속 실행됩니다.

객체 지향 클라이언트

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from functools import partial

from example_interfaces.srv import AddTwoInts
 
class AddTwoIntsClientNode(Node):
    def __init__(self):
        super().__init__("add_two_ints_client")
        self.call_add_two_ints_server(6, 7)
    
    def call_add_two_ints_server(self, a, b):
        client = self.create_client(AddTwoInts, "add_two_ints")
        while not client.wait_for_service(1.0):
            self.get_logger().warn("Waiting for Server Add Two Ints...")

        request = AddTwoInts.Request()
        request.a = a
        request.b = b       

        future = client.call_async(request)
        future.add_done_callback(partial(self.callback_call_add_two_ints, a=a, b=b))

    def callback_call_add_two_ints(self, future, a, b):
        try:
            response = future.result()
            self.get_logger().info(str(a) + " + " + 
                                   str(b) + " = " + str(response.sum))
        except Exception as e:
            self.get_logger().error("Service call failed %r" % (e,))
 
def main(args=None):
    rclpy.init(args=args)
    node = AddTwoIntsClientNode()
    rclpy.spin(node)
    rclpy.shutdown()
 
 
if __name__ == "__main__":
    main()

스핀이 노드 내에서 호출되면 다른 스핀 함수를 호출해서는 안 됩니다. main 함수에서 이미 spin을 호출했기 때문에 spin_until_future_complete를 호출할 수 있습니다.

디버그 서비스 ROS2 도구

서비스 일람을 볼 수 있습니다

ros2 service list
"
/add_two_ints
/add_two_ints_server/describe_parameters
/add_two_ints_server/get_parameter_types
/add_two_ints_server/get_parameters
/add_two_ints_server/list_parameters
/add_two_ints_server/set_parameters
/add_two_ints_server/set_parameters_atomically
"

우리는 서비스의 인터페이스를 알 수 있습니다

ros2 node list
# /add_two_ints_server

ros2 node info /add_two_ints_server
"
/add_two_ints_server
  Subscribers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
  Publishers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /rosout: rcl_interfaces/msg/Log
  Service Servers:
    **/add_two_ints: example_interfaces/srv/AddTwoInts <-- you can find it !**
    /add_two_ints_server/describe_parameters: rcl_interfaces/srv/DescribeParameters
    /add_two_ints_server/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
    /add_two_ints_server/get_parameters: rcl_interfaces/srv/GetParameters
    /add_two_ints_server/list_parameters: rcl_interfaces/srv/ListParameters
    /add_two_ints_server/set_parameters: rcl_interfaces/srv/SetParameters
    /add_two_ints_server/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
  Service Clients:

  Action Servers:

  Action Clients:
"
# or
ros2 service type /add_two_ints
# example_interfaces/srv/AddTwoInts

ros2 interface show example_interfaces/srv/AddTwoInts
"
int64 a
int64 b
---
int64 sum
"

명령줄에서 서비스 호출

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 3, b: 5}"
#or
rqt



식에 데이터를 입력하고 통화버튼을 누르면 본 서비스를 이용하실 수 있습니다.


리매핑 서비스의 주제 이름

ros2 run my_cpp_pkg add_two_ints_server
ros2 service list
"
/add_two_ints
/add_two_ints_server/describe_parameters
/add_two_ints_server/get_parameter_types
/add_two_ints_server/get_parameters
/add_two_ints_server/list_parameters
/add_two_ints_server/set_parameters
/add_two_ints_server/set_parameters_atomically
"

ros2 run my_cpp_pkg add_two_ints_server --ros-args -r add_two_ints:=new_name
ros2 service list
"
/add_two_ints_server/describe_parameters
/add_two_ints_server/get_parameter_types
/add_two_ints_server/get_parameters
/add_two_ints_server/list_parameters
/add_two_ints_server/set_parameters
/add_two_ints_server/set_parameters_atomically
/new_name
"

서버와 클라이언트가 통신하며 서비스 주제 이름이 일치해야 합니다.

# it can't work because of different topic name of service
ros2 run my_cpp_pkg add_two_ints_client

# so you must match the topic name of service of server and client
ros2 run my_cpp_pkg add_two_ints_client --ros-args -r add_two_ints:=new_name

활동

ROS2 서비스로 연습하자!

이전 섹션의 주제에서 수행한 주제 활동부터 시작합니다.

다음과 같은 결과를 얻을 수 있습니다.


빠른 요약:

  • “number_publisher” 노드는 주제 /”number”에 번호를 게시합니다.
  • “number_counter” 노드는 번호를 가져와 카운터에 추가하고 “/number_count” 항목에 카운터를 게시합니다.

이제 추가할 내용은 다음과 같습니다.


카운터를 0으로 재설정하는 기능을 추가합니다.

  • “number_counter” 노드 내부에 서비스 서버를 생성합니다.
  • 서비스 이름: “/reset_counter”
  • 서비스 유형: example_interfaces/srv/SetBool. “ros2 interface show”를 사용하여 내부에 무엇이 있는지 알아보십시오!
  • 서버를 호출할 때 요청에서 부울 데이터를 확인합니다. true인 경우 카운터 변수를 0으로 설정합니다.

그런 다음 명령줄에서 직접 서비스를 호출합니다. 또한 (연습을 위해) 이 “/reset_counter” 서비스를 호출하기 위해 고유한 사용자 지정 노드를 만들기로 결정할 수도 있습니다.

물론 평소와 같이 Python 및 Cpp 버전 모두에서 이 작업을 수행할 수 있습니다.

다음 수업에서 만나요, 솔루션.

해결책

number_counter.py

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node

from example_interfaces.msg import Int64
from example_interfaces.srv import SetBool

class NumberCounterNode(Node):
    def __init__(self):
        super().__init__("number_counter")
        self.counter_ = 0
        self.number_count_publisher_ = self.create_publisher(
            Int64, "number_count", 10)
        self.number_subscriber_ = self.create_subscription(
            Int64, "number", self.callback_number, 10)
        self.reset_counter_service_ = self.create_service(
            SetBool, "reset_counter", self.callback_reset_counter)
        self.get_logger().info("Number Counter has been started.")

    def callback_number(self, msg):
        self.counter_ += msg.data
        new_msg = Int64()
        new_msg.data = self.counter_
        self.number_count_publisher_.publish(new_msg)

    def callback_reset_counter(self, request, response):
        if request.data:
            self.counter_ = 0
            response.success = True
            response.message = "Counter has been reset"
        else:
            response.success = False
            response.message = "Counter has not been reset"
        return response

def main(args=None):
    rclpy.init(args=args)
    node = NumberCounterNode()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == "__main__":
    main()

number_count.cpp

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/msg/int64.hpp"
#include "example_interfaces/srv/set_bool.hpp"

using std::placeholders::_1;
using std::placeholders::_2;

class NumberCounterNode : public rclcpp::Node
{
public:
    NumberCounterNode() : Node("number_counter"), counter_(0)
    {
        counter_publisher_ = this->create_publisher<example_interfaces::msg::Int64>("number_count", 10);
        number_subscriber_ = this->create_subscription<example_interfaces::msg::Int64>(
            "number", 10, std::bind(&NumberCounterNode::callbackNumber, this, std::placeholders::_1));
        reset_number_service_ = this->create_service<example_interfaces::srv::SetBool>(
            "reset_counter",
            std::bind(&NumberCounterNode::callbackResetCounter, this, _1, _2));
        RCLCPP_INFO(this->get_logger(), "Number Counter has been started.");
    }

private:
    void callbackNumber(const example_interfaces::msg::Int64::SharedPtr msg)
    {
        counter_ += msg->data;
        auto newMsg = example_interfaces::msg::Int64();
        newMsg.data = counter_;
        counter_publisher_->publish(newMsg);
    }

    void callbackResetCounter(const example_interfaces::srv::SetBool::Request::SharedPtr request,
                              const example_interfaces::srv::SetBool::Response::SharedPtr response)
    {
        if (request->data)
        {
            counter_ = 0;
            response->success = true;
            response->message = "Counter has been reset";
        }
        else
        {
            response->success = false;
            response->message = "Counter has not been reset";
        }
    }

    int counter_;
    rclcpp::Publisher<example_interfaces::msg::Int64>::SharedPtr counter_publisher_;
    rclcpp::Subscription<example_interfaces::msg::Int64>::SharedPtr number_subscriber_;
    rclcpp::Service<example_interfaces::srv::SetBool>::SharedPtr reset_number_service_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<NumberCounterNode>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

부분적인 결론

이 섹션에서는 ROS2 서비스를 발견하고 이를 사용하여 노드 간에 클라이언트/서버 통신을 추가하는 방법을 배웠습니다.

요약하면 서비스는 다음과 같습니다.

  • 클라이언트/서버 유형 통신용.
  • 동기식 또는 비동기식(스레드에서 대기하기로 결정한 경우에도 비동기식으로 사용하는 것이 권장됨).
  • 익명: 클라이언트는 어떤 노드가 서비스 뒤에 있는지 알지 못하고 서비스를 호출합니다. 그리고 서버는 어떤 노드가 클라이언트인지 알지 못하며 요청을 수신하고 이에 응답할 뿐입니다.

노드 내부에 서비스를 구현하려면:

  • 노드를 생성하거나 기존 노드로 시작하십시오. 원하는 만큼 서비스 서버 추가(모두 다른 이름 사용)
  • 서비스 클라이언트에서 서비스 서버를 호출할 때 서비스 이름과 서비스 유형(요청 + 응답)이 동일한지 확인하십시오.
  • 서비스에 대해 하나의 서버만 만들 수 있지만 여러 클라이언트를 만들 수 있습니다.


노드 사이에 새로운 통신을 추가하고 싶을 때 스스로에게 다음과 같은 질문을 해보세요. 테마나 서비스를 사용해야 하는지 알려줍니다. 그리고 ROS2를 진행하면서 결국에는 매우 명백해질 것입니다.

이제 노드를 만들고 서로 통신할 수 있습니다. 그러나 지금까지는 기존 메시지만 사용했습니다. 주제 및 서비스에 대해 다른 메시지 유형을 사용해야 하는 경우에는 어떻게 합니까?

이 경우 다음 섹션에서 볼 수 있는 고유한 메시지 유형을 빌드해야 합니다.