11. [Option] Action Server & Client ์์ฑํด๋ณด๊ธฐ โ python
์ด๋ฒ ์๊ฐ์๋, ์ง๋ ๊ฐ์์์ ์ดํด๋ณด์๋ ๋ฏธ๋ก์ฐพ๊ธฐ ์์ ์ ์ฝ๋๋ฅผ ๋ถ์ํด๋ณด๊ณ ์ ํ๋ฉฐ, ๋ถ์ ๊ณผ์ ์์, ์ ๊ฐ ๋ง์ฃผ์ณค๋ ๋ฌธ์ ๋ค๊ณผ, ๋๋ฒ๊น ํฌ์ธํธ๋ค๋ ํจ๊ป ๊ณต์ ํ๊ณ ์ ํฉ๋๋ค.
$ roslaunch gcamp_gazebo maze_world.launch
# ์ ํฐ๋ฏธ๋
$ rosrun action_tutorial maze_action_server.py
==== MazeActionClass Constructed ====
==== Waiting for Client Goal... ====
# ์ ํฐ๋ฏธ๋
$ rosrun action_tutorial maze_action_client.py
[INFO] [1610254457.453111, 322.000000]: Action Server Found.../maze_action_server
Enter numbers [or stop] :
- ์ฐ์ , ํจํค์ง ํ์ผ ๊ตฌ์กฐ๋ฅผ ์ดํด๋ณผ๊น์??
action_tutorial/
โโโ CMakeLists.txt
โโโ action
โ โโโ Maze.action
โโโ launch
โ โโโ fibonacci_server.launch
โ โโโ maze_runner.launch
โโโ package.xml
โโโ src
โโโ fibonacci_action_client.py
โโโ fibonacci_action_server.py
โโโ gazebo_handler_test.py
โโโ maze_action_client.py
โโโ maze_action_server.py
โโโ mazepkg
โโโ __init__.py
โโโ basic_cmd_vel.py
โโโ gazebo_handler.py
โโโ image_converter.py
gcamp_gazebo/
โโโ CMakeLists.txt
โโโ launch
โ โโโ gazebo_world.launch
โ โโโ maze_world.launch
โ โโโ robot_description.launch
โโโ ...
maze_world.launch๋ฅผ ํตํด ์ฌ์ง๊ณผ ๊ฐ์ ์๋ก์ด world๋ฅผ gazebo_ros์ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค.
maze_runner.launchํ์ผ์ ๋จ์ํ action server๋ฅผ ์คํ์ํค๊ธฐ ์ํ ๊ฐ๋จํ launch file์ ๋๋ค.
<launch>
<node pkg="action_tutorial" type="maze_action_server.py" name="maze_action_server" output="screen" />
</launch>
server ๋ถํฐ ์ดํด๋ณด๊ฒ ์ต๋๋ค. class ์ค๊ณ๋ฅผ ํ๊ธฐ ์์, ํด๋น ํด๋์ค์๊ฒ ํ์ํ instance, method๋ฅผ ๊ธฐ๋ฅ์ ๋ง์ถ์ด ์๊ฐํด ๋ณด๊ฒ ์ต๋๋ค.
- client๋ก๋ถํฐ ๊ฒฝ๋ก์ ๋ฐฐ์ด์ด ๋ค์ด์ค๋ฉด
- ํด๋น ๋ฐฉํฅ์ผ๋ก ์ฐ์ ์์น๋ฅผ ํ๊ณ (์ด์ service ์๊ฐ์ ํ๋ ๊ธฐ๋ฅ๊ณผ ๋์ผํ๋ค์)
- ๋ฒฝ์ด ๋์ฌ๋๊น์ง ์ ์งํฉ๋๋ค. (์ด๊ฑด, topic์๊ฐ์ parking ์์ ๋ก ์ดํด๋ณด์์ต๋๋ค.)
- ๊ฒฝ๋ก์ ๋ฐฐ์ด ๋์ ๋ค๋ค๋ฅผ ๋๊น์ง ์ ๊ณผ์ ์ ๋ฐ๋ณตํฉ๋๋ค. (action์ผ๋ก ์ค๊ณํ๋ฉด, feedback์ผ๋ก ์งํ ์ํฉ์ ๊ณต์ ํ ์ ์๊ณ , ๊ทธ๋์ client๋ ๋ค๋ฅธ ์ผ์ ํ ์ ์๊ฒ ์ง์.)
- ์ต์ข ์์น์ ๋์ฐฉํ๋ฉด, ์ด๋ก ๋ฐ์ค๊ฐ ๋ณด์ด๋์ง ํ์ธํฉ๋๋ค. (์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ ๋ค๋ค์ผ ํฉ๋๋ค.)
- ์ด๋ก ๋ฐ์ค์ ๋๋ฌํ๋ค๋ฉด success, ์คํจํ๋ค๋ฉด fail!!
์, ์ ์ํฉ๋ค์ ์ ๋ ํ๋ฉด์ server์ MazeActionClass๋ฅผ ๋ถ์ํด ๋ณด๊ฒ ์ต๋๋ค.
Import
import math
import time
import rospy
import actionlib
from mazepkg.basic_cmd_vel import GoForward, Stop, Turn
from mazepkg.gazebo_handler import GazeboResetSimulation
from mazepkg.image_converter import ImageConverter
from nav_msgs.msg import Odometry
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from tf.transformations import euler_from_quaternion
from action_tutorial.msg import MazeAction, MazeFeedback, MazeResult
Constructor ๋ถ์
class MazeActionClass(object):
_feedback = MazeFeedback()
_result = MazeResult()
def __init__(self, name):
self._action_name = name
self._yaw = 0.0
self._scan = []
self._current_direction = 3 # 0 1 2 3
self._cmd_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1)
self._odom_sub = rospy.Subscriber("/odom", Odometry, self.odom_callback)
self._scan_sub = rospy.Subscriber("/scan", LaserScan, self.scan_calback)
# self._image_sub = rospy.Subscriber('')
self._action_server = actionlib.SimpleActionServer(
self._action_name, MazeAction, execute_cb=self.ac_callback, auto_start=False
)
self._action_server.start()
self._rate = rospy.Rate(5)
print("==== MazeActionClass Constructed ====")
print("==== Waiting for Client Goal... ====")
- ์ธ๋ถ์์๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ฒ static variable๋ก _feedback, _result๋ฅผ ๋ด๊ณ ์์ผ๋ฉฐ, ActionServer๋ฅผ ์ ์ธํ๊ณ ํฌ๊ฒ ์ธ๊ฐ์ง class๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค.
- _cmd_pub : ๋ก๋ด์๊ฒ ์ด๋ ๋ช ๋ น์ ์ค๋๋ค.
- _odom_sub: ๋ค์ ์ด์ผ๊ธฐํ๊ฒ ์ง๋ง ๋ก๋ด์ ํ์ ์ ๋ณด๋ค ์ ํํ๊ฒ ํ๊ธฐ ์ํด์ ์ฌ์ฉํฉ๋๋ค.
- _scan_sub : parking ์์ ์ ๊ฐ์ด lidar data๋ฅผ ๋ฐ๊ธฐ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
self._odom_sub = rospy.Subscriber("/odom", Odometry, self.odom_callback)
์ ๋ถ๋ถ์ด ์ ๋ค์ด๊ฐ๋์ง์ ๋ํด ์ค๋ช ํด๋ณด๊ฒ ์ต๋๋ค.
while time.time() - start_time < request.time_duration:
velocity_publisher.publish(cmd_vel)
print("Done ...")
cmd_vel.angular.z = 0.0
velocity_publisher.publish(cmd_vel)
์ด์ , turning service server์ ์ผ๋ถ์ธ๋ฐ์, python time ํจํค์ง๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ์๊ฐ๋งํผ ๋ก๋ด์ ์ด๋์ํค๊ณ ์์ต๋๋ค.
์ ๋ฐฉ์๋ ๋์์ง๋ ์์ง๋ง, ๋ฌธ์ ์ ์ด ์์ต๋๋ค.
- ๋ฐ๋ก ์ค์ฐจ๊ฐ ๋์ ๋๋ค๋ ์ ์ ๋๋ค. (์ ํ์ฑ)
์๋ฅผ ๋ค์ด, ๋์ ๊ฐ๊ณ , ๋๋ฐ๋ก ๊ฑธ์ด๋ณด๋ฉด, ์๋ฌด๋ฆฌ ๋ด๊ฐ ๋ฐ๋ฏํ๊ฒ ๊ฑธ์๋ค ํ ์ง๋ผ๊ณ ๋์ ๋ ๋ณด๋ฉด ์ค์ ๊ฑธ์ด์จ ๊ธธ์ด ๋ฐ๋ฏํ์ง ์์ง์?
ํ๊ฑธ์ ํ๊ฑธ์์ ๋๋ฐ๋ก ๊ฑธ์์์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง, ์กฐ๊ธ์ฉ์ ์ค์ฐจ๊ฐ ๋ชจ์ฌ ๊ฒฐ๊ณผ์ ํฐ ์ฐจ์ด๋ฅผ ๋ง๋๋ ๊ฒฝ์ฐ์ ๋๋ค.
์ฐ๋ฆฌ ๋ก๋ด๋ ๋ฏธ๋ก ์์์ ๊ณ์ ํ์ ์ ํ๋ฉด์ ์ค์ฐจ๊ฐ ๋์ ๋ ๊ฒ์ ๋๋ค.
๊ทธ๋ผ, ์ด๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ๊น์?
Odom์ ๋ฑ์ฅ
image from : answer.ros.org
self._odom_sub = rospy.Subscriber("/odom", Odometry, self.odom_callback)
(...)
def odom_callback(self, data):
orientation_q = data.pose.pose.orientation
orientation_list = [
orientation_q.x,
orientation_q.y,
orientation_q.z,
orientation_q.w,
]
_, _, self._yaw = euler_from_quaternion(orientation_list)
์ง๊ธ ์ฐ๋ฆฌ๋ gazebo ์๋ฎฌ๋ ์ด์
์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์, ๋ก๋ด์ ์ ๋์ ์ธ ์์น, ๋ฐฉํฅ์ ์์๋ผ ์
์์ต๋๋ค. ๐๐ ๊ทธ๋ฆฌ๊ณ , ์ด๋ ๊ฒ ๋์ถ๋ ๋ฐ์ดํฐ๊ฐ ๋ฐ๋ก /odom topic์์ ๋ด๊ฒจ์์ต๋๋ค. ๋์ ๊ณ์ฐ์ ํธ์๋ฅผ ์ํด quaternion์ผ๋ก ๋ค์ด์์ผ๋ฏ๋ก ๋ค์๊ธ
euler angle๋ก ๋ฐ๊ฟ์ฃผ์์ต๋๋ค.
from tf.transformations import euler_from_quaternion, quaternion_from_euler
์๋ฎฌ๋ ์ด์ ์ด ์๋, ์ค์ ๋ก๋ด์ ์ด๋ฅผ ์ด๋ป๊ฒ ์์๋ผ๊น์?
์ฌ์ค ์ด๋ ๋ก๋ณดํฑ์ค์์ ํด๊ฒฐํ๊ณ ํ๋ ์ฃผ๋ ๋ฌธ์ ์ค ํ๋์
๋๋ค.
Localization in Robotics์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ๊ฒ์ํด ๋ณด์๋ฉด ํ๋ฐํ๊ฒ ์งํ์ค์ธ ์ฐ๊ตฌ๋ค์ ์ฐพ์๋ณด์ค ์ ์์ต๋๋ค.
def robot_turn(self, euler_angle):
target_rad = euler_angle * math.pi / 180
turn_offset = 100
self._rate.sleep()
while abs(turn_offset) > 0.005:
turn_offset = 0.7 * (target_rad - self._yaw)
Turn.angular.z = turn_offset
self._cmd_pub.publish(Turn)
self._cmd_pub.publish(Stop)
๊ฒฐ๋ก ์ ์ผ๋ก, ๋ฏธ๋ก์ฐพ๊ธฐ ์์ ์์๋ ๋ก๋ด์ ํ์ ์ odom์ ์ด์ฉํด ๋ณด๋ค ์ ํํ๊ฒ ๊ตฌํํ์์ต๋๋ค.
- ์ฌ๊ธฐ์์
turn_offset = 0.7 * (target_rad - self._yaw)์ P Gain Control ์ด๋ฉฐ,์ด ๋ํ
PID Control์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ๊ฒ์ํด ๋ณด์๋ฉด ํฅ๋ฏธ๋ก์ด ๋ด์ฉ๋ค์ ์ต๋ํ์ค ์ ์์ ๊ฒ์ ๋๋ค. ๐
- Matlab - Understanding PID Control, Part 1: What is PID Control?
Action Callback ๋ถ์
- sequence loop
Maze.action
int32[] turning_sequence
---
bool success
---
string feedback_msg
def ac_callback(self, goal):
success = True
print("==== Maze Action Server Executing ====")
# python enumerate
for i, val in enumerate(goal.turning_sequence):
# check that preempt has not been requested by the client
if self._action_server.is_preempt_requested():
rospy.logwarn("%s: Preempted" % self._action_name)
self._action_server.set_preempted()
success = False
break
# ํฐ๋ฏธ๋์ ํ์๋ ๋ฉ์ธ์ง
self._feedback.feedback_msg = "Turning to " + direction_str_dict[val]
self._action_server.publish_feedback(self._feedback)
# ํ์ ๊ณผ ์ ์ง์ ๋ฐ๋ณตํฉ๋๋ค.
print("Turning Sequence : " + str(val))
self.robot_turn(direction_dict[val])
self._feedback.feedback_msg = "Moving Forward ..."
self._action_server.publish_feedback(self._feedback)
self.robot_go_forward()
self._rate.sleep()
๋๋ถ๋ถ์ ์ฝ๋๋ ์ด์ ์๊ฐ์ ์ดํด๋ณด์์ผ๋ฉฐ, ์์ ๋ง์ง๋ง ๋จ์ํ
goal.turning_sequence๋ฅผ ์ ํํ๋ฉด์ ํ์ , ์ ์ง์ ๋ฐ๋ณต์ํค๋ ๋ถ๋ถ์์ ์ ์ ์์ต๋๋ค.
- robot_go_forward
def robot_go_forward(self):
self._rate.sleep()
while self._scan[360] > 0.9:
self._cmd_pub.publish(GoForward)
self._cmd_pub.publish(Stop)
- Success Condition
if success:
ic = ImageConverter()
center_pixel = ic.center_pixel
if sum(center_pixel) < 300 and center_pixel[1] > 100:
self._result.success = True
rospy.loginfo("Maze Escape Succeeded")
else:
self._result.success = False
rospy.logerr("Maze Escape Failed")
self._action_server.set_succeeded(self._result)
์ฃผ์ด์ง ์ด๋๊ฒฝ๋ก๋๋ก ๋ก๋ด์ด ์๋ฌด๋ฅผ ์๋ฃํ ์๊ฐ!! ์ด๋ก ๋ฐ์ค์ ์ ๋ฌด์ ๋ฐ๋ผ ์ต์ข ์๋ฌด ์ฑํจ๋ฅผ ๊ฒฐ์ ์ง๋๋ค๊ณ ํ์์ง์? ์ ์ฝ๋๊ฐ ๋ฐ๋ก ๊ทธ ๋ถ๋ถ์ ๋๋ค.
center_pixel์ ๋์ฐฉ ์, ๋ก๋ด์ ์ฅ์ฐฉ๋ ์นด๋ฉ๋ผ ์ด๋ฏธ์ง ์ค์ฌ ํฝ์ ๊ฐ์ ๋๋ค. ์ด๋ฏธ์ง ํฝ์ ์ ๋ค๋ฃจ๊ธฐ ์ํด OpenCV๋ฅผ ์ฌ์ฉํ์์ผ๋ฉฐ, cv::Mat ํฌ๋งท์ ๋ฐ๋ผ ๋ น์์ ํด๋นํ๋ ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์กฐ๊ฑด์ ๊ฑธ์์ต๋๋ค.
OpenCV์ ๋ํ ๊น์ ์ค๋ช ์ ์๋ตํ๊ณ , ROS์ image message๋ฅผ cv::Mat๋ก ์ ํํ๋ ๋ถ๋ถ์ ์ง๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
Image Converter
#! /usr/bin/env python
import rospy
import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image
class ImageConverter:
def __init__(self):
# self.image_pub = rospy.Publisher("image_topic_2", Image, queue_size=1)
self._center_pixel = []
self._bridge = CvBridge()
self._image_sub = rospy.Subscriber("camera/rgb/image_raw", Image, self.callback)
def callback(self, data):
try:
cv_image = self._bridge.imgmsg_to_cv2(data, "bgr8")
except CvBridgeError as e:
print(e)
# green
# [ 0 110 0]
# gray
# [105 105 105]
self._center_pixel = cv_image[400, 400]
@property
def center_pixel(self):
self._rate = rospy.Rate(5)
self._rate.sleep()
return self._center_pixel
bgr8 ํ์์ผ๋ก ๋ฐ๊พธ์๊ธฐ ๋๋ฌธ์, center_pixel์์๋ blue, green, red ์์ผ๋ก pixel ๊ฐ์ด ์ฑ์์ ธ ์์ต๋๋ค.
- ๋๋จธ์ง ์ฝ๋๋ค์ ๊ธฐ์กด๊ณผ ํฐ ์ฐจ์ด๊ฐ ์์ผ๋ฉฐ, ๋ค์๊ณผ ๊ฐ์ด ๋ฐฉํฅ์ ๋ํ๋ด๊ธฐ ์ํด ์ฌ์ฉํ dictionary๋ฑ์ด ์์ต๋๋ค.
direction_dict = {0: -90, 1: 180, 2: 90, 3: 0}
direction_str_dict = {0: "Up", 1: "Right", 2: "Down", 3: "Left"}
์ข์ต๋๋ค! ๊ทธ๋ผ client๋ก ์ด๋ํด ๋ณด๊ฒ ์ต๋๋ค!! ๐ฅณ๐ฅณ
Maze Action Client
#! /usr/bin/env python
"""
action client node for maze gazebo example
referenced from robotigniteacademy
url : https://get-help.robotigniteacademy.com/t/understanding-ros-actions-client-class-exercise/1498
list input part is referenced from geeksforgeeks
url : https://www.geeksforgeeks.org/python-get-a-list-as-input-from-user/
"""
import time
import rospy
import actionlib
from enum import IntEnum
from action_tutorial.msg import MazeAction, MazeGoal
from tf.transformations import euler_from_quaternion, quaternion_from_euler
# For more detail, search actionlib_msgs/GoalStatus
class ActionState(IntEnum):
PENDING = 0
ACTIVE = 1
PREEMPTED = 2
SUCCEEDED = 3
ABORTED = 4
REJECTED = 5
PREEMPTING = 6
RECALLING = 7
RECALLED = 8
LOST = 9
def fb_callback(feedback):
print(feedback)
action_server_name = "/maze_action_server"
rospy.init_node("maze_action_client")
action_client = actionlib.SimpleActionClient(action_server_name, MazeAction)
rospy.loginfo("Action Server Found..." + action_server_name)
goal = MazeGoal()
user_list = []
# try block to handle the exception
try:
print("Enter numbers [or stop] : ")
while True:
user_list.append(int(input()))
# if the input is not-integer, just print the list
except:
print("Your sequence list : ", user_list)
goal.turning_sequence = user_list
action_client.send_goal(goal, feedback_cb=fb_callback)
state_result = action_client.get_state()
rate = rospy.Rate(1)
rospy.loginfo("State Result from Server : " + str(state_result))
while state_result < ActionState.PREEMPTED:
# Doing Stuff while waiting for the Server to give a result....
rate.sleep()
state_result = action_client.get_state()
if state_result == ActionState.SUCCEEDED:
rospy.loginfo("Action Done State Result : " + str(state_result))
else:
rospy.logwarn("Something went wrong, result state : " + str(state_result))
ใด ํผ๋ณด๋์น ์์ ์ ํฌ๊ฒ ๋ค๋ฅด์ง ์์ง์?
- ์ฐจ์ด๊ฐ ์๋ค๋ฉด , Action Status๋ฅผ
enum class๋ก ๋ฏธ๋ฆฌ ์ค์ ํด ๋์์ต๋๋ค. (์ฌ์ค ๋ด๋ถ์ ์ผ๋ก๋ ์ด๋ฏธ ์๋์ ๊ฐ์ด ์ ์๋์ด ์๋ต๋๋ค.)
from enum import IntEnum
(...)
class ActionState(IntEnum):
PENDING = 0
ACTIVE = 1
PREEMPTED = 2
SUCCEEDED = 3
ABORTED = 4
REJECTED = 5
PREEMPTING = 6
RECALLING = 7
RECALLED = 8
LOST = 9
- ์ด์ ๋๋ถ์ด ๋ก๋ด ํ์ service ์์์์๋ ๋ณด์๋ ์ฌ์ฉ์๋ก๋ถํฐ input์ ๋ฐ๋ ๋ถ๋ถ, ๊ทธ์ ๋ฐ๋ฅธ ์์ธ์ฒ๋ฆฌ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค. ๐
# try block to handle the exception
try:
print("Enter numbers [or stop] : ")
while True:
user_list.append(int(input()))
# if the input is not-integer, just print the list
except:
print("Your sequence list : ", user_list)
์ง๊ธ๊น์ง ๋ฏธ๋ก์ฐพ๊ธฐ Action ์์ ์ ์ค๋ช ์ด์์ต๋๋ค. ๐ ๋ถ์กฑํ ์ฝ๋์ ์ค๋ช ์ด๊ธฐ์ ๋ถ๋๋ฝ์ง๋ง, ์ฌ๋ฏธ์๋ ์์ ์ด์์ต๋๋ค ๐๐
Contribution์ ์ธ์ ๋ ํ์ํฉ๋๋ค!!
Github Repo


