액션이란? 서비스와 마찬가지로 Request-Response 모델을 기반으로 한다.
- Goal과 Result 둘다 Request-Response 모델을 사용한다.
- Goal이라는 Request를 client가 보내지만 이것에 대한 직접적인 response를 우리가 지정해서 보내지는 않지만 GoalHandle을 통해 서버가 goal에 대해서 Accept했다는 것을 client에서 add_done_callback 함수의 future을 통해 client가 간접적으로 알 수 있다.
-> goal_callback을 ActionServer 클래스에 정의를 해서 들어오는 요청에 대해 Reject를 한다면 Accept 상태가 안되지만
goal_callback을 default 값으로 쓴다면 액션 클라이언트가 goal 요청을 보내는 것에 대해 항상 Accept한다.
-> 즉, Action server의 Goal에 대한 Response와 Action Client의 Result에 대한 Request는 사용자가 정의하는 것이아니라 ROS2 내부에서 알아서 정해진 응답(Goal State Machine 기반으로 응답)을 주고받으며 실행되는 구조이다.
- Goal이라는 Request를 client가 보내지만 이것에 대한 직접적인 response를 우리가 지정해서 보내지는 않지만 GoalHandle을 통해 서버가 goal에 대해서 Accept했다는 것을 client에서 add_done_callback 함수의 future을 통해 client가 간접적으로 알 수 있다.
-> 위의 예시에서 goal_handle.accept를 통해 간접적으로 나마 accept를 했는지 확인할 수 있다.
Goal State Machine이란?
ROS 2의 Action은 본질적으로 비동기적인 구조이다.
이러한 비동기 환경에서 목표(goal)의 상태를 명확하게 추적하고, 액션 클라이언트와 서버 간에 상태 정보를 동기화하여 안정적인 처리를 가능하게 하기 위해 Goal State Machine이 도입되었다.Goal State Machine은 목표가 전달된 이후부터 종료되기까지의 상태 흐름을 정의하며, 이 상태 정보를 클라이언트와 주고받음으로써 비동기와 동기 방식이 혼재된 상황에서도 액션 처리를 원활하게 수행할 수 있도록 도와준다.
- 실제 Terminal State는 클라이언트에서 future.result().status를 통해 담기게 되는 값이다.
- 액션 통신을 원활히 하고 있을때 액션 서버를 강제 종료한다면?
- 액션 서버와 클라이언트가 잘 동작하다 서버를 강제로 끄게 되면 클라이언트에서 피드백이 오다 멈추는데 이 경우에 클라이언트가 종료가 안되고 .add_done_callback()에 의해 결과를 무한정 기다리는 상태에 이르게 된다.
- 이것을 goal state machine로 보면 ACCEPTED하고 EXECUTING 하다 강제 종료로 인해 EXECUTING에 머무르는 상태가 되는 것으로 볼 수 있다.
-> 그래서 이렇게 예기치 못하게 좀비 노드가 되는 경우들도 생각해서 코딩해줘야함
=> ACCEPTED → EXECUTING → (서버 강제 종료) → 상태 전이 없음
액션과 서비스와의 두가지 주요 차이점
- 액션은 실행되는 동안 취소가 가능하다.
- 액션은 피드백을 제공한다 즉, 액션이 실행되는 동안 서버는 클라이언트에게 피드백을 보낼 수 있다.
- 액션 기능을 제공하는 노드에는 액션 서버가 포함되야 한다.
- 액션 서버를 통해 다른 노드가 해당 액션 기능을 호출할 수 있다.
- 다수의 클라이언트가 하나의 액션 서버에 요청을 보냈을 때 병렬로 처리가 가능하다.
- 서비스는 한 번에 하나의 요청만 처리하기 때문에 단일 스레드에서 직렬로 처리된다.
예외) 멀티 스레드 구현시는 동시 처리 비슷하게 동작 가능
- 서비스는 한 번에 하나의 요청만 처리하기 때문에 단일 스레드에서 직렬로 처리된다.
- 액션 기능을 호출하는 노드에는 액션 클라이언트가 포함되어야 한다.
- 액션 클라이언트를 통해 노드는 다른 노드의 액션 서버에 연결할 수 있다.
- Feedback은 토픽과 같은 형태로 구현이 된다.
- 긴 시간이 걸리는 작업에 적합하다 ex) NAV2 등
액션의 워크플로우
- 클라이언트가 서버로 Goal을 보내면 액션의 GoalHandle 객체를 통해 "시작"이 트리거 된다.
- 서버는 액션이 진행되는 동안 클라이언트에 Feedback을 보낸다.
- 액션이 완료되면 서버는 클라이언트에 Result를 반환한다.
GoalHandle 이란?
- ActionServer가 각 Goal 요청을 추적하기 위해 생성하는 객체
- 클라이언트의 각 Goal마다 따로 생성된다 (1:1 대응).
-> ActionServer에서 반환 하는 객체(Goal Handle)이고 이 객체를 통해 ActionServer의 함수들을 호출해서 아래와 같은 행위를 할 수 있게 되는 것이다.
- Action Handler의 역할
- goal_handle.accept() → Goal 수락
- goal_handle.publish_feedback(feedback) → 피드백 전송
- goal_handle.succeed() / abort() / canceled() → 결과 상태 설정
- goal_handle.set_result(result) → 결과 반환
- 이 모든 과정을 execute_callback(goal_handle) 함수 내에서 처리가 되며 사용자가 ActionServer에 넘겨준 콜백함수에 goal_handle가 전달이 되어서 모든 과정이 이루어지게 된다.
✅ execute_callback(goal_handle)
- 사용자가 직접 작성해서 ActionServer에 넘겨주는 핵심 콜백 함수
- 클라이언트가 Goal을 보내면 이 함수가 호출된다.
- 이 함수의 매개변수 goal_handle이 바로 서버가 생성한 GoalHandle 객체
✅ 결론
✔️ 클라이언트가 Goal을 보내면
➜ 서버는 GoalHandle 객체를 만들어서
➜ execute_callback(goal_handle) 함수에 전달하고
➜ 이 함수에서 그 Goal에 대한 처리, 피드백, 결과 반환까지 모두 수행하게 된다.
Action 관련 명령어
ros2 action -h # action에 어떤 명령어들이 있는지 보여주는 명령어 옵션
ros2 action list # 현재 ROS2 시스템에서 사용 가능한 모든 액션을 나열한다.
ros2 action info <action_name> # 특정 액션에 대한 정보를 볼 수 있는 명령어
ros2 action info <action_name> -t # -t 옵션을 추가하면 해당 액션에서 사용된 인터페이스에 대한 정보도 볼 수 있다.
-> 이 출력은 turtle1/rotate_absolute 라고 하는 액션이 "turtlesim/action/RotateAbsolute 인터페이스를 사용한다는 것을 알려준다.
<pkg_name>/action/<interface_name> # 액션 인터페이스 구조
-> 모든 액션 인터페이스는 action이라는 폴더 안에서 정의된다.
-> 다른 액션 서버에서는 패키지 이름과 인터페이스가 다를 수 있다.
ros2 interface show turtlesim/action/RotateAbsolute # 해당 action 인터페이스에 대한 자세한 데이터 출력
-> 액션 인터페이스는 --- 구분선이 2개이며 차례대로 Goal, Result, Feedback 으로 ROS2 내부에서 자동으로 section 명이 할당이 된다.
- Goal : 클라이언트가 서버에게 요청하는 작업의 정보를 담고 있다.
- 클라이언트 -> 서버
- Result : 작업이 완전히 끝났을 때 서버가 클라이언트에게 보내는 응답
- 서버 -> 클라이언트
- Feedback : 작업이 진행 중일떄 중간 상태를 실시간으로 보내는 메세지
- 서버 -> 클라이언트
ros2 action send_goal <action_name> <action_type> <values>
# 위의 명령어를 통해 액션 서버에 Goal(요청)을 보낼 수 있다.
# 즉 위의 명령어는 액션 클라이언트의 역할을 한다.
ros2 action send_goal -f <action_name> <action_type> <values>
# 위의 명령어 처럼 -f 옵션을 사용하면 피드백 데이터를 모니터링 할 수 있다.
# value에는 goal 값만 줘야 한다.
ros2 action send_goal -f /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 0.1}"
이제 간단하게 Empty goal을 보냈을때 즉, 어떠한 데이터도 담지 않고, 요청만 보냈을때 0~100 까지 5씩 증가해서 100이 되었을때 종료하는 action server를 만들어보자
-> feedback으로 그 과정을 계속 넘겨주고
-> 5,10,15.....100이 되었을 때 마지막 값으로 100을 넘겨주고, Successed를 띄우며 종료하는 프로그램
1. Action Server 만들기 전에 Action을 통해서 Server와 Client가 액션 메세지를 주고받을 인터페이스를 만들어야 한다.
이제 colcon build 후에 인터페이스 리스트를 봤을때 잘 생성된 것을 볼 수 있다.
* 주의할 점 : 처음에 colcon build할때 --symlink-install 옵션을 주지 않았다면 그 다음에도 이 옵션을 주지 않아야한다. 만약 주면 충돌나서 오류가 뜬다.
-> 만약 나중에라도 --symlink-install 옵션을 주고 싶다면 워크스페이스에서 src를 제외한 나머지 디렉토리를 지우고 다시 colcon build --symlink-install 를 해줘야한다.
2. 이제 Action Server를 만들어 보자
ros2 pkg create --build-type ament_python action_pkg --dependencies rclpy ros_study_msgs
# 무조건 src의 경로에서 패키지를 생성해야한다.
- Action Server 코드
- 코드 설명
1. 필수 패키지들 호출
import rclpy
from rclpy.node import Node
# 액션 서버를 생성할 수 있도록 rclpy.action에서 ActionServer 클래스 호출
from rclpy.action import ActionServer
afrom ros_study_msgs.action import CountChecker
#sleep()함수를 사용하기 위해 time을 import
import time
** Node의 기본 틀은 Class와 Main 함수로 구성되어 있다.
2. Node를 만들기 위해서는 해당 클래스가 Node를 상속해야한다.
-> ROS2에서 제공하는 Node 클래스에는 기본적인 함수들이 정의되어있다.
-> 부모 클래스를 상속받으면 자식클래스는 부모 클래스에 선언된 함수와 변수 등을 사용할 수 있다.
class ActionCountServer(Node): # Node 클래스(부모)를 상속받는 ActionCountServer 클래스(자식)
3. 클래스 내부에는 __init__(생성자 : 클래스 초기화 함수), execute_checker(클라이언트로부터 Goal 요청이 들어왔을때 Result을 주기 위해 처리하는 함수) 로 이루어져 있다.
- __init__ 내부 기본 형태
def __init__(self):
'''''
'''''
def execute_checker(self):
'''''
'''''''
3-1 __init__ 함수 내부 코드
def __init__(self):
# 부모 클래스 생성자를 호출하는 것으로 인자로 넘겨준 'action_server' 가 Node의 이름이 된다.
super().__init__('action_server')
self.get_logger().info("action_server start!!") # 액션 서버가 시작되었다는 것을 알려주는 로그 메세지
# ActionServer에 4개의 인자를 줘서 액션 서버 객체 생성
# 서비스의 서버와 마찬가지로 self.action_server를 종료할떄 말고는 직접적으로 사용하지 않는다
# -> 내부적으로 자동으로 관리 하기 때문(백그라운드에서 처리)
self.action_server = ActionServer( self, # 액션 서버를 포함하는 ROS2 노드이므로 self
CountChecker, # action_type (인터페이스)
'count_checker', # action_name
self.execute_checker ) # 액션이 목표(Goal)를 수신할 때 실행할 콜백 메서드
의문) 왜 ActionServer의 첫번째 인자로 self를 넣는 걸까?
- ActionServer가 ROS2 노드(Node) 객체의 일부로 동작하기 때문이다.
-> topic, service, action 과 같은 통신은 Node를 기반으로 동작한다 하지만 action과 달리 topic과 service는 self로 자동으로 노드를 전달하지만 action은 내장된 헬퍼 함수(create_publisher와 같은 self를 가져오도록 도우는 함수)가 없어서 우리가 직접 전달해줘야한다.
-> action의 경우 서비스와 토픽의 성질을 둘다 가지고 있는 복잡한 통신 구조이기 때문
-> action의 경우 class를 불러(호출)와서 사용하지만 service와 topic의 경우 Node를 상속 후 내장된 함수(create_publisher, create_service 등)를 사용하기 때문
3-2 execute_checker(클라이언트로부터 Goal 요청이 들어왔을때 Result을 주기 위해 처리하는 함수)
# 액션이 목표(Goal)를 수신할 때 실행할 콜백 메서드
def execute_checker(self, goal_handle):
self.get_logger().info("goal arrived") # goal을 액션 클라이언트가 보냈다는 것을 알려주는 로그 메세지
feedback_msg = CountChecker.Feedback() #사용자 정의 액션 인터페이스의 Feedback section 객체 생성
feedback_msg.feedback = 0 # 초기 값 세팅
while feedback_msg.feedback <96: # 100에서 멈추도록 하는 조건문
goal_handle.publish_feedback(feedback_msg) # feedback topic 형태로 발행
time.sleep(1) # 피드백을 너무 빨리 보낼 경우 클라이언트가 처리하기 힘들 수 있으니 1초 간격으로 보내도록 세팅
feedback_msg.feedback += 5 # 5씩 피드백 값 증가 시킴
goal_handle.succeed() # 액션 서버에서 목표에 성공적으로 도달했음을 클라이언트에게 알리는 함수
result_msg = CountChecker.Result() #사용자 정의 액션 인터페이스의 Result section 객체 생성
result_msg.result = feedback_msg.feedback # 최종 결과를 담고 전송
return result_msg
4. 나머지는 서비스 서버와 동일하게 main문이 구성된다 서버는 spin을 통해 항시 client의 요청을 받을 준비가 되어 있어야한다.
def main(args=None):
rclpy.init(args=args)
action_server_node = ActionCountServer()
rclpy.spin(action_server_node)
action_server_node.action_server.destroy() # ㅅㅓㅂㅓ 종료
action_server_node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__': # 파이썬 스크립트가 직접 실행될때만 main함수가 실행되도록 하는 구문
main()
#if __name__ == '__main__':은 절대 들여쓰기 하면 안 되고, **맨 바깥 레벨(전역 스코프)**에 있어야 한다.
5. 해당 스크립트를 실행 시키려면 토픽을 만들때와 마찬가지로 setup.py의 entry_points에 추가해줘야한다
-> 방식이 이해 안되면 topic 생성하는 부분 다시 보고오기를 바란다.
6. 이제 colcon build 한 후에 해당 스크립트를 실행하고 "ros2 action goal_send" 명령어로 클라이언트의 goal을 보냈을때 아래 사진처럼 잘 동작하는 것을 볼 수 있다.
- 왼쪽 터미널 입력한 명령어
주의할 사항 : ROS 2 CLI(ros2 action send_goal)는 goal 인자는 비어있어도 무조건 요구한다
-> CLI에서는 goal이 비어 있어도 "{}"를 반드시 명시해줘야 한다.
ros2 action send_goal -f /count_checker ros_study_msgs/action/CountChecker "{}"
아래 그림과 같은 틀을 기반으로 필요한 코드를 추가해서 만드는 것이 액션 서버이다!
- 각 함수 안에 들어가는 내용들은 입맞에 맞게 알고리즘을 구성해주면 된다.
'ROS2 이론 정리' 카테고리의 다른 글
ROS2 스레드 (0) | 2025.04.26 |
---|---|
Action Client 구현(2) (0) | 2025.04.24 |
서비스 클라이언트 만들기(2) (0) | 2025.04.23 |
서비스 서버 만들기(1) (0) | 2025.04.22 |
사용자 정의 Interface 생성 및 활용 (0) | 2025.04.18 |