1. Cancel 기능 실습 개요
이번 실습에서는 실행 중인 Action Goal을 중간에 취소하는 방법을 확인합니다.
ROS 2 Action은 Client가 Server에 Goal을 보내고, Server는 작업을 수행하면서 Feedback을 보내다가 마지막에 Result를 반환하는 구조입니다.
여기서 Cancel 기능은 이미 실행 중인 Goal을 중간에 취소할 때 사용합니다.
주의할 점은 일반적인 ROS 2 CLI 환경에서는 실행 중인 Action Goal을 직접 취소하는 다음 명령을 사용할 수 없는 경우가 많다는 것입니다.
ros2 action cancel /dist_turtle
따라서 이번 실습에서는 별도의 Action Client를 작성해서 Cancel 요청을 보냅니다.
Cancel 요청의 핵심 코드는 다음입니다.
self._cancel_future = self.goal_handle.cancel_goal_async()
이 코드는 현재 Client가 보낸 Goal에 대해 Action Server로 Cancel 요청을 전송합니다.
2. Cancel 테스트용 Action Client 파일 만들기
먼저 Cancel 요청을 보내는 Action Client 파일을 새로 만듭니다.
cd ~/ros2_study/src/my_first_package/my_first_package
touch dist_turtle_action_client_cancel.py
파일 이름은 다음과 같습니다.
dist_turtle_action_client_cancel.py
이 파일은 기존 Action Client와 비슷하지만, Goal을 보낸 뒤 일정 시간이 지나면 자동으로 Cancel 요청을 보내도록 작성합니다.
3. Cancel 테스트용 Action Client 전체 코드
dist_turtle_action_client_cancel.py 파일에 다음 코드를 작성합니다.
import rclpy as rp
from rclpy.action import ActionClient
from rclpy.node import Node
from my_first_package_msgs.action import DistTurtle
class DistTurtleCancelClient(Node):
def __init__(self):
super().__init__('dist_turtle_action_client_cancel')
self._action_client = ActionClient(
self,
DistTurtle,
'dist_turtle'
)
self.goal_handle = None
self.timer = None
def send_goal(self):
goal_msg = DistTurtle.Goal()
goal_msg.linear_x = 1.0
goal_msg.angular_z = 0.0
goal_msg.dist = 10.0
self._action_client.wait_for_server()
self.get_logger().info('Sending goal')
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
self._send_goal_future.add_done_callback(
self.goal_response_callback
)
def goal_response_callback(self, future):
self.goal_handle = future.result()
if not self.goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
self.get_logger().info('Goal accepted')
self._get_result_future = self.goal_handle.get_result_async()
self._get_result_future.add_done_callback(
self.get_result_callback
)
self.timer = self.create_timer(2.0, self.cancel_goal)
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(
f'Received feedback: remained_dist = {feedback.remained_dist}'
)
def cancel_goal(self):
self.get_logger().info('Sending cancel request')
self.timer.cancel()
self._cancel_future = self.goal_handle.cancel_goal_async()
self._cancel_future.add_done_callback(
self.cancel_done_callback
)
def cancel_done_callback(self, future):
cancel_response = future.result()
if len(cancel_response.goals_canceling) > 0:
self.get_logger().info('Cancel request accepted')
else:
self.get_logger().info('Cancel request rejected')
def get_result_callback(self, future):
result = future.result().result
status = future.result().status
self.get_logger().info(f'Action finished with status: {status}')
self.get_logger().info(f'Result distance: {result.result_dist}')
rp.shutdown()
def main(args=None):
rp.init(args=args)
action_client = DistTurtleCancelClient()
action_client.send_goal()
rp.spin(action_client)
if __name__ == '__main__':
main()
4. import 부분 설명
먼저 코드의 import 부분입니다.
import rclpy as rp
from rclpy.action import ActionClient
from rclpy.node import Node
from my_first_package_msgs.action import DistTurtle
다음 코드는 Action Client를 사용하기 위해 필요합니다.
from rclpy.action import ActionClient
ActionClient는 Action Server에 Goal을 보내고, Feedback과 Result를 받으며, Cancel 요청도 보낼 수 있는 클래스입니다.
마지막 import는 직접 만든 Action 인터페이스를 가져오는 부분입니다.
from my_first_package_msgs.action import DistTurtle
DistTurtle은 앞에서 만든 Action 메시지입니다.
이 안에는 다음 세 가지 타입이 포함되어 있습니다.
DistTurtle.Goal
DistTurtle.Feedback
DistTurtle.Result
이번 Client에서는 DistTurtle.Goal()을 만들어 Server로 보내고, Server가 보내는 Feedback과 Result를 받습니다.
6. 생성자 __init__() 설명
다음은 생성자 부분입니다.
def __init__(self):
super().__init__('dist_turtle_action_client_cencel')
self._action_client = ActionClient(
self,
DistTurtle,
'dist_turtle'
)
self.goal_handle = None
self.timer = None
먼저 다음 코드는 Node 이름을 설정합니다.
super().__init__('dist_turtle_action_client_cancel')
이 Client Node의 이름은 dist_turtle_cancel_client가 됩니다.
실행 로그에서는 이 이름으로 출력됩니다.
[INFO] [dist_turtle_action_client_cencel]: Sending goal
다음 코드는 Action Client를 생성합니다.
self._action_client = ActionClient(
self,
DistTurtle,
'dist_turtle'
)
각 인자의 의미는 다음과 같습니다.
self → 현재 Node
DistTurtle → 사용할 Action 타입
'dist_turtle' → 접속할 Action 이름
여기서 'dist_turtle'은 Action Server에서 사용한 Action 이름과 같아야 합니다.
Server도 같은 이름으로 Action Server를 열고 있어야 Client가 연결할 수 있습니다.
다음 코드는 Goal Handle을 저장할 변수입니다.
self.goal_handle = None
Goal Handle은 Client가 보낸 Goal을 제어하기 위한 객체입니다.
Cancel 요청을 보낼 때 이 Goal Handle이 필요합니다.
다음 코드는 Timer 객체를 저장할 변수입니다.
self.timer = None
이번 실습에서는 Goal을 보낸 뒤 2초 후 Cancel 요청을 보내기 위해 Timer를 사용합니다.
7. send_goal() 함수 설명
다음 함수는 Action Server로 Goal을 보내는 함수입니다.
def send_goal(self):
goal_msg = DistTurtle.Goal()
goal_msg.linear_x = 0.0
goal_msg.angular_z = 0.0
goal_msg.dist = 10.0
self._action_client.wait_for_server()
self.get_logger().info('Sending goal')
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
self._send_goal_future.add_done_callback(
self.goal_response_callback
)
먼저 Goal 메시지를 생성합니다.
goal_msg = DistTurtle.Goal()
DistTurtle.Goal()은 Action Server에 보낼 목표값입니다.
그다음 Goal 메시지 안의 값을 설정합니다.
goal_msg.linear_x = 0.0
goal_msg.angular_z = 0.0
goal_msg.dist = 10.0
각 값은 Action 정의 파일에 작성한 Goal 필드입니다.
linear_x → 직선 속도 값
angular_z → 회전 속도 값
dist → 목표 거리 값
이번 실습에서는 Cancel 기능 확인이 목적이므로 dist를 10.0으로 설정합니다.
다음 코드는 Action Server가 실행될 때까지 기다립니다.
self._action_client.wait_for_server()
Server가 실행되지 않은 상태에서 Goal을 보내면 정상적으로 처리할 수 없습니다.
그래서 Client는 먼저 Server가 준비될 때까지 대기합니다.
다음 코드는 Goal을 비동기 방식으로 보냅니다.
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
send_goal_async()는 Goal을 보내고 바로 다음 코드로 넘어갑니다.
작업 결과를 기다리는 동안 프로그램이 멈추지 않습니다.
여기서 feedback_callback은 Server가 Feedback을 보낼 때 실행될 함수입니다.
feedback_callback=self.feedback_callback
마지막으로 Goal 요청 결과를 받을 콜백을 등록합니다.
self._send_goal_future.add_done_callback(
self.goal_response_callback
)
Server가 Goal을 수락했는지 거부했는지 결과가 오면 goal_response_callback() 함수가 실행됩니다.
8. goal_response_callback() 함수 설명
다음 함수는 Server가 Goal을 수락했는지 확인하는 함수입니다.
def goal_response_callback(self, future):
self.goal_handle = future.result()
if not self.goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
self.get_logger().info('Goal accepted')
self._get_result_future = self.goal_handle.get_result_async()
self._get_result_future.add_done_callback(
self.get_result_callback
)
self.timer = self.create_timer(2.0, self.cancel_goal)
먼저 Goal Handle을 가져옵니다.
self.goal_handle = future.result()
이 Goal Handle은 이후 Cancel 요청을 보낼 때 사용합니다.
다음 코드는 Goal이 거부되었는지 확인합니다.
if not self.goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
Server가 Goal을 거부했다면 더 이상 진행하지 않고 함수를 종료합니다.
Goal이 정상적으로 수락되면 다음 로그가 출력됩니다.
self.get_logger().info('Goal accepted')
그다음 Result를 받기 위한 요청을 등록합니다.
self._get_result_future = self.goal_handle.get_result_async()
self._get_result_future.add_done_callback(
self.get_result_callback
)
get_result_async()는 Action이 끝났을 때 Result를 받기 위해 사용합니다.
마지막 줄이 Cancel 실습에서 중요한 부분입니다.
self.timer = self.create_timer(2.0, self.cancel_goal)
이 코드는 2초마다 cancel_goal() 함수를 실행하는 Timer를 만듭니다.
하지만 cancel_goal() 함수 안에서 바로 Timer를 취소하기 때문에 실제로는 2초 후 한 번만 Cancel 요청을 보냅니다.
9. feedback_callback() 함수 설명
다음 함수는 Server에서 Feedback을 보낼 때마다 실행됩니다.
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(
f'Received feedback: remained_dist = {feedback.remained_dist}'
)
feedback_msg.feedback에는 Server가 보낸 Feedback 데이터가 들어 있습니다.
feedback = feedback_msg.feedback
이번 예제에서는 남은 거리를 의미하는 remained_dist 값을 출력합니다.
f'Received feedback: remained_dist = {feedback.remained_dist}'
Action Server가 작업 중 Feedback을 계속 보내면 Client는 이 함수를 통해 값을 확인할 수 있습니다.
10. cancel_goal() 함수 설명
다음 함수는 실제로 Cancel 요청을 보내는 함수입니다.
def cancel_goal(self):
self.get_logger().info('Sending cancel request')
self.timer.cancel()
self._cancel_future = self.goal_handle.cancel_goal_async()
self._cancel_future.add_done_callback(
self.cancel_done_callback
)
먼저 Cancel 요청을 보낸다는 로그를 출력합니다.
self.get_logger().info('Sending cancel request')
다음 코드는 Timer를 중지합니다.
self.timer.cancel()
create_timer(2.0, self.cancel_goal)로 만든 Timer는 원래 2초마다 계속 실행될 수 있습니다.
하지만 Cancel 요청은 한 번만 보내면 되므로 Timer를 바로 중지합니다.
가장 중요한 코드는 다음입니다.
self._cancel_future = self.goal_handle.cancel_goal_async()
이 코드가 Action Server로 Cancel 요청을 보냅니다.
여기서 goal_handle을 사용한다는 점이 중요합니다.
Cancel 요청은 Action 이름만으로 보내는 것이 아니라, Client가 보낸 Goal의 Handle을 통해 보내야 합니다.
마지막 코드는 Cancel 요청 결과를 받을 콜백을 등록합니다.
self._cancel_future.add_done_callback(
self.cancel_done_callback
)
Server가 Cancel 요청을 허용했는지 거부했는지 결과가 오면 cancel_done_callback() 함수가 실행됩니다.
11. cancel_done_callback() 함수 설명
다음 함수는 Cancel 요청 결과를 확인하는 함수입니다.
def cancel_done_callback(self, future):
cancel_response = future.result()
if len(cancel_response.goals_canceling) > 0:
self.get_logger().info('Cancel request accepted')
else:
self.get_logger().info('Cancel request rejected')
먼저 Cancel 요청 결과를 가져옵니다.
cancel_response = future.result()
다음 코드는 취소 처리 중인 Goal이 있는지 확인합니다.
if len(cancel_response.goals_canceling) > 0:
goals_canceling 안에 값이 하나라도 있으면 Server가 Cancel 요청을 받아들였다는 의미입니다.
이 경우 다음 로그를 출력합니다.
self.get_logger().info('Cancel request accepted')
반대로 goals_canceling이 비어 있으면 Cancel 요청이 거부되었거나 취소할 Goal이 없는 상태입니다.
self.get_logger().info('Cancel request rejected')
12. get_result_callback() 함수 설명
다음 함수는 Action이 최종 종료되었을 때 실행됩니다.
def get_result_callback(self, future):
result = future.result().result
status = future.result().status
self.get_logger().info(f'Action finished with status: {status}')
self.get_logger().info(f'Result distance: {result.result_dist}')
rp.shutdown()
먼저 Result 값을 가져옵니다.
result = future.result().result
그리고 Action 상태 값을 가져옵니다.
status = future.result().status
Cancel이 정상 처리되면 보통 다음과 같은 상태 값이 출력됩니다.
Action finished with status: 5
일반적으로 status: 5는 CANCELED 상태를 의미합니다.
다음 코드는 Result에 들어 있는 거리 값을 출력합니다.
self.get_logger().info(f'Result distance: {result.result_dist}')
마지막으로 ROS 2 노드를 종료합니다.
rp.shutdown()
13. main() 함수 설명
마지막은 실행 진입점입니다.
def main(args=None):
rp.init(args=args)
action_client = DistTurtleCancelClient()
action_client.send_goal()
rp.spin(action_client)
if __name__ == '__main__':
main()
action_client = DistTurtleCancelClient()
다음 코드로 Goal을 전송합니다.
action_client.send_goal()
마지막으로 Node를 계속 실행하면서 콜백이 처리되도록 합니다.
rp.spin(action_client)
rp.spin(action_client)가 실행 중이기 때문에 다음 콜백들이 정상적으로 동작할 수 있습니다.
goal_response_callback()
feedback_callback()
cancel_goal()
cancel_done_callback()
get_result_callback()
14. setup.py에 실행 파일 등록하기
새로 만든 Python 파일을 ros2 run 명령으로 실행하려면 setup.py에 등록해야 합니다.
setup.py 파일을 엽니다.
cd ~/ros2_study/src/my_first_package
gedit setup.py
또는 VS Code를 사용한다면 다음처럼 열 수 있습니다.
code setup.py
entry_points의 console_scripts에 다음 줄을 추가합니다.
'dist_turtle_action_client_cancel = my_first_package.dist_turtle_action_client_cancel:main',
예시는 다음과 같습니다.
entry_points={
'console_scripts': [
'dist_turtle_action_server = my_first_package.dist_turtle_action_server:main',
'dist_turtle_action_client_cancel = my_first_package.dist_turtle_action_client_cancel:main',
],
},
따라서 다음 명령을 실행하면
ros2 run my_first_package dist_turtle_action_client_cancel
ROS 2는 내부적으로 다음 파일의 main() 함수를 실행합니다.
my_first_package/dist_turtle_action_client_cancel.py
15. 빌드하기
코드를 수정했으므로 다시 빌드합니다.
cd ~/ros2_study
colcon build
source install/setup.bash
빌드가 완료되면 새로 등록한 실행 파일을 사용할 수 있습니다.
16. Action Server 실행하기
첫 번째 터미널에서 Action Server를 실행합니다.
cd ~/ros2_study
source install/setup.bash
ros2 run my_first_package dist_turtle_action_server
Action Server가 실행되면 Client의 Goal 요청을 기다립니다.
17. Cancel 테스트용 Action Client 실행하기
두 번째 터미널을 열고 Cancel 테스트용 Action Client를 실행합니다.
cd ~/ros2_study
source install/setup.bash
ros2 run my_first_package dist_turtle_action_client_cancel
이 Client는 Goal을 보낸 뒤 약 2초 후 자동으로 Cancel 요청을 보냅니다.
18. 실행 결과 확인하기
Client 쪽 출력은 다음과 비슷합니다.
[INFO] [dist_turtle_action_client_cancel]: Sending goal
[INFO] [dist_turtle_action_client_cancel]: Goal accepted
[INFO] [dist_turtle_action_client_cancel]: Received feedback: remained_dist = 10.0
[INFO] [dist_turtle_action_client_cancel]: Received feedback: remained_dist = 9.0
[INFO] [dist_turtle_action_client_cancel]: Sending cancel request
[INFO] [dist_turtle_action_client_cancel]: Cancel request accepted
[INFO] [dist_turtle_action_client_cancel]: Action finished with status: 5
[INFO] [dist_turtle_action_client_cancel]: Result distance: 2.0
Server 쪽 출력은 다음과 비슷합니다.
[INFO] [dist_turtle_action_server]: Goal received
[INFO] [dist_turtle_action_server]: Feedback: remained_dist = 10.0
[INFO] [dist_turtle_action_server]: Feedback: remained_dist = 9.0
[INFO] [dist_turtle_action_server]: Cancel request received
[INFO] [dist_turtle_action_server]: Goal canceled
Client에서 다음 로그가 출력되면 Cancel 요청이 Server에 의해 허용된 것입니다.
Cancel request accepted
Server에서 다음 로그가 출력되면 Server가 Cancel 요청을 받은 것입니다.
Cancel request received
Server에서 다음 로그가 출력되면 실행 중이던 Goal이 취소 처리된 것입니다.
Goal canceled
Client에서 다음 로그가 출력되면 Action이 취소 상태로 종료된 것입니다.
Action finished with status: 5

'강좌 > ROS2' 카테고리의 다른 글
| ROS 2 Python Topic 실습 : RobotStatus 메시지로 로봇 상태 주고받기 (0) | 2026.05.26 |
|---|---|
| Python 기본 타입 메시지 Publisher / Subscriber 예제 (0) | 2026.05.26 |
| 로봇 개발자를 위한 Python 기초 교육 #3 (0) | 2026.05.25 |
| VS Code 원격 개발 환경 (0) | 2026.05.24 |
| ROS 2 Humble rqt Plugins 정리 #3 (0) | 2026.05.24 |