目录

前言

一、ROS程序总体开发流程

1.创建工作空间及功能包

2.CmakeLists.txt

3.package.xml

二、ROS官方数据结构

三、话题通讯

1.话题发布&订阅

2.自定义话题发布&订阅

四、服务通讯

1.客户端&服务端

2.自定义客户端&服务端

五、参数通讯

1.参数

2.动态参数

六、配置launch文件

1、launch根标签设置

2、launch的node标签

3、launch文件的include标签(常用)

4、launch文件的remap标签(实用)

5、launch文件的param标签

 6、launch文件的rosparam

①导入参数

②导出参数

 ③删除参数(用的少) 

7、launch文件的group标签

8、launch文件的arg标签

9、launch文件使用

七、tf树

1、典型的TF坐标系

2、乌龟例程中的TF

2.1创建TF广播器

2.2创建TF监听器

2.3实现乌龟跟随运动

3.运行

八、动态调参

1、创建功能包(名为dynamic_reconfigure_test)

2、编写配置文件

3、配置CMakeList文件以及package.xml文件

 4、编译

5、创建服务器节点

6、运行

参考文献

总结

前言

    本篇讲述ROS进阶级教程,介绍ROS程序的开发过程,举例说明如何使用话题通讯、服务通讯和参数通讯三种通讯方式,此外还包括常用的launch文件编写方法、tf树使用、动态调参。


提示:本文基于ROS Melodic,编程语言C++。

一、ROS程序总体开发流程

1.创建工作空间及功能包

创建工作空间

mkdir catkin_ws  创建工作空间

cd catkin_ws/   进入工作空间

mkdir src  在工作空间下创建src文件

catkin_init_workspace   初始化当前文件夹,编成一个ROS  Workspace

编译工作空间

cd ..        回到工作空间catkin_ws下

catkin_make        编译工作空间catkin_ws

创建功能包

cd src/        进入src文件夹下

创建一个名为test_pkg的功能包,功能包test_pkg下的src文件夹用来放置代码文件,include文件夹放一些.h文件,CMakeLists.txt和package.xml是每个功能包必须存在的两个文件,这样才标志这个文件夹叫功能包,而不是一个普通的文件夹。

#语法

#catkin_create_pkg<package_name>[depend1][depend2][depend3]

#例

catkin_create_pkg test_pkg roscpp rospy std_msgs

cd ..        回到catkin_ws工作空间

catkin_make        编译工作空间

source devel/setup.bash         设置工作空间的环境变量,系统才能找到工作空间和其对应的功能包

echo $ROS_PACKAGE_PATH         通过环境变量检查所有ROS功能包的路径

需注意以下3点:

        1.功能包test_pkg 下的package.xml包含了功能包的依赖(roscpp rospy std_msgs等)以及开发者的信息。

        2.功能包test_pkg 下的CMakeLists.txt描述功能包的编译规则,使用的是Cmake语法。

        3.同一个工作空间下的功能包不能重名,不同工作空间下功能包可以重名。

2.CmakeLists.txt

   catkin package 必须满足三个条件:

package 中必须包含符合catkin的package.xml文件,该文件描述package的包名、版本号、作者、依赖等信息。
package 中必须包含使用catkin的CMakeLists.txt。CMakeLists.txt定义package的包名、依赖、源文件、目标文件等编译规则。
每个 package 必须有自己的文件夹。即,没有嵌套软件包,也没有多个软件包共享同一目录。
        最简单的package只包含两个文件:CMakeLists.txt、package.xml。

        CMakeLists.txt原本是Cmake编译系统的规则文件,而Catkin编译系统基本沿用了CMake的编译风格,只是针对ROS工程添加了一些宏定义。所以在写法上,catkin 的 CMakeLists.txt 与CMake的基本一致。
        这个文件直接规定了这个package要依赖哪些package,要编译生成哪些目标,如何编译等等 流程。所以 CMakeLists.txt 非常重要,它指定了由源码到目标文件的规则,catkin编译系统在 工作时首先会找到每个package下的 CMakeLists.txt ,然后按照规则来编译构建。

整体结构:

       您的CMakeLists.txt文件必须遵循此格式,否则您的包将无法正确构建。 配置中的顺序计数。

所需CMake版本(cmake_minimum_required)
软件包名称(project())

查找构建所需的其他CMake / Catkin软件包(find_package())
启用Python模块支持(catkin_python_setup())

消息/服务/动作生成器(add_message_files(),add_service_files(),add_action_files())

调用消息/服务/动作生成(generate_messages())
指定package build info export(catkin_package())

要建立的库/可执行文件(add_library()/ add_executable()/ target_link_libraries())

测试建立(catkin_add_gtest())

安装规则(install())

CMakeLists.txt模板

#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.10)
#设置c++版本
set(CMAKE_CXX_STANDARD 11)
#声明一个cmake工程
project("Hello world")
#添加头文件
include_directories("库头文件名")
find_package(库名 REQUIRED)
#find_package命令是cmake提供的寻找某个库的头文件与库文件的指令。如果cmake能够找到它,就会提供头文件和库文件所在的目录的变量,一般为库名_DIRECTORIES和库名_LIBRARIES。
include_directories( ${库名_DIRECTORIES} )
#添加可执行程序
add_executable(main main.cpp)
#把可执行程序链接到库文件上
target_link_libraries(main ${库名_LIBRARIES})

常用库头文件添加和库文件链接(在CmakeLists.txt)

#Eigen库是一个仅由头文件组成的库,所以只需添加头文件即可,而无需链接到库文件
include_directories("/usr/include/eigen3")
 
#OpenCV
#添加OpenCV的头文件
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_DIRECTORIES})
#链接OpenCV的库文件
target_link_libraries(main ${OpenCV_LIBRARIES})
 
#Pangolin
#添加头文件
find_package(Pangolin REQUIRED)
include_directories(${Pangolin_DIRECTORIES})
链接库文件
target_link_libraries(main ${Pangolin_LIBRARIES})

调用头文件(在用户编写的程序)

//Eigen头文件
#include <Eigen/Core>
#include <Eigen/Dense>
#include <Eigen/Geometry>
 
//OpenCV头文件
#include <opencv2/core.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <opencv2/highgui.hpp>
 
//Pangolin头文件
#include <pangolin/pangolin.h>

ROS程序常用CmakeLists.txt关键模块示例

     # Get the information about this package's buildtime dependencies
      find_package(catkin REQUIRED
        COMPONENTS message_generation std_msgs sensor_msgs)
     
      # Declare the message files to be built
      add_message_files(FILES
        MyMessage1.msg
        MyMessage2.msg
      )
     
      # Declare the service files to be built
      add_service_files(FILES
        MyService.srv
      )
     
      # Actually generate the language-specific message and service files
      generate_messages(DEPENDENCIES std_msgs sensor_msgs)
     
      # Declare that this catkin package's runtime dependencies
      catkin_package(
       CATKIN_DEPENDS message_runtime std_msgs sensor_msgs
      )
     
      # define executable using MyMessage1 etc.
      add_executable(message_program src/main.cpp)
      add_dependencies(message_program ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
     
      # define executable not using any messages/services provided by this package
      add_executable(does_not_use_local_messages_program src/main.cpp)
      add_dependencies(does_not_use_local_messages_program ${catkin_EXPORTED_TARGETS})

CmakeLists.txt自定义找包

    //以opencv为例(假定系统中存在多个版本opencv,通过以下指令强制程序使用某版本;或者当cmake编译找不到相关库时也可以使用类似方法指定路径)
    // 明确find_package的寻找路径
    set(OpenCV_DIR /usr/local/opencv2.4.13/share/OpenCV)
    find_package(OpenCV 2.4 REQUIRED)
    // 告诉系统头文件在哪里
    include_directories(SYSTEM ${OpenCV_INCLUDE_DIRS} /usr/local/opencv2.4.13/include)
    // 告诉第三方库文件在哪里
    link_directories(${OpenCV_LIBRARY_DIRS} /usr/local/opencv2.4.13/lib)
    // 将库文件与项目链接上
    target_link_libraries(${项目名} ${OpenCV_LIBRARY_DIRS})

详细见官网教程:catkin/CMakeLists.txt - ROS Wiki

中文译本:ROS初级教程 cmake cmakelist.txt 的编写教程_TYINY的博客-CSDN博客

3.package.xml

 package.xml也是一个catkin的package必备文件,它是这个软件包的描述文件,在较早的 ROS版本(rosbuild编译系统)中,这个文件叫做 manifest.xml ,用于描述pacakge的基本信息。

        pacakge.xml包含了package的名称、版本号、内容描述、维护人员、软件许可、编译构建工具、编译依赖、运行依赖等信息。 实际上rospack find 、 rosdep 等命令之所以能快速定位和分析出package的依赖项信息,就是直接读取了每一个pacakge中的 package.xml 的渠道。

        pacakge.xml遵循xml标签文本的写法,由于版本更迭原因,现在有两种格式并存(format1与 format2),不过区别不大。老版本(format1)的 pacakge.xml 通常包含以下标签:

在新版本(format2)中,包含的标签为:

<package format="2"> <!--在声明pacakge时指定format2,为新版格式-->
用depend整合了build_depend,build_export_depend和run_depend。
run_depend 改为了exec_depend。

<buildtool_depend> - 指定编译此功能包的编译系统工具。
<build_depend> - 指定编译此功能包需要的其他功能包,可以是功能包中的头文件、链接库、其他源文件。
<run_depend> - 指定运行此功能包代码需要的其他功能包。
<test_depend> - 指定单元测试需要的其他功能包

示例

<?xml version="1.0"?>
<package format="2"> <!--在声明pacakge时指定format2, 为新版格式-->
<name>turtlesim</name>
<version>0.8.1</version>
<description>
turtlesim is a tool made for teaching ROS and ROS packages.
</description>
<maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
<license>BSD</license>
<url type="website">http://www.ros.org/wiki/turtlesim</url>
<url type="bugtracker">https://github.com/ros/ros_tutorials/issues</url>
<url type="repository">https://github.com/ros/ros_tutorials</url>
<author>Josh Faust</author>
<!--编译工具为catkin-->
<buildtool_depend>catkin</buildtool_depend>
<!--用depend来整合build_depend和run_depend-->
<depend>geometry_msgs</depend>
<depend>rosconsole</depend>
<depend>roscpp</depend>
<depend>roscpp_serialization</depend>
<depend>roslib</depend>
<depend>rostime</depend>
<depend>std_msgs</depend>
<depend>std_srvs</depend>
<!--build_depend标签未变-->
<build_depend>qtbase5-dev</build_depend>
<build_depend>message_generation</build_depend>
<build_depend>qt5-qmake</build_depend>
<!--run_depend要改为exec_depend-->
<exec_depend>libqt5-core</exec_depend>
<exec_depend>libqt5-gui</exec_depend>
<exec_depend>message_runtime</exec_depend>
</package>

 详细见官网教程:catkin/package.xml - ROS Wiki

二、ROS官方数据结构

ROS程序常用数据结构std_msgs、geometry_msgs、sensor_msgs等。这些预定义的数据结构能够完成大部分通讯任务。如果节点通讯时,需要特殊定制数据格式,将在第三章讲述如何自定义ROS数据结构。

官方API说明:Index of /en/api

三、话题通讯

1.话题发布&订阅

在工作空间创建功能包

cd ~/catkin_ws/src
catkin_create_pkg learning_topic std_msgs rospy roscpp

发布者代码

#include<ros/ros.h>
#include<std_msgs/String.h>
​
const char* node_name = "topic_publisher";
const char* topic_name = "test_topic";
​
int main(int argc,char *argv[])
{
    ros::init(argc,argv,node_name);     //ROS节点初始化
    ros::NodeHandle n;          //(注册)创建节点句柄,管理节点的资源
​
    ros::Publisher topic_publisher = n.advertise<std_mags::String>(topic_name,10);//(创建消息)创建一个Publisher,发布名为test_topic的topic,消息类型为std_mags::String,队列长度10
​
    ros::Rate loop_rate(10);        //(发布消息)设置循环的频率
    ROS_INFO("%s start publishing msg",node_name);
​
    while(ros::ok())            //while循环主要就是封装数据、并发布数据、延时满足频率进入到下次循环
    {
        std_msgs::String test_msgs; //类
        test_msgs.data = "this is msg publisher!";
        topic_publisher.publish(test_msgs); //发布消息
​
        loop_rate.sleep();      // 按照循环频率延时
    }
​
    return 0;
}

订阅者代码

#include<ros/ros.h>
#include<std_msgs/String.h>
​
const char* node_name = "topic_subscriber";
const char* topic_name = "test_topic";
​
void show_msg_callback(const std_msgs::String::ConstPtr& msg)//引用传递,这时存放的是由主调函数放进来的实参变量的地址
//When messages are automatically generated into C++ code, there are several typedefs defined. One of them is ::Ptr, which is typedef-ed to be a boost::shared_ptr<MSG>, and another is ::ConstPtr which is boost::shared_ptr<MSG const>.
//By passing a const pointer into the callback, we avoid doing a copy. While this might not make much difference for std_msgs::String, it can make a huge difference for sensor_msgs::PointCloud2.
{
    ROS_INFO("get msg:[%s] from topic:[%s]",msg->data.c_str(),topic_name);
}
​
int main(int argc,char *argv[])
{
    ros::init(argc,argv,node_name);
    ros::NodeHandle n;
    
    ros::Subscriber topic_subscriber = n.subscribe(topic_name,10,show_msg_callback);
​
    ros::spin();        //循环等待回调函数
    return 0;
}

在CmakeLists.txt中修改

add_executable(topic_publisher src/topic_publisher.cpp)     //描述将哪一个程序文件(src/velocity_publisher. cpp),编译成哪一个可执行文件(velocity_publisher)
target_link_libraries(topic_publisher ${catkin_LIBRARIES})  //帮助将生成的可执行文件与ROS的一些库建立连接(c++的库)
//以下同理
add_executable(topic_subscriber src/topic_subscriber.cpp)
target_link_libraries(topic_subscriber ${catkin_LIBRARIES}) 

 返回~/catkin_ws路径下编译

catkin_make

运行

//打开终端,加载当前工作空间下devel/setup.bash
roscore
//另开一个终端
rosrun learning_topic topic_publisher
//再开一个终端
rosrun learning_topic topic_subscriber

2.自定义话题发布&订阅

①在功能包文件夹下新建一个msg文件夹,在msg文件夹里创建一个Person.msg文件,这里消息文件名最好首字母大写,否则可能出现一些问题。

string name
uint8 sex
uint8 age
 
uint8 unknown = 0
uint8 male = 1
uint8 female = 2

②在package.xml文件的文件末尾添加如下语句,实现对应功能包的编译和运行的相关依赖

  <build_depend>message_generation</build_depend>    //编译依赖
  <exec_depend>message_runtime</exec_depend>        //运行依赖

③在CMakeLists.txt文件中,进行如下修改

第一块:修改find_package部分,确保编译时找到对应文件

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)

第二块:设置msg文件

 add_message_files(
   FILES
   Person.msg
 )
 
 generate_messages(
   DEPENDENCIES
   std_msgs
 )

第三块:catkin依赖部分

如果我们编写的ros程序不打算给别人使用,这块就无所谓。

catkin_package(
...
  CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
...
)

在源文件中使用自定义的消息类型需要包含对应编译生成的头文件

#include "learning_topic/Person.h"

 即可。

person_publisher.cpp代码

/**
 * 该例程将发布/person_info话题,自定义消息类型learning_topic::Person
 */
 
// person_publisher.cpp
 
#include <ros/ros.h>
#include "learning_topic/Person.h"
 
int main(int argc, char **argv)
{
    // ROS节点初始化
    ros::init(argc, argv, "person_publisher");
 
    // 创建节点句柄
    ros::NodeHandle n;
 
    // 创建一个Publisher,发布名为/person_info的topic,消息类型为learning_topic::Person,队列长度10
    ros::Publisher person_info_pub = n.advertise<learning_topic::Person>("/person_info", 10);
 
    // 设置循环的频率
    ros::Rate loop_rate(1);
 
    int count = 0;
    while (ros::ok())
    {
        // 初始化learning_topic::Person类型的消息
    	learning_topic::Person person_msg;
		person_msg.name = "Tom";
		person_msg.age  = 18;
		person_msg.sex  = learning_topic::Person::male;
 
        // 发布消息
		person_info_pub.publish(person_msg);
 
       	ROS_INFO("Publish Person Info: name:%s  age:%d  sex:%d", 
				  person_msg.name.c_str(), person_msg.age, person_msg.sex);
 
        // 按照循环频率延时
        loop_rate.sleep();
    }
 
    return 0;
}

person_subscriber.cpp代码

/**
 * 该例程将订阅/person_info话题,自定义消息类型learning_topic::Person
 */
 
// person_subscriber.cpp
 
#include <ros/ros.h>
#include "learning_topic/Person.h"
 
// 接收到订阅的消息后,会进入消息回调函数
void personInfoCallback(const learning_topic::Person::ConstPtr& msg)
{
    // 将接收到的消息打印出来
    ROS_INFO("Subcribe Person Info: name:%s  age:%d  sex:%d", 
			 msg->name.c_str(), msg->age, msg->sex);
}
 
int main(int argc, char **argv)
{
    // 初始化ROS节点
    ros::init(argc, argv, "person_subscriber");
 
    // 创建节点句柄
    ros::NodeHandle n;
 
    // 创建一个Subscriber,订阅名为/person_info的topic,注册回调函数personInfoCallback
    ros::Subscriber person_info_sub = n.subscribe("/person_info", 10, personInfoCallback);
 
    // 循环等待回调函数
    ros::spin();
 
    return 0;
}

修改CmakeLists.txt

add_executable(person_publisher src/person_publisher.cpp)
target_link_libraries(person_publisher ${catkin_LIBRARIES})
add_dependencies(person_publisher ${PROJECT_NAME}_generate_messages_cpp)
 
add_executable(person_subscriber src/person_subscriber.cpp)
target_link_libraries(person_subscriber ${catkin_LIBRARIES})
add_dependencies(person_subscriber ${PROJECT_NAME}_generate_messages_cpp)

之后,再编译运行,具体过程请读者自行参悟。

四、服务通讯

1.客户端&服务端

注意:

1.客户端请求被处理时,需要保证服务器已经启动;

2.服务端和客户端都可以存在多个。

在工作空间创建功能包

cd ~/catkin_ws/src
catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim

实现客户端

/**
 * 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn
 */
 
// turtle_spawn.cpp
 
#include <ros/ros.h>
#include <turtlesim/Spawn.h>
 
int main(int argc, char** argv)
{
    // 初始化ROS节点
	ros::init(argc, argv, "turtle_spawn");
 
    // 创建节点句柄
	ros::NodeHandle node;
 
    // 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
	ros::service::waitForService("/spawn");
	ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
 
    // 初始化turtlesim::Spawn的请求数据
	turtlesim::Spawn srv;
	srv.request.x = 2.0;
	srv.request.y = 2.0;
	srv.request.name = "turtle2";
 
    // 请求服务调用
	ROS_INFO("Call service to spwan turtle[x:%0.6f, y:%0.6f, name:%s]", 
			 srv.request.x, srv.request.y, srv.request.name.c_str());
 
	add_turtle.call(srv);
 
	// 显示服务调用结果
	ROS_INFO("Spwan turtle successfully [name:%s]", srv.response.name.c_str());
 
	return 0;
};

设置CmakeLists.txt

add_executable(turtle_spawn src/turtle_spawn.cpp)        #设置需要编译的代码和生成的可执行文件;
target_link_libraries(turtle_spawn ${catkin_LIBRARIES})        #设置链接库;

编译运行客户端

cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_spawn

实现服务端

/**
 * 该例程将执行/turtle_command服务,服务数据类型std_srvs/Trigger
 */
 
// turtle_command_server.cpp
 
 
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <std_srvs/Trigger.h>
 
ros::Publisher turtle_vel_pub;
bool pubCommand = false;
 
// service回调函数,输入参数req,输出参数res
bool commandCallback(std_srvs::Trigger::Request  &req,
         			std_srvs::Trigger::Response &res)
{
	pubCommand = !pubCommand;
 
    // 显示请求数据
    ROS_INFO("Publish turtle velocity command [%s]", pubCommand==true?"Yes":"No");
 
	// 设置反馈数据
	res.success = true;
	res.message = "Change turtle command state!"
 
    return true;
}
 
int main(int argc, char **argv)
{
    // ROS节点初始化
    ros::init(argc, argv, "turtle_command_server");
 
    // 创建节点句柄
    ros::NodeHandle n;
 
    // 创建一个名为/turtle_command的server,注册回调函数commandCallback
    ros::ServiceServer command_service = n.advertiseService("/turtle_command", commandCallback);
 
	// 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
	turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
 
    // 循环等待回调函数
    ROS_INFO("Ready to receive turtle command.");
 
	// 设置循环的频率
	ros::Rate loop_rate(10);
 
	while(ros::ok())
	{
		// 查看一次回调函数队列
    	ros::spinOnce();
		
		// 如果标志为true,则发布速度指令
		if(pubCommand)
		{
			geometry_msgs::Twist vel_msg;
			vel_msg.linear.x = 0.5;
			vel_msg.angular.z = 0.2;
			turtle_vel_pub.publish(vel_msg);
		}
 
		//按照循环频率延时
	    loop_rate.sleep();
	}
 
    return 0;
}

设置CmakeLists.txt

add_executable(turtle_command_server src/turtle_command_server.cpp)      #设置需要编译的代码和生成的可执行文件;
target_link_libraries(turtle_command_server ${catkin_LIBRARIES})        #设置链接库;

编译运行服务端

 cd ~/catkin_ws
 catkin_make
 source devel/setup.bash
 roscore
 rosrun turtlesim turtlesim_node
 rosrun learning_service turtle_command_server        //等待请求服务
 rosservice call /turtle_command "{}"        //发送请求,相当于开关,发送一次开始运动,再发送一次就停止

2.自定义客户端&服务端

在文件夹learning_service中新建一个命名为srv的文件夹,存放消息类文件,方便管理,创建.srv文件 touch Person.srv
打开自定义消息文件,输入以下信息后保存

string name
uint8 sex
uint8 age
 
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
---
string result
//这里写的并不是cpp,也不是python。但他在编译过程中会由ros自动编译成cpp或python

在package.xml中添加功能包依赖

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
//<build_depend>用于添加编译依赖,这里添加message_generation功能包
//<exec_depend<用于添加执行依赖,这里添加message_runtime功能包

在CMakeLists.txt添加编译选项

//在最上方的find_package中添加以下语句
message_generation
//找到Declare ROS dynamic reconfigure parameters的位置,在上方添加以下语句
add_message_files(FILES Person.msg)
generate_messages(DEPENDENCIES std_msgs)
//第一句,告诉编译器Person.msg是我们定义的消息接口
//第二句,告诉编译器在编译Person.msg文件的时候,需要依赖ROS哪些已有的库或者包。
//找到catkin specific configuration(即Build上方),添加以下语句
CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs turtlesim message_runtime

编译生成语言相关文件

//回到工作空间的根目录catkin_ws下,打开命令行,尝试进行编译。输入指令
catkin_make

创建客户端

/**
 * 该例程将请求/show_person服务,服务数据类型learning_service::Person
 */
 
// person_client.cpp
 
#include <ros/ros.h>
#include "learning_service/Person.h"
 
int main(int argc, char** argv)
{
    // 初始化ROS节点
	ros::init(argc, argv, "person_client");
 
    // 创建节点句柄
	ros::NodeHandle node;
 
    // 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
	ros::service::waitForService("/show_person");
	ros::ServiceClient person_client = node.serviceClient<learning_service::Person>("/show_person");
 
    // 初始化learning_service::Person的请求数据
	learning_service::Person srv;
	srv.request.name = "Tom";
	srv.request.age  = 20;
	srv.request.sex  = learning_service::Person::Request::male;
 
    // 请求服务调用
	ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]", 
			 srv.request.name.c_str(), srv.request.age, srv.request.sex);
 
	person_client.call(srv);
 
	// 显示服务调用结果
	ROS_INFO("Show person result : %s", srv.response.result.c_str());
 
	return 0;
};

创建服务器

/**
 * 该例程将执行/show_person服务,服务数据类型learning_service::Person
 */
 
// person_server.cpp
 
#include <ros/ros.h>
#include "learning_service/Person.h"
 
// service回调函数,输入参数req,输出参数res
bool personCallback(learning_service::Person::Request  &req,
         			learning_service::Person::Response &res)
{
    // 显示请求数据
    ROS_INFO("Person: name:%s  age:%d  sex:%d", req.name.c_str(), req.age, req.sex);
 
	// 设置反馈数据
	res.result = "OK";
 
    return true;
}
 
int main(int argc, char **argv)
{
    // ROS节点初始化
    ros::init(argc, argv, "person_server");
 
    // 创建节点句柄
    ros::NodeHandle n;
 
    // 创建一个名为/show_person的server,注册回调函数personCallback
    ros::ServiceServer person_service = n.advertiseService("/show_person", personCallback);
 
    // 循环等待回调函数
    ROS_INFO("Ready to show person informtion.");
    ros::spin();
 
    return 0;
}

设置CmakeLists.txt

add_executable(person_server src/person_server.cpp)
target_link_libraries(person_server ${catkin_LIBRARIES})
add_dependencies(person_server ${PROJECT_NAME}_gencpp)
 
 
add_executable(person_client src/person_client.cpp)
target_link_libraries(person_client ${catkin_LIBRARIES})
add_dependencies(person_client ${PROJECT_NAME}_gencpp)

编译并运行客户端和服务端

//编译并运行
cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
//新开一个终端,运行server
rosrun learning_service person_server
//新开一个终端,运行client
rosrun learning_service person_client

这里有另一个例子:ROS——服务通信_~Old的博客-CSDN博客_ros服务通信

五、参数通讯

1.参数

  rosparam命令可对ROS参数服务器上的参数进行操作。通过rosparam -h命令,可以看到有下面的一些方法:

Commands:
    rosparam set        set parameter                 设置参数
    rosparam get        get parameter                 获得参数值
    rosparam load        load parameters from file     从文件中加载参数到参数服务器
    rosparam dump       dump parameters to file       将参数服务器中的参数写入到文件
    rosparam delete     delete parameter              删除参数
    rosparam list       list parameter names          列出参数服务器中的参数

获取/配置参数

/**
 * 该例程设置/读取海龟例程中的参数
 */
#include <string>
#include <ros/ros.h>
#include <std_srvs/Empty.h>
 
int main(int argc, char **argv)
{
	int red, green, blue;
 
    // ROS节点初始化
    ros::init(argc, argv, "parameter_config");
 
    // 创建节点句柄
    ros::NodeHandle node;
 
    // 读取背景颜色参数
	ros::param::get("/background_r", red);
	ros::param::get("/background_g", green);
	ros::param::get("/background_b", blue);
 
	ROS_INFO("Get Backgroud Color[%d, %d, %d]", red, green, blue);
 
	// 设置背景颜色参数
	ros::param::set("/background_r", 255);
	ros::param::set("/background_g", 255);
	ros::param::set("/background_b", 255);
 
	ROS_INFO("Set Backgroud Color[255, 255, 255]");
 
    // 读取背景颜色参数
	ros::param::get("/background_r", red);
	ros::param::get("/background_g", green);
	ros::param::get("/background_b", blue);
 
	ROS_INFO("Re-get Backgroud Color[%d, %d, %d]", red, green, blue);
 
	// 调用服务,刷新背景颜色
	ros::service::waitForService("/clear");
	ros::ServiceClient clear_background = node.serviceClient<std_srvs::Empty>("/clear");
	std_srvs::Empty srv;
	clear_background.call(srv);
	
	sleep(1);
 
    return 0;
}

设置CMakeLists.txt

add_executable(parameter_config src/parameter_config.cpp)
target_link_libraries(parameter_config ${catkin_LIBRAIES})

编译并运行参数设置

//编译并运行
cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
//新开一个终端,开启turtlesim
rosrun turtlesim turtlesim_node
//新开一个终端,运行参数设置
rosrun learning_parameter parameter_config

 可以看到,在运行了rosrun learning_parameter parameter_config后,我们成功的将小海龟的背景颜色改成了白色。

2.动态参数

   见第八部分。

六、配置launch文件

   通过XML文件实现多节点的配置和启动(可自动启动ROS Master),可以启动本地和远程的多个节点,还可以在参数服务器中设置参数。

        roslaunch 命令执行launch文件时,首先会判断是否启动了 roscore,如果启动了,则不再启动,否则,会自动调用 roscore。

1、launch根标签设置

当我们的launch文件已经弃用或者过时,可以添加备注,让其他使用者知道弃用情况!

<launch deprecated = "该launch文件已经过时,建议不使用">
    <!-- 启动显示界面节点 -->
    <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen"/>
    <!-- 启动键盘控制节点 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>

在launch根标签加入 deprecated="该launch文件已经过时,建议不使用"

2、launch的node标签

①属性

pkg:功能包名(常用)
type:node节点名(常用)
name:自定义节点名(常用)
args:参数(将参数传递给节点)
respawn:如果节点退出,是否自动重启(true/false)<这个在后续的激光雷达启动时十分有用,若异常断开,可以自动重连>(实用)
respawn_delay="N"(若respawn为true,那么延迟N秒后启动节点)

required:(true/false)若为true,如果该节点退出,将结束整个roslaunch

 ns="xxx" 添加命名空间namespace(目的是防止重名)

3、launch文件的include标签(常用)

        我们都知道,不可能把所以launch代码都放在一块,显得十分臃肿,因此可以采用include标签进行代码复用!

file="$(find 功能包名)/xxx/xxx.launch"
<launch>
    <include file="$(find launch_learning)/launch/start_turtle.launch" />   
</launch>

4、launch文件的remap标签(实用)


        我们可以看到下图,启动turtle节点和turtle的键盘控制节点,然后启动键盘控制节点,我们发现新启动的键盘控制节点并不能控制小乌龟,原因在于话题名称不对应,一个是/cmd_vel 另一个是/turtle1/cmd_vel ,如果想要新启动的键盘控制节点控制小乌龟移动,那就可以采用remap进行话题重命名!

<launch>
    <!-- 启动显示界面节点 -->
    <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen" >
        <remap from="/turtle1/cmd_vel" to="/cmd_vel" />
    </node>
    <!-- 启动键盘控制节点 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>

5、launch文件的param标签

<param name ="xxx" type="xx" value="xx" />
<launch>
    <param name="param_A" type="int" value="10"/>
    <!-- 启动显示界面节点 -->
    <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen" >
        <param name="param_B" type="double" value="3.1415"/>
    </node>
    <!-- 启动键盘控制节点 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>

 param放置的外置的区别是后者添加了命名空间

 6、launch文件的rosparam

        <rosparam>标签可以从yaml文件中导入参数,或者将参数导出到yaml文件

①导入参数

        有点像param标签,但是区别在于rosparam可以导入yaml文件一次性导入参数!

<launch>
    <rosparam command="load" file="$(find launch_learning)/launch/params.yaml" />
    <!-- 启动显示界面节点 -->
    <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen" >
        <rosparam command="load" file="$(find launch_learning)/launch/params.yaml" />
    </node>
    <!-- 启动键盘控制节点 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>

②导出参数

<rosparam command="dump" file="$(find launch_learning)/launch/params_out.yaml" />

 ③删除参数(用的少) 

<rosparam command="delete" param="bg_B" />

7、launch文件的group标签

  我们都知道如果启动两个相同的节点,后一个节点会把前一个节点干掉!但如何两个都启动呢!那就需要使用group这个标签了!

<launch>
    <group ns="first">
         <!-- 启动显示界面节点 -->
        <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen" />
        <!-- 启动键盘控制节点 -->
         <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
    </group>
 
    <group ns="second">
         <!-- 启动显示界面节点 -->
        <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen" />
        <!-- 启动键盘控制节点 -->
         <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
    </group>
</launch>

    我们可以发现启动了两个小乌龟界面,话题也更新了两个不一样的命名空间的话题!这样就可以两个相同的节点同时启动了!

8、launch文件的arg标签

<launch>
    <arg name="car_length" default="0.55" />
    <arg name="car_width" default="0.6" />
    <arg name="car_height" default="0.3" />
 
    <param name="A" value="$(arg car_length)"/>
    <param name="B" value="$(arg car_width)"/>
    <param name="C" value="$(arg car_height)"/>
</launch>

  如果在启动launch文件没有设置参数的话,就默认使用default的参数值

 也可以在启动launch文件时设置参数,如下:

roslaunch launch_learning arg.launch car_length:=0.9 car_width:=0.1 car_height:=0.99

9、launch文件使用

我们以turtlesim为例进行编写launch文件!

我们如果要启动一个turtle节点,如下:

rosrun turtlesim turtlesim_node 

启动键盘节点,如下:

rosrun turtlesim turtle_teleop_key

需要两步!而launch文件如下:

<launch>
    <!-- 启动显示界面节点 -->
    <node pkg="turtlesim" type="turtlesim_node" name="my_turtle" output="screen"/>
    <!-- 启动键盘控制节点 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>

node pkg:功能包名

type:节点名

name:自定义名称

output:输出到屏幕

运行只需:

roslaunch launch_learning start_turtle.launch 

launch_learning:功能包名     

start_turtle.launch:launch文件名

七、tf树

机器人系统通常具有许多随时间变化的坐标系,例如世界坐标系、基座坐标系、夹持器坐标系、头部坐标系等。TF是一个变换系统,它允许在一个坐标系中进行计算,然后在任何所需的时间点将它们转换到另一个坐标系。TF能够回答以下问题:地图坐标系中机器人基座的当前位姿是什么?相对于机器人基座,夹持器中物体的位姿是什么?5秒前,头部坐标系在世界坐标系的什么地方?

        tf树是分布式系统,即没有单点故障;转换多次时没有数据丢失;没有坐标系之间的中间数据转换的计算成本;用户无需担心其数据的起始坐标系;有关过去位置的信息进行存储并且可访问(在本地记录开始后)。

1、典型的TF坐标系

odom: 仅使用测距测量的自洽坐标系;

base_footprint: 机器人在地面以上零高度的基础;

base_link: 机器人的基座,位于机器人旋转中心;

base_laser_link: 激光传感器的位置;

2、乌龟例程中的TF

   调用两只乌龟,使用键盘方向键控制一只乌龟移动,会发现另一只乌龟会跟随移动。

其TF树如下:

 如上所示,当前系统中存在三个坐标系,时间坐标系world、乌龟坐标系turtle1和乌龟坐标系turtle2。
        世界坐标系是该系统的基础坐标系,其它坐标系都相对该坐标系建立,所以world是TF树的根节点,而两只乌龟坐标系的原点就是乌龟在世界坐标系下的坐标位置。

2.1创建TF广播器

  创建一个节点,主要用于发布乌龟坐标系与世界坐标系之间TF变换,turtle_tf_broadcaster.cpp内容如下:

#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>
 
std::string turtle_name;
 
void poseCallback(const turtlesim::PoseConstPtr& msg)
{
    // tf广播器
    static tf::TransformBroadcaster br;
 
    // 根据乌龟当前的位姿,设置相对于世界坐标系的坐标变换
    tf::Transform transform;
    //设置平移变换
    transform.setOrigin( tf::Vector3(msg->x, msg->y, 0.0) );
    tf::Quaternion q;
    q.setRPY(0, 0, msg->theta);
    //设置旋转变换
    transform.setRotation(q);
 
    // 将坐标变换插入TF树并发布坐标变换
    br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "world", turtle_name));
}
 
int main(int argc, char** argv)
{
    // 初始化节点
    ros::init(argc, argv, "my_tf_broadcaster");
    if (argc != 2)
    {
        ROS_ERROR("need turtle name as argument"); 
        return -1;
    };
    turtle_name = argv[1];
 
    // 订阅乌龟的pose信息
    ros::NodeHandle node;
    ros::Subscriber sub = node.subscribe(turtle_name+"/pose", 10, &poseCallback);
 
    ros::spin();
 
    return 0;
};

2.2创建TF监听器

创建一个节点,主要用于监听TF消息,从中获取turtle2相对于turtle1的坐标系的变换,从而控制turtle2的移动。turtle_tf_listener.cpp内容如下:

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>
 
int main(int argc, char** argv)
{
    // 初始化节点
    ros::init(argc, argv, "my_tf_listener");
 
    ros::NodeHandle node;
 
    // 通过服务调用,产生第二只乌龟turtle2
    ros::service::waitForService("spawn");
    ros::ServiceClient add_turtle =
    node.serviceClient<turtlesim::Spawn>("spawn");
    turtlesim::Spawn srv;
    add_turtle.call(srv);
 
    // 定义turtle2的速度控制发布器
    ros::Publisher turtle_vel =
    node.advertise<geometry_msgs::Twist>("turtle2/cmd_vel", 10);
 
    // tf监听器
    tf::TransformListener listener;
	//监听器会自动接收TF树的消息,并且缓存10秒
    ros::Rate rate(10.0);
    while (node.ok())
    {
        tf::StampedTransform transform;
        try
        {
            // 查找turtle2与turtle1的坐标变换
            listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
            listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform);
        }
        catch (tf::TransformException &ex) 
        {
            ROS_ERROR("%s",ex.what());
            ros::Duration(1.0).sleep();
            continue;
        }
 
        // 根据turtle1和turtle2之间的坐标变换,计算turtle2需要运动的线速度和角速度
        // 并发布速度控制指令,使turtle2向turtle1移动
        geometry_msgs::Twist vel_msg;
        vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(),
                                        transform.getOrigin().x());
        vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) +
                                      pow(transform.getOrigin().y(), 2));
        turtle_vel.publish(vel_msg);
 
        rate.sleep();
    }
    return 0;
};

2.3实现乌龟跟随运动

 编写start_demo_with_listener.launch文件,内容如下:

 <launch>
    <!-- 海龟仿真器 -->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>
 
    <!-- 键盘控制 -->
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
 
    <!-- 两只海龟的tf广播 -->
    <node pkg="learning_tf" type="turtle_tf_broadcaster"
          args="/turtle1" name="turtle1_tf_broadcaster" />
    <node pkg="learning_tf" type="turtle_tf_broadcaster"
          args="/turtle2" name="turtle2_tf_broadcaster" />
 
    <!-- 监听tf广播,并且控制turtle2移动 -->
    <node pkg="learning_tf" type="turtle_tf_listener"
          name="listener" />
 
 </launch>

3.运行

roslaunch learning_tf start_demo_with_listener.launch

打开键盘控制节点,命令如下:

rosrun turtlesim turtle_teleop_key

八、动态调参

1、创建功能包(名为dynamic_reconfigure_test)

catkin_create_pkg dynamic_reconfigure_test rospy roscpp dynamic_reconfigure

2、编写配置文件

2.1在该功能包内新建cfg文件夹

mkdir cfg

2.2新建一个cfg文件(我们的可视化界面内容)

touch pid.cfg

2.3编写cfg文件(这里名为pid.cfg)

#!/usr/bin/env python
PACKAGE = "dynamic_reconfigure_test"
 
# 导入dynamic_reconfigure功能包提供的参数生成器(parameter generator)
from dynamic_reconfigure.parameter_generator_catkin import *
 
# 创建一个参数生成器
gen = ParameterGenerator()
 
# 定义动态配置的参数
 
gen.add("KP", double_t, 0, "KP_param", 0.0,  0, 500)
gen.add("KI", double_t, 0, "KI_param", 0.0,  0, 500)
gen.add("KD", double_t, 0, "KD_param", 0.0,  0, 500)
 
# 退出
exit(gen.generate(PACKAGE, "dynamic_PID", "pid"))

详细注释

 动态参数语法:

add(name,type,level,description,default,min,max)

    name:参数名,用字符串表示

    type:数据类型,(int_t,double_t,str_t,bool_t)

    level:掩码(可用于查看参数是否修改)默认写0就行

    description:描述,用于描述该参数的作用,简单明了即可~

    default:默认值,设置参数的默认值

    min:参数的最小值

    max:参数的最大值

2.4给配置文件设置权限

我们给它添加可执行的功能!

chmod a+x pid.cfg 

3、配置CMakeList文件以及package.xml文件

3.1CMakeList文件

find_package中必需包含dynamic_reconfigure;

往下查找修改成自己的cfg文件名,如下图;

继续往下查找,将这一行取消注释即可,如果是后面在自己的原有基础上加功能的话,记得写上!

 3.2package.xml文件

因为我们一开始创建新的功能包就包含了dynamic_reconfigure 这个功能包,所以已经有了!

如果是后面在自己的原有基础上加功能的话,记得写上!

 4、编译

先进行编译的目的是生成需要的头文件。

我们可以只对该功能包编译,catkin_make可以这样写:

catkin_make -DCATKIN_WHITELIST_PACKAGES="dynamic_reconfigure_test"

在devel/include中我们可以看到生成的一个名为dynamic_reconfigure_test/pidConfig.h的头文件!后面的文件我们需要包含它!

这样的话我们的配置文件就初步完成了!接下来编写服务器节点!

5、创建服务器节点

5.1创建一个cpp文件(名dynamic_test.cpp)

#include <ros/ros.h>
#include <dynamic_reconfigure/server.h>
#include <dynamic_reconfigure_test/pidConfig.h>
 
float KP = 0.0;
float KI = 0.0;
float KD = 0.0;
 
 
//回调函数
void Callback(dynamic_reconfigure_test::pidConfig &config)
{
    KP = config.KP;
    KI = config.KI;
    KD = config.KD;
 
    ROS_INFO("KP KI KD = [%f %f %f]",KP,KI,KD);
}
 
 
int main(int argc,char **argv)
{
    //初始化,创建节点
    ros::init(argc,argv,"dynamic_test");
 
    //创建一个参数动态配置的服务器实例
    dynamic_reconfigure::Server<dynamic_reconfigure_test::pidConfig> server;
    //定义回调函数
    dynamic_reconfigure::Server<dynamic_reconfigure_test::pidConfig>::CallbackType f;
 
    //将回调函数和服务端绑定,当客户端请求修改参数时,服务器跳转到回调函数进行处理
    f = boost::bind(&Callback,_1);
    server.setCallback(f);
 
    ros::spin();
 
    return 0;
}

5.2修改CMakeList文件

6、运行

打开3个终端分别运行:

roscore
rosrun dynamic_reconfigure_test dynamic_test
rosrun rqt_reconfigure rqt_reconfigure 

参考文献

1.【ROS学习】launch文件的使用_Q小鑫的博客-CSDN博客_launch文件使用

2.【ROS学习】动态调参_Q小鑫的博客-CSDN博客_ros动态调参

3.   ROS学习(22)TF变换_敲代码的雪糕的博客-CSDN博客_tf 变换


总结

  本篇介绍了ROS的开发基础操作,单个功能包的开发流程和方法。