지금까지 Topic, Service에 대해 모두 배워보았습니다. 마지막 통신 메커니즘인 Action에 대해서 배워봅시다.
Action은 Service와 Topic의 특성을 모두 갖고 있으며, 실제로 가장 늦게 탄생한 통신 메커니즘입니다. 일전 Service의 단점을 상기시켜보면서 Action의 필요성에 대해 체감해봅시다.
Service의 중요한 특징 한 가지 추가하자면, 하나의 Service Server에 여러 Client가 request 할 수 있지만, Server는 동시에 여러 request를 처리하지 못합니다.
Action은 바로 이러한 Service의 단점을 극복하기 위해 탄생한 통신 메커니즘입니다.
⇒ 하지만, 여러 request를 동시에 작업하는 것이나, Feedback 중에 topic subscribe와 같은 작업은 본질적으로 불가합니다. 이에 대한 해결 방법도 후에 살펴보겠습니다.
image from : https://docs.ros.org/en/foxy/Tutorials/Understanding-ROS2-Actions.html
사진과 같이 Action Client와 Server가 주고받는 내용은 크게 5가지가 있습니다.
만약 4번 도중 cancel이 발생하면 Action은 종료됩니다.
이렇게 Action은 Topic, Service의 특징을 모두 갖고 있으며 Cancel이라는 추가 기능까지 갖추고 있는 복잡한 통신 메커니즘입니다.
ros2 run turtlesim turtlesim_node
$ ros2 action list
/turtle1/rotate_absolute
$ ros2 action info /turtle1/rotate_absolute
Action: /turtle1/rotate_absolute
Action clients: 0
Action servers: 1
/turtlesim
$ ros2 interface show turtlesim/action/RotateAbsolute
# The desired heading in radians
float32 theta
---
# The angular displacement in radians to the starting position
float32 delta
---
# The remaining rotation in radians
float32 remaining
$ ros2 action send_goal <action_name> <action_type> <values>
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta : 0.0}" --feedback
1657646954.088327 [0] ros2: using network interface enp7s0 (udp/166.104.135.89) selected arbitrarily from: enp7s0, docker0
Waiting for an action server to become available...
Sending goal:
theta: 0.0
Goal accepted with ID: d3ddca85948d4099a13dbeb8183e5ecc
Result:
delta: -0.7839995622634888
Goal finished with status: SUCCEEDED
$ colcon build --packages-select py_action_tutorial
$ source install/local_setup.bash
$ ros2 run py_action_tutorial turtle_turning_client
[INFO] [1672647544.783857088] [turtle_rotate_client]: === Turtle Rotate Action Client Started ====
Enter Target Angle : 3.14
[INFO] [1672647549.097662572] [turtle_rotate_client]: Goal accepted
[INFO] [1672647549.193173672] [turtle_rotate_client]: Received feedback: 0.051996707916259766
[WARN] [1672647549.226111271] [turtle_rotate_client]: Action Done !! Result: -0.12800025939941406
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
# The desired heading in radians
float32 theta
---
# The angular displacement in radians to the starting position
float32 delta
---
# The remaining rotation in radians
float32 remaining
# Create Action Client
self.action_client = ActionClient(self, RotateAbsolute, 'turtle1/rotate_absolute')
# Wait for server first
if self.action_client.wait_for_server(10) is False:
self.get_logger().error('Server Not exists')
# Send Goal then receive future
self._send_goal_future = self.action_client.send_goal_async(
goal_msg, feedback_callback=self.feedback_callback
)
# Done callback Add
self._send_goal_future.add_done_callback(self.goal_response_callback)
Action은 메커니즘 자체가 어렵기 때문에 일부러 쉬운 예제를 가져왔습니다. 코드의 로직은 간단하며, Action Client API 자체에 집중하며 따라와주세요.
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
self.get_logger().info('Goal accepted')
# Add Result cb
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(f'Received feedback: {feedback.partial_sequence}')
def get_result_callback(self, future):
result = future.result().result
self.get_logger().warn(f'Action Done !! Result: {result.sequence}')
rclpy.shutdown()
# Cancel the goal
future = self.goal_handle.cancel_goal_async()
future.add_done_callback(self.cancel_done)
# Cancel the timer
self.timer.cancel()
def cancel_done(self, future):
cancel_response = future.result()
if len(cancel_response.goals_canceling) > 0:
self.get_logger().info('Goal successfully canceled')
else:
self.get_logger().info('Goal failed to cancel')
rclpy.shutdown()
image from : 기호일보
cbp custom_interfaces
cbp parking_action_server
source install/local_setup.bash
# Terminal 1
ros2 launch src_gazebo wall_world.launch.py
# Terminal 2
ros2 run py_action_tutorial parking_action_server
[INFO] [1672654091.689062703] [parking_action_server]: Action Ready...
[INFO] [1672654101.355609636] [parking_action_server]: Executing goal...
[INFO] [1672654101.356973636] [parking_action_server]: Distance from forward obstacle : 100.0
...
# Terminal 3
ros2 action send_goal /src_parking custom_interfaces/action/Parking "start_flag: true" --feedback
Waiting for an action server to become available...
Sending goal:
start_flag: true
Goal accepted with ID: d7bcfec5a6e94bd4a349f4955cc495c8
Feedback:
distance: 100.0
Feedback:
distance: 1.7898634672164917
...
벽에 인접하여 흰색 상자들이 주차 공간을 배정해줄 것입니다. rqt_robot_steering으로 로봇을 잘 제어하여 주어진 주차 공간에 알맞게 주차를 해보세요!
Feedback을 통해 벽과의 거리를 확인할 수 있으며, 이 거리가 0.5m 이내가 되면 주차가 완료됩니다. 이 시점에서 좌우 공간이 얼마가 균형이 맞는지에 따라 다른 Result를 얻게 됩니다.
프로그래밍을 시작하기 전, 필요한 통신 메커니즘들을 살펴봅시다.
Feedback Callback과 Subscription Callback 두 함수도 구현해야 할 것입니다.
#goal definition
bool start_flag
---
#result definition
string message
---
#feedback definition
float32 distance
import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer
class ParkingActionServer(Node):
def __init__(self):
super().__init__('parking_action_server')
self.laser_sub = self.create_subscription(
LaserScan, 'scan', self.sub_callback, 10
)
self.action_server = ActionServer(
self, Parking, 'src_parking', self.execute_callback
)
def execute_callback(self, goal_handle):
self.is_sub = True
self.get_logger().info('Executing goal...')
feedback_msg = Parking.Feedback()
while self.f_obs_distance > 0.5:
feedback_msg.distance = self.f_obs_distance
goal_handle.publish_feedback(feedback_msg)
self.get_logger().info(
f"Distance from forward obstacle : {self.f_obs_distance}"
)
time.sleep(1)
goal_handle.succeed()
result = Parking.Result()
lr_diff = abs(self.r_obs_distance - self.l_obs_distance)
print(lr_diff)
if lr_diff < 0.15:
result.message = "[Success!] Oh... Teach me how you did :0"
else:
result.message = "[Fail] Be careful, Poor Driver! "
return result
try:
parking_action_server = ParkingActionServer()
# MultiThreadedExecutor ref
# https://url.kr/x4kf2b
executor = MultiThreadedExecutor()
executor.add_node(parking_action_server)
try:
executor.spin()
except KeyboardInterrupt:
parking_action_server.get_logger().info('Keyboard Interrupt (SIGINT)')
finally:
executor.shutdown()
parking_action_server.destroy_node()
finally:
rclpy.shutdown()
def sub_callback(self, data):
if self.is_sub:
self.f_obs_distance = data.ranges[60]
self.r_obs_distance = data.ranges[30]
self.l_obs_distance = data.ranges[90]
self.get_logger().info("sub success")
...
# parking_action_server = ParkingActionServer()
# rclpy.spin(parking_action_server)
# parking_action_server.destroy_node()
# rclpy.shutdown()
현재의 시스템은 두 종류의 callback을 갖고 있습니다. execute_callback이 실행되면서 while loop로 진입하면, 자원을 점유하여 sub_callback이 동작할 수 없는 구조가 됩니다. 이를 해결하기 위해 ROS 2에서는 rclpy단에서 멀티 스레딩을 구현해 두었습니다.
이후 여러가지 구현을 하다 보면 지금처럼 다중 Subscribe를 해야하는 경우가 반드시 생깁니다. 혹은 하나의 프로세스에서 여러개의 Node를 실행시켜야 하는 경우가 발생합니다. 이때, Node Composition과 MultiThreadedExecutor를 적극 사용해보세요!
참고자료