토픽결과물: 로봇이 간단히 주행할 수 있는 클래스가 만들어 집니다.
Gazebo 주행 시뮬레이션을 하기 위한 class를 생성합니다. 편의상 cpp 파일 하나에 코딩합니다. 단, class와 관련성이 떨어지는 함수는 별도의 utils.hpp 파일에 코딩합니다.
설명 동영상
utils.hpp
몇 가지 편의 함수들을 정의하여 사용합니다.
#include <math.h>
#include <cstdlib>
#include <sys/time.h>
#define DEG2RAD(x) (M_PI/180.0)*x
/*
* measure time in milliseconds
*/
double mstime()
{
timeval tv;
gettimeofday(&tv,NULL);
long long ts = tv.tv_sec;
ts *= 1000000;
ts += tv.tv_usec;
return (double)ts*.001;
}
/*
* set a seed for random number generation
* @param seed the seed
*/
void setSeed( const unsigned long int _seed )
{
srand48( _seed );
}
/*
* sample from the given gaussian distribution (zero mean, std)
* @param std standard deviation
* @return resulting sample
*/
double sampleGaussian( const double _std )
{
double x1, x2, w;
double r;
do
{
do
{
r= drand48();
}while( r == 0.0f );
x1= 2.0f * r - 1.0f;
do
{
r= drand48();
}while( r == 0.0f );
x2= 2.0f * drand48() - 1.0f;
w= x1*x1 + x2*x2;
}while( w > 1.0f || w==0.0f );
return( _std * x2 * sqrt(-2.0*log(w)/w) );
}
mobile_robot_sim.cpp 파일을 아래와 같이 작성합니다.
Include files
ROS 의존 패키지들을 인클루드 합니다.
// util functions
#include "utils.hpp"
//////////// ros related
// ros
#include <ros/ros.h>
// ros package to access package directory
#include <ros/package.h>
// laser scan message
#include <sensor_msgs/LaserScan.h>
// tf2
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>
#include <tf2_ros/transform_broadcaster.h>
// tf2 matrix
#include <tf2/LinearMath/Matrix3x3.h>
#include <tf2/LinearMath/Quaternion.h>
#include <tf2/LinearMath/Transform.h>
//
#include <geometry_msgs/TransformStamped.h>
// move base to move the kobuki
#include <move_base_msgs/MoveBaseAction.h>
// velocity message
#include <geometry_msgs/Twist.h>
// 2d pose
#include <geometry_msgs/Pose2D.h>
// ground truth pose message
#include <nav_msgs/Odometry.h>
structs and enums
편의를 위한 구조체와 enum 들을 선언합니다. 이 중 몇가지는 장애물 회피 및 Wall Following에 사용될 것입니다.
//////// struct and enum
// range vector of a laser scan
typedef std::vector<double> ScanRanges;
// obstacle sections: four sections for convenience
enum
{
RIGHT=0,
FRIGHT=1, // front right
FLEFT=2, // front left
LEFT=3,
OBS_SECTION_SIZE,
};
//// obstacle states
// left wall (L), right wall (R), front left (FL), front right (FR), front (F)
// order: left -> front -> right
enum
{
OBS_CLEAR= 0,
// --------- side sect line -----------
OBS_L= 1, // left wall only, 1000
OBS_R= 2, // right wall only, 0001
OBS_L_R= 3, // left wall and right wall, 1001
// -------- front sect line -----------
OBS_FL= 4, // front left only, 0100
OBS_FR= 5, // front right only, 0010
OBS_F= 6, // front, 0110
OBS_L_FL= 7, // left wall and front left, 1100
OBS_FR_R= 8, // front right and right wall, 0011
OBS_L_F= 9, // right wall clear, 1110, 1010
OBS_F_R= 10, // left wall clear, 0111, 0101
OBS_ALL= 11, // left wall, right wall, and either front left or front right, 1111, 1101, 1011
OBS_STATE_SIZE,
// --------------------------------------
OBS_FRONT_SECT= 3,
};
// obstacle state strings
const char *OBS_STRINGS[]=
{
"OBS_CLEAR", "OBS_L", "OBS_R", "OBS_L_R", "OBS_FL", "OBS_FR", "OBS_F", "OBS_L_FL", "OBS_FR_R",
"OBS_L_F", "OBS_F_R", "OBS_ALL", "OBS_STATE_SIZE"
};
// driving states
enum
{
DRIVE= 0, // normal drive
DRIVE_APPROACH= 1, // approach drive
DRIVE_AVOID= 2, // avoid drive
DRIVE_WF= 3, // wall following drive
};
// wall following directions
enum
{
WF_LEFT= -1,
WF_RIGHT= 1,
};
MRS Paramters
Mobile Robot Sim 패키지에서 사용하는 파라미터는 별도의 struct에 모아서 사용합니다. 이렇게 함으로써 코드상에서 어떤 변수가 파라미터로 launch 파일에서 수정가능한지 알 수 있습니다.
// Mobile Robot Simulation Parameters
struct MRSParameters
{
/// odometry frame
std::string odom_frame;
/// base frame: currently base_footprint
std::string base_frame;
/// scan frame: hokuyo sensor
std::string scan_frame;
/// max sensing distance
double max_range;
/// min sensing distance
double min_range;
/// ratio to add noise to the range data
double range_noise_ratio;
/// regulate the scan processing frequency
int throttle_scans;
/// angles of rays indicating the sections in radians
double section_angles[OBS_SECTION_SIZE];
/// obstacle distance threshold including the approach threshold
double obs_dist_threshs[OBS_SECTION_SIZE+1];
/// obstacle count threshold
int obs_count_threshs[OBS_SECTION_SIZE];
/// offset indices from the side ray
int side_section_interval;
/// approaching linear velocity
double obs_approach_linvel;
/// avoiding angular velocity
double obs_avoid_angvel;
/// normal driving linear velocity
double drive_linvel;
/// desired wall following distance
double desired_wf_dist;
/// wall following control p gain
double wf_slow_pgain;
/// wall following control d gain
double wf_slow_dgain;
};
MRS class
Mobile Robot Sim 패키지의 클래스 본체입니다. 여기서도 장애물 회피 및 Wall Following 관련 멤버들이 먼저 소개되고 있습니다. 본 토픽에서는 ground truth pose 메세지 수신, hokuyo scan 수신, odometry pose 획득 등만 설명됩니다. 이 때, tf가 아니라 tf2가 사용됨을 확인할 수 있습니다.
/*
* Mobile Robot Simulation (MRS) class
*/
class MRS
{
public:
/// constructor
MRS();
/// destructor
~MRS();
/// initialize
void init( const geometry_msgs::Pose2D& _init_pose );
/// run by setting topics and publishers and subscribers
void run();
/// initialize scan data
bool initScan( const sensor_msgs::LaserScanConstPtr& _scan );
/// scan message callback
void scanCB( const sensor_msgs::LaserScanConstPtr& _scan );
/// ground truth pose message callback
void groundTruthCB( const nav_msgs::Odometry::ConstPtr& _odom_msg );
/// get odometry pose
bool getOdomPose( const ros::Time& _stamp, geometry_msgs::Pose2D *_odom_pose );
////// move base
/// publish robot velocities
void publishVelocity( const double _v, const double _w );
/// scan obstacle sections
void scanObsSections( const ScanRanges &_ranges );
/// determine obstacle state based on the obstacle scan result
bool determineObsState( const ScanRanges& _ranges );
/// decide wall following direction
/// determine wall following direction based on the obstacle state
int determineWFDirection();
/// do wall following
void doWallFollowing( const int _wf_dir, const double _target_dist );
private:
//////// ros
/// ros node handle
ros::NodeHandle ros_nh_;
//////// message topics
ros::Subscriber scan_sub_;
/////// tf2
/// tf2 buffer
boost::shared_ptr<tf2_ros::Buffer> tf2_buffer_;
/// tf2 listener
boost::shared_ptr<tf2_ros::TransformListener> tf2_listener_;
/// tf2 broadcaster
tf2_ros::TransformBroadcaster *tf2_broadcaster_;
/////// ground truth
/// initial pose
geometry_msgs::Pose2D init_pose_;
/// ground truth pose topic subscriber
ros::Subscriber gt_pose_sub_;
/// ground truth pose
geometry_msgs::Pose2D gt_pose_;
/// transformed ground truth pose publisher
ros::Publisher gt_pose_pub_;
/////// move base
ros::Publisher vel_pub_;
/////// scan
/// min angle of scan in radians
double scan_angle_min_;
/// max angle of scan in radians
double scan_angle_max_;
/// angle increment of scan in radians
double scan_angle_inc_;
/// count the scans
int scan_count_;
//////// parameters
MRSParameters params_;
/// the first scan receiving flag
bool got_first_scan_;
//////// obstacle [obs]
/// obstacle distance
double obs_dist_[OBS_SECTION_SIZE];
/// obstacle count
int obs_count_[OBS_SECTION_SIZE];
/// bit0: right, bit1: front_right, bit2: front_left, bit3: left
std::bitset<4> obs_bits_;
/// obstacle state
int obs_state_;
/// driving state
int driving_state_;
/// old distance error
double old_dist_error_;
}; // end of MRS class
클래스 생성자, 소멸자
/*
* Constructor
*/
MRS::MRS()
: got_first_scan_(false)
{
// reset the obstacle bit set
obs_bits_.reset();
// initial driving state is normal drive
driving_state_= DRIVE;
}
/*
* Destructor
*/
MRS::~MRS()
{
}
파라미터 초기화 멤버함수
launch 파일에서 파라미터를 읽어서 초기화 합니다.
/*
* Initialize parameters
*/
void MRS::init( const geometry_msgs::Pose2D& _init_pose )
{
// set the initial pose
init_pose_= _init_pose;
// get parameters. set default values unless the values exist
ros::NodeHandle ros_nh("~");
ros_nh.param<std::string>( "odom_frame", params_.odom_frame, "odom" );
ros_nh.param<std::string>( "base_frame", params_.base_frame, "base_link" );
ros_nh.param<std::string>( "scan_frame", params_.scan_frame, "hokuyo_frame" );
ros_nh.param<double>( "max_range", params_.max_range, 3.0 );
ros_nh.param<double>( "min_range", params_.min_range, 0.0 );
ros_nh.param<double>( "range_noise_ratio", params_.range_noise_ratio, 0.0 );
ros_nh.param<int>("throttle_scans", params_.throttle_scans, 1 );
ros_nh.param<double>( "right_section_angle", params_.section_angles[RIGHT], -80.0 );
ros_nh.param<double>( "front_section_min_angle", params_.section_angles[FLEFT], -30.0 );
ros_nh.param<double>( "front_section_max_angle", params_.section_angles[FRIGHT], 30.0 );
ros_nh.param<double>( "left_section_angle", params_.section_angles[LEFT], 80.0 );
ros_nh.param<int>( "side_section_interval", params_.side_section_interval, 4 );
ros_nh.param<double>( "obs_dist_thresh_r", params_.obs_dist_threshs[RIGHT], 0.3 );
ros_nh.param<double>( "obs_dist_thresh_fr", params_.obs_dist_threshs[FRIGHT], 0.3 );
ros_nh.param<double>( "obs_dist_thresh_fl", params_.obs_dist_threshs[FLEFT], 0.3 );
ros_nh.param<double>( "obs_dist_thresh_l", params_.obs_dist_threshs[LEFT], 0.3 );
ros_nh.param<double>( "obs_dist_thresh_emerg", params_.obs_dist_threshs[OBS_SECTION_SIZE], 0.1 );
ros_nh.param<int>( "obs_count_thresh_r", params_.obs_count_threshs[RIGHT], 1 );
ros_nh.param<int>( "obs_count_thresh_fr", params_.obs_count_threshs[FRIGHT], 1 );
ros_nh.param<int>( "obs_count_thresh_fl", params_.obs_count_threshs[FLEFT], 1 );
ros_nh.param<int>( "obs_count_thresh_l", params_.obs_count_threshs[LEFT], 1 );
ros_nh.param<int>( "obs_count_thresh_emerg", params_.obs_count_threshs[OBS_SECTION_SIZE], 1 );
ros_nh.param<double>( "drive_linvel", params_.drive_linvel, 0.2 );
ros_nh.param<double>( "obs_approach_linvel", params_.obs_approach_linvel, 0.1 );
ros_nh.param<double>( "obs_avoid_angvel", params_.obs_avoid_angvel, 0.3 );
ros_nh.param<double>( "desired_wf_dist", params_.desired_wf_dist, 0.2 );
ros_nh.param<double>( "wf_slow_pgain", params_.wf_slow_pgain, 1.0 );
ros_nh.param<double>( "wf_slow_pgain", params_.wf_slow_dgain, 1.0 );
}
ROS Subsriber, Publisher 설정
publisher와 subscriber를 설정합니다. 또한 tf2를 초기화 하여 tf2로 부터 odometry pose를 추출할 수 있게 합니다.
/*
* Run and set publishers and subscribers.
*/
void MRS::run()
{
int queue_size= 50;
// subscribe the scan message
scan_sub_= ros_nh_.subscribe( "scan", queue_size, &MRS::scanCB, this );
// subscribe the ground truth pose message
gt_pose_sub_= ros_nh_.subscribe( "ground_truth_pose", queue_size, &MRS::groundTruthCB, this );
// publish transformed ground truth pose. @note "gt_pose" topic name is not working, why?
gt_pose_pub_= ros_nh_.advertise<geometry_msgs::PoseStamped>( "ground_truth_pose2", queue_size, true );
// velocity publisher
vel_pub_= ros_nh_.advertise<geometry_msgs::Twist>( "cmd_vel", queue_size, true );
// initialize tf2
tf2_buffer_.reset(new tf2_ros::Buffer());
tf2_listener_.reset(new tf2_ros::TransformListener( *tf2_buffer_ ));
tf2_broadcaster_= new tf2_ros::TransformBroadcaster();
}
로봇 속도 publish
로봇의 선속도, 각속도를 메시지로 퍼블리쉬 합니다. 속도 메시지는 geometry_msgs::Twist를 사용합니다.
/*
* Publish velocity message.
* @param v linear velocity in m/s
* @param w angular velocity in rad/s
*/
void MRS::publishVelocity( const double _v, const double _w )
{
geometry_msgs::Twist vel;
vel.linear.x= _v;
vel.angular.z = _w;
vel_pub_.publish( vel );
}
Scan 데이터 초기화
수신한 hokuyo 스캔 데이터로 스캔 관련 변수를 초기화 합니다.
/*
* Initialize the scan data with the fist scan data
* @param scan the first scan data to initialize the map
*/
bool MRS::initScan( const sensor_msgs::LaserScanConstPtr& _scan )
{
#if __DEBUG__
ROS_WARN( "Initializing scan..." );
#endif
// sensor parameters
scan_angle_min_= _scan->angle_min;
scan_angle_max_= _scan->angle_max;
scan_angle_inc_= _scan->angle_increment;
ROS_INFO( "Scan angles top down in scan frame: min: %.3f, max: %.3f, inc: %.3f",
scan_angle_min_, scan_angle_max_, scan_angle_inc_ );
#if __DEBUG__
ROS_INFO( "Scan initialization completed" );
#endif
// return success
return true;
}
Ground Truth Pose 콜백함수
ground_truth_pose topic을 수신합니다. 여기서 메시지로부터 수신한 ground truth pose는 gazebo 좌표계로 표시되므로, 이것을 로봇의 초기 위치 기준 좌표계로 변경시켜야 합니다. 즉, 로봇 초기 위치의 ground truth pose는 (0, 0, 0) 되도록 변환해야 합니다.
/*
* Callback for ground truth pose.
* Apply the initial pose to the ground truth pose.
*
*/
void MRS::groundTruthCB( const nav_msgs::Odometry::ConstPtr& _odom_msg )
{
//// compute the frame transform. [G] to [R0]. [G] is ground truth frame. [R0] is the initial robot frame.
// The transform is R0_T_G.
static double cos_theta= cos(-init_pose_.theta);
static double sin_theta= sin(-init_pose_.theta);
// need to transform the initial translation
static double init_x= cos_theta*(-init_pose_.x) - sin_theta*(-init_pose_.y);
static double init_y= sin_theta*(-init_pose_.x) + cos_theta*(-init_pose_.y);
// transform the ground truth to the R
double x= cos_theta*_odom_msg->pose.pose.position.x - sin_theta*_odom_msg->pose.pose.position.y;
double y= sin_theta*_odom_msg->pose.pose.position.x + cos_theta*_odom_msg->pose.pose.position.y;
gt_pose_.x= x + init_x;
gt_pose_.y= y + init_y;
// build tf quaternion
tf2::Quaternion q( _odom_msg->pose.pose.orientation.x, _odom_msg->pose.pose.orientation.y, _odom_msg->pose.pose.orientation.z, _odom_msg->pose.pose.orientation.w );
// build tf matrix from the quaternion
tf2::Matrix3x3 m(q);
// get angles
double roll, pitch, yaw;
m.getRPY(roll, pitch, yaw);
// ground truth of yaw
gt_pose_.theta= yaw - init_pose_.theta;
// build ground pose message
geometry_msgs::PoseStamped gt_pose_msg;
gt_pose_msg.header.frame_id= params_.odom_frame;
gt_pose_msg.header.stamp= _odom_msg->header.stamp;
gt_pose_msg.pose.position.x= gt_pose_.x;
gt_pose_msg.pose.position.y= gt_pose_.y;
gt_pose_msg.pose.position.z= 0.0;
tf2::Quaternion q2;
q2.setRPY( 0., 0., gt_pose_.theta );
gt_pose_msg.pose.orientation.x= q2.x();
gt_pose_msg.pose.orientation.y= q2.y();
gt_pose_msg.pose.orientation.z= q2.z();
gt_pose_msg.pose.orientation.w= q2.w();
// publish the ground truth pose message
gt_pose_pub_.publish( gt_pose_msg );
#if __DEBUG__
ROS_WARN( "ground truth pose: %.2f, %.2f, %.2f", gt_pose_.x, gt_pose_.y, gt_pose_.theta );
#endif
}
프로그램을 실행했을 때 아래와 같이 초기 위치값이 있을 때, ground truth pose 메시지 출력은 (0, 0, 0)이 되어야 합니다.
[ WARN] [1455280186.374946772]: initial pose x, y, th: -2.00, -2.70, 0.00
[ INFO] [1455280195.149926468, 934.408000000]: odom_pose: 0.00, 0.00, -0.00
[ WARN] [1455280195.155493656, 934.410000000]: ground truth pose: 0.00, 0.00, -0.00
Odometry Pose 추출
tf2에서 base_frame to odom_frame 값을 추출합니다. 즉, odom frame기준으로 base_frame (여기서는 base_footprint)의 pose를 추출하게 됩니다.
/*
* Get odom pose from the scan pose
* @param stamp ros time of the scan
* @param odom_pose resulting pose
*/
bool MRS::getOdomPose( const ros::Time& _stamp, geometry_msgs::Pose2D *_odom_pose )
{
geometry_msgs::TransformStamped odom_msg;
try
{
odom_msg= tf2_buffer_->lookupTransform( params_.odom_frame, params_.base_frame, _stamp );
}
catch( tf2::TransformException &ex )
{
ROS_WARN( "Failed to compute odom pose, skipping scan (%s)", ex.what() );
return false;
}
// get the odometry pose: actually it is the pose of scan frame based on the odom_frame_
_odom_pose->x= odom_msg.transform.translation.x;
_odom_pose->y= odom_msg.transform.translation.y;
//// get the yaw
// build tf2 quaternion
tf2::Quaternion q( odom_msg.transform.rotation.x, odom_msg.transform.rotation.y, odom_msg.transform.rotation.z, odom_msg.transform.rotation.w );
// build tf2 matrix
tf2::Matrix3x3 m(q);
// get angles
double roll, pitch, yaw;
m.getRPY(roll, pitch, yaw);
// set the yaw
_odom_pose->theta= yaw;
#if __DEBUG__
ROS_INFO( "odom_pose: %.2f, %.2f, %.2f", _odom_pose->x, _odom_pose->y, _odom_pose->theta );
#endif
// return true since the odom pose has been obtained
return true;
}
Scan 콜백 함수
hokuyo 센서가 송신하는 scan 메시지를 수신합니다. 추가적으로 수신한 센서값에 noise를 추가할 수 있게 구현되었습니다.
/*
* scan callback
*/
void MRS::scanCB( const sensor_msgs::LaserScanConstPtr& _scan )
{
// increase the scan count
scan_count_++;
// handle only if the throttle condition is met
if( scan_count_%params_.throttle_scans != 0 )
return;
// initialize scan data
if( !got_first_scan_ )
{
// initialize scan
initScan( _scan );
// now we got the first scan
got_first_scan_= true;
return;
}
// get odometry pose
geometry_msgs::Pose2D odom_pose;
getOdomPose( _scan->header.stamp, &odom_pose );
// filter range readings and detect obstacles
ScanRanges ranges;
for( int i=0; i<_scan->ranges.size(); i++ )
{
// filter out (set max range) shorter or farther ranges
if( _scan->ranges[i] < _scan->range_min || _scan->ranges[i] > params_.max_range || std::isnan(_scan->ranges[i]) )
ranges.push_back( params_.max_range );
else
{
//// add noise
// noise std
double noise_std= _scan->ranges[i]*params_.range_noise_ratio;
double noisy_scan= _scan->ranges[i] + sampleGaussian( noise_std );
ranges.push_back( noisy_scan );
}
}
} // end of scanCB
main 함수
main 함수에서는 먼저, 로봇 초기위치에 대한 argument를 parsing합니다. 이 arguments는 mobile robot sim 노드의 실행 인자로 입력받게 됩니다. 이 부분은 아래 launch 파일 부분에서 확인할 수 있습니다. 그 다음 위에서 생성한 클래스의 인스턴스를 생성하여 init(), run() 함수를 호출하고, 제어권을 ROS에게 넘깁니다.
/*
* main function
*/
int main( int argc, char **argv )
{
// ros init
ros::init( argc, argv, "mobile_robot_sim_node" );
// parse arguments
ROS_ASSERT( argc > 6 );
geometry_msgs::Pose2D init_pose;
if( std::string(argv[1]) == "-init_x" )
init_pose.x= atof( argv[2] );
if( std::string(argv[3]) == "-init_y" )
init_pose.y= atof( argv[4] );
if( std::string(argv[5]) == "-init_theta" )
init_pose.theta= atof( argv[6] );
ROS_WARN( "initial pose x, y, th: %.2f, %.2f, %.2f", init_pose.x, init_pose.y, init_pose.theta );
// create a MRS instance
MRS mrs;
// initialize mrs
mrs.init( init_pose );
// ros run
mrs.run();
// spin ROS
try
{
// ros takes all
ros::spin();
}
catch( std::runtime_error& e )
{
ROS_ERROR( "ros spin failed: %s", e.what() );
return -1;
}
return 0;
}
launch 파일
최상위 launch (turtlebot_hokuyo.launch) 파일으로서 로봇 초기위치 설정, gazebo 노드 실행, mobile robot sim 노드 실행 관련 설정이 포함되어 있습니다. 여기서 mobile robot sim 노드 실행을 위해 별도의 launch 파일을 include하고 있는데, 그 부분은 밑에서 설명합니다.
<!--
gazebo mobile robot simulation: kobuki, asus, hokuyo 2d laser
-->
<launch>
<!-- scan topic name -->
<arg name="scan_topic" default="/hokuyo_scan" />
<!-- gazebo initial pose -->
<arg name="init_x" default="-2.0" /> <!-- -2.0 -->
<arg name="init_y" default="-2.7" /> <!-- -2.7 -->
<arg name="init_theta" default="0.0" /> <!-- 0.0 -->
<!-- gazebo setting for turtlebot -->
<arg name="base" default="kobuki"/> <!-- create, roomba -->
<arg name="battery" default="$(optenv TURTLEBOT_BATTERY /proc/acpi/battery/BAT0)"/> <!-- /proc/acpi/battery/BAT0 -->
<arg name="stacks" default="hexagons"/> <!-- circles, hexagons -->
<arg name="3d_sensor" value="asus"/> <!-- asus_xtion_pro -->
<arg name="2d_laser" value="hokuyo"/> <!-- hokuyo 2d laser -->
<arg name="world_file" value="$(find mobile_robot_sim)/worlds/sim_simple.world"/>
<!-- include gazebo world -->
<include file="$(find gazebo_ros)/launch/empty_world.launch">
<arg name="use_sim_time" value="true"/>
<arg name="debug" value="false"/>
<arg name="world_name" value="$(arg world_file)"/>
</include>
<include file="$(find mobile_robot_sim)/launch/includes/$(arg base)_hokuyo.launch.xml">
<arg name="base" value="$(arg base)"/>
<arg name="stacks" value="$(arg stacks)"/>
<arg name="3d_sensor" value="$(arg 3d_sensor)"/>
<arg name="2d_laser" value="$(arg 2d_laser)"/>
<arg name="x" value="$(arg init_x)"/>
<arg name="y" value="$(arg init_y)"/>
<arg name="theta" value="$(arg init_theta)"/>
</include>
<node pkg="robot_state_publisher" type="robot_state_publisher" name="robot_state_publisher">
<param name="publish_frequency" type="double" value="30.0" />
</node>
<!-- include mobile robot sim launch file -->
<include file="$(find mobile_robot_sim)/launch/includes/mrs.launch.xml">
<arg name="scan_topic" value="$(arg scan_topic)" />
<arg name="init_x" value="$(arg init_x)"/>
<arg name="init_y" value="$(arg init_y)"/>
<arg name="init_theta" value="$(arg init_theta)"/>
</include>
</launch>
mobile robot sim 노드 관련 파라미터
mobile robot sim 노드 관련 파라미터를 별도의 파일 (mrs.launch.xml)로 만들어서 관리합니다.