ros는 다양한 언어를 지원하고 있습니다. 지금까지 살펴보았던 rospy는 가장 쉽고 빠르게 배울 수 있어서 사용하였지만, UDPROS, Nodelet, Plugin과 같은 Advanced ROS 개발을 위해서는 C++ 프로그래밍을 통한 Node 개발이 필요합니다.
rospy를 통해 개념을 모두 익혔기 때문에 이번 강의에서는 개발 API를 위주로 roscpp을 배워보겠습니다.
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
def my_first_node():
# ROS nodes require initialization
# It contains master registration, uploading parameters
rospy.init_node('my_first_node', anonymous=True)
# ROS safe timer
rate = rospy.Rate(10) # 10hz
# Loop control Example
while not rospy.is_shutdown():
hello_du = "hello du %s" % rospy.get_time()
rospy.loginfo(hello_du)
# Below line calls sleep method in Python internally.
rate.sleep()
if __name__ == '__main__':
try:
my_first_node()
except rospy.ROSInterruptException:
pass
#include <ros/ros.h>
#include <std_msgs/String.h>
int main(int argc, char** argv){
ros::init(argc, argv, "basic_node");
ros::NodeHandle nh;
ros::Rate r(5);
while ( ros::ok() ) {
ROS_INFO("This is Basic Node");
// ros::spinOnce();
r.sleep();
}
return 0;
}
간단히 표를 통해 비교해보면 아래와 같이 상당 부분 반복되는 점들을 확인할 수 있습니다.
rospy | roscpp | |
---|---|---|
Client Library | import rospy | #include <ros/ros.h> |
initialization | rospy.init_node | ros::init |
interface import | from std_msgs.msg import String | #include <std_msgs/String.h> |
logging | rospy.loginfo() | ROS_INFO() |
spin | rospy.spin() | ros::spin() |
rate | rospy.Rate() | ros::Rate() |
roscpp은 NodeHandle을 통해 parameter, publisher, subscriber, serviceServer들을 생성하며, 매개변수로 namespace를 받습니다.
cmd_vel_pub 예시코드에 namespace를 설정한 뒤 topic값의 변화를 살펴봅시다.
ros::NodeHandle nh("my_namespace");
더불어, NodeHandle은 ROS Node lifecycle 중 “start”의 트리거가 됩니다. Node의 Lifecycle에 대한 자세한 설명은 링크로 대체하겠습니다.
NodeHandle은 반드시 ros::init 보다 뒤에 생성되어야 합니다
C++로 작성된 코드는 빌드가 필요하며 caktin 시스템에서는 CMake가 빌드를 도와줍니다.
빌드 시에 필요한 공유 라이브러리, 외부 라이브러리들, 빌드 속성과 같은 상세 내용들이 CMakeLists.txt에 위치합니다.
본 강의에서는 CMake에 대해서는 자세히 다루지 않고, 프로그래밍 시 알아야 하는 부분만 살펴보겠습니다.
## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
roscpp
geometry_msgs
sensor_msgs
)
## Declare a C++ executable
## With catkin_make all packages are built within a single CMake context
## The recommended prefix ensures that target names across packages don't collide
add_executable(cmd_vel_pub_node src/cmdvel_pub.cpp)
add_executable(laser_sub_node src/laser_sub.cpp)
...
add_dependencies(cmd_vel_pub_node ${cmd_vel_pub_node_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(laser_sub_node ${laser_sub_node_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
...
target_link_libraries(cmd_vel_pub_node
${catkin_LIBRARIES}
)
target_link_libraries(laser_sub_node
${catkin_LIBRARIES}
)
catkin build <pkg-name>
source devel/setup.bash
rosrun <pkg-name> <executable-name>
# Terminal 1
roslaunch smb_gazebo smb_gazebo.launch
# Example 1
rosrun cpp_topic_pkg cmd_vel_pub
# Example 2
rosrun cpp_topic_pkg laser_scan_sub
#!/usr/bin/env python3
import rospy
from geometry_msgs.msg import Twist
class CmdVelPubNode:
def __init__(self):
# Publisher requires 3 paramters
# 1. topic name
# 2. topic msg type
# 3. topic queue size
self.cmd_vel_pub_ = rospy.Publisher("cmd_vel", Twist, queue_size=10)
self.timer_ = rospy.Timer(rospy.Duration(1.0/10.0), self.pub_msg)
self.twist_ = Twist()
def pub_msg(self, event=None):
# geometry_msgs.Twist
# ref: http://docs.ros.org/en/melodic/api/geometry_msgs/html/msg/Twist.html
self.twist_.linear.x = 0.5
self.twist_.angular.z = 1.0
self.cmd_vel_pub_.publish(self.twist_)
def cmd_vel_node():
rospy.init_node('cmd_vel_node', anonymous=True)
cmd_vel_pub_node = CmdVelPubNode()
rospy.spin()
if __name__ == '__main__':
try:
cmd_vel_node()
except rospy.ROSInterruptException:
pass
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
class CmdVelPubNode
{
private:
ros::Publisher cmd_vel_pub_;
ros::Timer timer_;
geometry_msgs::Twist twist_msg_;
public:
CmdVelPubNode(ros::NodeHandle *nh) {
ROS_INFO("Publisher and Subscriber initialized");
timer_ = nh->createTimer(ros::Duration(0.1), &CmdVelPubNode::timerCallback, this);
cmd_vel_pub_ = nh->advertise<geometry_msgs::Twist>("cmd_vel", 10);
}
void timerCallback(const ros::TimerEvent& event){
twist_msg_.linear.x = 0.5;
twist_msg_.angular.z = 0.5;
cmd_vel_pub_.publish(twist_msg_);
}
};
int main(int argv, char** argc) {
ros::init(argv, argc, "cmd_vel_node");
// ros::NodeHandle nh("my_namespace");
ros::NodeHandle nh;
CmdVelPubNode cmd_pub_node(&nh);
ros::spin();
return 0;
}
#!/usr/bin/env python3
import rospy
from sensor_msgs.msg import LaserScan
class LaserSubNode:
def __init__(self):
# Publisher requires 3 paramters
# 1. topic name
# 2. topic msg type
# 3. sub callback method
self.laser_sub_ = rospy.Subscriber("scan", LaserScan, self.laser_cb)
# first param of callback method is always topic msg
def laser_cb(self, data):
rospy.loginfo( len(data.ranges))
print(f"""
data.ranges[0]: {data.ranges[0]}
data.ranges[90]: {data.ranges[90]}
data.ranges[179]: {data.ranges[179]}
data.ranges[270]: {data.ranges[270]}
data.ranges[360]: {data.ranges[360]}
""")
def laser_sub_node():
rospy.init_node('laser_sub_node', anonymous=True)
laser_sub_node = LaserSubNode()
rospy.spin()
if __name__ == '__main__':
try:
laser_sub_node()
except rospy.ROSInterruptException:
pass
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <sensor_msgs/LaserScan.h>
class LaserSubNode
{
private:
ros::Subscriber laser_sub_;
ros::Timer timer_;
public:
LaserSubNode(ros::NodeHandle *nh) {
ROS_INFO("Publisher and Subscriber initialized");
// TCPROS
// laser_sub_ = nh->subscribe("scan", 10, &LaserSubNode::laserSubCallback, this);
// UDPROS
laser_sub_ = nh->subscribe("scan", 10, &LaserSubNode::laserSubCallback, this,
ros::TransportHints()
.unreliable()
.reliable()
.maxDatagramSize(1000)
.tcpNoDelay()
);
}
void laserSubCallback(const sensor_msgs::LaserScanConstPtr data){
ROS_INFO_STREAM("data.ranges[0]: " << data->ranges[0]);
ROS_INFO_STREAM("data.ranges[90]: " << data->ranges[90]);
ROS_INFO_STREAM("data.ranges[179]: " << data->ranges[179]);
ROS_INFO_STREAM("data.ranges[270]: " << data->ranges[270]);
ROS_INFO_STREAM("data.ranges[360]: " << data->ranges[360]);
// std::cout << std::to_string(data.ranges[0]) << std::endl;
ROS_INFO("Publisher and Subscriber initialized");
}
};
int main(int argv, char** argc) {
ros::init(argv, argc, "laser_sub_node");
// ros::NodeHandle nh("my_namespace");
ros::NodeHandle nh;
LaserSubNode laser_sub_node(&nh);
ros::spin();
return 0;
}
CmdVelPubNode(ros::NodeHandle *nh) {
ROS_INFO("Publisher and Subscriber initialized");
timer_ = nh->createTimer(ros::Duration(0.1), &CmdVelPubNode::timerCallback, this);
cmd_vel_pub_ = nh->advertise<geometry_msgs::Twist>("cmd_vel", 10);
}
void sub_callback(const sensor_msgs::LaserScanConstPtr &data ){
ROS_INFO_STREAM("data.ranges[0]: " << data->ranges[0]);
}
ROS_INFO_STREAM("data.ranges[0]: " << data->ranges[0]);
ROS Topic 통신을 위해 Publisher와 Subscriber간의 negotiation이 이루어지며, 이 시점에서 Subscriber에 의해 TCP/UDPROS 중 어떠한 통신이 사용될 지 결정됩니다. Subscriber 코드를 수정하여 UDPROS를 사용해봅시다.
laser_sub_ = nh->subscribe("scan", 10, &LaserSubNode::laserSubCallback, this,
ros::TransportHints()
.unreliable()
.reliable()
.maxDatagramSize(1000)
.tcpNoDelay()
);
위 코드에서, unreliable은 UDPROS를, reliable은 TCPROS를 뜻합니다. UDPROS를 사용하는 경우, maxDatagramSize를 지정할 수 있으며, TCPROS를 사용하는 경우 tcpNoDelay를 사용 가능합니다. 위 코드는 UDPROS 통신을 먼저 시도한 뒤, 응답이 없다면 TCPROS를 사용하게 됩니다.
이 같은 설정에 대한 상세 내용은 링크를 확인합시다. ⇒ 참고링크 : ros::TransportHints Class Reference
catkin build cpp_topic_pkg
source devel/setup.bash
rosrun cpp_topic_pkg laser_scan_sub
$ rosnode info /laser_sub_node
--------------------------------------------------------------------------------
...
* topic: /scan
* to: /pointcloud_to_laserscan (http://192.168.55.236:39875/)
* direction: inbound
* transport: UDPROS
# Terminal 1
roslaunch smb_gazebo smb_gazebo.launch
# Example 1
rosrun cpp_service_pkg emergency_stop
# Example 2
rosrun cpp_service_pkg spawn_model_client
#! /usr/bin/env python3
import rospy
from roslaunch.pmon import start_process_monitor
from geometry_msgs.msg import Twist
from std_srvs.srv import SetBool, SetBoolResponse
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)
self.pm_ = start_process_monitor()
self.twist_msg_ = Twist()
self.response_ = SetBoolResponse()
rospy.loginfo("E Stop Server Started")
self.twist_pub()
rospy.sleep(0.1)
def twist_pub(self):
self.twist_msg_.linear.x = 0.5
self.twist_msg_.angular.z = 1.0
self.cmd_vel_pub_.publish(self.twist_msg_)
def stop_cb(self, request):
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"
return self.response_
def main():
rospy.init_node("emergency_stop_node")
e_stop_node = EmergencyStopNode()
rospy.sleep(1.0)
e_stop_node.twist_pub()
rospy.spin()
if __name__ == '__main__':
try:
main()
except rospy.ROSInterruptException:
pass
#include <ros/ros.h>
#include <std_srvs/SetBool.h>
#include <geometry_msgs/Twist.h>
using SetBool = std_srvs::SetBool;
class EmergencyStopNode {
private:
ros::ServiceServer service_;
ros::Publisher cmd_vel_pub_;
geometry_msgs::Twist twist_msg_;
public:
EmergencyStopNode(ros::NodeHandle *nh){
service_ = nh->advertiseService("emergency_stop", &EmergencyStopNode::eStopCallback, this);
cmd_vel_pub_ = nh->advertise<geometry_msgs::Twist>("cmd_vel", 10);
ROS_INFO_STREAM("EmergencyStopNode Started");
}
void moveRobot(){
twist_msg_.linear.x = 0.5;
twist_msg_.angular.z = 1.0;
cmd_vel_pub_.publish(twist_msg_);
}
bool eStopCallback(SetBool::Request &req, SetBool::Response &res){
if(req.data == true){
twist_msg_.linear.x = 0.0;
twist_msg_.angular.z = 0.0;
cmd_vel_pub_.publish(twist_msg_);
res.success = true;
res.message = "Successfully Stopped";
return true;
} else {
res.success = false;
res.message = "Stop Failed";
return false;
}
}
};
int main(int argc, char **argv)
{
ros::init(argc, argv, "emergency_stop_node");
ros::NodeHandle nh;
EmergencyStopNode e_stop_service(&nh);
auto start_time = ros::Time::now();
auto cur_time = ros::Time::now();
while( (cur_time - start_time) < ros::Duration(3.0)){
e_stop_service.moveRobot();
cur_time = ros::Time::now();
}
ros::spin();
ros::shutdown();
return 0;
}
#! /usr/bin/env python3
"""
referenced from programcreek
url : https://www.programcreek.com/python/example/93572/rospkg.RosPack
"""
import math
import rospy
import rospkg
from geometry_msgs.msg import Pose
from gazebo_msgs.srv import SpawnModel
def spawn_helix():
rospy.init_node("gazebo_spawn_model")
# model_name
model_name = "box"
# 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", "")
# robot_namespace
robot_namespace = ""
# 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
# reference_frame
reference_frame = "world"
theta = 0.0
spawn_model_prox = rospy.ServiceProxy("gazebo/spawn_urdf_model", SpawnModel)
for i in range(100):
# service call
initial_pose.position.x = theta * math.cos(theta)
initial_pose.position.y = theta * math.sin(theta)
theta += 0.2
entity_name = model_name + str(i)
result = spawn_model_prox(
entity_name, model_xml, robot_namespace, initial_pose, reference_frame
)
""" result fromat
bool success
string status_message
"""
rospy.loginfo(result)
if __name__ == '__main__':
try:
spawn_helix()
except rospy.ROSInterruptException:
pass
#include <fstream> // ros.h doesn't contain this lib
#include <ros/ros.h>
#include <ros/package.h>
#include <geometry_msgs/Pose.h>
#include <gazebo_msgs/SpawnModel.h>
void addXml(gazebo_msgs::SpawnModel& model_in, const std::string& file_path ){
std::ifstream file(file_path);
std::string line;
while (!file.eof()){
std::getline(file, line);
model_in.request.model_xml += line;
}
file.close();
}
class SpawnModelClient {
private:
ros::ServiceClient spawn_model_prox;
int model_num_ = 0;
double theta_ = 0.0;
public:
SpawnModelClient(ros::NodeHandle *nh){
spawn_model_prox = nh->serviceClient<gazebo_msgs::SpawnModel>("gazebo/spawn_urdf_model");
for(auto i = 0; i < 100; i++){
this->serviceCall();
}
}
void serviceCall(){
gazebo_msgs::SpawnModel model;
// add roslib in find_package()
auto file_path = ros::package::getPath("cpp_service_pkg") + "/urdf/box.urdf";
addXml(model, file_path);
model.request.model_name = "box" + std::to_string(model_num_++);
model.request.reference_frame = "world";
model.request.initial_pose = getPose();
// ServiceClient.call() => return bool type
if (spawn_model_prox.call(model)){
auto response = model.response;
ROS_INFO("%s", response.status_message.c_str()); // Print the result given by the service called
}
else {
ROS_ERROR("Failed to call service /trajectory_by_name");
ros::shutdown();
}
model_num_++;
}
geometry_msgs::Pose getPose(){
geometry_msgs::Pose initial_pose;
initial_pose.position.x = theta_ * cos(theta_);
initial_pose.position.y = theta_ * sin(theta_);
theta_ += 0.2;
initial_pose.position.z = 0.2;
initial_pose.orientation.z = -0.707;
initial_pose.orientation.w = 0.707;
return initial_pose;
}
};
int main(int argc, char** argv){
ros::init(argc, argv, "gazebo_spawn_model");
ros::NodeHandle nh;
SpawnModelClient spawn_model_client(&nh);
ros::shutdown();
return 0;
}
rospy | roscpp | |
---|---|---|
Server | rospy.Service(service_name, srv_type, callback) | advertiseService(service_name, srv_type, callback) |
Client | rospy.ServiceProxy(server_name, srv_type) | serviceClient<srv_type>(service_name) |
Time | rospy.Time.now() | ros::Time::now() |
rospack | rospkg.RosPack().get_path() | ros::package::getPath() |
실제 로봇 프로그래밍시에는 python보다 c++가 우세하게 사용됩니다. 하지만 우리는 이미 rospy를 통해 통신 메커니즘에 대해 이해하였기 때문에 간단히 짚고 넘어갔으며, 관련된 추가 개발은 강의 노트를 통해 지속 업데이트할 예정입니다.
참고자료