학습 목표
- Topic, Service, Action과 Parameter 중 요구사항에 맞는 Interface를 선택할 수 있다.
- Goal·Feedback·Result·Cancel과 Goal Handle의 관계를 설명할 수 있다.
- CLI로 Action을 조사하고 Goal 전송, Feedback 관찰과 Cancel을 수행할 수 있다.
- Custom
.actionInterface를 만들고 Python Server·Client에서 사용할 수 있다. - Goal 수락·거절, 성공·취소·중단을 구분하여 구현할 수 있다.
- 동시 Goal과 Preemption 정책을 명시적으로 설계할 수 있다.
- Executor와 Callback Group이 Action 응답성에 미치는 영향을 설명할 수 있다.
- 실제 Robot에 Action을 적용할 때 Command 소유권, Watchdog와 독립 Safety 계층을 설계할 수 있다.
1. Action이 필요한 문제
ROS 2 Action은 완료까지 시간이 걸리고, 진행 상황을 알려야 하며, 실행 도중 취소할 수 있어야 하는 작업을 위한 Interface입니다. “몇 초 이상이면 Action”이라는 절대 기준은 없습니다. 시간보다 작업의 의미를 먼저 봅니다.
Action Client Action Server
│ Goal(distance=1.0) │
├────────────────────────────────────>│
│ Goal accepted │
│<────────────────────────────────────┤
│ Feedback(remaining=0.7) │
│<────────────────────────────────────┤
│ Feedback(remaining=0.3) │
│<────────────────────────────────────┤
│ Cancel request │
├────────────────────────────────────>│
│ Result(canceled) │
│<────────────────────────────────────┤
| 요구사항 | 권장 Interface | 예 |
|---|---|---|
| 연속 Data Stream | Topic | /scan, /odom, Camera Image |
| 짧은 요청과 한 번의 응답 | Service | Mode 변경, Controller Reset |
| 진행률·취소가 있는 작업 | Action | Navigation, Docking, Arm Trajectory |
| 유지되는 설정값 | Parameter | 최대 속도, Frame 이름 |
다음 중 하나라도 중요하면 Action을 우선 검토합니다.
- 작업 중 Feedback이 필요한가?
- 사용자가 중간에 취소할 수 있어야 하는가?
- 완료·취소·실패를 최종 상태로 구분해야 하는가?
- 하나의 요청이 여러 제어 주기에 걸쳐 실행되는가?
Emergency Stop은 일반 Action Cancel 왕복만으로 구현하지 않습니다. 위험 분석에 따른 독립 안전 회로와 안전 제어 계층이 필요합니다.
2. Action의 다섯 핵심 요소
| 요소 | 방향 | 역할 |
|---|---|---|
| Goal | Client → Server | 수행할 목표와 조건 |
| Goal response | Server → Client | Goal 수락 또는 거절 |
| Feedback | Server → Client | 실행 중 진행 상황 |
| Result | Server → Client | 최종 결과 Data |
| Cancel | Client → Server | 실행 중단 요청과 수락 여부 |
Client가 Goal을 보내면 Server는 각 Goal을 구분하는 UUID와 상태를 관리합니다. Client와 Server의 GoalHandle은 특정 Goal의 수락 여부, 상태, 취소와 Result를 다루는 손잡이입니다.
Goal 요청 → ACCEPTED → EXECUTING ─┬→ SUCCEEDED
├→ CANCELING → CANCELED
└→ ABORTED
Goal 요청 → REJECTED
succeed(), canceled(), abort()는 서로 다른 의미입니다.
- SUCCEEDED: 목표를 정상 완료했다.
- CANCELED: 수락한 취소 요청에 따라 안전하게 작업을 끝냈다.
- ABORTED: 장애물, Sensor 고장, Timeout 등으로 Server가 목표를 완료할 수 없다.
- REJECTED: 실행을 시작하기 전에 Goal 자체를 받지 않았다.
3. Action 내부 통신을 이해하기
Action은 ROS Graph에서 하나의 이름으로 보이지만 내부적으로 Goal, Result, Cancel을 위한 Service와 Feedback, Status를 위한 Topic을 사용합니다. 사용자는 이 내부 Endpoint를 직접 조합하기보다 Action API를 사용합니다.
ros2 node info /fibonacci_action_server
ros2 action list -t
내부 Topic·Service의 존재는 “Service 여러 개를 직접 호출하면 Action과 같다”는 뜻이 아닙니다. Goal UUID, 상태 전이, Feedback, Cancel과 Result 보관 규칙을 Action Middleware와 Client Library가 일관되게 처리해 줍니다.
4. 준비와 작업 공간
이 실습은 ROS 2가 설치된 Ubuntu Terminal을 기준으로 합니다. 배포판 이름은 환경에 맞게 바꾸십시오.
source /opt/ros/$ROS_DISTRO/setup.bash
printenv ROS_DISTRO
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
기존 Package를 수정했다면 새 Terminal마다 Overlay도 Source합니다.
source ~/ros2_ws/install/setup.bash
실습 Terminal은 보통 세 개를 사용합니다.
Terminal A: Action Server
Terminal B: Action Client 또는 CLI
Terminal C: Graph·Topic·Log 관찰
5. CLI로 표준 Fibonacci Action 체험하기
5.1 Demo Server 실행
ros2 run action_tutorials_py fibonacci_action_server
다른 Terminal에서 Action 목록과 Type을 확인합니다.
ros2 action list
ros2 action list -t
ros2 action type /fibonacci
ros2 interface show action_tutorials_interfaces/action/Fibonacci
5.2 Goal 전송과 Feedback 보기
ros2 action send_goal /fibonacci \
action_tutorials_interfaces/action/Fibonacci \
"{order: 10}" --feedback
order가 Goal, 수열 전체가 Result, 계산 도중 커지는 수열이 Feedback입니다. 문법이 헷갈리면 Prototype을 확인합니다.
ros2 interface proto action_tutorials_interfaces/action/Fibonacci
ros2 action info /fibonacci
5.3 Cancel 실험
긴 Goal을 보냅니다.
ros2 action send_goal /fibonacci \
action_tutorials_interfaces/action/Fibonacci \
"{order: 1000}" --feedback
실행 중 Ctrl+C를 눌렀을 때 CLI 종료와 Server 측 Goal 취소 처리가 같은지 Server Log로 확인합니다. 실제 Application에서는 아래 Python Client처럼 명시적인 cancel_goal_async()를 사용합니다.
6. Custom Action 설계
이동 Robot이 지정 거리만큼 전진하는 DriveDistance.action을 설계합니다.
# Goal
float32 distance_m
float32 max_speed_mps
---
# Result
float32 traveled_m
string message
---
# Feedback
float32 traveled_m
float32 remaining_m
.action 파일은 첫 번째 --- 위가 Goal, 중간이 Result, 마지막이 Feedback입니다. 상태는 Action Protocol이 이미 제공하므로 success Boolean을 무조건 중복할 필요는 없습니다. Domain 결과가 더 세밀해야 한다면 Error Code를 추가할 수 있습니다.
int32 OK=0
int32 BLOCKED=1
int32 ODOMETRY_LOST=2
int32 error_code
string message
입력 설계 원칙은 다음과 같습니다.
- 단위를 이름에 넣습니다:
distance_m,max_speed_mps. - 허용 범위와 음수 의미를 문서화합니다.
- NaN과 Infinity를 거절합니다.
- Result에는 실제 달성량과 진단 가능한 이유를 둡니다.
- Feedback은 너무 빠르게 보내 Network를 포화시키지 않습니다.
7. Interface Package 생성과 Build
cd ~/ros2_ws/src
ros2 pkg create lab_interfaces --build-type ament_cmake
mkdir -p lab_interfaces/action
lab_interfaces/action/DriveDistance.action을 앞 절의 내용으로 저장합니다. CMakeLists.txt에 생성 규칙을 추가합니다.
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"action/DriveDistance.action"
)
ament_export_dependencies(rosidl_default_runtime)
ament_package()
package.xml에는 다음 의존성을 추가합니다.
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Build하고 생성 결과를 확인합니다.
cd ~/ros2_ws
colcon build --packages-select lab_interfaces --symlink-install
source install/setup.bash
ros2 interface show lab_interfaces/action/DriveDistance
Interface를 변경한 뒤에는 Server와 Client를 다시 Build하고 새 Overlay를 Source해야 합니다.
8. Python Package 만들기
cd ~/ros2_ws/src
ros2 pkg create drive_action_py \
--build-type ament_python \
--dependencies rclpy lab_interfaces
setup.py의 entry_points를 추가합니다.
entry_points={
'console_scripts': [
'drive_server = drive_action_py.drive_server:main',
'drive_client = drive_action_py.drive_client:main',
'cancel_client = drive_action_py.cancel_client:main',
],
},
9. 학습용 Action Server 구현
먼저 Hardware 없이 동작하는 거리 Simulation Server를 만듭니다. asyncio.sleep()을 사용해 Executor가 다른 Callback을 처리할 기회를 줍니다.
# drive_action_py/drive_server.py
import asyncio
import math
import rclpy
from rclpy.action import ActionServer, CancelResponse, GoalResponse
from rclpy.node import Node
from lab_interfaces.action import DriveDistance
class DriveDistanceServer(Node):
def __init__(self):
super().__init__('drive_distance_server')
self._server = ActionServer(
self,
DriveDistance,
'drive_distance',
execute_callback=self.execute_callback,
goal_callback=self.goal_callback,
cancel_callback=self.cancel_callback,
)
def goal_callback(self, goal_request):
distance = float(goal_request.distance_m)
speed = float(goal_request.max_speed_mps)
if not math.isfinite(distance) or not math.isfinite(speed):
self.get_logger().warning('NaN 또는 Infinity Goal 거절')
return GoalResponse.REJECT
if abs(distance) > 5.0 or not 0.02 <= speed <= 0.5:
self.get_logger().warning('허용 범위를 벗어난 Goal 거절')
return GoalResponse.REJECT
return GoalResponse.ACCEPT
def cancel_callback(self, goal_handle):
self.get_logger().info('Cancel 요청 수락')
return CancelResponse.ACCEPT
async def execute_callback(self, goal_handle):
goal = goal_handle.request
direction = 1.0 if goal.distance_m >= 0.0 else -1.0
traveled = 0.0
dt = 0.1
feedback = DriveDistance.Feedback()
while traveled < abs(goal.distance_m):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return self.make_result(
direction * traveled, '사용자 요청으로 취소됨')
await asyncio.sleep(dt)
traveled = min(
traveled + float(goal.max_speed_mps) * dt,
abs(float(goal.distance_m)),
)
feedback.traveled_m = direction * traveled
feedback.remaining_m = max(abs(goal.distance_m) - traveled, 0.0)
goal_handle.publish_feedback(feedback)
goal_handle.succeed()
return self.make_result(direction * traveled, '목표 거리 도달')
@staticmethod
def make_result(traveled, message):
result = DriveDistance.Result()
result.traveled_m = float(traveled)
result.message = message
return result
def destroy_node(self):
self._server.destroy()
super().destroy_node()
def main(args=None):
rclpy.init(args=args)
node = DriveDistanceServer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
핵심은 상태 전이 순서입니다.
입력 검증 → Goal 수락 → 반복 실행 → Feedback
├ Cancel 확인 → canceled() → Result
├ 정상 완료 → succeed() → Result
└ 실행 불가 → abort() → Result
10. Python Action Client 구현
# drive_action_py/drive_client.py
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from lab_interfaces.action import DriveDistance
class DriveDistanceClient(Node):
def __init__(self):
super().__init__('drive_distance_client')
self._client = ActionClient(
self, DriveDistance, 'drive_distance')
def send_goal(self, distance, speed):
if not self._client.wait_for_server(timeout_sec=5.0):
self.get_logger().error('Action Server 발견 Timeout')
rclpy.shutdown()
return
goal = DriveDistance.Goal()
goal.distance_m = float(distance)
goal.max_speed_mps = float(speed)
future = self._client.send_goal_async(
goal, feedback_callback=self.feedback_callback)
future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
try:
self._goal_handle = future.result()
except Exception as exc:
self.get_logger().error(f'Goal 전송 실패: {exc}')
rclpy.shutdown()
return
if not self._goal_handle.accepted:
self.get_logger().warning('Goal이 거절되었습니다.')
rclpy.shutdown()
return
self.get_logger().info('Goal 수락됨')
result_future = self._goal_handle.get_result_async()
result_future.add_done_callback(self.result_callback)
def feedback_callback(self, message):
feedback = message.feedback
self.get_logger().info(
f'이동={feedback.traveled_m:.2f} m, '
f'남음={feedback.remaining_m:.2f} m')
def result_callback(self, future):
try:
wrapped = future.result()
result = wrapped.result
self.get_logger().info(
f'status={wrapped.status}, '
f'traveled={result.traveled_m:.2f}, '
f'message={result.message}')
except Exception as exc:
self.get_logger().error(f'Result 수신 실패: {exc}')
finally:
rclpy.shutdown()
def main(args=None):
rclpy.init(args=args)
node = DriveDistanceClient()
node.send_goal(distance=1.0, speed=0.2)
try:
rclpy.spin(node)
finally:
node.destroy_node()
if __name__ == '__main__':
main()
Client의 비동기 흐름을 놓치지 마십시오.
Server 발견 → send_goal_async
↓
Goal 수락 여부
├ 거절 → 종료
└ 수락 → Feedback 반복 + get_result_async
↓
최종 Status·Result
11. Build와 실행
cd ~/ros2_ws
colcon build --packages-select lab_interfaces drive_action_py --symlink-install
source install/setup.bash
Terminal A:
source ~/ros2_ws/install/setup.bash
ros2 run drive_action_py drive_server
Terminal B:
source ~/ros2_ws/install/setup.bash
ros2 run drive_action_py drive_client
CLI에서도 같은 Server를 시험합니다.
ros2 action send_goal /drive_distance \
lab_interfaces/action/DriveDistance \
"{distance_m: 1.0, max_speed_mps: 0.2}" --feedback
거절 Test도 수행합니다.
ros2 action send_goal /drive_distance \
lab_interfaces/action/DriveDistance \
"{distance_m: 100.0, max_speed_mps: 9.0}" --feedback
12. 명시적으로 Cancel하는 Client
Cancel 요청을 보냈다는 사실과 실제로 CANCELED Result를 받은 사실은 다릅니다. Server가 요청을 거절할 수도 있고, 정지 과정 중 Fault가 발생할 수도 있습니다.
# drive_action_py/cancel_client.py
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from lab_interfaces.action import DriveDistance
class CancelingClient(Node):
def __init__(self):
super().__init__('canceling_drive_client')
self.client = ActionClient(self, DriveDistance, 'drive_distance')
self.goal_handle = None
self.timer = None
def start(self):
if not self.client.wait_for_server(timeout_sec=5.0):
raise RuntimeError('Action Server를 찾지 못했습니다.')
goal = DriveDistance.Goal()
goal.distance_m = 3.0
goal.max_speed_mps = 0.2
future = self.client.send_goal_async(goal)
future.add_done_callback(self.on_goal_response)
def on_goal_response(self, future):
self.goal_handle = future.result()
if not self.goal_handle.accepted:
self.get_logger().warning('Goal 거절')
rclpy.shutdown()
return
result_future = self.goal_handle.get_result_async()
result_future.add_done_callback(self.on_result)
self.timer = self.create_timer(2.0, self.request_cancel)
def request_cancel(self):
self.timer.cancel()
self.get_logger().info('Cancel 요청 전송')
future = self.goal_handle.cancel_goal_async()
future.add_done_callback(self.on_cancel_response)
def on_cancel_response(self, future):
response = future.result()
if response.goals_canceling:
self.get_logger().info('Server가 Cancel 요청을 수락함')
else:
self.get_logger().warning('Cancel 요청이 수락되지 않음')
def on_result(self, future):
wrapped = future.result()
self.get_logger().info(
f'최종 status={wrapped.status}, message={wrapped.result.message}')
rclpy.shutdown()
def main(args=None):
rclpy.init(args=args)
node = CancelingClient()
node.start()
try:
rclpy.spin(node)
finally:
node.destroy_node()
if __name__ == '__main__':
main()
실제 Robot Server는 Cancel을 확인한 즉시 최종 Result부터 보내지 말고 먼저 감속·정지 명령을 수행하고 정지 상태를 확인한 뒤 canceled()로 끝내야 합니다.
13. Goal 거절, 동시 Goal과 Preemption 정책
Server는 “새 Goal이 오면 어떻게 할 것인가”를 명확히 정해야 합니다.
| 정책 | 동작 | 적합한 예 |
|---|---|---|
| Reject new | 실행 중이면 새 Goal 거절 | Calibration, Firmware Update |
| Queue | 순서대로 대기 | 비긴급 Batch 작업 |
| Preempt old | 기존 Goal 취소 후 새 Goal 실행 | Navigation 목적지 변경 |
| Parallel | 여러 Goal 동시 실행 | 독립 자원이 명확한 작업 |
단일 Robot Base가 두 거리 Goal을 동시에 실행할 수는 없습니다. 가장 단순한 거절 정책은 Lock으로 보호한 active_goal 상태를 두고 goal_callback에서 확인하는 것입니다. 그러나 Goal 수락과 실제 활성화 사이의 Race를 고려해야 하므로 운영용 구현은 handle_accepted_callback에서 Goal 소유권을 원자적으로 관리하는 편이 좋습니다.
from threading import Lock
from rclpy.action import GoalResponse
self._goal_lock = Lock()
self._busy = False
def goal_callback(self, request):
with self._goal_lock:
if self._busy:
return GoalResponse.REJECT
self._busy = True
return GoalResponse.ACCEPT
모든 종료 경로에서 finally로 _busy = False를 복구해야 합니다. 더 복잡한 Preemption은 다음 순서를 지킵니다.
새 Goal 검증 → 기존 Goal Cancel 요청 → 실제 Actuator 정지 확인
→ 새 Goal 소유권 부여 → 새 Goal 실행
단순히 “최신 Goal 변수”만 바꾸면 기존 Callback이 계속 Motor 명령을 발행할 수 있습니다.
14. Executor와 Callback Group
MultiThreadedExecutor가 모든 Action에 무조건 필요한 것은 아닙니다. 핵심은 긴 execute_callback이 Goal, Cancel, Sensor와 Watchdog Callback을 굶기지 않게 만드는 것입니다.
async실행이 자주 양보한다면 Single Thread에서도 응답할 수 있습니다.- Blocking I/O나 긴 계산이 있다면 별도 Worker 또는 적절한 Callback Group과 Multi-threading을 검토합니다.
- Multi-threaded여도 모든 Entity가 같은 Mutually Exclusive Group이면 동시에 실행되지 않을 수 있습니다.
- Reentrant Group에서는 공유 상태를 Lock이나 Message Passing으로 보호해야 합니다.
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.executors import MultiThreadedExecutor
self.action_group = ReentrantCallbackGroup()
self.server = ActionServer(
self,
DriveDistance,
'drive_distance',
execute_callback=self.execute_callback,
callback_group=self.action_group,
)
executor = MultiThreadedExecutor(num_threads=2)
executor.add_node(node)
executor.spin()
이 Code만 붙이면 안전한 병렬 처리가 완성되는 것은 아닙니다. Goal 소유권, /cmd_vel 발행, Odometry 상태와 종료 Flag에 대한 동시 접근을 함께 설계해야 합니다.
15. 실제 Robot용 DriveDistance 구조
학습용 Server의 traveled += speed × dt는 Command를 적분한 추정일 뿐 실제 이동량이 아닙니다. 실제 Robot에서는 /odom 또는 Localization Pose의 시작점과 현재점을 비교합니다.
import math
def planar_distance(start_x, start_y, current_x, current_y):
return math.hypot(current_x - start_x, current_y - start_y)
실제 구조는 다음처럼 나눕니다.
Action Server ──> /cmd_vel_action ──> Velocity Mux ──> Safety Filter
│
/odom ─────────> 진행 거리 계산 ↓
/scan ─────────> 장애물 조건 Motor Driver
/estop_state ──> 독립 정지 조건
Navigation, Joystick과 Action Server가 모두 /cmd_vel에 직접 발행하면 마지막으로 도착한 Message가 Motor를 움직여 Command 소유권이 불명확해집니다. Source별 Topic과 Velocity Mux를 사용합니다.
# 개념 예시: 실제 mux Package의 Schema에 맞게 조정
inputs:
joystick:
topic: /cmd_vel_joy
priority: 100
timeout: 0.3
navigation:
topic: /cmd_vel_nav
priority: 50
timeout: 0.5
action:
topic: /cmd_vel_action
priority: 40
timeout: 0.3
Server 반복문의 안전 조건 예시입니다.
if self.estop_active:
self.publish_stop()
goal_handle.abort()
return self.make_result(traveled, '비상정지 상태')
if self.odom_age_sec() > 0.2:
self.publish_stop()
goal_handle.abort()
return self.make_result(traveled, 'Odometry Timeout')
if self.front_clearance_m < self.stop_distance_m:
self.publish_stop()
goal_handle.abort()
return self.make_result(traveled, '전방 장애물')
정상, Cancel, Abort, Exception과 Node 종료 등 모든 경로에서 정지 명령이 나가야 합니다.
try:
await self.control_until_goal(goal_handle)
finally:
self.publish_stop()
첫 실험은 바퀴를 바닥에서 띄우거나 Motor Driver를 비활성화한 상태에서 수행합니다. 속도·가속도를 낮게 제한하고 Emergency Stop을 손이 닿는 곳에 두십시오.
16. Timeout, Watchdog와 Server 손실
Action에는 서로 다른 시간이 있습니다.
| 시간 | 의미 | 실패 시 예 |
|---|---|---|
| Discovery Timeout | Server 발견 대기 | 실행 중단·사용자 알림 |
| Goal response Timeout | 수락 여부 대기 | 통신 상태 진단 |
| Progress Timeout | Feedback 또는 진행 변화 없음 | Cancel 후 안전 상태 확인 |
| Overall Timeout | 전체 임무 제한시간 | Cancel·Abort 정책 수행 |
| Command Watchdog | 새 Motor 명령이 없는 시간 | Driver가 독립적으로 0 명령 적용 |
Client가 사라졌다고 Server가 자동으로 안전하게 멈춘다고 가정하지 마십시오. Server와 Actuator 계층이 작업 Lease, Command Timeout과 Watchdog를 가져야 합니다. 반대로 Client는 Result Timeout 뒤 Goal이 실행되지 않았다고 단정해서 같은 Goal을 무작정 재전송하면 안 됩니다.
17. Namespace와 Remap
Robot 두 대에 같은 Code를 재사용할 때 Namespace를 사용합니다.
ros2 run drive_action_py drive_server --ros-args -r __ns:=/robot1
ros2 run drive_action_py drive_server --ros-args -r __ns:=/robot2
ros2 action list -t
Client도 같은 Namespace에서 실행하거나 Action 이름을 Remap합니다.
ros2 run drive_action_py drive_client --ros-args \
-r drive_distance:=/robot1/drive_distance
Code에 절대 이름 /drive_distance를 고정하면 Namespace 재사용이 어려워집니다. 상대 이름 drive_distance를 기본으로 사용합니다.
18. Nav2 Action을 실제로 조사하기
Nav2의 NavigateToPose는 목표 Pose까지 이동하면서 Feedback과 Result를 제공하고 취소할 수 있는 대표적인 Action입니다. Nav2가 실행된 환경에서 확인합니다.
ros2 action list -t
ros2 action type /navigate_to_pose
ros2 interface show nav2_msgs/action/NavigateToPose
ros2 action info /navigate_to_pose
CLI Goal 예시는 Map Frame의 Pose와 Quaternion을 정확히 지정해야 합니다.
ros2 action send_goal /navigate_to_pose \
nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: map}, pose: {position: {x: 1.0, y: 0.5, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}}}" \
--feedback
ros2 topic echo /amcl_pose --once
ros2 run tf2_ros tf2_echo map base_link
ros2 topic hz /scan
ros2 topic hz /odom
Action의 실제 적용 예는 다음과 같습니다.
NavigateToPose: 이동 Robot 목적지 주행FollowJointTrajectory: Manipulator 관절 궤적 추종- Docking: 충전 Dock 탐색·접근·접촉·충전 확인
- Calibration: 여러 Sample을 모으고 중간 진행률과 취소 제공
- Pick and Place: 접근·파지·이송 단계를 Feedback으로 보고
19. 진단 명령과 증상별 점검
ros2 action list -t
ros2 action info /drive_distance
ros2 node list
ros2 node info /drive_distance_server
ros2 interface show lab_interfaces/action/DriveDistance
ros2 topic echo /rosout
| 증상 | 확인 순서 |
|---|---|
| Action이 목록에 없음 | Server Process → 이름·Namespace → Domain·Network |
| Goal이 즉시 거절됨 | Goal 범위 → NaN → Busy·Safety Preconditions |
| Feedback이 없음 | execute Callback 진입 → 발행 주기 → Client Callback |
| Cancel이 늦음 | Blocking 작업 → Callback Group → Executor → 정지 절차 |
| Result가 영원히 안 옴 | 모든 경로의 succeed/canceled/abort와 Result 반환 |
| Robot이 취소 후 움직임 | 0 명령 발행 → Mux 소유권 → Driver Watchdog |
| Simulation만 성공 | 실제 Odometry·TF·Sensor Timestamp와 Command Interface |
| 두 Goal이 충돌 | 동시 Goal·Preemption 정책과 공유 상태 보호 |
ros2 action info만으로 Application 내부 상태가 모두 보이지는 않습니다. Goal UUID, 수락, 시작, Feedback, Cancel 요청, 실제 정지, 종료 Status와 소요 시간을 구조화된 Log로 남기십시오.
20. 흔한 실수와 교정
- 특정 시간보다 길면 무조건 Action이라고 외운다. Feedback·Cancel·상태 의미로 선택합니다.
- Goal 수락을 작업 성공으로 해석한다. 수락은 실행 시작 허가이며 최종 Result가 아닙니다.
- Cancel 요청 직후 CANCELED라고 표시한다. 실제 정지 절차 후 종료 상태를 확정합니다.
- Exception 경로에서
abort()와 정지를 빠뜨린다.try/finally와 명시적 Result를 둡니다. - Feedback을 제어 주기마다 과도하게 발행한다. UI와 Network에 필요한 속도로 제한합니다.
- Command 적분값을 실제 이동 거리라고 믿는다. Odometry나 Localization을 사용합니다.
- 여러 Node가
/cmd_vel에 직접 발행한다. Mux와 Command 소유권을 둡니다. - MultiThreadedExecutor만 켜면 해결된다고 생각한다. Callback Group과 공유 상태를 함께 설계합니다.
- 새 Goal 정책을 정하지 않는다. 거절·Queue·Preemption·병렬 중 하나를 명시합니다.
- Client 종료를 Cancel로 간주한다. 명시적 Cancel과 Server Watchdog를 구현합니다.
- Result의 Status를 보지 않고 Data만 읽는다. SUCCEEDED·CANCELED·ABORTED를 먼저 구분합니다.
- Action Cancel을 Emergency Stop으로 사용한다. 독립 Safety Architecture를 유지합니다.
21. 실습 체크리스트
- [ ] Fibonacci Action을 CLI로 조사하고 Feedback을 확인했다.
- [ ] Goal·Result·Feedback의
.action순서를 설명할 수 있다. - [ ]
DriveDistance.action을 생성하고 Build했다. - [ ] Python Server와 Client를 실행했다.
- [ ] 정상 Goal과 범위 밖 Goal을 모두 시험했다.
- [ ] Cancel 요청 수락과 최종 CANCELED를 구분했다.
- [ ] Server의 성공·취소·중단 경로를 시험했다.
- [ ] 동시 Goal 정책을 정하고 검증했다.
- [ ] Blocking Callback이 Cancel 응답성에 미치는 영향을 확인했다.
- [ ] Namespace 두 개에서 Action 이름을 분리했다.
- [ ] 실제 이동량은 Odometry로 계산하도록 설계했다.
- [ ] Command Mux, Safety Filter와 Driver Watchdog를 검토했다.
- [ ] 모든 종료 경로가 정지 명령을 보장한다.
- [ ] Motor 비활성 상태와 Simulation에서 먼저 검증했다.
22. 정리
Action은 단순히 오래 걸리는 Service가 아닙니다. Goal을 수락하거나 거절하고, 실행 중 Feedback을 보내며, Client의 Cancel 요청을 처리하고, 최종적으로 성공·취소·중단 상태와 Result를 전달하는 작업 Protocol입니다.
좋은 Action Server는 입력을 검증하고, 동시 Goal 정책을 명시하며, 모든 종료 경로에서 자원을 안전한 상태로 복구합니다. 좋은 Client는 Server 발견, Goal 수락, Feedback, Cancel 응답과 최종 Status를 각각 구분합니다.
실제 Robot에서는 Action API만으로 안전이 완성되지 않습니다. Odometry와 Sensor 신선도, Command 소유권, Velocity Mux, Driver Watchdog, Emergency Stop과 독립 Safety 계층까지 연결해야 Action이 신뢰할 수 있는 Robot 기능이 됩니다.
- Action액션
- 완료까지 시간이 걸리는 작업에 Goal, Feedback, Result와 Cancel 기능을 제공하는 ROS 2 Interface입니다.
- Action Server액션 서버
- Goal을 검증·수락하고 작업을 실행하면서 Feedback과 최종 Result를 제공하는 ROS Entity입니다.
- Action Client액션 클라이언트
- Server에 Goal을 보내고 수락 여부, Feedback, Result를 받거나 Cancel을 요청하는 ROS Entity입니다.
- Goal목표
- Client가 Action Server에 요청하는 작업 목표와 실행 조건입니다.
- Goal Handle목표 핸들
- 특정 Goal의 수락 여부, 상태, 취소 요청과 Result를 다루는 객체입니다.
- Feedback피드백
- Action이 실행되는 동안 Server가 Client에 반복적으로 보내는 진행 상황입니다.
- Result결과
- Action 종료 후 Server가 반환하는 최종 작업 결과 Data입니다.
- Cancel취소
- 실행 중인 Goal을 중단해 달라는 Client 요청과 그 처리 절차입니다.
- Goal UUID목표 고유 식별자
- 동시에 존재할 수 있는 각 Action Goal을 구분하는 고유 식별자입니다.
- SUCCEEDED성공 완료
- Server가 Goal을 정상적으로 달성하고 Result를 반환한 최종 상태입니다.
- CANCELED취소 완료
- Server가 Cancel 요청을 수락하고 안전한 중단 절차를 마친 최종 상태입니다.
- ABORTED중단
- 장애물, Sensor 고장이나 Timeout 등으로 Server가 Goal을 완료할 수 없어 종료한 상태입니다.
- REJECTED거절
- 입력 오류, Busy 또는 안전 조건 때문에 Server가 Goal 실행을 시작하지 않은 결과입니다.
- Preemption선점
- 새 Goal을 실행하기 위해 기존 Goal을 취소하거나 중단하고 작업 소유권을 전환하는 정책입니다.
- Callback Group콜백 그룹
- Executor가 관련 Callback의 상호 배타 또는 재진입 병렬 실행을 결정하는 Group입니다.
- Reentrant재진입 가능
- 같은 Group의 Callback 또는 같은 Callback의 여러 실행을 병렬로 허용하는 실행 규칙입니다.
- Watchdog감시 타이머
- 명령이나 상태 갱신이 제한시간 안에 오지 않으면 독립적으로 안전 동작을 수행하는 기능입니다.
- Velocity Mux속도 명령 선택기
- 여러 속도 명령 Source에서 Priority와 Timeout에 따라 하나의 유효한 명령을 선택하는 구성 요소입니다.
- Progress Timeout진행 제한시간
- Feedback이나 실제 진행량이 일정 시간 갱신되지 않을 때 Goal 이상으로 판단하는 제한시간입니다.
연습 문제
- Topic, Service와 비교하여 Action이 적합한 요구사항 세 가지를 쓰세요.
.action파일에서 Goal, Result, Feedback의 순서와 구분자를 설명하세요.- Goal이 ACCEPTED되었다는 것과 SUCCEEDED의 차이는 무엇인가요?
- REJECTED, CANCELED와 ABORTED가 각각 발생하는 예를 드세요.
ros2 action list -t,type,info,interface show,send_goal --feedback의 역할을 설명하세요.- Cancel 요청이 수락된 직후 바로
canceled()로 끝내면 실제 Robot에서 위험할 수 있는 이유는 무엇인가요? - Server가 Goal의 NaN, Infinity와 범위를 검사해야 하는 이유는 무엇인가요?
- 동시 Goal 처리 정책 네 가지와 각각 적합한 예를 쓰세요.
- Preemption에서 기존 Goal의 실제 정지를 확인한 뒤 새 Goal을 실행해야 하는 이유는 무엇인가요?
- MultiThreadedExecutor가 있어도 Cancel Callback이 실행되지 않을 수 있는 이유는 무엇인가요?
- 학습용
속도 × 시간적분 대신 실제 Robot에서 Odometry를 사용해야 하는 이유는 무엇인가요? - 여러 Node가
/cmd_vel에 직접 발행할 때 생기는 문제와 해결 구조를 설명하세요. - Discovery, Progress, Overall Timeout과 Driver Watchdog의 차이를 설명하세요.
- Nav2의
NavigateToPose가 Service보다 Action에 적합한 이유는 무엇인가요? - 취소 후에도 Robot이 움직일 때 Action Callback부터 Motor Driver까지 진단 순서를 설명하세요.
COMMUNITY
강의 댓글
질문과 학습 경험을 함께 나눠보세요.댓글을 불러오는 중입니다.