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