지난 시간 마지막 예시였던 장애물 회피 코드부터 간단하게 리뷰해보고자 합니다.
제가 작성한 로직은 다음과 같습니다.
for i, point in enumerate(data.ranges):
if not math.isinf(point) and point < 1.0:
left_side_count = 0
right_side_count = 0
for i, point in enumerate(data.ranges):
if not math.isinf(point) and point < 1.0:
if i > 180:
left_side_count += 1
else:
right_side_count += 1
image from : 오로카
def laser_cb(self, data):
left_side_count = 0
right_side_count = 0
for i, point in enumerate(data.ranges):
if not math.isinf(point) and point < 1.0:
if i > 180:
left_side_count += 1
else:
right_side_count += 1
self.twist_.linear.x = 0.3
self.twist_.angular.z = (right_side_count - left_side_count) / 100
self.cmd_vel_pub_.publish(self.twist_)
지난 강의에서 이야기한 것과 같이 이 문제의 정답은 없습니다.
다만, Topic의 Pub / Sub을 모두 사용할 수 있는지 스스로 점검해볼 수 있을 것입니다.
앞선 저의 예시에서 마지막 속도로 변환하는 부분 수식에 나누기 100이 있었던 것을 기억하시나요? 이런 상수를 직접 코드에 적는 것은 사실 추천되지 않습니다. 개발 이후 해당 상수를 변경하고자 하였을 시, 직접 코드를 수정하고 다시 실행해야 하기 때문에 불편을 야기합니다.
이러한 문제의 해결 방법으로 ROS의 매개변수, parameter를 다루는 방법을 알아보겠습니다.
#!/usr/bin/env python3
import rospy
class ParamNode:
def __init__(self):
self.str_param_ = rospy.get_param('~str_param', 'hello_world')
self.int_param_ = rospy.get_param('~int_param', 2023)
self.double_param_ = rospy.get_param('~double_param', 3.14)
self.bool_param_ = rospy.get_param('~bool_param', True)
self.list_of_float_param_ = rospy.get_param('~list_of_float_param', [1., 2., 3., 4.])
rospy.loginfo(f"""
self.str_param_ = {self.str_param_}
self.int_param_ = {self.int_param_}
self.double_param_ = {self.double_param_}
self.bool_param_ = {self.bool_param_}
self.list_of_float_param_ = {self.list_of_float_param_}
""")
def param_node():
rospy.init_node('param_node', anonymous=True)
param_node = ParamNode()
rospy.spin()
if __name__ == '__main__':
try:
param_node()
except rospy.ROSInterruptException:
pass
$ rosrun py_param_pkg various_params.py
[INFO] [1672014267.630578]:
self.str_param_ = hello_world
self.int_param_ = 2023
self.double_param_ = 3.14
self.bool_param_ = True
self.list_of_float_param_ = [1.0, 2.0, 3.0, 4.0]
def __init__(self):
self.str_param_ = rospy.get_param('~str_param', 'hello_world')
self.int_param_ = rospy.get_param('~int_param', 2023)
self.double_param_ = rospy.get_param('~double_param', 3.14)
self.bool_param_ = rospy.get_param('~bool_param', True)
self.list_of_float_param_ = rospy.get_param('~list_of_float_param', [1., 2., 3., 4.])
global_name = rospy.get_param("/global_name")
relative_name = rospy.get_param("relative_name")
private_param = rospy.get_param('~private_name')
default_param = rospy.get_param('default_param', 'default_value')
추가 자료 : wiki.ros
<launch>
<node name="various_param_node" pkg="py_param_pkg" type="various_params.py" respawn="false" output="screen" >
<param name="str_param" type="string" value="roslaunch changed me" />
</node>
</launch>
ROS 프로그래밍을 하다 보면 매개변수가 아주 많이 필요한 경우가 있습니다. 이럴 때마다 launch file에 param 태그 라인을 추가하는 것은 매우 귀찮은 일입니다.
yaml이라는 형식의 파일로 매개변수를 한번에 관리할 수 있습니다.
str_param: "yaml string"
int_param: 9
double_param: 2.71828
bool_param: "false"
list_of_float_param: [3., 2., 1.]
<launch>
<node name="various_param_node" pkg="py_param_pkg" type="various_params.py" respawn="false" output="screen" >
<rosparam command="load" file="$(find py_param_pkg)/param/config.yaml" />
<param name="str_param" type="string" value="roslaunch changed me" />
</node>
</launch>
launch file에 rosparam 태그를 추가하고, load command를 사용하며 사용하는 yaml 파일의 위치를 file 옵션을 통해 전달합니다.
rostopic, rosnode와 같이 parameter 또한 터미널 명령어를 갖고 있습니다.
rosparam list
rosparam get <parameter_name>
rosparam set <parameter_name> <value>
여러분들이 작성한 회피 프로그램에도 매개변수로 작용하는 상수들이 있을 것입니다. 이를 parameter로 변경하여 launch file과 yaml file로 업데이트하는 작업을 해보세요
Topic에 이어 ROS의 통신 메커니즘 두번째로 Service를 배워보겠습니다.
Service가 동작하는 방식은 아래와 같습니다.
그림과 같이 Client Node가 Server Node로 request를 주면, 해당 request에 대응하는 적절한 response가 다시 Client에게로 전달됩니다. 이 과정을 Service Call이라고 부릅니다.
image from : docs.ros.org
image from : https://docs.ros.org/en/foxy/Tutorials/Services/Understanding-ROS2-Services.html
Topic과 비교하여 Service의 특징을 알아봅시다.
실제 로봇 프로그램에서 Service는 어떻게 사용될 수 있을지, 예시를 통해 살펴봅시다.
cd ~/catkin_ws
catkin build py_service_pkg
source devel/setup.bash
# Terminal 1
roslaunch py_service_pkg empty_gazebo.launch
# Terminal 2
rosrun py_service_pkg spawn_model_client.py
방금 실행한 예시는 Gazebo에게 box를 등장시켜달라고 하는 Service Client를 포함하고 있습니다.
Box가 등장하는 위치를 아래 사진과 같은 수식에 맞추어 설정한 것 뿐입니다.
그럼, 코드를 분석해 볼까요?
import math
import rospy
import rospkg
from geometry_msgs.msg import Pose
from gazebo_msgs.srv import SpawnModel
여기서 중요한 점은 msg와 srv 부분입니다. topic에서 사용되는 데이터 타입이 Message였고, 프로그래밍 시에는 msg로 사용하였습니다.
image from : rsl.eth
image from : docs.ros.org
gazebo_msgs/SpawnModel를 살펴보면 파란 글자로 geometry_msgs/Pose라는 부분이 있습니다. 이와 같이 srv는 다른 msg를 품을 수도 있고, 이렇게 만든 srv를 또다시 다른 srv에 포함시킬 수도 있습니다.
코드 구현 관점에서, geometry_msgs/Pose는 Model을 등장시킬 초기 위치를 지정하는데 사용됩니다.
# initial_pose
initial_pose = Pose()
initial_pose.position.x = 0.0
initial_pose.position.y = -1
initial_pose.position.z = 0.2
# z rotation -pi/2 to Quaternion
initial_pose.orientation.z = -0.707
initial_pose.orientation.w = 0.707
spawn_model_prox = rospy.ServiceProxy("gazebo/spawn_urdf_model", SpawnModel)
...
result = spawn_model_prox(
entity_name, model_xml, robot_namespace, initial_pose, reference_frame
)
rospy.ServiceProxy()
는 2개의 매개변수를 필요로 합니다.
생성한 client로 request를 하기 위해서는 생성한 인스턴스에 매개변수를 전달하기만 하면 됩니다. 마치 함수 호출처럼 말이지요. 이는 ServiceProxy 내부적으로 call 메소드가 구현되어있기 때문입니다.
service call의 결과로 result가 반환되며, 예시에서는 성공 여부를 반환하도록 되어 있습니다.
# model_xml
rospack = rospkg.RosPack()
model_path = rospack.get_path("py_service_pkg") + "/urdf/"
with open(model_path + model_name + ".urdf", "r") as xml_file:
model_xml = xml_file.read().replace("\n", "")
Gazebo는 urdf라는 파일을 전달하면 해당 파일을 기반으로 시뮬레이션에 물체를 등장시켜줍니다. 이 urdf라는 것은 로봇을 표현하기 위한 일종의 약속된 파일 확장명입니다.
image from : spart
<?xml version="1.0"?>
<robot name="box">
<link name="box">
<inertial>
<mass value="1"/>
<!-- Inertia values were calculated to be consistent with the mass and
geometry size, assuming a uniform density. -->
<inertia ixx="0.0108" ixy="0" ixz="0" iyy="0.0083" iyz="0" izz="0.0042"/>
</inertial>
<visual>
<geometry>
<box size=".1 .2 .3"/>
</geometry>
</visual>
<collision name="box">
<geometry>
<box size=".1 .2 .3"/>
</geometry>
</collision>
</link>
</robot>
gazebo/spawn_urdf_model과 같은 service는 gazebo_ros를 사용할 때 자동으로 함께 실행됩니다. 이렇게 현재 어떠한 service가 존재하며, 또 구체적인 정보는 어떻게 조회하는지 알아봅시다.
$ rosservice list
/delete_entity
/gazebo/describe_parameters
/gazebo/get_parameter_types
/gazebo/get_parameters
/gazebo/list_parameters
/gazebo/set_parameters
...
리눅스의 grep 명령어를 함께 사용해 보세요.
$ rosservice type /gazebo/spawn_urdf_model
gazebo_msgs/SpawnModel
$ rossrv show `rosservice type /gazebo/spawn_urdf_model`
string model_name
string model_xml
string robot_namespace
geometry_msgs/Pose initial_pose
geometry_msgs/Point position
float64 x
float64 y
float64 z
geometry_msgs/Quaternion orientation
float64 x
float64 y
float64 z
float64 w
string reference_frame
---
bool success
string status_message
$ rosservice info /gazebo/spawn_urdf_model
Node: /gazebo
URI: rosrpc://192.168.55.236:55405
Type: gazebo_msgs/SpawnModel
Args: model_name model_xml robot_namespace initial_pose reference_frame
gazebo_ros에서 제공하는 다양한 service들이 있습니다. rosservice 커멘드를 사용하여 조회해보고 여러분들만의 Application을 생각해 보세요.
# Terminal 1
roslaunch smb_gazebo smb_gazebo.launch
# Terminal 2
rosrun py_service_pkg emergency_stop.py
# Terminal 3
rqt
실제 로봇 개발시에도 Service는 이렇게 단발성이고, 빠르게 실행되어야 하는 동작에 주로 사용됩니다. 더불어, 지금 실행한 예시가 Service Server임을 다시 한 번 상기시켜드립니다.
image from : rsl.eth
코드를 분석해 봅시다.
from geometry_msgs.msg import Twist
from std_srvs.srv import SetBool, SetBoolResponse
이번에 사용하는 데이터 타입은 크게 2 종류입니다.
image from : docs.ros.org
SetBoolResponse이라는 것은 SetBool srv 중 response 부분에 해당합니다. 기본 데이터 타입 이름 + Response를 붙여주기만 하면 사용 가능합니다.
ROS의 msg, srv는 다양한 언어와 상황을 고려하도록 만들어져 있으며, ROS 2에서는 IDL이라는 이름으로 더욱 발전하였습니다. 이후의 커스텀 데이터 타입 제작을 통해 이 과정을 다시 살펴봅시다.
class EmergencyStopNode(object):
def __init__(self):
self.cmd_vel_pub_ = rospy.Publisher("cmd_vel", Twist, queue_size=10)
self.stop_server_ = rospy.Service("emergency_stop", SetBool, self.stop_cb)
로봇 제어를 위한 topic publisher와 service server를 생성합니다.
rospy.Service()
를 통해 Service Server를 생성할 수 있으며 다음과 같은 매개변수를 필요로합니다.
callback 함수는 일전 subscriber에서 살펴본 바 있습니다. service server의 callback 함수는 항상 매개변수로 request srv를 받습니다. 그리고 return 값은 항상 response가 됩니다.
def stop_cb(self, request):
...
return self.response_
if request.data is True:
self.twist_msg_.linear.x = 0.0
self.twist_msg_.angular.z = 0.0
self.cmd_vel_pub_.publish(self.twist_msg_)
self.response_.success = True
self.response_.message = "Successfully Stopped"
else:
self.response_.success = False
self.response_.message = "Stop Failed"
마지막에 사용한 rqt의 service caller는 별도의 프로그래밍이나 복잡한 터미널 명령어 없이도 손쉽게 service를 다룰 수 있게 해주는 ROS의 툴입니다.
지금까지 ROS Service에 대해 배워보았습니다. Topic과 더불어 많이 사용되는 통신 메커니즘이므로 잘 숙지하고 복습하시기 바랍니다.
참고자료