ROS学习笔记

 参考文档:

Introduction · Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程

官方导航文档:导航/教程/机器人设置 - ROS 维基

1.ROS概述与环境搭建

1.1环境配置

1)终端Terminator的安装与启用

安装方法:sudo apt install terminator

启动方法:Ctrl+Alt+T

2) VSCODE的安装与使用

vscode 下载:Documentation for Visual Studio Code

历史版本下载链接: Visual Studio Code September 2022

卸载:sudo dpkg --purge  code

为正常使用,需要安装以下常用插件:

 

3)vscode使用方法

1.创建 ROS 工作空间

mkdir -p demon_ws/src(必须得有 src)
cd xxx_ws
catkin_make

2.启动 vscode

进入demon_ws,启动vscode

cd demon_ws
code .

3.vscode 中编译 ros

快捷键 ctrl + shift + B 调用编译,选择:catkin_make:build

可以点击配置设置为默认,修改.vscode/tasks.json 文件

{
// 有关 tasks.json 格式的文档,请参见
    // https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
        {
            "label": "catkin_make:debug", //代表提示的描述性信息
            "type": "shell",  //可以选择shell或者process,如果是shell代码是在shell里面运行一个命令,如果是process代表作为一个进程来运行
            "command": "catkin_make",//这个是我们需要运行的命令
            "args": [],//如果需要在命令后面加一些后缀,可以写在这里,比如-DCATKIN_WHITELIST_PACKAGES=“pac1;pac2”
            "group": {"kind":"build","isDefault":true},
            "presentation": {
                "reveal": "always"//可选always或者silence,代表是否输出信息
            },
            "problemMatcher": "$msCompile"
        }
    ]
}

4.创建ROS功能包

选定 src 右击 ---> create catkin package

设置包名,添加依赖(roscpp rospy std_msgs)

5.编译代码实现功能

6.修改与src同级CMakeLists.txt文件

add_executable(步骤3的源文件名
  src/步骤3的源文件名.cpp
)
target_link_libraries(步骤3的源文件名
  ${catkin_LIBRARIES}
)
7.编译(Ctrl+shift+b)快捷键

8.启动ROS 

roscore

9.source ~/devel/setup.bash

10.rosrun 功能包名 文件名

注:解决中文乱码问题
setlocale(LC_ALL, "");
在代码中加入上述代码即可解决。

#include "ros/ros.h"
int main(int argc, char  *argv[])
{
    ros::init(argc,argv,"hello");
    setlocale(LC_ALL, "");
    ROS_INFO("HAHA,因瑞");
    return 0;
}

4)vscode快捷键

1.打开终端  Ctrl+~

2.编译 Ctrl+Shift+b(自己定义的)

1.2launch文件(一次启动多节点)

1)实现

1.选定功能包右击 ---> 添加 launch 文件夹

2.选定 launch 文件夹右击 ---> 添加 launch 文件

3.编辑launch文件内容

<launch>
    <node pkg="helloworld" type="demo_hello" name="hello" output="screen" />
    <node pkg="turtlesim" type="turtlesim_node" name="t1"/>
    <node pkg="turtlesim" type="turtle_teleop_key" name="key1" />
</launch>

  • node ---> 包含的某个节点

  • pkg -----> 功能包

  • type ----> 被运行的节点文件

  • name --> 为节点命名

  • output-> 设置日志的输出目标

4.运行launch文件

 roslauch  功能包名  文件名

5.一次启动多个节点

1.3ros架构

1)ROS工作空间目录结构

WorkSpace --- 自定义的工作空间

    |--- build:编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。

    |--- devel:开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等。

    |--- src: 源码

        |-- package:功能包(ROS基本单元)包含多个节点、库与配置文件,包名所有字母小写,只能由字母、数字与下划线组成

            |-- CMakeLists.txt 配置编译规则,比如源文件、依赖项、目标文件

            |-- package.xml 包信息,比如:包名、版本、作者、依赖项...(以前版本是 manifest.xml)

            |-- scripts 存储python文件

            |-- src 存储C++源文件

            |-- include 头文件

            |-- msg 消息通信格式文件

            |-- srv 服务通信格式文件

            |-- action 动作格式文件

            |-- launch 可一次性运行多个节点 

            |-- config 配置信息

        |-- CMakeLists.txt: 编译的基本配置

1.4 ROS系统常用指令

1.增

catkin_create_pkg 自定义包名 依赖包 === 创建新的ROS功能包

sudo apt install xxx === 安装 ROS功能包

2.删

sudo apt purge xxx ==== 删除某个功能包

3.查

rospack list === 列出所有功能包

rospack find 包名 === 查找某个功能包是否存在,如果存在返回安装路径

roscd 包名 === 进入某个功能包

rosls 包名 === 列出某个包下的文件

apt search xxx === 搜索某个功能包

4.改

rosed 包名 文件名 === 修改功能包文件

需要安装 vim

比如:rosed turtlesim Color.msg

5.执行

5.1roscore

roscore === 是 ROS 的系统先决条件节点和程序的集合, 必须运行 roscore 才能使 ROS 节点进行通信。

roscore 将启动:

  • ros master

  • ros 参数服务器

  • rosout 日志节点

用法:

roscore

或(指定端口号)

roscore -p xxxx

补充:删除roscore进程

killall -9 roscore

killall -9 rosmaster

5.2rosrun

rosrun 包名 可执行文件名 === 运行指定的ROS节点

比如:rosrun turtlesim turtlesim_node

5.3roslaunch

roslaunch 包名 launch文件名 === 执行某个包下的 launch 文件

1.5 ROS计算图

1)计算图简介

前面介绍的是ROS文件结构,是磁盘上 ROS 程序的存储结构,是静态的,而 ros 程序运行之后,不同的节点之间是错综复杂的,ROS 中提供了一个实用的工具:rqt_graph。

rqt_graph能够创建一个显示当前系统运行情况的动态图形。ROS 分布式系统中不同进程需要进行数据交互,计算图可以以点对点的网络形式表现数据交互过程。rqt_graph是rqt程序包中的一部分。

2)计算图启动方式

rosrun rqt_graph rqt_graph (或者直接输入rqt_graph )

3)计算图安装

如果未安装则在终端(terminal)中输入

$ sudo apt install ros-<distro>-rqt
$ sudo apt install ros-<distro>-rqt-common-plugins

请使用你的ROS版本名称(比如:kinetic、melodic、Noetic等)来替换掉<distro>。

例如当前版本是 Noetic,就在终端窗口中输入

$ sudo apt install ros-noetic-rqt
$ sudo apt install ros-noetic-rqt-common-plugins

第 2 章 ROS通信机制

机器人是一种高度复杂的系统性实现,在机器人上可能集成各种传感器(雷达、摄像头、GPS...)以及运动控制实现,为了解耦合,在ROS中每一个功能点都是一个单独的进程,每一个进程都是独立运行的。更确切的讲,ROS是进程(也称为Nodes)的分布式框架。 因为这些进程甚至还可分布于不同主机,不同主机协同工作,从而分散计算压力。不过随之也有一个问题: 不同的进程是如何通信的?也即不同进程间如何实现数据交换的?在此我们就需要介绍一下ROS中的通信机制了。

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

  • 话题通信(发布订阅模式)

  • 服务通信(请求响应模式)

  • 参数服务器(参数共享模式)

2.1话题通信

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

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

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

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

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

1)理论模型 

 注:1.master可以根据话题建立发布者和订阅者之间的连接;

        2.步骤0和步骤1没有先后关系;

        3.talker和listener都可以存在多个;

        4.talker和listener建立连接后,master就可以关闭了;

        5.上述实现流程已经封装了,以后直接调用即可(话题通信应用时的关注点(1)话题设置(2)发布者实现(3)订阅者实现(4)传输的信息)

2)案例实操

需求:

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

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 发布方
  2. 接收方
  3. 数据(此处为普通文本)

流程:

     0.创建工作空间,功能包,发布者实现文件(以C/C++为例)

  1. 编写发布方实现;
#include "ros/ros.h"
#include "std_msgs/String.h"
/*
      发布方实现:
                   1.包含头文件;
                                 ROS中文本类型--.>std_msgs/String.h
                    2.初始化ROS节点;
                    3.创建节点句柄;
                    4.创建发布者对象;
                    5.编写发布逻辑并发布数据。
*/
int main(int argc, char *argv[])
{
    ros::init(argc,argv,"ErGouZi");//2.初始化ROS节点;
    ros::NodeHandle nh;//   3.创建节点句柄;
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10);  //4.创建发布者对象;
    //5.编写发布逻辑并发布数据。
    std_msgs::String msg;//创建被发布的消息msg;
    while(ros::ok())
    {
        msg.data="hello";
        pub.publish(msg);
    }//创建发布消息
      return 0;
}

检验编写发布者实现是否正确步骤

    1)开启ROS

           命令:roscore

     2)配置源文件,开启节点

       命令:1.source ~/devel/setup.bash

                  2.rosrun 功能包名     文件名

      3)rostopic echo 话题名

输出如下:

2.编写订阅方实现;

#include "ros/ros.h"
#include "std_msgs/String.h"
/*
      订阅者实现:
                   1.包含头文件;
                                 ROS中文本类型--.>std_msgs/String.h
                    2.初始化ROS节点;
                    3.创建节点句柄;
                    4.创建订阅者对象;
                    5.处理订阅到的数据

*/
void doMSG(const std_msgs::String::ConstPtr  &msg){
    ROS_INFO("%s",msg->data.c_str());
}

int main(int argc, char *argv[])
{
    ros::init(argc,argv,"CuiHua");//2.初始化ROS节点;
    ros::NodeHandle nh;//   3.创建节点句柄;
    ros::Subscriber sub = nh.subscribe<std_msgs::String>("fang",10,doMSG);  //4.创建发布者对象;
    //5.处理订阅到的数据
    ros::spin();
      return 0;
}

3.编辑配置文件;

4.编译并执行

查看计算图得到如下关系:

3)话题通信自定义msg

在 ROS 通信协议中,数据载体是一个较为重要组成部分,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. 编译生成可以被 Python 或 C++ 调用的中间文件

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>
  <!-- 
  exce_depend 以前对应的是 run_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)

4)调用自定义msg信息

1.vscode 配置

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

{
    "configurations": [
        {
            "browse": {
                "databaseFilename": "",
                "limitSymbolsToIncludedHeaders": true
            },
            "includePath": [
                "/opt/ros/noetic/include/**",
                "/usr/include/**",
               "/home/yinrui/demon_ws/devel/include/pub_sub/person.h" //配置 head 文件的路径 
            ],

            "name": "ROS",
            "intelliSenseMode": "gcc-x64",
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}

2.发布方实现

/*
    需求: 循环发布人的信息

*/

#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;
}

3.订阅方实现

/*
    需求: 订阅人的信息

*/

#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_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)



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.启动订阅节点。

2.2服务通信

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

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

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

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

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

1)服务通信理论模型

服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:

  • ROS master(管理者)
  • Server(服务端)
  • Client(客户端)

ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

整个流程由以下步骤实现:

0.Server注册

Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

1.Client注册

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

2.ROS Master实现信息匹配

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

3.Client发送请求

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

4.Server发送响应

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

注意:

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

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

2)服务通信自定义srv

需求:

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

流程:

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

  1. 按照固定格式创建srv文件

  2. 编辑配置文件

  3. 编译生成中间文件

1.定义srv文件

服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---分割,具体实现如下:

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

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

2.编辑配置文件

package.xml中添加编译依赖与执行依赖

  <build_depend>message_generation</build_depend>
  <exec_depend>message_runtime</exec_depend>
  <!-- 
  exce_depend 以前对应的是 run_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
)

3.编译

编译后的中间文件查看:

C++ 需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)

Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/srv)

3)服务通信自定义srv调用

需求:

编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 服务端
  2. 客户端
  3. 数据

流程:

  1. 编写服务端实现;
  2. 编写客户端实现;
  3. 编辑配置文件;
  4. 编译并执行。

0.vscode配置

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

SRV文件名为ADD.srv

配置后生成ADD.h文件

1.服务端


/*
    需求: 
        编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
        服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
        客户端再解析
    服务器实现:
        1.包含头文件
        2.初始化 ROS 节点
        3.创建 ROS 句柄
        4.创建 服务 对象
        5.回调函数处理请求并产生响应
        6.由于请求有多个,需要调用 ros::spin()
*/
#include "ros/ros.h"
#include "srv_ex/ADD.h"//包含头文件为“功能包名/生成.h文件”
 
// bool 返回值由于标志是否处理成功
bool doReq(srv_ex::ADD::Request& req,
          srv_ex::ADD::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.创建 服务 对象
    ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
    ROS_INFO("服务已经启动....");
    //     5.回调函数处理请求并产生响应
    //     6.由于请求有多个,需要调用 ros::spin()
    ros::spin();
    return 0;
}

2.客户端

/*/*
    需求: 
        编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
        服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
        客户端再解析
    服务器实现:
        1.包含头文件
        2.初始化 ROS 节点
        3.创建 ROS 句柄
        4.创建 客户端 对象
        5.请求服务,接收响应
*/
// 1.包含头文件
#include "ros/ros.h"
#include "srv_ex/ADD.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<srv_ex::ADD>("AddInts");
    //等待服务启动成功
    //方式1
    ros::service::waitForService("AddInts");
    //方式2
    // client.waitForExistence();
    // 5.组织请求数据
   srv_ex::ADD 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;
}

3.配置 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}
)

4.执行

流程:

  • 需要先启动服务:rosrun 包名 服务

  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

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

注意:

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

优化:

在客户端发送请求前添加:client.waitForExistence();

或:ros::service::waitForService("AddInts");

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

此处可以使用 launch 文件优化,但是需要注意 args 传参特点

2.3参数服务期

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

导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径

上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:

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

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

1)参数服务器概论模型

整个流程由以下步骤实现:

1.Talker 设置参数

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

2.Listener 获取参数

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

3.ROS Master 向 Listener 发送参数值

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


参数可使用数据类型:

  • 32-bit integers

  • booleans

  • strings

  • doubles

  • iso8601 dates

  • lists

  • base64-encoded binary data

  • 字典

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

2) 参数操作

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

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

  • ros::NodeHandle

  • ros::param

下面为具体操作演示

1.增、改参数(改参数可以通过在下面增加同“键”名参数的不同值实现)

#include  "ros/ros.h"

/*
        需要实现参数的新增与修改
        需求:首先设置机器人的共享参数,类型,半径
                      再修改半径
         实现:
               ros::NodeHandle
                        setparam()
                ros::param
                        set()
*/
int main(int argc, char  *argv[])
{
    ros::init(argc,argv,"set_param");//创建节点
    ros::NodeHandle nh;//创建句柄
    //句柄法参数增
    nh.setParam("type","yinruishuaige");
    nh.setParam("radius",0.15);
   //ros::param发增参
   ros::param::set("type_ros","yinrui");
   ros::param::set("radius_ros",0.2);
    return 0;
}

2.查参数

#include "ros/ros.h"

/*
  参数服务器操作之查询_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 类似
*/

int main(int argc, char  *argv[])
{
     setlocale(LC_ALL,"");//解决乱码问题
     ros::init(argc,argv,"get_param");

    //NodeHandle--------------------------------------------------------
    
    ros::NodeHandle nh;
    double radius = nh.param("/radius",15.0);//默认值类型要和返回值相同
    ROS_INFO ("所得半径radius=%f",radius);

    return 0;
}

结果:

3.删参数

#include "ros/ros.h"
/*
实现:
 ros::NodeHandle
                        deleteParam("key")(删除成功返回1否则返回0)
                ros::param
                        del("key")
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");//解决乱码问题
    ros::init(argc,argv,"Param_del");
    ros::NodeHandle nh;
    bool   back1 = nh.deleteParam("radius");
    if (back1)
    {
        ROS_INFO("删除成功!");
    }
    else
        ROS_INFO("不存在改参数!");
    return 0;
}

3.ROS常用命令

机器人系统中启动的节点少则几个,多则十几个、几十个,不同的节点名称各异,通信时使用话题、服务、消息、参数等等都各不相同,一个显而易见的问题是: 当需要自定义节点和其他某个已经存在的节点通信时,如何获取对方的话题、以及消息载体的格式呢?

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

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

作用

和之前介绍的文件系统操作命令比较,文件操作命令是静态的,操作的是磁盘上的文件,而上述命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息。

3.1rosnode

rosnode 是用于获取节点信息的命令(如图打开小海龟显示,键盘控制节点)


1)rosnode list    列出活动节点

2)rosnode ping    测试到节点的连接状态(可以知道节点连接是否正常)

 如图所示则海龟节点连接正常

3)rosnode info    打印节点信息(重要)

4)rosnode machine    列出指定设备上节点
5)rosnode kill    杀死某个节点
6)rosnode cleanup    清除不可连接的节点

3.2rostopic

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

1)rostopic bw     显示主题使用的带宽
2)rostopic delay  显示带有 header 的主题延迟
3)rostopic echo   打印消息到屏幕(将话题发布消息输出,测试时可当作订阅方)


4)rostopic find   根据类型查找主题
5)rostopic hz     显示主题的发布频率
6)rostopic info   显示主题相关信息


7)rostopic list   显示所有活动状态下的主题 


8)rostopic pub    将数据发布到主题(测试时可当作发布方)

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"
//只发布一次运动信息

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"
// 以 10HZ 的频率循环发送运动信息
 

9)rostopic type   打印主题类型

3.3rosmsg

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

rosmsg list

会列出当前 ROS 中的所有 msg

rosmsg packages

列出包含消息的所有包

rosmsg package

列出某个包下的所有msg

//rosmsg package 包名 
rosmsg package turtlesim
Copy

rosmsg show

显示消息描述

//rosmsg show 消息名称
rosmsg show turtlesim/Pose
结果:
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity

rosmsg info

作用与 rosmsg show 一样

rosmsg md5 (资料:ROS/Technical Overview - ROS Wiki)

一种校验算法,保证数据传输的一致性

3.4rosservice

rosservice包含用于列出和查询ROSServices的rosservice命令行工具。

调用部分服务时,如果对相关工作空间没有配置 path,需要进入工作空间调用 source ./devel/setup.bash

打开海龟节点(rosrun turtlesim turtlesim_node) 

1)rosservice list

列出所有活动的 service

2)rosservice args

打印服务参数

3)rosservice call

调用服务

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

 

4)rosservice find

根据消息类型获取话题

5)rosservice info

获取服务话题详情

6)rosservice type

获取消息类型

7)rosservice uri

获取服务器 uri

3.5rossrv

rossrv是用于显示有关ROS服务类型的信息的命令行工具,与 rosmsg 使用语法高度雷同。

1)rossrv list

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

2)rossrv packages

列出包含服务消息的所有包

3)rossrv package

列出某个包下的所有msg

//rossrv package 包名 
rossrv package turtlesim


4)rossrv show

显示消息描述

//rossrv show 消息名称
rossrv show turtlesim/Spawn
结果:
float32 x
float32 y
float32 theta
string name
---
string name



5)rossrv info

作用与 rossrv show 一致

6)rossrv md5

对 service 数据使用 md5 校验(加密)

3.6rosparam

rosparam包含rosparam命令行工具,用于使用YAML编码文件在参数服务器上获取和设置ROS参数(打开乌龟节点)

1)rosparam list

列出所有参数

2) rosparam set

修改参数

初始化小海龟背景(蓝色)

 修改后小海龟背景颜色变为红色

 3)rosparam get

获取参数

4) rosparam delete

删除参数

5)rosparam load(先准备 yaml 文件)

从外部文件加载参数

rosparam load xxx.yaml

6)rosparam dump

将参数写出到外部文件

rosparam dump yyy.yaml

4.ROS常用API(C++)

API:API就是用于负责一个程序和其他软件的沟通所预先定义的函数。

参考文档:

4.1初始化API

注:argc个数等于传入参数个数加函数名既等于n+1;

使用:1.argc与argv的使用

按照ROS给定格式进行传参,可以得到一个全局参数

 设置了length参数

(erGouzi为节点名)

2.options参数的使用

 若按上图所示进行设置,可同一时间多次启动相同节点,否则同一时间只能启动一次节点。

 

4.2话题服务相关API

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

NodeHandle有一个重要作用是可以用于设置命名空间。

1)发布方API

 4.3回旋函数API

C++

在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() 后的语句可以执行

4.4有关时间的API

1)当前时间API

ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
ros::Time right_now = ros::Time::now();//将当前时刻封装成对象

2)持续时间API

#include "ros/ros.h"
int main(int argc, char  *argv[])
{
    
    ros::init(argc,argv,"time_test");
    ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
    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());
    return 0;
}

du.sleep();语句使得下面的命令在10S后才执行!

中文乱码()setlocale(LC_ALL, )可解决

3)时间与时刻间的计算

#include "ros/ros.h"
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"" );
    ros::init(argc,argv,"time_test");
    ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
 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());
    return 0;
}

4)设置运行频率 

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

5)定时器

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

#include "ros/ros.h"

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


int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"" );
    ros::init(argc,argv,"time_test");
    ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败

 // ROS 定时器
 /**
* \brief 创建一个定时器,按照指定频率调用回调函数。
*
* \param period 时间间隔
* \param callback 回调函数
* \param oneshot 如果设置为 true,只执行一次回调函数,设置为 false,就循环执行。
* \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

    return 0;
}

写完记得保存,记得保存,记得保存!!!!

5.其他API

1)关闭节点API

C++中可以通过 ros::ok() 来判断节点状态是否正常,而 python 中则通过 rospy.is_shutdown() 来实现判断,导致节点退出的原因主要有如下几种:

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

2)输出日志API

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

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

 

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

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

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

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

1)自定义头文件调用

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

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

1.头文件

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

#ifndef _HELLO_H
#define _HELLO_H

namespace hello_ns{

class HelloPub {

public:
    void run();
};

}

#endif

2.添加头文件路径

3.可执行文件

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

#include "ros/ros.h"
#include "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;
}

4.配置文件CMakeLists.txt

配置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)自定义源文件调用

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

流程:

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

1.头文件

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

#ifndef _HELLO_H
#define _HELLO_H

namespace hello_ns{

class HelloPub {

public:
    void run();
};

}

#endif

2.添加头文件路径

3.源文件

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

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

namespace hello_ns{

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

}

4.可执行文件

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

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

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

5.配置文件

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

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}
)

6.ROS元功能包

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

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


概念

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

例如:

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

还有一些常见的MetaPackage:navigation moveit! turtlebot3 ....

作用

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

实现:

首先:新建一个功能包

然后:修改package.xml ,内容如下:

 <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 中不可以有换行。

以Navagation元功能包为例:

 

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

补充参考博客:ROS笔记(一)xxx.launch文件详解_Littlelsu的博客-CSDN博客_launch

1) launch文件标签之launch

<launch>标签是所有 launch 文件的根标签,充当其他标签的容器

1.属性

  • deprecated = "弃用声明"

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

2.子级标签

所有其它标签都是launch的子级

2)launch文件标签之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 参数设置

3)launch文件标签之include

include标签用于将另一个 xml 格式的 launch 文件导入到当前文件以实现功能的复用

属性
file=“$(find 包名)/xxx/xxx.launch”

要包含的文件路径

ns=“xxx” (可选)

在指定命名空间导入文件

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

4)launch文件标签之remap

用于话题重命名

属性

  • from=“xxx”

    原始话题名称

  • to=“yyy”

    目标名称

子级标签

需求: 使用Ros内置的键盘控制节点去控制乌龟的运动

实现:

话题不同需要修改一样

将乌龟控制话题改为键盘控制话题:

 5)launch文件标签之param

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

1.属性

  • name="命名空间/参数名"

    参数名称,可以包含命名空间

  • value="xxx" (可选)

    定义参数值,如果此处省略,必须指定外部文件作为参数源

  • type="str | int | double | bool | yaml" (可选)

    指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:

    • 如果包含 '.' 的数字解析为浮点型,否则为整型

    • "true" 和 "false" 是 bool 值(不区分大小写)

    • 其他是字符串

2.子级标签

使用:向参数服务器设置参数

​ 格式1. launch下,node外

<param name="param_A" type="int" value="100" />
    <node...> 
          ...
     </node...>

生成param_A参数

格式2. node下

<node...> 
    <param name="param_B" type="double" value="10.5" />
</node...>

 

格式2与格式1相比, 多了以NODE节点名为前缀的命名空间。

6)launch文件标签之rosparam

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

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

1.属性

  • name="命名空间/参数名"

    参数名称,可以包含命名空间

  • value="xxx" (可选)

    定义参数值,如果此处省略,必须指定外部文件作为参数源

  • type="str | int | double | bool | yaml" (可选)

    指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:

    • 如果包含 '.' 的数字解析未浮点型,否则为整型

    • "true" 和 "false" 是 bool 值(不区分大小写)

    • 其他是字符串

2.子级标签

8.ROS节点名称重命名

8.1rosrun节点名称重命名法 

1)命名空间重命名法

设置命名空间演示:

语法: rosrun 包名 节点名 __ns:=新名称

同时运行:

rosrun turtlesim turtlesim_node

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

rosnode list

2)名称重映射

语法: rosrun 包名 节点名 __name:=新名称

案例:

rosrun turtlesim  turtlesim_node __name:=t1 
rosrun turtlesim  turtlesim_node __name:=t2

同时启动两个同样节点

结果:

3)rosrun命名空间与名称重映射叠加

语法: rosrun 包名 节点名 __ns:=新名称 __name:=新名称

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

结果:/xxx/tn

8.2launch设置命名空间与重映射

介绍 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>

9.ROS话题名称设置

在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话题。需要将话题名称修改为一致,才能使用,如何实现?

 9.1rosrun设置话题重映射

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

二者可以实现正常通信

2)launch文件设置话题重映射

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

9.3编码设置话题名称

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

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

结合编码演示具体关系。

1)全局:

rostopic list :

/chatter

/yyy/chatter

话题名与命名空间没关系。

 2)相对:

rostopic list:

/xxx/yyy/chatter (xxx为命名空间)

3)私有:

需要创捷特别的节点

rostopic list:

/xxx/hello/chatter (xxx为命名空间.hello为节点名)

10.ROS参数名称设置

在ROS中节点名称话题名称可能出现重名的情况,同理参数名称也可能重名。

当参数名称重名时,那么就会产生覆盖,如何避免这种情况?

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

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

设置参数的方式也有三种:

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

1)rosrun设置参数

rosrun turtlesim turtlesim_node _A:=100

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

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

2)launch文件设置参数

通过 launch 文件设置参数的方式前面已经介绍过了,可以在 node 标签外,或 node 标签中通过 param 或 rosparam 来设置参数。在 node 标签外设置的参数是全局性质的,参考的是 / ,在 node 标签中设置的参数是私有性质的,参考的是 /命名空间/节点名称。

<launch>

    <param name="p1" value="100" />
    <node pkg="turtlesim" type="turtlesim_node" name="t1">
        <param name="p2" value="100" />
    </node>

</launch>

2.运行

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

/p1
/t1/p1

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



10.分布式通信

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

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

  • 所有端口上的所有机器之间必须有完整的双向连接。

  • 每台计算机必须通过所有其他计算机都可以解析的名称来公告自己。

在需要进行环境配置的时候,看Autolabor文档即可。

11.TF坐标变换

坐标系:ROS 中是通过坐标系统开标定物体的,确切的将是通过右手坐标系来标定的。

作用:在 ROS 中用于实现不同坐标系之间的点或向量的转换。

 11.1坐标msg消息

订阅发布模型中数据载体 msg 是一个重要实现,首先需要了解一下,在坐标转换实现中常用的 msg:geometry_msgs/TransformStampedgeometry_msgs/PointStamped

前者用于传输坐标系相关位置信息,后者用于传输某个坐标系内坐标点的信息。在坐标变换中,频繁的需要使用到坐标系的相对关系以及坐标点信息。

1.geometry_msgs/TransformStamped

命令行键入:rosmsg info geometry_msgs/TransformStamped

std_msgs/Header header                     #头信息
  uint32 seq                                #|-- 序列号
  time stamp                                #|-- 时间戳
  string frame_id                            #|-- 坐标 ID
string child_frame_id                    #子坐标系的 id
geometry_msgs/Transform transform        #坐标信息
  geometry_msgs/Vector3 translation        #偏移量
    float64 x                                #|-- X 方向的偏移量
    float64 y                                #|-- Y 方向的偏移量
    float64 z                                #|-- Z 方向上的偏移量
  geometry_msgs/Quaternion rotation        #四元数
    float64 x                                
    float64 y                                
    float64 z                                
    float64 w

2.geometry_msgs/PointStamped

命令行键入:rosmsg info geometry_msgs/PointStamped

std_msgs/Header header                    #头
  uint32 seq                                #|-- 序号
  time stamp                                #|-- 时间戳
  string frame_id                            #|-- 所属坐标系的 id
geometry_msgs/Point point                #点坐标
  float64 x                                    #|-- x y z 坐标
  float64 y
  float64 z

 

11.2静态坐标变换

所谓静态坐标变换,是指两个坐标系之间的相对位置是固定的。

需求描述:

现有一机器人模型,核心构成包含主体与雷达,各对应一坐标系,坐标系的原点分别位于主体与雷达的物理中心,已知雷达原点相对于主体原点位移关系如下: x 0.2 y0.0 z0.5。当前雷达检测到一障碍物,在雷达坐标系中障碍物的坐标为 (2.0 3.0 5.0),请问,该障碍物相对于主体的坐标是多少?

实现分析:

  1. 坐标系相对关系,可以通过发布方发布
  2. 订阅方,订阅到发布的坐标系相对关系,再传入坐标点信息(可以写死),然后借助于 tf 实现坐标变换,并将结果输出

实现流程:C++ 与 Python 实现流程一致

  1. 新建功能包,添加依赖
  2. 编写发布方实现
  3. 编写订阅方实现
  4. 执行并查看结果

C++实现:

1).创建功能包

创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs

2)发布方实现

1.代码:

#include "ros/ros.h"
#include "tf2_ros/static_transform_broadcaster.h"
#include "geometry_msgs/TransformStamped.h"
#include "tf2/LinearMath/Quaternion.h"

int main(int argc, char *argv[])
{
    ros::init(argc,argv,"publish");
    tf2_ros::StaticTransformBroadcaster  boradcaster;//创建静态坐标转换广播器
    geometry_msgs::TransformStamped ts;//创建坐标系信息
    ts.header.seq =100;
    ts.header.frame_id="base_link";
    ts.header.stamp=ros::Time::now();
    ts.child_frame_id="laser";
    ts.transform.translation.x=0.2;//子坐标系相对主坐标系的偏移位置
    ts.transform.translation.y=0.0;
    ts.transform.translation.z=0.5;
    tf2::Quaternion   qtn;//设置四元数:将 欧拉角数据转换成四元数
    qtn.setRPY(0,0,0);
    ts.transform.rotation.x=qtn.getX();
    ts.transform.rotation.y=qtn.getY();
    ts.transform.rotation.z=qtn.getZ();
    ts.transform.rotation.w=qtn.getW();
    boradcaster.sendTransform(ts);//广播静态坐标信息
    ros::spin();
    return 0;
}

2.配置CMakeLists.txt

3)订阅方实现

1.代码:

#include "ros/ros.h"
#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"
#include "geometry_msgs/PointStamped.h"
#include "tf2_geometry_msgs/tf2_geometry_msgs.h"//调用transform需要该头文件

int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"sub");
    ros::NodeHandle nh;
    tf2_ros::Buffer buffer;//创建坐标点信息缓存区
    tf2_ros::TransformListener listener(buffer);
    geometry_msgs::PointStamped point_laser;//创建两个坐标点信息
    geometry_msgs::PointStamped point_base;
    point_laser.header.seq = 1000;
    point_laser.header.frame_id = "laser";
    point_laser.header.stamp = ros::Time::now();
    point_laser.point.x=1.0;
    point_laser.point.y=2.0;
    point_laser.point.z=7.3;
    ros::Duration(2).sleep();//短暂休眠不然要报错
    ros::Rate r(1);
    while (ros::ok)
    {
     point_base=buffer.transform(point_laser,"base_link");//转换信息
     ROS_INFO("转换后的数据:(%.2f,%.2f,%.2f),参考的坐标系是:%s",point_base.point.x,point_base.point.y,point_base.point.z,point_base.header.frame_id.c_str() );
     r.sleep();
    }
    return 0;
}

2.配置文件(同上)

4)执行查看结果

结果符合预期! 

快捷方式:

当坐标系之间的相对位置固定时,那么所需参数也是固定的: 父系坐标名称、子级坐标系名称、x偏移量、y偏移量、z偏移量、x 翻滚角度、y俯仰角度、z偏航角度,实现逻辑相同,参数不同,那么 ROS 系统就已经封装好了专门的节点,使用方式如下:

rosrun tf2_ros static_transform_publisher x偏移量 y偏移量 z偏移量 z偏航角度 y俯仰角度 x翻滚角度 父级坐标系 子级坐标系

示例:rosrun tf2_ros static_transform_publisher 0.2 0 0.5 0 0 0 /baselink /laser

也建议使用该种方式直接实现静态坐标系相对信息发布。

11.3动态坐标变换

所谓动态坐标变换,是指两个坐标系之间的相对位置是变化的

需求描述:

启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘控制乌龟运动,将两个坐标系的相对位置动态发布。

1)小乌龟节点启动

1. 启动小乌龟模拟器节点

rosrun turtlesim turtlesim_node

2. 启动小乌龟键盘输入节点

rosrun turtlesim turtle_teleop_key

实现分析:

  1. 乌龟本身不但可以看作坐标系,也是世界坐标系中的一个坐标点

  2. 订阅 turtle1/pose,可以获取乌龟在世界坐标系的 x坐标、y坐标、偏移量以及线速度和角速度

  3. 将 pose 信息转换成 坐标系相对信息并发布

实现流程:C++ 与 Python 实现流程一致

  1. 新建功能包,添加依赖

  2. 创建坐标相对关系发布方(同时需要订阅乌龟位姿信息)

  3. 创建坐标相对关系订阅方

  4. 执行

C++实现:

2)创建功能包

创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim

3)发布方实现

代码:

#include "ros/ros.h"
#include "turtlesim/Pose.h"
#include "tf2_ros/transform_broadcaster.h"
#include "geometry_msgs/TransformStamped.h"
#include "tf2/LinearMath/Quaternion.h"

void dopose(const turtlesim::Pose::ConstPtr&  pose)
{
tf2_ros::TransformBroadcaster broadcaster;//创建动态广播broadcaster
geometry_msgs::TransformStamped ts;
ts.header.frame_id = "world";//设置ts头信息
ts.header.stamp = ros::Time::now();
ts.child_frame_id="turtle";
ts.transform.translation.x=pose->x;//j将位姿信息传入坐标点中
ts.transform.translation.y=pose->y;
ts.transform.translation.z=0.0;//乌龟是2D的没有Z方向速度
tf2::Quaternion qtn;
qtn.setRPY(0,0,pose->theta);//乌龟只有z轴方向的旋转
ts.transform.rotation.x=qtn.getX();
ts.transform.rotation.y=qtn.getY();
ts.transform.rotation.z=qtn.getZ();
ts.transform.rotation.w=qtn.getW();
broadcaster.sendTransform(ts);
ROS_INFO("动态坐标变换节点开启成功!");
}

int main(int argc, char  *argv[]) 
{
    ros::init(argc,argv,"dtzb");
    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("/turtle1/pose",100,dopose);
    ros::spin();
    return 0;
}

4)订阅方实现:

与静态坐标变换类似。

11.4多坐标变换

需求描述:

现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,son1 相对于 world,以及 son2 相对于 world 的关系是已知的,求 son1原点在 son2中的坐标,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标

实现分析:

  1. 首先,需要发布 son1 相对于 world,以及 son2 相对于 world 的坐标消息
  2. 然后,需要订阅坐标发布消息,并取出订阅的消息,借助于 tf2 实现 son1 和 son2 的转换
  3. 最后,还要实现坐标点的转换

实现流程:C++ 与 Python 实现流程一致

  1. 新建功能包,添加依赖

  2. 创建坐标相对关系发布方(需要发布两个坐标相对关系)

  3. 创建坐标相对关系订阅方

  4. 执行

C++实现:

1.创建功能包

创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim

2.发布方实现

可通过Launch文件实现三个静态坐标的关系

代码:

<launch>
    <node pkg="tf2_ros" type="static_transform_publisher" name="z1" args="2.0 0.0 0.0 0 0 0 /world /z1" output="screen" />
    <node pkg="tf2_ros" type="static_transform_publisher" name="z2" args="5.0 0.0 0.0 0 0 0 /world /z2" output="screen" />
</launch>

3.订阅方实现

代码:(编译遇见了问题,大致思路懂的即可)

#include"ros/ros.h"
 #include "tf2_ros/buffer.h"
 #include "tf2_ros/transform_listener.h"
 #include "geometry_msgs/TransformStamped.h"
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"dzb");
    ros::NodeHandle nh;
    tf2_ros::Buffer buffer;
    tf2_ros::TransformListener listener(buffer);
    ros::Rate r(1);
    while  (ros::ok())
    {
        static geometry_msgs::TransformStamped tsf=buffer.lookupTransform("z2","z1",ros::Time(0));//关键函数
        ROS_INFO ("父系坐标系:%s,子系坐标系:%s,相对坐标关系%f,%f,%f",tsf.header.frame_id.c_str(),tsf.child_frame_id.c_str,tsf.transform.translation.x,tsf.transform.translation.y,tsf.transform.translation.z);
        r.sleep();
    } 
    return 0;
}

结果:

11.5查看坐标关系的方法

1)rviz

1.打开launch文件,设置三个坐标点

2.打开rviz查看结果

2)rosrun tf2_tools view_frames.py

首先调用rospack find tf2_tools查看是否包含该功能包,如果没有,请使用如下命令安装:

sudo apt install ros-noetic-tf2-tools

用rosrun tf2_tools view_frames.py生成的PDF图查看坐标点关系

参考文档:Introduction · Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程

12.rosbag

13.代价地图组成

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值