ROS2 이론 정리

서비스 클라이언트 만들기(2)

dawon-project 2025. 4. 23. 02:17

https://dawon-project.tistory.com/115

 

서비스 서버 만들기(1)

서비스란? -> 동기식 양방향 메세지 송수신 방식토픽과 마찬가지로 서비스도 ROS2의 노드 간 통신 방식토픽 vs 서비스토픽은 Publisher-Subscriber 모델을 사용노드는 토픽을 Subscribe하여 지속적인 업데

dawon-project.tistory.com

이제 터미널에서 service 명령어를 통해 request 요청을 보내지 말고, 서비스 클라이언트를 만들어보자

  • 코드 분석

1. 필수 라이브러리들 호출

from ros_study_msgs.srv  import ArithmeticOperator # 생성한 사용자 인터페이스 패키지 호출
import rclpy # 서비스 클라이언트를 생성하는 함수 등 Python 클라이언트 라이브러리 호출
from rclpy.node import Node # Node를 생성하기 위한 라이브러리 호출
import random # 랜덤 함수를 사용하기 위한 라이브러리 호출

 

** Node의 기본 틀은 Class와 Main 함수로 구성되어 있다.

2. Node를 만들기 위해서는 해당 클래스가 Node를 상속해야한다.
-> ROS2에서 제공하는 Node 클래스에는 기본적인 함수들이 정의되어있다.
-> 부모 클래스를 상속받으면 자식클래스는 부모 클래스에 선언된 함수와 변수 등을 사용할 수 있다

class Service_client(Node):

3. 클래스 내부에는 __init__(생성자 : 클래스 초기화 함수), send_request(서버에게 요청을 보내는 함수) 로 이루어져 있다.

def __init__(self):
'''''
                 '''''
def send_request(self):
'''''
              '''''''

3-1 __init__ 함수 내부 코드

def __init__(self):
# 부모 클래스 생성자를 호출하는 것으로 인자로 넘겨준 'service_client' 가 Node의 이름이 된다.
      super().__init__('service_client')
# self.create_client는 ROS2에서 서비스 클라이언트 객체를 생성할 때 사용하는 함수로
# Client 객체를 반환한다.
      self.service_client = self.create_client(ArithmeticOperator, # 사용할 서비스 타입
                                               'arithmetic_operator') # 요청을 보낼 서비스의 이름

# self.service_client.wait_for_service(timeout_sec=1.0) : 서버가 살아있는지 1초간 기다리는 함수
# 이 함수는 timeout_sec=1.0을 인자로 주지 않게 되면 기본값이 None이 되어서 무한정 대기하게 된다.
# 이렇게 코딩한 이유는 서비스 클라이언트가 살아있는지 확인하기 위함도 있다.
      while not self.service_client.wait_for_service(timeout_sec=1.0): # 서버가 준비될때까지 계속 기다리기 위한 코드
              self.get_logger().warning("service server service not available")

 

서비스 클라이언트 객체를 반환하는 함수와 서비스 서버 객체를 반환하는 함수의 차이

항목 create_client() create_service()
역할 클라이언트를 생성하며 서버에게 요청을 보내는 객체 반환 서비스 서버 생성하고, 클라이언트의 요청을 처리 할 수 있도록 콜백을 등록
반환값을 담은 객체 사용 여부 반환된 Client 객체를 변수에 저장해 참조(".") 연산자를 통해 직접 사용 반환된 service server객체는 내부적으로 자동 관리 및 실행되어서 직접 참조할 필요 X
-> 서비스 서버를 수동으로 종료하려 할때만 사용
사용 방식 직접  요청을 보내기 위해 ".call_async()" 등을 객체 참조를 통해 호출 등록만 하면 요청이 들어왔을때 자동으로 콜백 함수 실행

 

3-2 send_request 함수 내부 코드

# 서비스 서버에게 미리 정의해 놓은 인터페이스 형식에 맞게 요청을 보내면 서버로부터 받은 응답을 반환하는 함수
def send_request(self):
# 서비스 클라이언트가 서비스 서버에게 요청을 보내기 위한 데이터를 담기 위한 객체 생성
      service_request = ArithmeticOperator.Request()
#1~4의 사이의 상수 값으로 mapping된 부호 중 하나를 랜덤으로 선택해서 request 객체에 담는 코드
      service_request.arithmetic_operator = random.randint(1,4)
# self.service_client.call_async 함수를 통해 만든 request 객체인 "service_request"를 담아서
#서버에게 비동기 요청을 하고, 응답이 올때까지 서버 클라이언트는 다른 작업이 있으면 다른 작업을 하다가
#응답이 오면 futures 변수에에 담고, 그 futures 반환
# 요청을 한 후 서버는 즉시 호출 및 응답이 완료 되었는지 여부를 나타내는 future를 반환한다.(future 객체의 함수 2가지를 통해)
# 1. future.done() : 응답 완료되었다는 것을 알려주는 함수
# 2. future.result() : 응답을 반환하는 함수
      futures = self.service_client.call_async(service_request)
      return futures

-> 서비스 클라이언트는 call_async 함수를 통해 직접 서버에게 비동기 요청을 보내는 구조이다.

비동기(async)란?  어떠한 요청을 했을때 다른 작업을 하다가 그 요청에 대한 응답이 왔을때 처리하는 방식

  • 서비스 클라이언트의 요청, 액션 골 전송, 일종의 이벤트 기반 비동기(timer 콜백, subscription 콜백 등) 등
  • 콜백 함수를 통해 처리하거나 self.future.done()으로 확인을 한 후 처리 
    => call_async() 사용

동기(sync)란? 어떠한 요청을 했을때 그 요청에 대한 응답이 올때까지 계속 기다리다 처리하는 방식
-> 권장하지 않는다.

  • 순차적으로 처리됨
  • 기다리는 동안 다른 작업 불가
  • 실시간성이 중요한 시스템에선 지연 발생 가능

4. main 함수 내부 코드

def main(args=None):
# ROS2 노드를 실행할 준비를 함(RCL 초기화)
      rclpy.init(args=args)
# 위에서 만든 클래스(-> Node로 만듬)의 객체 즉 인스턴스(노드의 역할을 수행) 생성
      service_client = Service_client() # service client
# send_request() 함수를 호출해서 서비스 서버에게 요청을 보냄
      futures = service_client.send_request()

#노드가 작동하는 동안 서비스 응답이 완료되었는지 계속 확인
      while rclpy.ok(): 
#노드를 한번만 실행
            rclpy.spin_once(service_client).
# futures.done() :  응답이 왔다면 true 안 왔으면 False를 반환
            if futures.done():
                   try: 
#. 응답 결과 추출
                        service_response = futures.result()
                   except Exception as e :
# 응답 실패 했을 경우의 예외 처리
                        service_client.get_logger().warn('service call failed %r' %(e, ))
                   else:
# 예외가 발생하지 않았을 경우 응답 결과 로그 메세지로 출력
                        service_client.get_logger().info(f'response : {service_response}')
                   break
# 서비스 클라이언트 노드 종료
    service_client.destroy_node()
#rclpy 종료
    rclpy.shutdown()

if __name__ == '__main__':
    main()
  • 해당 스크립트를 실행 시키려면 토픽을 만들때와 마찬가지로 setup.py의 entry_points에 추가해줘야한다

  • 이제 colcon build 후에 service server를 먼저 키고 그다음에 service client를 실행했을떄 잘 동작하는 것을 볼 수 있다.

 

서비스 클라이언트 기본 틀


-> 해당 부분의 send_request 함수 부분에 사용자의 입맛대로 코드를 작성해주면 된다.

 

rclpy.spin vs rclpy.spin_once

rclpy.spin(node)

  • 이 함수는 지정된 노드에 대해 무한 루프를 실행하여 콜백을 계속해서 호출한다
  • 프로그램이 종료될 때까지 계속 실행되며, 모든 콜백, 타이머, 서비스 등이 처리될 수 있도록 한다.
  • 노드가 계속해서 실행되어야 할 때 주로 사용된다.


rclpy.spin_once(node,timeout_sec=None)

  • 이 함수는 지정된 노드에 사용 가능한 콜백을 호출한다.
  • 주로 while 루프 안에서 호출된다. 이렇게 하면 프로그램이 계속 실행되면서 필요에 따라 콜백을 실행하고 다른 작업을 수행할 수 있다.
  • 노드를 더 세밀하게 제어할 필요가 있을때 유용