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 루프 안에서 호출된다. 이렇게 하면 프로그램이 계속 실행되면서 필요에 따라 콜백을 실행하고 다른 작업을 수행할 수 있다.
- 노드를 더 세밀하게 제어할 필요가 있을때 유용