ROS自学笔记(一)

其他文章

ROS自学笔记(二)
ROS自学笔记(三)


前言

该文章仅仅是个人笔记,笔记内容可能不够准确。若想更好地学习ros,可自行阅读《ROS机器人编程原理与应用》。

一、ROS通信机制

1.简介

ROS 中的基本通信机制主要有如下三种实现策略:

  • 话题通信(发布订阅模式)
  • 服务通信(请求响应模式)
  • 参数服务器(参数共享模式)

2. 话题通信

2.1 概念及理论模型

话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的。应用场景也极其广泛,比如下面一个常见场景:

机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。

在上述场景中,就不止一次使用到了话题通信。

  • 以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
  • 再以运动消息的发布为例,导航模块会根据传感器采集的数据时时的计算出运动控制信息并发布给底盘,底盘也可以有一个节点订阅运动信息并最终转换成控制电机的脉冲信号。

以此类推,像雷达、摄像头、GPS… 等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。

作用
用于不断更新的、少逻辑处理的数据传输场景。


该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)

ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。
在这里插入图片描述
整个流程如以下:

Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。

Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。

ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。

Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。

Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。

Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。

Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。

注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
注意2: Talker 与 Listener 的启动无先后顺序要求
注意3: Talker 与 Listener 都可以有多个
注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。

2.2 案例一

需求:
编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出。

流程:

  1. 编写发布方;
  2. 编写订阅方;
  3. 编辑配置文件;
  4. 编译并执行。

发布方

#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
#include <sstream>

int main(int argc, char  *argv[])
{   
    //设置编码
    setlocale(LC_ALL,"");

    //2.初始化 ROS 节点:命名(唯一)
    // 参数1和参数2 后期为节点传值会使用
    // 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
    ros::init(argc,argv,"talker");
    //3.实例化 ROS 句柄,该类封装了 ROS 中的一些常用功能
    ros::NodeHandle nh;

    //4.实例化 发布者 对象
    //泛型: 发布的消息类型
    //参数1: 要发布到的话题
    //参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);

    //5.组织被发布的数据,并编写逻辑发布数据
    std_msgs::String msg;
    std::string msg_front = "Hello 你好!"; //消息前缀
    int count = 0; //消息计数器

    //逻辑(一秒10次)
    ros::Rate r(10);

    //节点不死时,ros::ok()为true
    while (ros::ok())
    {
        //使用 stringstream 拼接字符串与编号
        std::stringstream ss;
        ss << msg_front << count;
        msg.data = ss.str();
      
        //发布消息
        pub.publish(msg);
        //加入调试,打印发送的消息
        ROS_INFO("发送的消息:%s",msg.data.c_str());

        //根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
        r.sleep();
        count++;
        //暂无应用
        ros::spinOnce();
    }

    return 0;
}

订阅方

#include "ros/ros.h"
#include "std_msgs/String.h"

//回调函数
void doMsg(const std_msgs::String::ConstPtr& msg_p){
    ROS_INFO("我听见:%s",msg_p->data.c_str());
}
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    //2.初始化 ROS 节点:命名(唯一)
    ros::init(argc,argv,"listener");
    //3.实例化 ROS 句柄
    ros::NodeHandle nh;

    //4.实例化 订阅者 对象
    ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
    //5.处理订阅的消息(回调函数)

    //6.设置循环调用回调函数
    ros::spin();//循环读取接收的数据,并调用回调函数处理

    return 0;
}

配置 CMakeLists.txt

add_executable(Hello_pub
  src/Hello_pub.cpp
)
add_executable(Hello_sub
  src/Hello_sub.cpp
)

target_link_libraries(Hello_pub
  ${catkin_LIBRARIES}
)
target_link_libraries(Hello_sub
  ${catkin_LIBRARIES}
)

执行
1.启动 roscore;
2.启动发布节点;
3.启动订阅节点。

2.3 案例二(自定义msg)

ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型.

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint*)
  • float32, float64
  • string
  • time, duration
  • other msg files
  • variable-length array[] and fixed-length array[C]

ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。

需求:
创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。

流程:

  1. 按照固定格式创建 msg 文件
  2. 编辑配置文件
  3. 编译生成可以被C++ 调用的中间文件
  4. 发布方实现
  5. 订阅方实现
  6. 运行

1.定义msg文件
功能包下新建 msg 目录,添加文件 Person.msg

string name
uint16 age
float64 height

2.编辑配置文件
package.xml中添加编译依赖与执行依赖

  <build_depend>message_generation</build_depend>
  <exec_depend>message_runtime</exec_depend>

CMakeLists.txt编辑 msg 相关配置

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)
# 需要加入 message_generation,必须有 std_msgs
## 配置 msg 源文件
add_message_files(
  FILES
  Person.msg
)
# 生成消息时依赖于 std_msgs
generate_messages(
  DEPENDENCIES
  std_msgs
)
#执行时依赖
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES demo02_talker_listener
  CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
#  DEPENDS system_lib
)

3.编译
编译后的中间文件查看:
C++ 需要调用的中间文件位置:(…/工作空间/devel/include/包名/xxx.h)
在这里插入图片描述后续调用相关 msg 时,是从这些中间文件调用的

0.vscode 配置
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件(.h结尾的文件)路径配置进 c_cpp_properties.json 的 includepath属性:

{
    "configurations": [
        {
            "browse": {
                "databaseFilename": "",
                "limitSymbolsToIncludedHeaders": true
            },
            "includePath": [
                "/opt/ros/noetic/include/**",
                "/usr/include/**",
                "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 
            ],
            "name": "ROS",
            "intelliSenseMode": "gcc-x64",
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}

4.发布方

/*
    循环发布人的信息
*/

#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");

    //1.初始化 ROS 节点
    ros::init(argc,argv,"talker_person");

    //2.创建 ROS 句柄
    ros::NodeHandle nh;

    //3.创建发布者对象
    ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);

    //4.组织被发布的消息,编写发布逻辑并发布消息
    demo02_talker_listener::Person p;
    p.name = "sunwukong";
    p.age = 2000;
    p.height = 1.45;

    ros::Rate r(1);
    while (ros::ok())
    {
        pub.publish(p);
        p.age += 1;
        ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);

        r.sleep();
        ros::spinOnce();
    }
    return 0;
}

2.订阅方

#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

//回调函数
void doPerson(const demo02_talker_listener::Person::ConstPtr& person_p){
    ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height);
}

int main(int argc, char *argv[])
{   
    setlocale(LC_ALL,"");

    //1.初始化 ROS 节点
    ros::init(argc,argv,"listener_person");
    //2.创建 ROS 句柄
    ros::NodeHandle nh;
    //3.创建订阅对象
    ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson);

    //4.回调函数中处理 person
    //5.ros::spin();
    ros::spin();    
    return 0;
}

3.配置 CMakeLists.txt
需要添加 add_dependencies 用以设置所依赖的消息相关的中间文件。

add_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)

#如果未提前编译生成Person.h文件,编译器在编译代码时,会因为找不到Person类而编译失败
#添加以下依赖可以解决此问题,即无须先编译msg文件再编译c++代码,可一次性编译整个项目
add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp)
add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp)

target_link_libraries(person_talker
  ${catkin_LIBRARIES}
)
target_link_libraries(person_listener
  ${catkin_LIBRARIES}
)

4.执行
1.启动 roscore;
2.启动发布节点;
3.启动订阅节点。
PS:可以使用 rqt_graph 查看节点关系。

3.服务通信

3.1 概念及理论模型

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人… 此时需要拍摄照片并留存。

在上述场景中,就使用到了服务通信。

  • 一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果

与上述应用类似的,服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景。

作用
用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。


理论模型如下图所示,该模型中涉及到三个角色:

  1. ROS master(管理者)
  2. Server(服务端)
  3. Client(客户端)

ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。
在这里插入图片描述
整个流程由以下步骤实现:
Server注册
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

Client注册
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。

Client发送请求
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

Server发送响应
Server 接收、解析请求的数据,并产生响应结果返回给 Client。

注意:
1.客户端请求被处理时,需要保证服务器已经启动;
2.服务端和客户端都可以存在多个。

3.2 服务通信自定义srv案例

需求:

客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。

流程:

srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:

  1. 按照固定格式创建srv文件
  2. 编辑配置文件
  3. 编译生成中间文件
  4. 编写服务端实现
  5. 编写客户端实现
  6. 编辑配置文件
  7. 编译并执行

1.定义srv文件
服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用连续的三个横杆分割,具体实现如下:

功能包下新建 srv 目录,添加 xxx.srv 文件(AddInts.srv),内容:

# 客户端请求时发送的两个数字
int32 num1
int32 num2
---
# 服务器响应发送的数据
int32 sum

2.编辑配置文件
package.xml中添加编译依赖与执行依赖

  <build_depend>message_generation</build_depend>
  <exec_depend>message_runtime</exec_depend>

CMakeLists.txt编辑 srv 相关配置

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)
# 需要加入 message_generation,必须有 std_msgs
add_service_files(
  FILES
  AddInts.srv
)
generate_messages(
  DEPENDENCIES
  std_msgs
)
#执行时依赖
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES demo02_talker_listener
  CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
#  DEPENDS system_lib
)

3.编译
编译后的中间文件查看:
C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)
在这里插入图片描述后续调用相关 srv 时,是从这些中间文件调用的

0.vscode配置
像之前自定义 msg 实现一样配置c_cpp_properies.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同.

4.服务端

#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"

// bool 返回值由于标志是否处理成功
bool doReq(demo03_server_client::AddInts::Request& req,
          demo03_server_client::AddInts::Response& resp){
    int num1 = req.num1;
    int num2 = req.num2;

    ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);

    //逻辑处理
    if (num1 < 0 || num2 < 0)
    {
        ROS_ERROR("提交的数据异常:数据不可以为负数");
        return false;
    }

    //如果没有异常,那么相加并将结果赋值给 resp
    resp.sum = num1 + num2;
    return true;


}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化 ROS 节点
    ros::init(argc,argv,"AddInts_Server");
    // 3.创建 ROS 句柄
    ros::NodeHandle nh;
    // 4.创建 服务 对象
    /*
     *参数一:服务的名称
     *参数二:回调函数,返回值为bool,代表逻辑处理是否成功
    */
    ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
    ROS_INFO("服务已经启动....");
    //     5.回调函数处理请求并产生响应
    //     6.由于请求有多个,需要调用 ros::spin()
    ros::spin();
    return 0;
}

5.客户端

// 1.包含头文件
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");

    // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
    if (argc != 3)
    // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
    {
        ROS_ERROR("请提交两个整数");
        return 1;
    }


    // 2.初始化 ROS 节点
    ros::init(argc,argv,"AddInts_Client");
    // 3.创建 ROS 句柄
    ros::NodeHandle nh;
    // 4.创建 客户端 对象 参数为服务的名称
    ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts");
    //等待服务启动成功
    //方式1 参数为服务的名称
    ros::service::waitForService("AddInts");
    //方式2
    // client.waitForExistence();
    // 5.组织请求数据
    demo03_server_client::AddInts ai;
    ai.request.num1 = atoi(argv[1]);
    ai.request.num2 = atoi(argv[2]);
    // 6.发送请求,返回 bool 值,标记是否成功
    bool flag = client.call(ai);
    // 7.处理响应
    if (flag)
    {
        ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
    }
    else
    {
        ROS_ERROR("请求处理失败....");
        return 1;
    }

    return 0;
}

6.配置 CMakeLists.txt

add_executable(AddInts_Server src/AddInts_Server.cpp)
add_executable(AddInts_Client src/AddInts_Client.cpp)

add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)

target_link_libraries(AddInts_Server
  ${catkin_LIBRARIES}
)
target_link_libraries(AddInts_Client
  ${catkin_LIBRARIES}
)

7.编译并执行

流程:

  1. 需要先启动服务:rosrun 包名 服务
  2. 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

会根据提交的数据响应相加后的结果。

注意:

如果先启动客户端,那么会导致运行失败

优化:

在客户端发送请求前添加:client.waitForExistence();或:ros::service::waitForService(服务的名称);

这是一个阻塞式函数,只有服务启动成功后才会继续执行

4. 在ROS中使用C++类

4.1 编写头文件

在功能包下的 include/功能包名 目录下新建头文件: ros_class.h

#ifndef ROS_CLASS_H_
#define ROS_CLASS_H_

#include <math.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include "ros/ros.h"

#include "std_msgs/Bool.h"
#include "std_msgs/Float32.h"
#include "std_srvs/Trigger.h"

class RosClass{
public:
    RosClass(ros::NodeHandle * nh);
private:
    ros::NodeHandle nh;
    ros::Subscriber minimal_subscriber;
    ros::ServiceServer minimal_service;
    ros::Publisher minimal_publisher;

    double val_from_subscriber;
    double val_to_remember;

    void initSubscribers();
    void initPublishers();
    void initServices();
    void subscriberCallBack(const std_msgs::Float32 & mes_holder);
    bool serviceCallBack(std_srvs::TriggerRequest & request, std_srvs::TriggerResponse &response);
};

#endif

4.2 编写 ros_class.cpp 文件

【注意】
在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下c_cpp_properties.json 的 includepath属性
"/home/用户/工作空间/src/功能包/include/**"

#include "example_ros_class/ros_class.h"

RosClass::RosClass(ros::NodeHandle * nh):nh(*nh){
    ROS_INFO("RosClass constructor");
    initSubscribers();
    initPublishers();
    initServices();
    
    val_to_remember = 0.0;
}


void RosClass::initSubscribers(){
    ROS_INFO("init subscribers");
    minimal_subscriber = nh.subscribe("input_topic",1, &RosClass::subscriberCallBack, this);
}

void RosClass::initPublishers(){
    ROS_INFO("init publishers");
    minimal_publisher = nh.advertise<std_msgs::Float32>("output_topic",1,true);
}

void RosClass::initServices(){
    ROS_INFO("init services");
    minimal_service = nh.advertiseService("minimal_service", &RosClass::serviceCallBack,this);
}

void RosClass::subscriberCallBack(const std_msgs::Float32 & mes_holder){
    val_from_subscriber = mes_holder.data;
    ROS_INFO("subscriberCallBack activated");
    std_msgs::Float32 output_msg;
    val_to_remember += val_from_subscriber;
    output_msg.data = val_to_remember;
    minimal_publisher.publish(output_msg);
}

bool RosClass::serviceCallBack(std_srvs::TriggerRequest & request, std_srvs::TriggerResponse &response){
    ROS_INFO("serviceCallBack activated");
    response.success = true;
    response.message = "here is a response string";
    return true;
}

int main(int argc, char  *argv[])
{
    /* code */
    ros::init(argc, argv, "RosClass");
    ros::NodeHandle nh;
    ROS_INFO("main init a instance of RosClass ");

    RosClass rosClass(&nh);
    ROS_INFO("main: go into spin; let the callback do all the work ");
    ros::spin();
    return 0;
}

4.3 配置文件

配置CMakeLists.txt文件,头文件相关配置如下:

include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

可执行配置文件配置方式与之前一致:

add_executable(ros_class src/ros_class.cpp)

add_dependencies(ros_class ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(ros_class
  ${catkin_LIBRARIES}
)

4.4 编译执行

启动ros核心:roscore
启动节点:rosrun example_ros_class ros_class

调用服务:

rosservice call minimal_service
#响应结果
success: True
message: "here is a response string"

发布数据:

rostopic pub -r 1 input_topic std_msgs/Float32 2.0

向话题input_topic发布数据,订阅方收到后调用回调函数,向题output_topic发布数据。(可用rostopic echo /output_topic指令查看)

5.参数服务器

5.1 概念及理论模型

参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,关于参数服务器的典型应用场景如下:

导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径
上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:

  • 路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数

参数服务器,一般适用于存在数据共享的一些应用场景

概念
以共享的方式实现不同节点之间数据交互的通信模式。

作用
存储一些多节点共享的数据,类似于全局变量。


参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (参数设置者)
  • Listener (参数调用者)

ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。
在这里插入图片描述
整个流程由以下步骤实现:

Talker 设置参数
Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。

Listener 获取参数
Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。

ROS Master 向 Listener 发送参数值
ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。


参数可使用数据类型:

  • 32-bit integers

  • booleans

  • strings

  • doubles

  • iso8601 dates

  • lists

  • base64-encoded binary data

  • 字典

注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据

5.2 参数服务器案例

需求:实现参数服务器参数的增删改查操作。

在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:

  • ros::NodeHandle
  • ros::param

1.参数服务器新增(修改)参数

/*
    参数服务器操作之新增与修改(二者API一样)_C++实现:
    在 roscpp 中提供了两套 API 实现参数操作
    ros::NodeHandle
        setParam("键",值)
    ros::param
        set("键","值")

    示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数
        修改(相同的键,不同的值)

*/
#include "ros/ros.h"

int main(int argc, char *argv[])
{
    ros::init(argc,argv,"set_update_param");

    std::vector<std::string> stus;
    stus.push_back("zhangsan");
    stus.push_back("李四");
    stus.push_back("王五");
    stus.push_back("孙大脑袋");

    std::map<std::string,std::string> friends;
    friends["guo"] = "huang";
    friends["yuang"] = "xiao";

    //NodeHandle--------------------------------------------------------
    ros::NodeHandle nh;
    nh.setParam("nh_int",10); //整型
    nh.setParam("nh_double",3.14); //浮点型
    nh.setParam("nh_bool",true); //bool
    nh.setParam("nh_string","hello NodeHandle"); //字符串
    nh.setParam("nh_vector",stus); // vector
    nh.setParam("nh_map",friends); // map

    //修改演示(相同的键,不同的值)
    nh.setParam("nh_int",10000);

    //param--------------------------------------------------------
    ros::param::set("param_int",20);
    ros::param::set("param_double",3.14);
    ros::param::set("param_string","Hello Param");
    ros::param::set("param_bool",false);
    ros::param::set("param_vector",stus);
    ros::param::set("param_map",friends);

    //修改演示(相同的键,不同的值)
    ros::param::set("param_int",20000);

    return 0;
}

2.参数服务器获取参数

/*
    参数服务器操作之查询_C++实现:
    在 roscpp 中提供了两套 API 实现参数操作
    ros::NodeHandle

        param(键,默认值) 
            存在,返回对应结果,否则返回默认值

        getParam(键,存储结果的变量)
            存在,返回 true,且将值赋值给参数2
            若果键不存在,那么返回值为 false,且不为参数2赋值

        getParamCached键,存储结果的变量)--提高变量获取效率
            存在,返回 true,且将值赋值给参数2
            若果键不存在,那么返回值为 false,且不为参数2赋值

        getParamNames(std::vector<std::string>)
            获取所有的键,并存储在参数 vector 中 

        hasParam(键)
            是否包含某个键,存在返回 true,否则返回 false

        searchParam(参数1,参数2)
            搜索键,参数1是被搜索的键,参数2存储搜索结果的变量

    ros::param ----- 与 NodeHandle 类似

*/

#include "ros/ros.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"get_param");

    //NodeHandle--------------------------------------------------------
    
    ros::NodeHandle nh;
    // param 函数
    int res1 = nh.param("nh_int",100); // 键存在
    int res2 = nh.param("nh_int2",100); // 键不存在
    ROS_INFO("param获取结果:%d,%d",res1,res2);

    // getParam 函数
    int nh_int_value;
    double nh_double_value;
    bool nh_bool_value;
    std::string nh_string_value;
    std::vector<std::string> stus;
    std::map<std::string, std::string> friends;

    nh.getParam("nh_int",nh_int_value);
    nh.getParam("nh_double",nh_double_value);
    nh.getParam("nh_bool",nh_bool_value);
    nh.getParam("nh_string",nh_string_value);
    nh.getParam("nh_vector",stus);
    nh.getParam("nh_map",friends);

    ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
            nh_int_value,
            nh_double_value,
            nh_string_value.c_str(),
            nh_bool_value
            );
    for (auto &&stu : stus)
    {
        ROS_INFO("stus 元素:%s",stu.c_str());        
    }

    for (auto &&f : friends)
    {
        ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
    }

    // getParamCached()
    nh.getParamCached("nh_int",nh_int_value);
    ROS_INFO("通过缓存获取数据:%d",nh_int_value);

    //getParamNames()
    std::vector<std::string> param_names1;
    nh.getParamNames(param_names1);
    for (auto &&name : param_names1)
    {
        ROS_INFO("名称解析name = %s",name.c_str());        
    }
    ROS_INFO("----------------------------");

    ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int"));
    ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt"));

    std::string key;
    nh.searchParam("nh_int",key);
    ROS_INFO("搜索键:%s",key.c_str());
    
    //param--------------------------------------------------------
    ROS_INFO("++++++++++++++++++++++++++++++++++++++++");
    int res3 = ros::param::param("param_int",20); //存在
    int res4 = ros::param::param("param_int2",20); // 不存在返回默认
    ROS_INFO("param获取结果:%d,%d",res3,res4);

    // getParam 函数
    int param_int_value;
    double param_double_value;
    bool param_bool_value;
    std::string param_string_value;
    std::vector<std::string> param_stus;
    std::map<std::string, std::string> param_friends;

    ros::param::get("param_int",param_int_value);
    ros::param::get("param_double",param_double_value);
    ros::param::get("param_bool",param_bool_value);
    ros::param::get("param_string",param_string_value);
    ros::param::get("param_vector",param_stus);
    ros::param::get("param_map",param_friends);

    ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
            param_int_value,
            param_double_value,
            param_string_value.c_str(),
            param_bool_value
            );
    for (auto &&stu : param_stus)
    {
        ROS_INFO("stus 元素:%s",stu.c_str());        
    }

    for (auto &&f : param_friends)
    {
        ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
    }

    // getParamCached()
    ros::param::getCached("param_int",param_int_value);
    ROS_INFO("通过缓存获取数据:%d",param_int_value);

    //getParamNames()
    std::vector<std::string> param_names2;
    ros::param::getParamNames(param_names2);
    for (auto &&name : param_names2)
    {
        ROS_INFO("名称解析name = %s",name.c_str());        
    }
    ROS_INFO("----------------------------");

    ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int"));
    ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));

    std::string key;
    ros::param::search("param_int",key);
    ROS_INFO("搜索键:%s",key.c_str());

    return 0;
}

3.参数服务器删除参数

/* 
    参数服务器操作之删除_C++实现:

    ros::NodeHandle
        deleteParam("键")
        根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false

    ros::param
        del("键")
        根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false


*/
#include "ros/ros.h"


int main(int argc, char *argv[])
{   
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"delete_param");

    ros::NodeHandle nh;
    bool r1 = nh.deleteParam("nh_int");
    ROS_INFO("nh 删除结果:%d",r1);

    bool r2 = ros::param::del("param_int");
    ROS_INFO("param 删除结果:%d",r2);

    return 0;
}

6. 常用命令

在 ROS 同提供了一些实用的命令行工具,可以用于获取不同节点的各类信息,常用的命令如下:

  • rosnode : 操作节点
  • rostopic : 操作话题
  • rosservice : 操作服务
  • rosmsg : 操作msg消息
  • rossrv : 操作srv消息
  • rosparam : 操作参数

作用
在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息。


rosnode

rosnode ping    测试到节点的连接状态
rosnode list    列出活动节点
rosnode info    打印节点信息
rosnode machine    列出指定设备上节点
rosnode kill    杀死某个节点
rosnode cleanup    清除不可连接的节点,清除无用节点,启动乌龟节点,然后 ctrl + c 关闭,该节点并没被彻底清除,可以使用 cleanup 清除节点

rostopic

用于显示有关ROS 主题的调试信息,包括发布者,订阅者,发布频率和ROS消息。它还包含一个实验性Python库,用于动态获取有关主题的信息并与之交互。

rostopic bw     显示主题使用的带宽
rostopic delay  显示带有 header 的主题延迟
rostopic echo   打印消息到屏幕
rostopic find   根据类型查找主题
rostopic hz     显示主题的发布频率
rostopic info   显示主题相关信息
rostopic list   显示所有活动状态下的主题
rostopic pub    将数据发布到主题
rostopic type   打印主题类型

示例如下:

  • rostopic list(-v)
    直接调用即可,控制台将打印当前运行状态下的主题名称
    rostopic list -v : 获取话题详情(比如列出:发布者和订阅者个数…)

  • rostopic pub
    可以直接调用命令向订阅者发布消息

为roboware 自动生成的 发布/订阅 模型案例中的 订阅者 发布一条字符串

rostopic pub /主题名称 消息类型 消息内容
rostopic pub /chatter std_msgs gagaxixi

为 小乌龟案例的 订阅者 发布一条运动信息

//只发布一次运动信息
rostopic pub /turtle1/cmd_vel geometry_msgs/Twist
 "linear:
  x: 1.0
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 2.0"

// 以 10HZ 的频率循环发送运动信息
rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist
 "linear:
  x: 1.0
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 2.0"

  • rostpic echo 话题
    获取指定话题当前发布的消息
  • rostopic info 话题
    获取当前话题的相关信息
    消息类型
    发布者信息
    订阅者信息
  • rostopic type 话题
    列出话题的消息类型
  • rostopic find 消息类型
    根据消息类型查找话题
  • rostopic delay
    列出消息头信息
  • rostopic hz
    列出消息发布频率
  • rostopic bw
    列出消息发布带宽

rosmsg

rosmsg是用于显示有关 ROS消息类型的 信息的命令行工具。

rosmsg show    显示消息描述
rosmsg info    显示消息信息
rosmsg list    列出所有消息
rosmsg md5    显示 md5 加密后的消息
rosmsg package    显示某个功能包下的所有消息
rosmsg packages    列出包含消息的功能包
  • rosmsg list
    会列出当前 ROS 中的所有 msg
  • rosmsg packages
    列出包含消息的所有包
  • rosmsg package 包名
    列出某个包下的所有msg
//列出turtlesim的所有消息体
rosmsg package turtlesim
  • rosmsg show 消息名称
    显示消息描述
rosmsg show turtlesim/Pose
结果:
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity
  • rosmsg info 消息名称
    作用与 rosmsg show 一样

rosservice

rosservice args 打印服务参数
rosservice call    使用提供的参数调用服务
rosservice find    按照服务类型查找服务
rosservice info    打印有关服务的信息
rosservice list    列出所有活动的服务
rosservice type    打印服务类型
rosservice uri    打印服务的 ROSRPC uri
  • rosservice list

列出所有活动的 service,显示所有的服务的名称

  • rosservice args 服务名
    打印服务参数
rosservice args /spawn
x y theta name

  • rosservice call
    调用服务

为小乌龟的案例生成一只新的乌龟

rosservice call /spawn "x: 1.0
y: 2.0
theta: 0.0
name: 'xxx'"
name: "xxx"

//生成一只叫 xxx 的乌龟
  • rosservice find
    根据消息类型获取话题
  • rosservice info 服务名称
    获取该服务的详情信息
  • rosservice type 服务名称
    获取该服务的消息类型
  • rosservice uri
    获取服务器 uri

rossrv

rossrv show    显示服务消息详情
rossrv info    显示服务消息相关信息
rossrv list    列出所有服务信息
rossrv md5    显示 md5 加密后的服务消息
rossrv package    显示某个包下所有服务消息
rossrv packages    显示包含服务消息的所有包

  • rossrv list
    会列出当前 ROS 中的所有 srv 消息

  • rossrv packages
    列出包含服务消息的所有包

  • rossrv package 包名
    列出某个包下的所有srv

//rossrv package 包名 
rossrv package turtlesim
  • rossrv show 消息名称
    显示消息描述
//rossrv show 消息名称
rossrv show turtlesim/Spawn
结果:
float32 x
float32 y
float32 theta
string name
---
string name
  • rossrv info 消息名称
    作用与 rossrv show 一致

rosparam

rosparam set    设置参数
rosparam get    获取参数
rosparam load    从外部文件加载参数
rosparam dump    将参数写出到外部文件
rosparam delete    删除参数
rosparam list    列出所有参数
  • rosparam list
    列出所有参数
  • rosparam set
    设置参数
rosparam set name huluwa

//再次调用 rosparam list 结果
/name
/rosdistro
/roslaunch/uris/host_helloros_virtual_machine__42911
/rosversion
/run_id
  • rosparam get
    获取参数
rosparam get name
//结果
huluwa
  • rosparam delete
    删除参数
rosparam delete name

//结果
//去除了name
  • rosparam load(先准备 yaml 文件)
    从外部文件加载参数
rosparam load xxx.yaml
  • rosparam dump
    将参数写出到外部文件
rosparam dump yyy.yaml

7. 实操

四个小实操

8. 通信机制比较

三种通信机制中,参数服务器是一种数据共享机制,可以在不同的节点之间共享数据,话题通信与服务通信是在不同的节点之间传递数据的,三者是ROS中最基础也是应用最为广泛的通信机制。

其中,话题通信和服务通信有一定的相似性也有本质上的差异,在此将二者做一下简单比较:

二者的实现流程是比较相似的,都是涉及到四个要素:

  • 要素1: 消息的发布方/客户端(Publisher/Client)

  • 要素2: 消息的订阅方/服务端(Subscriber/Server)

  • 要素3: 话题名称(Topic/Service)

  • 要素4: 数据载体(msg/srv)

二者的实现也是有本质差异的,具体比较如下:

Topic(话题)Service(服务)
通信模式发布/订阅请求/响应
同步性异步同步
底层协议ROSTCP/ROSUDPROSTCP/ROSUDP
缓冲区
时时性
节点关系多对多一对多(一个 Server)
通信数据msgsrv
使用场景连续高频的数据发布与接收:雷达、里程计偶尔调用或执行某一项特定功能:拍照、语音识别

9. action通信

概念

关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下:

机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。

乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信

案例

创建两个ROS 节点,服务器和客户端,客户端可以向服务器发送目标数据N(一个整型数据)服务器会计算 1 到 N 之间所有整数的和,这是一个循环累加的过程,返回给客户端,这是基于请求响应模式的,又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s,为了良好的用户体验,需要服务器在计算过程中,每累加一次,就给客户端响应一次百分比格式的执行进度,使用 action实现

(1) 定义action文件
在功能包下创建action目录,并创建sum.action文件,内容如下:

#目标数据
int32 num
---
#结果 最终响应变量
int32 res
---
#反馈 连续反馈变量
float64  progress_bar

(2)服务器代码

#include "ros/ros.h"
#include "example_ros_action/sumAction.h"
#include "actionlib/server/simple_action_server.h"

void myCallBack(const example_ros_action::sumGoalConstPtr &goalPtr,
                actionlib::SimpleActionServer<example_ros_action::sumAction> * server)
{
    //1.解析客户端提交的目标
    double goal_num = goalPtr->num;    
    ROS_INFO("the goal num is %f", goal_num);

    //2.产生连续反馈
    ros::Rate r(10);
    int sum = 0;
    for (int i = 0; i < goal_num; i++)
    {
        sum += i;
        r.sleep();

        //产生连续反馈
        example_ros_action::sumFeedback fdbk;
        fdbk.progress_bar = i / goal_num;
        server->publishFeedback(fdbk);
    }
    
    //3.最终结果响应
    example_ros_action::sumResult finalRes;
    finalRes.res = sum;
    server->setSucceeded(finalRes);
}

int main(int argc, char  *argv[])
{
    ros::init(argc, argv, "sum_action_server");
    ros::NodeHandle nh;

    /**
     * 参数一:NodeHandle对象
     * 参数二:服务名
     * 参数三:回调函数
     * 参数四:是否自动启动
    */
    actionlib::SimpleActionServer<example_ros_action::sumAction> server(
        nh, 
        "addints",
        boost::bind(&myCallBack, _1, &server),
        false
    );
    server.start();
    ROS_INFO("server start....");
    
    ros::spin();
    /* code */
    return 0;
}

(3)客户端代码

#include "ros/ros.h"
#include "example_ros_action/sumAction.h"
#include "actionlib/client/simple_action_client.h"

//响应成功的回调
void doneCallBack(const actionlib::SimpleClientGoalState &state,
            const example_ros_action::sumResultConstPtr &result)
{
    //判断响应状态
    if(state.state_ == state.SUCCEEDED){
        ROS_INFO("response success, sum = %d", result->res);
    }else{
        ROS_INFO("error");
    }
}

//客户端与服务器连接成功时回调
void activeCallBack(){
    ROS_INFO("connect to server succeed");
}


//连续反馈的回调
void feedbackCallBack(const example_ros_action::sumFeedbackConstPtr &fdbk){
    ROS_INFO("progress: %.2f", fdbk->progress_bar);
}

int main(int argc, char  *argv[])
{
    ros::init(argc, argv, "sum_action_client");
    ros::NodeHandle nh;

    actionlib::SimpleActionClient<example_ros_action::sumAction> client(nh, "addints");

    //等待服务器开启
    client.waitForServer();

    example_ros_action::sumGoal g;
    g.num = 100;
    /**
     * 参数一:设置目标值
     * 参数二:
     * 参数三:
    */
    client.sendGoal(g, &doneCallBack, &activeCallBack, &feedbackCallBack);
    ros::spin();
    return 0;
}

(4)编写配置文件

find_package(catkin REQUIRED COMPONENTS
  actionlib
  actionlib_msgs
  roscpp
  std_msgs
)

add_action_files(
  FILES
  demo.action
  sum.action
)

generate_messages(
  DEPENDENCIES
  actionlib_msgs
  std_msgs
)

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES example_ros_action
  CATKIN_DEPENDS actionlib actionlib_msgs roscpp std_msgs
#  DEPENDS system_lib
)

add_executable(demo01_action_server src/demo01_action_server.cpp)
add_executable(demo02_action_client src/demo02_action_client.cpp)
 
add_dependencies(demo01_action_server ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(demo02_action_client ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})


target_link_libraries(demo01_action_server
  ${catkin_LIBRARIES}
)
target_link_libraries(demo02_action_client
  ${catkin_LIBRARIES}
)

(5)编译并运行

二、ROS通信机制进阶

1 常用的API

首先,建议参考官方API文档或参考源码:

  • ROS节点的初始化相关API;
  • NodeHandle 的基本使用相关API;
  • 话题的发布方,订阅方对象相关API;
  • 服务的服务端,客户端对象相关API;
  • 时间相关API;
  • 日志输出相关API。

1.1 初始化

/** @brief ROS初始化函数。
 *
 * 该函数可以解析并使用节点启动时传入的参数(通过参数设置节点名称、命名空间...) 
 *
 * 该函数有多个重载版本,如果使用NodeHandle建议调用该版本。 
 *
 * \param argc 参数个数
 * \param argv 参数列表
 * \param name 节点名称,需要保证其唯一性,不允许包含命名空间
 * \param options 节点启动选项,被封装进了ros::init_options
 *
 */
void init(int &argc, char **argv, const std::string& name, uint32_t options = 0);

1.2 话题与服务相关对象

在 roscpp 中,话题和服务的相关对象一般由 NodeHandle 创建。

1.发布对象
发布对象获取:

/**
* \brief 根据话题生成发布对象
*
* 在 ROS master 注册并返回一个发布者对象,该对象可以发布消息
*
* 使用示例如下:
*
*   ros::Publisher pub = handle.advertise<std_msgs::Empty>("my_topic", 1);
*
* \param topic 发布消息使用的话题
*
* \param queue_size 等待发送给订阅者的最大消息数量
*
* \param latch (optional) 如果为 true,该话题发布的最后一条消息将被保存,并且后期当有订阅者连接时会将该消息发送给订阅者
*
* \return 调用成功时,会返回一个发布对象
*
*
*/
template <class M>
Publisher advertise(const std::string& topic, uint32_t queue_size, bool latch = false)

消息发布函数:

/**
* 发布消息          
*/
template <typename M>
void publish(const M& message) const

2.订阅对象
对象获取:

/**
   * \brief 生成某个话题的订阅对象
   *
   * 该函数将根据给定的话题在ROS master 注册,并自动连接相同主题的发布方,每接收到一条消息,都会调用回调
   * 函数,并且传入该消息的共享指针,该消息不能被修改,因为可能其他订阅对象也会使用该消息。
   * 
   * 使用示例如下:

void callback(const std_msgs::Empty::ConstPtr& message)
{
}

ros::Subscriber sub = handle.subscribe("my_topic", 1, callback);

   *
* \param M [template] M 是指消息类型
* \param topic 订阅的话题
* \param queue_size 消息队列长度,超出长度时,头部的消息将被弃用
* \param fp 当订阅到一条消息时,需要执行的回调函数
* \return 调用成功时,返回一个订阅者对象,失败时,返回空对象
* 

void callback(const std_msgs::Empty::ConstPtr& message){...}
ros::NodeHandle nodeHandle;
ros::Subscriber sub = nodeHandle.subscribe("my_topic", 1, callback);
if (sub) // Enter if subscriber is valid
{
...
}

*/
template<class M>
Subscriber subscribe(const std::string& topic, uint32_t queue_size, void(*fp)(const boost::shared_ptr<M const>&), const TransportHints& transport_hints = TransportHints())

3.服务对象
对象获取:

/**
* \brief 生成服务端对象
*
* 该函数可以连接到 ROS master,并提供一个具有给定名称的服务对象。
*
* 使用示例如下:
\verbatim
bool callback(std_srvs::Empty& request, std_srvs::Empty& response)
{
return true;
}

ros::ServiceServer service = handle.advertiseService("my_service", callback);
*
\endverbatim
*
* \param service 服务的主题名称
* \param srv_func 接收到请求时,需要处理请求的回调函数
* \return 请求成功时返回服务对象,否则返回空对象:
*
\verbatim
bool Foo::callback(std_srvs::Empty& request, std_srvs::Empty& response)
{
return true;
}
ros::NodeHandle nodeHandle;
Foo foo_object;
ros::ServiceServer service = nodeHandle.advertiseService("my_service", callback);
if (service) // Enter if advertised service is valid
{
...
}
\endverbatim

*/
template<class MReq, class MRes>
ServiceServer advertiseService(const std::string& service, bool(*srv_func)(MReq&, MRes&))

4.客户端对象
对象获取:

/** 
  * @brief 创建一个服务客户端对象
  *
  * 当清除最后一个连接的引用句柄时,连接将被关闭。
  *
  * @param service_name 服务主题名称
  */
 template<class Service>
 ServiceClient serviceClient(const std::string& service_name, bool persistent = false, 
                             const M_string& header_values = M_string())

请求发送函数:

/**
   * @brief 发送请求
   * 返回值为 bool 类型,true,请求处理成功,false,处理失败。
   */
  template<class Service>
  bool call(Service& service)

等待服务函数1:

ros::service::waitForService("addInts");
/**
 * ros::service::waitForService("addInts");
 * \brief 等待服务可用,否则一致处于阻塞状态
 * \param service_name 被"等待"的服务的话题名称
 * \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭
 * \return 成功返回 true,否则返回 false。
 */
ROSCPP_DECL bool waitForService(const std::string& service_name, ros::Duration timeout = ros::Duration(-1));

等待服务函数2:

client.waitForExistence();
/**
* client.waitForExistence();
* \brief 等待服务可用,否则一致处于阻塞状态
* \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭
* \return 成功返回 true,否则返回 false。
*/
bool waitForExistence(ros::Duration timeout = ros::Duration(-1));

1.3 回旋函数

在ROS程序中,频繁的使用了 ros::spin() 和 ros::spinOnce() 两个回旋函数,可以用于处理回调函数。

1.spinOnce()

/**
 * \brief 处理一轮回调
 *
 * 一般应用场景:
 *     在循环体内,处理所有可用的回调函数
 * 
 */
ROSCPP_DECL void spinOnce();

2.spin()

/** 
 * \brief 进入循环处理回调 
 */
ROSCPP_DECL void spin();

3.二者比较

相同点: 二者都用于处理回调函数;

不同点: ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在 ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行。

1.4 时间

ROS中时间相关的API是极其常用,比如:获取当前时刻、持续时间的设置、执行频率、休眠、定时器…都与时间相关。

1.时刻
获取时刻,或是设置指定时刻

ros::init(argc,argv,"hello_time");
ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
ros::Time right_now = ros::Time::now();//将当前时刻封装成对象
ROS_INFO("当前时刻:%.2f",right_now.toSec());//获取距离 1970年01月01日 00:00:00 的秒数
ROS_INFO("当前时刻:%d",right_now.sec);//获取距离 1970年01月01日 00:00:00 的秒数

ros::Time someTime(100,100000000);// 参数1:秒数  参数2:纳秒
ROS_INFO("时刻:%.2f",someTime.toSec()); //100.10
ros::Time someTime2(100.3);//直接传入 double 类型的秒数
ROS_INFO("时刻:%.2f",someTime2.toSec()); //100.30

2.持续时间
设置一个时间区间(间隔):

ROS_INFO("当前时刻:%.2f",ros::Time::now().toSec());

ros::Duration du(10);//持续10秒钟,参数是double类型的,以秒为单位
du.sleep();//按照指定的持续时间休眠

ROS_INFO("持续时间:%.2f",du.toSec());//将持续时间换算成秒
ROS_INFO("当前时刻:%.2f",ros::Time::now().toSec());

3.持续时间与时刻运算
为了方便使用,ROS中提供了时间与时刻的运算:

ROS_INFO("时间运算");
ros::Time now = ros::Time::now();
ros::Duration du1(10);
ros::Duration du2(20);
ROS_INFO("当前时刻:%.2f",now.toSec());

//1.time 与 duration 运算
ros::Time after_now = now + du1;
ros::Time before_now = now - du1;
ROS_INFO("当前时刻之后:%.2f",after_now.toSec());
ROS_INFO("当前时刻之前:%.2f",before_now.toSec());

//2.duration 之间相互运算
ros::Duration du3 = du1 + du2;
ros::Duration du4 = du1 - du2;
ROS_INFO("du3 = %.2f",du3.toSec());
ROS_INFO("du4 = %.2f",du4.toSec());
//PS: time 与 time 不可以加运算
// ros::Time nn = now + before_now;//异常

time 与 time 不可以加运算,但考研减运算
duration 与duration 考研加减运算
time 与duration 考研加减运算

4.设置运行频率

ros::Rate rate(1);//指定频率
while (true)
{
    ROS_INFO("-----------code----------");
    rate.sleep();//休眠,休眠时间 = 1 / 频率。
}

5.定时器
ROS 中内置了专门的定时器,可以实现与 ros::Rate 类似的效果:

ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败

 // ROS 定时器
 /**
* \brief 创建一个定时器,按照指定频率调用回调函数。
*
* \param period 时间间隔
* \param callback 回调函数
* \param oneshot 如果设置为 true,只执行一次回调函数,设置为 false,就循环执行,必须 spin。
* \param autostart 如果为true,返回已经启动的定时器,设置为 false,需要手动启动。
*/
 //Timer createTimer(Duration period, const TimerCallback& callback, bool oneshot = false,
 //                bool autostart = true) const;

 // ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing);
 ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,true);//只执行一次

 //-----------------------------------------------------------------------------//
 ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,false,false);//需要手动启动
 timer.start();
 ros::spin(); //必须 spin

定时器的回调函数:

void doSomeThing(const ros::TimerEvent &event){
    ROS_INFO("-------------");
    ROS_INFO("event:%s",std::to_string(event.current_real.toSec()).c_str());
}

1.5 其他函数

在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制,C++中可以通过 ros::ok() 来判断节点状态是否正常。导致节点退出的原因主要有如下几种:

  • 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
  • 同名节点启动,导致现有节点退出;
  • 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown())

另外,日志相关的函数也是极其常用的,在ROS中日志被划分成如下级别:

  • DEBUG(调试):只在调试时使用,此类消息不会输出到控制台
  • INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
  • WARN(警告):提醒一些异常情况,但程序仍然可以执行;
  • ERROR(错误):提示错误信息,此类错误会影响程序运行;
  • FATAL(严重错误):此类错误将阻止节点继续运行。

使用示例

ROS_DEBUG("hello,DEBUG"); //不会输出
ROS_INFO("hello,INFO"); //默认白色字体
ROS_WARN("Hello,WARN"); //默认黄色字体
ROS_ERROR("hello,ERROR");//默认红色字体
ROS_FATAL("hello,FATAL");//默认红色字体

2.ROS中的头文件与源文件

本节主要介绍ROS的C++实现中,如何使用头文件与源文件的方式封装代码,具体内容如下:

  1. 设置头文件,可执行文件作为源文件;
  2. 分别设置头文件,源文件与可执行文件。

在ROS中关于头文件的使用,核心内容在于CMakeLists.txt文件的配置,不同的封装方式,配置上也有差异。

2.1 自定义头文件调用

需求:设计头文件,可执行文件本身作为源文件。

流程:

  1. 编写头文件;
  2. 编写可执行文件(同时也是源文件);
  3. 编辑配置文件并执行。

1.头文件
在功能包下的include/功能包名 目录下新建头文件: hello.h,示例内容如下:

#ifndef _HELLO_H
#define _HELLO_H

namespace hello_ns{

class HelloPub {

public:
    void run();
};

}

#endif

注意:

在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性

"/home/用户/工作空间/src/功能包/include/**"

2.可执行文件
在 src 目录下新建文件:hello.cpp,示例内容如下:

#include "ros/ros.h"
#include "test_head/hello.h"

namespace hello_ns {

void HelloPub::run(){
    ROS_INFO("自定义头文件的使用....");
}

}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"test_head_node");
    hello_ns::HelloPub helloPub;
    helloPub.run();
    return 0;
}

3.配置文件
配置CMakeLists.txt文件,头文件相关配置如下:

include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

可执行配置文件配置方式与之前一致:

add_executable(hello src/hello.cpp)

add_dependencies(hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(hello
  ${catkin_LIBRARIES}
)

最后,编译并执行,控制台可以输出自定义的文本信息。

2.2 自定义源文件调用

需求: 设计头文件与源文件,在可执行文件中包含头文件。

流程:

  1. 编写头文件;
  2. 编写源文件;
  3. 编写可执行文件;
  4. 编辑配置文件并执行。

1.头文件
头文件设置于 2.1 类似,在功能包下的 include/功能包名 目录下新建头文件: haha.h,示例内容如下:

#ifndef _HAHA_H
#define _HAHA_H

namespace hello_ns {

class My {

public:
    void run();

};

}

#endif

注意:

在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性

"/home/用户/工作空间/src/功能包/include/**"

2.源文件
在 src 目录下新建文件:haha.cpp,示例内容如下:

#include "test_head_src/haha.h"
#include "ros/ros.h"

namespace hello_ns{

void My::run(){
    ROS_INFO("hello,head and src ...");
}

}

3.可执行文件
在 src 目录下新建文件: use_head.cpp,示例内容如下:

#include "ros/ros.h"
#include "test_head_src/haha.h"

int main(int argc, char *argv[])
{
    ros::init(argc,argv,"hahah");
    hello_ns::My my;
    my.run();
    return 0;
}

4.配置文件
头文件与源文件相关配置:

include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

## 声明C++add_library(head
  include/test_head_src/haha.h
  src/haha.cpp
)

add_dependencies(head ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(head
  ${catkin_LIBRARIES}
)

可执行文件配置:

add_executable(use_head src/use_head.cpp)

add_dependencies(use_head ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

#此处需要添加之前设置的 head 库
target_link_libraries(use_head
  head
  ${catkin_LIBRARIES}
)

三、ROS运行管理

ROS是多进程(节点)的分布式框架,一个完整的ROS系统实现:

可能包含多台主机;
每台主机上又有多个工作空间(workspace);
每个的工作空间中又包含多个功能包(package);
每个功能包又包含多个节点(Node),不同的节点都有自己的节点名称;
每个节点可能还会设置一个或多个话题(topic)…

在这里插入图片描述

1.ROS元功能包

场景:完成ROS中一个系统性的功能,可能涉及到多个功能包,比如实现了机器人导航模块,该模块下有地图、定位、路径规划…等不同的子级功能包。那么调用者安装该模块时,需要逐一的安装每一个功能包吗?

显而易见的,逐一安装功能包的效率低下,在ROS中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包(metapackage)。


概念
MetaPackage是Linux的一个文件管理系统的概念。是ROS中的一个虚包,里面没有实质性的内容,但是它依赖了其他的软件包,通过这种方法可以把其他包组合起来,我们可以认为它是一本书的目录索引,告诉我们这个包集合中有哪些子包,并且该去哪里下载。

例如:

sudo apt install ros-noetic-desktop-full 命令安装ros时就使用了元功能包,该元功能包依赖于ROS中的其他一些功能包,安装该包时会一并安装依赖。

作用
方便用户的安装,我们只需要这一个包就可以把其他相关的软件包组织到一起安装了。

实现

首先:新建一个功能包
然后:修改package.xml ,内容如下:

 <exec_depend>被集成的功能包</exec_depend>
 <exec_depend>被集成的功能包</exec_depend>
				。。。 。。。
 <exec_depend>被集成的功能包</exec_depend>
 <export>
   <metapackage />
 </export>

最后:修改 CMakeLists.txt,内容如下:

cmake_minimum_required(VERSION 3.0.2)
project(demo)
find_package(catkin REQUIRED)
catkin_metapackage()

PS:CMakeLists.txt 中不可以有换行。

2.ROS节点运行管理launch文件

2.1 标签 launch

<launch>标签是所有 launch 文件的根标签,充当其他标签的容器
1.属性
deprecated = “弃用声明”

告知用户当前 launch 文件已经弃用

2.子级标签
以下所有其它标签都是launch的子级

2.2 标签 node

<node>标签用于指定 ROS 节点。

需要注意的是: roslaunch 命令不能保证按照 node 的声明顺序来启动节点(节点的启动是多进程的)

1.属性

  • pkg=“包名”
    节点所属的包
  • type=“nodeType”
    节点类型(与之相同名称的可执行文件)
  • name=“nodeName”
    节点名称(在 ROS 网络拓扑中节点的名称)
  • args=“xxx xxx xxx” (可选)
    将参数传递给节点
  • machine=“机器名”
    在指定机器上启动节点
  • respawn=“true | false” (可选)
    如果节点退出,是否自动重启
  • respawn_delay=" N" (可选)
    如果 respawn 为 true, 那么延迟 N 秒后启动节点
  • required=“true | false” (可选)
    该节点是否必须,如果为 true,那么如果该节点退出,将杀死整个 roslaunch
  • ns=“xxx” (可选)
    在指定命名空间 xxx 中启动节点
  • clear_params=“true | false” (可选)
    在启动前,删除节点的私有空间的所有参数
  • output=“log | screen” (可选)
    日志发送目标,可以设置为 log 日志文件,或 screen 屏幕,默认是 log

2.子级标签

  • env 环境变量设置
  • remap 重映射节点名称
  • rosparam 参数设置
  • param 参数设置

2.3 标签 include

include标签用于将另一个 xml 格式的 launch 文件导入到当前文件
1.属性

  • file=“$(find 包名)/xxx/xxx.launch”
    要包含的文件路径

  • ns=“xxx” (可选)
    在指定命名空间导入文件

2.子级标签

  • env 环境变量设置
  • arg 将参数传递给被包含的文件

2.4 标签 remap

用于话题重命名
1.属性

  • from=“xxx”
    原始话题名称

  • to=“yyy”
    目标名称

2.子级标签

2.5 标签 param

<param>标签主要用于在参数服务器上设置参数,参数源可以在标签中通过 value 指定,也可以通过外部文件加载,在标签中时,相当于私有命名空间。

1.属性

  • name=“命名空间/参数名”
    参数名称,可以包含命名空间
  • value=“xxx” (可选)
    定义参数值,如果此处省略,必须指定外部文件作为参数源
  • type=“str | int | double | bool | yaml” (可选)
    指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:
    1.如果包含 ‘.’ 的数字解析未浮点型,否则为整型
    2.“true” 和 “false” 是 bool 值(不区分大小写)
    3.其他是字符串

2.子级标签

2.6 标签 rosparam

<rosparam>标签可以从 YAML 文件导入参数,或将参数导出到 YAML 文件,也可以用来删除参数,标签在标签中时被视为私有。

1.属性

  • command=“load | dump | delete” (可选,默认 load)
    加载、导出或删除参数

  • file=“$(find xxxxx)/xxx/yyy…”
    加载或导出到的 yaml 文件

  • param=“参数名称”

  • ns=“命名空间” (可选)

2.子级标签

2.7 标签 group

<group>标签可以对节点分组,具有 ns 属性,可以让节点归属某个命名空间
1.属性

  • ns=“名称空间” (可选)
  • clear_params=“true | false” (可选)
    启动前,是否删除组名称空间的所有参数(慎用…此功能危险)

2.子级标签
除了launch 标签外的其他标签

2.8 标签 arg

1.属性

  • name=“参数名称”
  • default=“默认值” (可选)
  • value=“数值” (可选)
    不可以与 default 并存
  • doc=“描述”
    参数说明

2.子级标签

3.示例
launch文件传参语法实现,hello.lcaunch

<launch>
    <arg name="xxx" />
    <param name="param" value="$(arg xxx)" />
</launch>

命令行调用launch传参

roslaunch hello.launch xxx:=值

3.ROS工作空间覆盖

3.1 概念

所谓工作空间覆盖,是指不同工作空间中,存在重名的功能包的情形。
ROS 开发中,会自定义工作空间且自定义工作空间可以同时存在多个,可能会出现一种情况: 虽然特定工作空间内的功能包不能重名,但是自定义工作空间的功能包与内置的功能包可以重名或者不同的自定义的工作空间中也可以出现重名的功能包,那么调用该名称功能包时,会调用哪一个呢?
比如:自定义工作空间A存在功能包 turtlesim,自定义工作空间B也存在功能包 turtlesim,当然系统内置空间也存在turtlesim,如果调用turtlesim包,会调用哪个工作空间中的呢?


3.2 案例测试

实现
0.新建工作空间A与工作空间B,两个工作空间中都创建功能包: turtlesim。

1.在 ~/.bashrc 文件下追加当前工作空间的 bash 格式如下:

source /home/用户/路径/工作空间A/devel/setup.bash
source /home/用户/路径/工作空间B/devel/setup.bash

2.新开命令行:source .bashrc加载环境变量

3.查看ROS环境环境变量echo $ROS_PACKAGE_PATH

结果:自定义工作空间B:自定义空间A:系统内置空间

4.调用命令:roscd turtlesim会进入自定义工作空间B

原因
ROS 会解析 .bashrc 文件,并生成 ROS_PACKAGE_PATH ROS包路径,该变量中按照 .bashrc 中配置设置工作空间优先级,在设置时需要遵循一定的原则:ROS_PACKAGE_PATH 中的值,和 .bashrc 的配置顺序相反,后配置的优先级更高,如果更改自定义空间A与自定义空间B的source顺序,那么调用时,将进入工作空间A。

结论
功能包重名时,会按照 ROS_PACKAGE_PATH 查找,配置在前的会优先执行。

隐患
存在安全隐患,比如当前工作空间B优先级更高,意味着当程序调用 turtlesim 时,不会调用工作空间A也不会调用系统内置的 turtlesim,如果工作空间A在实现时有其他功能包依赖于自身的 turtlesim,而按照ROS工作空间覆盖的涉及原则,那么实际执行时将会调用工作空间B的turtlesim,从而导致执行异常,出现安全隐患。


BUG 说明:

当在 .bashrc 文件中 source 多个工作空间后,可能出现的情况,在 ROS PACKAGE PATH 中只包含两个工作空间,可以删除自定义工作空间的 build 与 devel 目录,重新 catkin_make,然后重新载入 .bashrc 文件,问题解决。

4.ROS节点名称重名

场景:ROS 中创建的节点是有名称的。在ROS的网络拓扑中,是不可以出现重名的节点的,因为假设可以重名存在,那么调用时会产生混淆,这也就意味着,不可以启动重名节点或者同一个节点启动多次。,在ROS中如果启动重名节点的话,之前已经存在的节点会被直接关闭。

在ROS中给出的解决策略是使用命名空间名称重映射


命名空间就是为名称添加前缀,名称重映射是为名称起别名。 两种策略的实现途径有多种:

  • rosrun 命令
  • launch 文件
  • 编码实现

4.1 rosrun设置命名空间与重映射

1.rosrun设置命名空间
(1)语法
rosrun 包名 节点名 __ns:=新名称

rosrun turtlesim turtlesim_node __ns:=/xxx
rosrun turtlesim turtlesim_node __ns:=/yyy

(2)运行结果
rosnode list查看节点信息,显示结果:

/xxx/turtlesim
/yyy/turtlesim

2.rosrun名称重映射
(1)语法
rosrun 包名 节点名 __name:=新名称

rosrun turtlesim  turtlesim_node __name:=t1 
#或者
rosrun turtlesim   turtlesim_node /turtlesim:=t1
rosrun turtlesim  turtlesim_node __name:=t2 
#或者
rosrun turtlesim   turtlesim_node /turtlesim:=t2

(2)运行结果
rosnode list 查看节点信息,显示结果:

/t1
/t2

3.rosrun命名空间与名称重映射叠加
(1)语法
rosrun 包名 节点名 __ns:=新名称 __name:=新名称

rosrun turtlesim turtlesim_node __ns:=/xxx __name:=tn

(2)运行结果
rosnode list查看节点信息,显示结果:

/xxx/tn

4.2 launch文件设置命名空间与重映射

介绍 launch 文件的使用语法时,在 node 标签中有两个属性: name 和 ns,二者分别是用于实现名称重映射与命名空间设置的。

launch文件如下:

<launch>
    <node pkg="turtlesim" type="turtlesim_node" name="t1" />
    <node pkg="turtlesim" type="turtlesim_node" name="t2" />
    <node pkg="turtlesim" type="turtlesim_node" name="t1" ns="hello"/>
</launch>

rosnode list查看节点信息,显示结果:

/t1
/t2
/t1/hello

4.3 编码设置命名空间与重映射

1.重映射(起别名)
核心代码:ros::init(argc,argv,"zhangsan",ros::init_options::AnonymousName);
会在名称后面添加时间戳。

2.命名空间
核心代码

  std::map<std::string, std::string> map;
  map["__ns"] = "xxxx";
  ros::init(map,"wangqiang");

5.ROS话题名称设置

在 ROS 中节点终端,不同的节点之间通信都依赖于话题,话题名称也可能出现重复的情况,这种情况下,系统虽然不会抛出异常,但是可能导致订阅的消息非预期的,从而导致节点运行异常。这种情况下需要将两个节点的话题名称由相同修改为不同。

又或者,两个节点是可以通信的,两个节点之间使用了相同的消息类型,但是由于,话题名称不同,导致通信失败。这种情况下需要将两个节点的话题名称由不同修改为相同。

在实际应用中,按照逻辑,有些时候可能需要将相同的话题名称设置为不同,也有可能将不同的话题名设置为相同。在ROS中给出的解决策略与节点名称重命类似,也是使用名称重映射或为名称添加前缀。根据前缀不同,有全局、相对、和私有三种类型之分。

  • 全局(参数名称直接参考ROS系统,与节点命名空间平级)
  • 相对(参数名称参考的是节点的命名空间,与节点名称平级)
  • 私有(参数名称参考节点名称,是节点名称的子级)

比节点重名更复杂些,不单是使用命名空间作为前缀、还可以使用节点名称最为前缀。两种策略的实现途径有多种:

  • rosrun 命令
  • launch 文件
  • 编码实现

案例
在ROS中提供了一个比较好用的键盘控制功能包: ros-noetic-teleop-twist-keyboard,该功能包,可以控制机器人的运动,作用类似于乌龟的键盘控制节点,可以使用 sudo apt install ros-noetic-teleop-twist-keyboard 来安装该功能包,然后执行: rosrun teleop_twist_keyboard teleop_twist_keyboard.py,在启动乌龟显示节点,不过此时前者不能控制乌龟运动,因为,二者使用的话题名称不同,前者使用的是 cmd_vel话题,后者使用的是 /turtle1/cmd_vel话题。需要将话题名称修改为一致,才能使用。

5.1 rosrun设置话题重映射

语法 rorun 包名 节点名 话题名:=新话题名称
1.方案1
将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel
启动键盘控制节点:

rosrun teleop_twist_keyboard teleop_twist_keyboard.py /cmd_vel:=/turtle1/cmd_vel

启动乌龟显示节点:

 rosrun turtlesim turtlesim_node

二者可以实现正常通信

2.方案2
将乌龟显示节点的话题设置为 /cmd_vel

启动键盘控制节点:

rosrun teleop_twist_keyboard teleop_twist_keyboard.py

启动乌龟显示节点:

 rosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel

二者可以实现正常通信

5.2 launch文件设置话题重映射

launch 文件设置话题重映射语法:

<node pkg="xxx" type="xxx" name="xxx">
    <remap from="原话题" to="新话题" />
</node>

实现teleop_twist_keyboard与乌龟显示节点通信方案由两种:
1.方案1
将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel

<launch>
    <node pkg="turtlesim" type="turtlesim_node" name="t1" />
    <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key">
        <remap from="/cmd_vel" to="/turtle1/cmd_vel" />
    </node>
</launch>

2.方案2
将乌龟显示节点的话题设置为 /cmd_vel

<launch>
    <node pkg="turtlesim" type="turtlesim_node" name="t1">
        <remap from="/turtle1/cmd_vel" to="/cmd_vel" />
    </node>
    <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key" />
</launch>

5.3 编码设置话题名称

话题的名称与节点的命名空间、节点的名称是有一定关系的,话题名称大致可以分为三种类型:

  • 全局(话题参考ROS系统,与节点命名空间平级)
  • 相对(话题参考的是节点的命名空间,与节点名称平级)
  • 私有(话题参考节点名称,是节点名称的子级)

演示准备
1.初始化节点设置一个节点名称ros::init(argc,argv,“hello”)
2.设置不同类型的话题
3.启动节点时,传递一个 __ns:= xxx
4.节点启动后,使用 rostopic 查看话题信息

1.全局名称
以 / 开头的名称,和节点名称无关

//全局
ros::Publisher publisher = nh.advertise<std_msgs::String>("/hao",1000);
//,也可以设置自己的命名空间
ros::Publisher publisher = nh.advertise<std_msgs::String>("/yyy/hao",1000);

2.相对名称
非 / 开头

//若节点的命名空间为xxx(节点名称/xxx/publish) 则话题为/xxx/hao
ros::Publisher publisher = nh.advertise<std_msgs::String>("hao",1000);
//若节点的命名空间为xxx(/xxx/publish) 则话题为/xxx/yyy/hao
ros::Publisher publisher = nh.advertise<std_msgs::String>("yyy/hao",1000);

3.私有名称
以~开头的名称
私有需要创建特定的 NodeHandle

ros::NodeHandle nh("~");
//若节点的命名空间为xxx(节点名称/xxx/publish) 则话题为/xxx/publish/hao
ros::Publisher publisher = nh.advertise<std_msgs::String>("hao",1000);
//若节点的命名空间为xxx(节点名称/xxx/publish) 则话题为/xxx/publish/yyy/hao
ros::Publisher publisher = nh.advertise<std_msgs::String>("yyy/hao",1000);

注意:如果私有的NodeHandle创建的话题以 / 开头,则生成的话题为全局话题
ros::Publisher publisher = nh.advertise<std_msgs::String>(“/yyy/hao”,1000);

6.ROS参数名称设置

当参数名称重名时,那么就会产生覆盖。
关于参数重名的处理,没有重映射实现,为了尽量的避免参数重名,都是使用为参数名添加前缀的方式,实现类似于话题名称,有全局、相对、和私有三种类型之分。

  • 全局(参数名称直接参考ROS系统,与节点命名空间平级)

  • 相对(参数名称参考的是节点的命名空间,与节点名称平级)

  • 私有(参数名称参考节点名称,是节点名称的子级)

6.1 rosrun设置参数

语法
rosrun 包名 节点名称 _参数名:=参数值
1.设置参数
启动乌龟显示节点,并设置参数 A = 100

rosrun turtlesim turtlesim_node _A:=100

2.运行
rosparam list查看节点信息,显示结果:

/turtlesim/A
/turtlesim/background_b
/turtlesim/background_g
/turtlesim/background_r

结果显示,参数A前缀节点名称,也就是说rosrun执行设置参数参数名使用的是私有模式

6.2 launch文件设置参数

通过 launch 文件设置参数的方式前面已经介绍过了,可以在 node 标签外,或 node 标签中通过 param 或 rosparam 来设置参数。

  • 在 node 标签外设置的参数是全局性质的,参考的是 / ,
  • 在 node 标签中设置的参数是私有性质的,参考的是 /命名空间/节点名称。

案例 请回看本章第2节的2.5和2.6知识点

6.3 编码设置参数

在 C++ 中,可以使用 ros::param 或者 ros::NodeHandle 来设置参数。

1.ros::param设置参数
设置参数调用API是ros::param::set(),该函数中,参数1传入参数名称参数2是传入参数值。参数1中参数名称设置时,
如果以 / 开头,那么就是全局参数;
如果以 ~ 开头,那么就是私有参数;
既不以 / 也不以 ~ 开头,那么就是相对参数。

代码示例:

ros::param::set("/set_A",100); //全局,和命名空间以及节点名称无关
ros::param::set("set_B",100); //相对,参考命名空间
ros::param::set("~set_C",100); //私有,参考命名空间与节点名称

运行时,假设设置的 namespace 为 xxx,节点名称为 yyy,使用 rosparam list 查看:

/set_A
/xxx/set_B
/xxx/yyy/set_C

2.ros::NodeHandle设置参数
设置参数时,首先需要创建 NodeHandle 对象,然后调用该对象的 setParam 函数,该函数参数1为参数名参数2为要设置的参数值

如果参数名以 / 开头,那么就是全局参数;

如果参数名不以 / 开头,那么,该参数是相对参数还是私有参数与NodeHandle 对象有关。

  • 如果NodeHandle 对象创建时如果是调用的默认的无参构造,那么该参数是相对参数,
  • 如果NodeHandle 对象创建时是使用: ros::NodeHandle nh(“~”),那么该参数就是私有参数。

代码示例:

ros::NodeHandle nh;
nh.setParam("/nh_A",100); //全局,和命名空间以及节点名称无关

nh.setParam("nh_B",100); //相对,参考命名空间

ros::NodeHandle nh_private("~");
nh_private.setParam("nh_C",100);//私有,参考命名空间与节点名称

运行时,假设设置的 namespace 为 xxx,节点名称为 yyy,使用 rosparam list 查看:

/nh_A
/xxx/nh_B
/xxx/yyy/nh_C

7.ROS分布式通信

一个运行中的ROS系统可以包含分布在多台计算机上多个节点。根据系统的配置方式,任何节点可能随时需要与任何其他节点进行通信。

因此,ROS对网络配置有某些要求:

  • 所有端口上的所有机器之间必须有完整的双向连接。
  • 每台计算机必须通过所有其他计算机都可以解析的名称来公告自己。

实现
1.准备

先要保证不同计算机处于同一网络中,最好分别设置固定IP,如果为虚拟机,需要将网络适配器改为桥接模式。

2.配置文件修改
分别修改不同计算机的 /etc/hosts 文件,在该文件中加入对方的IP地址和计算机名:
主机端:(启动roscore的节点称为主机)

从机的IP    从机计算机名

从机端:

主机的IP    主机计算机名

设置完毕,可以通过 ping 命令测试网络通信是否正常。

3.配置主机IP
配置主机的 IP 地址
~/.bashrc 追加

export ROS_MASTER_URI=http://主机IP:11311
export ROS_HOSTNAME=主机IP

4.配置从机IP
配置从机的 IP 地址,从机可以有多台,每台都做如下设置:
~/.bashrc 追加

export ROS_MASTER_URI=http://主机IP:11311
export ROS_HOSTNAME=从机IP

测试

  1. 主机启动 roscore(必须)

  2. 主机启动订阅节点,从机启动发布节点,测试通信是否正常

  3. 反向测试,主机启动发布节点,从机启动订阅节点,测试通信是否正常

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值