代码模板

c++ 模板

1
2
3
4
5
6
7
8
9
10
#include "ros/ros.h"

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

return 0;
}

python 模板

1
2
3
4
5
6
7
#! /usr/bin/env python

import rospy

if __name__ == "__main__":
rospy.init_node("")

ROS环境搭建与初认识

ROS安装

具体细节看教程

采用虚拟机安装 ubuntu,再安装 ROS

虚拟机软件:virtualbox(免费) 官网下载软件安装包以及拓展包

ubuntu版本:ubuntu-20.04.6 镜像文件下载地址

ROS版本:Noetic

1
sudo apt install ros-noetic-desktop-full

配置环境变量,方便在任意终端中使用 ROS:

1
2
echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc
source ~/.bashrc

安装构建依赖:

1
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential

测试ROS安装环境:

  1. 启动三个命令行(ctrl + alt + T)
  2. 命令行1键入:roscore
  3. 命令行2键入:rosrun turtlesim turtlesim_node(此时会弹出图形化界面)
  4. 命令行3键入:rosrun turtlesim turtle_teleop_key(可以控制小乌龟运动)

以下命令行内容中[ ]包围的为替换内容

ROS简单程序

ROS中的程序即便使用不同的编程语言,实现流程也大致类似,以当前HelloWorld程序为例,实现流程大致如下:

  1. 先创建一个工作空间;
  2. 再创建一个功能包;
  3. 编辑源文件;
  4. 编辑配置文件;
  5. 编译并执行

上述流程中,C++和Python只是在步骤3和步骤4的实现细节上存在差异,其他流程基本一致

  1. 创建工作空间(demo01_ws)并初始化

    1
    2
    3
    mkdir -p [demo01_ws]/src
    cd [demo01_ws]
    catkin_make

    初始化后 demo01_ws文件夹中有src build devel三个文件夹

  2. 进入 src 创建 ros 包(helloworld)并添加依赖(roscpp rospy std_msgs)

    1
    2
    cd src
    catkin_create_pkg [helloworld] roscpp rospy std_msgs

c++

  1. 进入 ros 包的 src 目录编辑源文件

    创建helloworld_c.cpp文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include "ros/ros.h"

    int main(int argc, char *argv[])
    {
    //执行 ros 节点初始化
    ros::init(argc,argv,"hello");
    //创建 ros 节点句柄(非必须)
    ros::NodeHandle n;
    //控制台输出 hello world
    ROS_INFO("hello world!");

    return 0;
    }
  2. 编辑 ros 包下的 Cmakelist.txt文件

    1
    2
    3
    4
    5
    6
    add_executable([helloworld_c]
    src/helloworld_c.cpp
    )
    target_link_libraries([自定义名称]
    ${catkin_LIBRARIES}
    )

    分别在初始的136行与149行

    通过自定义名称可以映射到src/helloworld_c.cpp文件,一般取名和文件名相同

  3. 进入工作空间目录(demo01_ws)打开终端并编译

    1
    catkin_make

    如果正常会显示[100%]

  4. 执行

    先启动命令行1:

    1
    roscore

    再启动命令行2:

    1
    2
    source ./devel/setup.bash
    rosrun [helloworld] [helloworld_c]

    helloworld是ros包名称,helloworld_c是cpp文件映射

    命令行输出: hello world!

    注意,这个source ./devel/setup.bash 命令只针对当前的终端窗口可用

    将下面这条指令添加进.bashrc文件(根目录的隐藏文件),即可全局使用

    1
    source ~/[demo01_ws]/devel/setup.bash

python

  1. 进入 ros 包添加 scripts 目录并编辑 python 文件

    创建helloworld_p.py

    1
    2
    3
    4
    5
    6
    7
    8
    #! /usr/bin/env python
    ## 指定解释器

    import rospy

    if __name__ == "__main__":
    rospy.init_node("hello_p");
    rospy.loginfo("hello world! by python");
  2. 为 python 文件添加可执行权限

    在scripts文件夹下打开终端

    1
    2
    chmod +x *.py
    ll //查看已有权限的内容
  3. 编辑 ros 包下的 CamkeList.txt 文件

    1
    2
    3
    catkin_install_python(PROGRAMS scripts/[helloworld_p].py
    DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
    )

    python3需要这一步

    在初始的162行

  4. 进入工作空间目录(demo01_ws)打开终端并编译

    1
    catkin_make

    如果正常会显示[100%]并且输出Built target [自定义名称]

  5. 执行

    先启动命令行1:

    1
    roscore

    再启动命令行2:

    1
    2
    source ./devel/setup.bash
    rosrun [helloworld] [helloworld_p.py]

    命令行输出: hello world! by python

    注意,与c++不同,python文件没有自定义映射名称,在rosrun时需要写入完整文件名

ROS集成开发环境

终端

在 ROS 中,需要频繁的使用到终端,且可能需要同时开启多个窗口,推荐使用Terminator

安装:

1
sudo apt install terminator

常用快捷键:

1
2
3
4
5
6
Alt+Up                          //移动到上面的终端
Alt+Down //移动到下面的终端
Alt+Left //移动到左边的终端
Alt+Right //移动到右边的终端
Ctrl+Shift+O //水平分割终端
Ctrl+Shift+E //垂直分割终端

VScode

下载:vscode 下载 选择适用于linux系统的.deb文件

安装:

  • 方式1:双击安装即可(或右击选择安装) [可能会出现没反应或闪退行为]
  • 方式2:sudo dpkg -i xxxx.deb 复制具体的下载文件名称

vscode 集成 ROS 插件:c++,CMake,python,ROS

vscode 使用_基本配置:

  1. 创建 ROS 工作空间

    1
    2
    3
    mkdir -p [工作空间名称]/src(必须得有 src)
    cd [工作空间名称]
    catkin_make
  2. 启动 vscode

    1
    code .
  3. vscode 中编译 ros

    快捷键 ctrl + shift + B 调用编译,选择catkin_make:build右边的小齿轮配置

    修改.vscode/tasks.json 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    // 有关 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

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

  5. C++ 实现

    在功能包的 src 下新建 cpp 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include "ros/ros.h"

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

    ROS_INFO("hello vscode");

    return 0;
    }

    如果没有代码提示

    修改 .vscode/c_cpp_properties.json,设置 “cppStandard”: “c++17”

    光标指向括号内,ctrl+shift+空格提示函数格式

  6. python 实现

    在 功能包 下新建 scripts 文件夹,添加 python 文件

    1
    2
    3
    4
    5
    6
    7
    #! /usr/bin/env python

    import rospy

    if __name__ == "__main__":
    rospy.init_node("hello_p")
    rospy.loginfo("hello vscode! it's python")

    在scripts文件夹下打开终端并添加可执行权限

    1
    chmod +x *.py
  7. 配置 CMakeLists.txt

  8. 编译执行

    编译:ctrl + shift + B

    执行:和之前一致,只是可以在 VScode 中添加终端

    首先执行:source ./devel/setup.bash

    c++:rosrun [包名] [自定义名称]

    python:rosrun [包名] [源文件名称]

    如果不编译直接执行 python 文件,会抛出异常

    解决方案:

    1. 第一行解释器声明,可以使用绝对路径定位到 python3 的安装路径 #! /usr/bin/python3(不建议)
    2. 创建一个链接符号到 python 命令:sudo ln -s /usr/bin/python3 /usr/bin/python(建议)

launch文件

解决需要启动多个节点的效率问题

实现:

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

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

  3. 编辑 launch 文件内容

    以小乌龟示例程序启动为例:

    1
    2
    3
    4
    5
    6
    7
    <launch>
    <!-- 添加被执行节点-->
    <!-- 乌龟GUI -->
    <node pkg="turtlesim" type="turtlesim_node" name="turtle_GUI" />
    <node pkg="turtlesim" type="turtle_teleop_key" name="turtle_key" />
    <node pkg="hello_vscode" type="hello_vscode_c" name="hello" output="screen"/>
    </launch>
    • node —> 包含的某个节点
    • pkg -----> 功能包
    • type ----> 被运行的节点文件
    • name --> 为节点命名
    • output-> 设置日志的输出目标
  4. 运行 launch 文件

    1
    roslaunch [功能包名] [launch文件名]
  5. 运行结果:一次性启动了多个节点

ROS架构

文件系统

ROS_file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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. 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. 执行

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

    roscore 将启动:

    • ros master
    • ros 参数服务器
    • rosout 日志节点

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

    比如:rosrun turtlesim turtlesim_node

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

计算图

rqt_graph能够创建一个显示当前系统运行情况的动态图形

计算图可以以点对点的网络形式表现数据交互过程

运行程序后,启动新终端,键入: rqt_graph 或 rosrun rqt_graph rqt_graph,可以看到网络拓扑图,该图可以显示不同节点之间的关系

ROS通信机制

话题通信

话题通信是ROS中使用频率最高的一种通信模式,基于发布订阅模式,即一个节点发布消息,另一个节点订阅该消息

以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据

以此类推,像雷达、摄像头、GPS… 等等一些传感器数据的采集都使用了话题通信

话题通信适用于不断更新、少逻辑处理的数据传输场景

image-20250519215104543_compressed (1)
  • 在工作空间下功能包中,基本每一个可执行文件都会初始化一个节点(node),并唯一命名
  • 节点之间通过话题(topic)进行通信
  • 话题类型就是发布的消息(msg)
  • 传输的内容需要关注消息类型

普通文本

需求: 实现基本的话题通信,一方发布数据,一方接收数据

c++实现

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

  1. 发布方
  2. 接收方
  3. 数据

发布方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

/*
发布方实现:
1.包含头文件
2.初始化 ROS 节点:命名(唯一)
3.创建节点句柄
4.创建发布者对象
5.编写发布逻辑并发布数据
*/
int main(int argc, char *argv[])
{
// 设置编码格式
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点:命名(唯一)
ros::init(argc,argv,"talker");
// 3.创建节点句柄
ros::NodeHandle nh; //该类封装了 ROS 中的一些常用功能
// 4.创建发布者对象
//泛型: 发布的消息类型
//参数1: 要发布到的话题
//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
// 5.编写发布逻辑并发布数据
// 创建发布消息
std_msgs::String msg;
// 分布频率
ros::Rate rate (10);
// 设置编号
int count = 0;
// 编写循环
while (ros::ok())
{
// 实现字符串拼接
std::stringstream ss;
ss << "hello ---"<< count;
msg.data = ss.str();
pub.publish(msg);
// 添加日志
ROS_INFO("发布的数据: %s",ss.str().c_str());
count++;
//根据前面制定的发送频率自动休眠
rate.sleep();
ros::spinOnce();
}
return 0;
}

接收方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "ros/ros.h"
#include "std_msgs/String.h"

/*
订阅方实现:
1.包含头文件
2.初始化 ROS 节点:命名(唯一)
3.创建节点句柄
4.创建订阅者对象
5.处理订阅数据
6.设置循环调用回调函数

*/

void doMsg(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("我听见: %s",msg->data.c_str());
}

int main(int argc, char *argv[])
{
// 设置编码格式
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点:命名(唯一)
ros::init(argc,argv,"listener");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建订阅者对象
ros::Subscriber sub = nh.subscribe("chatter",10,doMsg);
// 5.处理订阅数据
ros::spin(); // 循环读取接收的数据,并调用回调函数处理

return 0;
}

ConstPtrboost::shared_ptr(智能指针),即 msg 是一个指针

必须用 -> 访问其成员的 data 字段

配置CmakeList.txt

1
2
3
4
5
6
7
8
9
add_executable(demo01_pub src/demo01_pub.cpp)
add_executable(demo02_sub src/demo02_sub.cpp)
target_link_libraries(demo01_pub
${catkin_LIBRARIES}
)

target_link_libraries(demo02_sub
${catkin_LIBRARIES}
)

vscode 中的 main 函数 声明 int main(int argc, char const *argv[]){},默认生成 argv 被 const 修饰,需要去除该修饰符

python实现

发布方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#! /usr/bin/env python
"""
实现流程:
1.导包
2.初始化 ROS 节点:命名(唯一)
3.实例化 发布者 对象
4.组织被发布的数据,并编写逻辑发布数据
"""

import rospy
from std_msgs.msg import String

if __name__ == "__main__":
#2.初始化 ROS 节点:命名(唯一)
rospy.init_node("talker_p")
#3.实例化 发布者 对象
pub = rospy.Publisher("chatter_p",String,queue_size= 10 )
#4.组织被发布的数据,并编写逻辑发布数据
msg = String()
#指定发布频率
rate = rospy.Rate(1)
#设置计数器
count = 0
#循环发布数据
while not rospy.is_shutdown():
count+=1
msg.data = "hello---"+str(count)
#发布数据
pub.publish(msg)
rate.sleep()
rospy.loginfo("写入的数据:%s",msg.data)

订阅方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#! /usr/bin/env python
"""
实现流程:
1.导包
2.初始化 ROS 节点:命名(唯一)
3.实例化 订阅者 对象
4.处理订阅的消息(回调函数)
5.设置循环调用回调函数
"""

import rospy
from std_msgs.msg import String

def doMsg(msg):
rospy.loginfo("I heard:%s",msg.data)

if __name__ == "__main__":
# 2.初始化 ROS 节点:命名(唯一)
rospy.init_node("listener_p")
# 3.实例化 订阅者 对象
sub = rospy.Subscriber("chatter_p",String,doMsg,queue_size=10)
# 4.处理订阅的消息(回调函数)
# 5.设置循环调用回调函数
rospy.spin()

添加可执行权限

终端下进入 scripts 执行:chmod +x *.py

配置 CMakeLists.txt:

1
2
3
4
5
catkin_install_python(PROGRAMS
scripts/demo01_pub_p.py
scripts/demo02_sub_p.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

自定义msg

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

如果需要自定义消息类型,则需要自定义msg

流程:

  1. 按照固定格式创建 msg 文件
  2. 编辑配置文件
  3. 编译生成可以被 Python 或 C++ 调用的中间文件

创建自定义消息

  1. 定义msg文件

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

    1
    2
    3
    string name
    uint16 age
    float64 height
  2. 编辑配置文件

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

    1
    2
    <build_depend>message_generation</build_depend>
    <exec_depend>message_runtime</exec_depend>

    CMakeLists.txt编辑 msg 相关配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    find_package(catkin REQUIRED COMPONENTS
    roscpp
    rospy
    std_msgs
    message_generation
    )
    # 需要加入 message_generation,必须有 std_msgs
    ...
    # 配置 msg 源文件
    add_message_files(
    FILES
    Person.msg
    )
    ...
    # 执行时依赖
    catkin_package(
    # INCLUDE_DIRS include
    # LIBRARIES demo02_talker_listener
    CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
    # DEPENDS system_lib
    )

    编译依赖find_package内的包

    find_package依赖catkin_package内的包

    如果catkin_package内的包不对,可能编译成功但运行失败

  3. 编译

    编译后查看

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

    Python 需要调用的中间文件(…/工作空间/devel/lib/python3/msg/xxx.py)

c++实现

需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性

1
2
3
4
5
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**",
"/home/ros/demo03_ws/devel/include/**" //增加的
],

发布方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ROS_INFO("发布方:");

ros::init(argc,argv,"talker_person");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<plumbing_pub_sub::Person>("chatter_person",1000);
plumbing_pub_sub::Person p;
p.name = "孙悟空";
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;
}

订阅方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

void doPerson(const plumbing_pub_sub::Person::ConstPtr& person)
{
ROS_INFO("订阅的人信息:%s,%d,%.2f",person->name.c_str(),person->age,person->height);
}

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ROS_INFO("订阅方:");

ros::init(argc,argv,"listener_person");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe("chatter_person",1000,doPerson);
ros::spin();
return 0;
}

配置CMakeList:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)

// 额外配置,避免编译报错
## Add cmake target dependencies of the executable
## same as for the library above
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}
)

add_dependencies如果考虑简单,可以仅将注释的内容

1
# add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

${PROJECT_NAME}_node处改为节点名称,其它保留原来格式也是可以的

python实现

将前面生成的 python 文件路径配置进 settings.json

1
2
3
4
"python.autoComplete.extraPaths": [
"/opt/ros/noetic/lib/python3/dist-packages",
"/home/ros/demo03_ws/devel/lib/python3/dist-packages" //添加的
],

发布方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /usr/bin/env python
import rospy
from plumbing_pub_sub.msg import Person

if __name__ == "__main__":
rospy.init_node("talker_person_p")
pub = rospy.Publisher("chatter_person",Person,queue_size=1000)
p = Person()
p.name = "葫芦娃"
p.age = 18
p.height =0.75

rate = rospy.Rate(1)
while not rospy.is_shutdown():
pub.publish(p)
rate.sleep()
rospy.loginfo("姓名:%s, 年龄:%d, 身高:%.2f",p.name,p.age,p.height)

订阅方:

1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env python
import rospy
from plumbing_pub_sub.msg import Person

def doPerson(p):
rospy.loginfo("订阅信息:%s,%d,%.2f",p.name,p.age,p.height)

if __name__ == "__main__":
rospy.init_node("listener_person_p")
sub = rospy.Subscriber("chatter_person",Person,doPerson,queue_size=1000)
rospy.spin()

终端下进入 scripts 执行:chmod +x *.py

配置 CMakeLists.txt:

1
2
3
4
5
6
7
catkin_install_python(PROGRAMS
scripts/talker_p.py
scripts/listener_p.py
scripts/person_talker.py
scripts/person_listener.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

服务通信

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制

即一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A

服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景

Snipaste_2025-05-22_16-29-54_compressed

案例:

实现两个数字的求和,客户端节点,运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端

自定义srv

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

  1. 按照固定格式创建srv文件
  2. 编辑配置文件
  3. 编译生成中间文件

流程:

  1. 定义srv文件

    功能包下新建 srv 目录,添加 xxx.srv 文件,请求和响应数据类型之间用---分开

    1
    2
    3
    4
    5
    6
    # 客户端请求时发送的两个数字
    int32 num1
    int32 num2
    ---
    # 服务器响应发送的数据
    int32 sum
  2. 编译配置文件

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

    1
    2
    <build_depend>message_generation</build_depend>
    <exec_depend>message_runtime</exec_depend>

    同话题通信自定义

    CMakeLists.txt编辑 srv 相关配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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 中配置 message_runtime,经测试配置也可以

  3. 编译,查看包方式同之前

需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性

  "includePath": [
    "/opt/ros/noetic/include/**",
    "/usr/include/**",
    "/home/ros/demo03_ws/devel/include/**" //增加的
  ],

c++

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
服务器实现:
1.包含头文件
2.初始化 ROS 节点
3.创建 ROS 句柄
4.创建 服务 对象
5.回调函数处理请求并产生响应
6.由于请求有多个,需要调用 ros::spin()
*/

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

bool doNums(plumbing_server_client::AddInts::Request &req,
plumbing_server_client::AddInts::Response &resp){
int num1 = req.num1;
int num2 = req.num2;
ROS_INFO("服务器接收到的数据为: num1 = %d,num2 = %d",num1,num2);
int sum = num1 + num2 ;
resp.sum = sum;
ROS_INFO("处理结果: sum = %d",sum);
return true;
}

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ROS_INFO("服务器启动");
ros::init(argc,argv,"AddInts_Server");
ros::NodeHandle nh;
ros::ServiceServer server = nh.advertiseService("AddInts",doNums);
ros::spin();
}

服务端的回调函数的参数是 demo03_server_client::AddInts::Request& req,没有ConstPtrreq 是请求对象的引用,不是指针

引用 (req) 的行为和对象实例一致,直接用 . 访问成员,因为服务是“一问一答”模式,不需要共享所有权,直接修改传入的请求和响应对象即可

话题通信中将接收到的消息封装为 boost::shared_ptr(智能指针)传递给回调函数,目的是为了高效管理内存(避免拷贝)

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
服务器实现:
1.包含头文件
2.初始化 ROS 节点
3.创建 ROS 句柄
4.创建 客户端 对象
5.请求服务,接收响应
实现参数的动态提交:
1. 格式 rosrun ..... (参数)
2. 节点执行时,需要获取命令中的参数,并组织进 request
问题:
如果先启动客户端,会请求异常
需求:
先启动客户端,不直接抛出异常,挂起,等待服务器启动再请求
解决:
ROS中内置函数,可以让客户端启动后挂起等待
*/

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

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 优化实现,获取命令中的参数
// 如果不传递任何ROS参数,argc 的值为 1(只有程序名本身)
if(argc != 3)
{
ROS_INFO("提交的参数个数不对");
return 1;
}
ros::init(argc,argv,"AddInts_Client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<plumbing_server_client::AddInts>("AddInts");
// 5.请求服务,接收响应
plumbing_server_client::AddInts ai;
// 组织请求
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
// 处理响应
// 函数1,等到服务启动后才继续
// client.waitForExistence();
// 函数2,需要传入具体的服务话题
ros::service::waitForService("AddInts");
bool flag = client.call(ai);
if(flag)
{
ROS_INFO("响应成功");
ROS_INFO("响应结果 = %d",ai.response.sum);
} else {
ROS_INFO("响应失败");
}

}

配置CMakeLists

1
2
3
4
5
6
7
8
9
10
11
12
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}
)

python

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /usr/bin/env python
import rospy
from plumbing_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse

def doReq(req):
num1 = req.num1
num2 = req.num2
sum = num1 + num2
resp = AddIntsResponse()
resp.sum = sum
rospy.loginfo("服务器解析结果:%d",sum)
return resp

if __name__ == "__main__":
rospy.init_node("AddInts_Server_p")
server = rospy.Service("AddInts",AddInts,doReq)
rospy.loginfo("服务器启动")
rospy.spin()

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#! /usr/bin/env python
import rospy
from plumbing_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse
import sys


if __name__ == "__main__":
#优化实现
if len(sys.argv) != 3:
rospy.logerr("请正确提交参数")
sys.exit(1)

rospy.init_node("AddInts_Client_p")
client = rospy.ServiceProxy("AddInts",AddInts)
client.wait_for_service()
req = AddIntsRequest()
req.num1 = int(sys.argv[1])
req.num2 = int(sys.argv[2])

resp = client.call(req)
rospy.loginfo("响应结果:%d",resp.sum)

配置CMakeLists

1
2
3
4
5
catkin_install_python(PROGRAMS
scripts/demo01_server_p.py
scripts/demo02_client_p.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

参数服务器

以共享的方式实现不同节点之间数据交互的通信模式,存储一些多节点共享的数据,类似于全局变量。

案例:实现参数增删改查操作

ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数

03ROS通信机制03_参数服务器

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

c++

  • 参数服务器新增(修改)参数

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

1
2
3
4
5
6
/*   
ros::NodeHandle
setParam("键","值")
ros::param
set("键","值")
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "ros/ros.h"

int main(int argc, char *argv[])
{
ros::init(argc,argv,"set_param_c");
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);
return 0;
}
  • 参数服务器获取参数

在 roscpp 中提供了两套 API 实现参数操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* 
ros::NodeHandle

param(键,默认值)
存在,返回对应结果,否则返回默认值
int res1 = nh.param("nh_int",100);

getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
如果键不存在,那么返回值为 false,且不为参数2赋值
nh.getParam("nh_int",nh_int_value);

getParamCached(键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
如果键不存在,那么返回值为 false,且不为参数2赋值
nh.getParamCached("nh_int",nh_int_value);

getParamNames(std::vector<std::string>)
获取所有的键,并存储在参数 vector 中
std::vector<std::string> param_names;
nh.getParamNames(param_names);
for (auto &&name : param_names)
{
ROS_INFO("名称解析name = %s",name.c_str());
}

hasParam(键)
是否包含某个键,存在返回 true,否则返回 false
ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int"));

searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
nh.searchParam("nh_int",key);
ROS_INFO("搜索键:%s",key.c_str());
-> 搜索键:/nh_int

ros::param ----- 与 NodeHandle 类似
*/
  • 参数服务器删除参数
1
2
3
4
5
6
7
8
9
/* 
ros::NodeHandle
deleteParam("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false

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

python

  • 参数服务器新增(修改)参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#! /usr/bin/env python

import rospy

if __name__ == "__main__":
rospy.init_node("set_update_paramter_p")

# 设置各种类型参数
rospy.set_param("p_int",10)
rospy.set_param("p_double",3.14)
rospy.set_param("p_bool",True)
rospy.set_param("p_string","hello python")
rospy.set_param("p_list",["hello","haha","xixi"])
rospy.set_param("p_dict",{"name":"hulu","age":8})

# 修改
rospy.set_param("p_int",100)
  • 参数服务器获取参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
rospy.init_node("get_param_p")

参数服务器操作之查询_Python实现:
get_param(键,默认值)
当键存在时,返回对应的值,如果不存在返回默认值
int_value = rospy.get_param("p_int",10000)

get_param_cached(键,默认值)--提高变量获取效率
int_cached = rospy.get_param_cached("p_int")

get_param_names--获取所有的键
names = rospy.get_param_names()
for name in names:
rospy.loginfo("name = %s",name)

has_param
是否包含某个键,存在返回 true,否则返回 false
flag = rospy.has_param("p_int")

search_param--寻找某个键,并返回完整的路径
key = rospy.search_param("p_int")
rospy.loginfo("搜索的键 = %s",key)
"""
  • 参数服务器删除参数
1
2
3
4
5
6
7
8
9
10
11
12
 """
rospy.delete_param("键")
键存在时,可以删除成功,键不存在时,会抛出异常



rospy.init_node("delete_param_p")
try:
rospy.delete_param("p_int")
except Exception as e:
rospy.loginfo("删除失败")
"""

常用命令

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

ROS/CommandLineTools - ROS Wiki

rosnode - ROS Wiki 用于获取节点信息的命令

  • rosnode list 列出所有的活动节点
  • rosnode ping [node_name] 测试到节点的连接状态
  • rosnode info [node_name] 打印节点信息
  • rosnode machine [machine_name] 列出指定设备上的节点
  • rosnode kill [node_name] 杀死某个节点
  • rosnode cleanup 清除无用节点,比如小乌龟示例

rostopic - ROS Wiki 用于显示有关ROS话题的调试信息,包括发布者,订阅者,发布频率和ROS消息

  • rostopic list 显示所有活动状态下的话题

  • rostpic echo [topic-name] 获取指定话题当前发布的消息

  • rostopic pub 直接调用命令向订阅者发布消息 [这类指令都需要先source 一下]

    1
    2
    rostopic pub -r [频率] /话题名称 消息类型 消息内容
    rostopic pub -r 10 chatter_person plumbing_pub_sub/Person [按两次Tab补齐]
  • rostopic info [topic-name] 获取当前话题的相关信息

  • rostopic type [topic-name] 打印话题类型

  • rostopic hz [topic-name] 显示话题的发布频率


rosmsg - ROS Wiki 用于显示有关 ROS消息类型的工具

  • rosmsg list 列出所有消息

  • rosmsg list | grep -i [消息种类] 显示消息的具体路径

  • rosmsg show [消息种类] 显示消息描述

  • rosmsg info [消息种类] 作用与 rosmsg show 一样


rosservice - ROS Wiki 用于列出和查询ROS Services的工具

  • rosservice list 列出所有活动的服务
  • rosservice call [service-name] 使用提供的参数调用服务
  • rosservice info [service-name] 打印有关服务的信息
  • rosservice type [service-name] 打印服务类型

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


rosparam - ROS Wiki 用于使用YAML编码文件在参数服务器上获取和设置ROS参数

  • rosparam list 列出所有参数

  • rosparam set 设置参数 [参数名] [参数值]

  • rosparam get 获取参数

  • rosparam delete 删除参数

  • rosparam dump 将参数写出到外部文件

    1
    rosparam dump ×××.yaml
  • rosparam load [yaml文件] 从外部文件加载参数

    1
    rosparam load xxx.yaml

通信机制比较

三种通信机制中

参数服务器是一种数据共享机制,可以在不同的节点之间共享数据

话题通信与服务通信是在不同的节点之间传递数据的

话题通信:Publisher→Subscriber [Topic] [msg]

服务通信:Client→Server [Service] [srv]

二者的实现流程相似,都是两个节点通过话题关联到一起,并使用某种类型的数据载体实现数据传输

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

ROS通信机制进阶

roscpp rospy

常用API

建议参考官方API文档或参考源码

在运行程序时rosrun 后接着写 _[参数名] = [参数值] 可以给节点

初始化函数

ros::init(argc, argv, “name”, [options])

  1. argc — 封装实参的个数(n+1)

  2. argv — 封装参数的数组

  3. name — 为节点命名(唯一性)

  4. options — 节点启动选项 ros::init_options::AnonymousName

    给前面的节点命名后增加一个随机数,使得同一个节点能够重复启动

    否则重名情况下,第二次启动时第一次会自动停止

返回值: void

rospy.init_node(name, argv=None, anonymous=False)

  1. name — 节点名称
  2. argv — 封装节点调用时传递的参数
  3. anonymous — 取值为 true 时,为节点名称后缀随机编号

话题与服务

在 roscpp 中,话题和服务的相关对象一般由 ros::NodeHandle nh 创建,python中不用专门创建NodeHandle

发布对象

ros::Publisher pub = nh.advertise< type >(“topic”, queue_size, [latch])

  1. type — 消息类型

  2. topic — 话题名称

  3. queue_size — 队列长度

  4. latch — 设置为ture时,该话题发布的最后一条消息将被保存,并且当有订阅者连接时会将该消息发送给订阅者

    以静态地图发送为例(短时间不变的数据)

    方案1:可以使用固定频率发送地图数据,但是效率低

    方案2:可以将地图发布对象的latch设置为true,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),这样提高了数据的发送效率

pub = rospy.Publisher(“topic”, type, queue_size, [lathc])

订阅对象

ros::Subscriber sub = nh.subscribe(“topic”, queue_size, callback);

void callback(const std_msgs::String::ConstPtr &msg)

sub = rospy.Subscriber(“topic”, type, callback, queue_size)

def callback(msg):

服务对象

ros::ServiceServer server = nh.advertiseService(“service”, callback);

bool callback(srv type::Request &req, srv type::Response &resp)

server = rospy.Service(“service”, srv type, callback)

def callback(req):

return resp

客户端对象

ros::ServiceClient client = nh.serviceClient< srv type >(“service”);

等待服务函数1 ros::service::waitForService(“service”)

等待服务函数2 client.waitForExistence();

请求写入:srv type req; req.request.input1 = atoi(argv[1]); (类推写完输入)

client = rospy.ServiceProxy(“service”, srv type)

等待服务函数 client.wait_for_service()

请求写入:req = srv typeRequets() req.input1 = sys.argv[1] (类推写完输入)

回调函数

spinOnce() 处理一轮回调,一般用于循环体内

spin() 进入循环去处理回调

不同点:在ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行

python 只有 rospy.spin()

时间

时刻

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

ros::Time::now()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "ros/ros.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"hello_time");
ros::NodeHandle nh;
// 获取当前时刻:now 被调用执行的那一刻
// 参考系:1970年01月01日 00:00:00
ros::Time current_time = ros::Time::now();

// 获取距离 1970年01月01日 00:00:00 的秒数
ROS_INFO("当前时刻:%.2f",current_time.toSec()); // 浮点型
ROS_INFO("当前时刻:%d",current_time.sec); // 整型

// 转化为年月日时分秒格式
std::time_t raw_time = current_time.toSec(); // 转换为秒
std::tm* time_info = std::localtime(&raw_time); // 转换为本地时间
// 如果需要UTC时间使用 std::gmtime
char buffer[80];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", time_info); // 格式化时间
ROS_INFO("当前时间: %s",buffer);

// 设置指定时刻
ros::Time t1(100,100000000); // 参数1:秒数 参数2:纳秒
ROS_INFO("时刻:%.2f",t1.toSec());
ros::Time t2(100.1); // 传入浮点型
ROS_INFO("时刻:%.2f",t2.toSec());

return 0;
}

rospy.Time.now()

1
2
3
4
5
6
7
8
9
10
11
current_time = rospy.Time.now()
rospy.loginfo("当前时刻:%.2f",right_now.to_sec()) # 浮点型
rospy.loginfo("当前时刻:%.2f",right_now.to_nsec()) # 整型

# 设置指定时刻
time1 = rospy.Time(100,100000000) # 参数1:秒数 参数2:纳秒
time2 = rospy.Time(100.1)
time3 = rospy.Time.from_sec(100.1)
rospy.loginfo("设置时刻1:%.2f",time1.to_sec())
rospy.loginfo("设置时刻2:%.2f",time2.to_sec())
rospy.loginfo("设置时刻2:%.2f",time3.to_sec())

持续时间

设置一个时间区间(间隔)

ros::Duration du()

1
2
3
4
ROS_INFO("开始休眠:%.2f",ros::Time::now().toSec());
ros::Duration du(4.5);//持续4.5秒,double类型,以秒为单位
du.sleep();
ROS_INFO("结束休眠:%.2f",ros::Time::now().toSec());

du = rospy.Duration()

1
2
3
4
rospy.loginfo("持续时间测试开始.....")
du = rospy.Duration(3.3)
rospy.sleep(du) #休眠函数
rospy.loginfo("持续时间测试结束.....")

时刻运算

1
2
3
4
5
ros::Time begin = ros::Time::now();
ros::Duration du1(5); // 模拟运行时间
ros::Time stop = begin + du1;
ROS_INFO("开始时刻:%.2f",begin.toSec());
ROS_INFO("结束时刻:%.2f",stop.toSec());

time 与 duration 可以+/-,duration 之间也可以+/-

但time 之间只可以 - ,不可以 + ,返回的是 ros::Duration 类型

python与c++相同

设置运行频率

1
2
3
4
5
ros::Rate rate(1);//指定频率
while (true)
{
rate.sleep(); // 要放在循环里
}
1
2
3
4
rate = rospy.Rate(1)
while not rospy.is_shutdown():
rate.sleep() #休眠,要放在循环里
rospy.loginfo("+++++++++++++++")

定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cb(const ros::TimerEvent &event){
ROS_INFO("----");
ROS_INFO("调用时刻:%.2f",event.current_real.toSec());
}
/*
Timer createTimer(ros::Duration(period), // 时间间隔
const TimerCallback& callback, // 回调函数
bool oneshot = false, // 是否一次性
bool autostart = true ) // 是否自动启动,设置为false时需要手动启动 time.start()
定时器启动需要ros::spin() 因为有回调函数
*/

ros::Timer timer = nh.createTimer(ros::Duration(1),cb);
// ros::Timer timer = nh.createTimer(ros::Duration(1),cb,false,false);
// timer.start(); // 手动启动
ros::spin();
1
2
3
4
5
6
7
8
9
10
11
12
def cb(event):
rospy.loginfo("+++++++++++")
rospy.loginfo("当前时刻:%.2f",event.current_real.to_sec())
"""
rospy.Timer(rospy.Duration(period), // 时间间隔
callback, // 回调函数
oneshot=False, // 是否一次性
reset=False)
"""
rospy.Timer(rospy.Duration(1),cb)
# rospy.Timer(rospy.Duration(1),cb,True) # 只执行一次
rospy.spin()

其它函数

在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制

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

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

日志函数:

1
2
3
4
5
ROS_DEBUG("hello,DEBUG"); //不会输出
ROS_INFO("hello,INFO"); //默认白色字体
ROS_WARN("Hello,WARN"); //默认黄色字体
ROS_ERROR("hello,ERROR");//默认红色字体
ROS_FATAL("hello,FATAL");//默认红色字体
1
2
3
4
5
rospy.logdebug("hello,debug")  #不会输出
rospy.loginfo("hello,info") #默认白色字体
rospy.logwarn("hello,warn") #默认黄色字体
rospy.logerr("hello,error") #默认红色字体
rospy.logfatal("hello,fatal") #默认红色字体

头文件与源文件

核心内容在于CMakeLists.txt文件的配置

自定义头文件调用

流程:

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

头文件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _HELLO_H
#define _HELLO_H
/*
声明namespace
|-- class
|- run
*/

namespace hello_ns{

class MyHello{

public:
void run();
};

}

#endif

为了后续包含头文件时不抛出异常,配置 .vscode 下 _cpp_properties.json 的 includepath属性(和之前配置服务srv文件生成的头文件过程相似)

1
2
3
4
"includePath": [
...
"/home/ros/demo03_ws/src/plumbing_head/include/**"
],

源文件

src目录下新建 hello.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "ros/ros.h"
#include "plumbing_head/hello.h"

namespace hello_ns{

void MyHello::run(){
ROS_INFO("run函数执行....");
}

}

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

配置文件

与之前配置多一步

1
2
3
4
5
6
7
8
9
10
11
12
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}
)

自定义源文件调用

流程:

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

可执行文件

src目录下新建 use_head.cpp

1
2
3
4
5
6
7
8
9
10
#include "ros/ros.h"
#include "plumbing_head/hello.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"hello_head_src");
hello_ns::MyHello my;
my.run();
return 0;

配置文件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
include_directories(
include // 这里解除注释
${catkin_INCLUDE_DIRS}
)

## 声明C++库
## Declare a C++ library
# add_library(${PROJECT_NAME}
# src/${PROJECT_NAME}/plumbing_pub_sub.cpp
# )
add_library(head_src
include/${PROJECT_NAME}/hello.h
src/hello.cpp
)

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

target_link_libraries(head_src
${catkin_LIBRARIES}
)

可执行文件配置:

1
2
3
4
5
6
7
8
9
add_executable(use_hello src/use_hello.cpp)

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

#此处需要添加之前设置的库
target_link_libraries(use_hello
head_src // 库声明
${catkin_LIBRARIES}
)

Python模块导入

实现:

  1. 新建两个Python文件,使用 import 实现导入关系;
  2. 添加可执行权限、编辑配置文件并执行UseA

tool.py文件

1
2
#! /usr/bin/env python
num = 1000

use_tool.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#! /usr/bin/env python

import os
import sys
import rospy

# 设置临时环境变量
# sys.path.insert(0,"/home/ros/demo03_ws/src/plumbing_pub_sub/scripts")
# 路径动态,保证移植可执行
path = os.path.abspath(".")
sys.path.insert(0,path + "/src/plumbing_head/scripts")

import tool

if __name__ == "__main__":
rospy.init_node("tool")
# 异常:ModuleNotFoundError:No module named'tools'
"""
原因: rosrun 执行时,参考路径是工作空间的路径,在工作空间下无法查找依赖的模块
解决:可以声明python的环境变量,当依赖某个模块时,先去指定环境变量中查找依赖
"""
rospy.loginfo("num=%d",tool.num)

ROS运行管理

元功能包

完成ROS中一个系统性的功能,可能涉及到多个功能包,逐一安装功能包的效率低下,在ROS中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包

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

例如:sudo apt install ros-noetic-desktop-full 命令安装ros时就使用了元功能包

实现

新建一个功能包,不需要添加依赖,修改package.xml

1
2
3
4
5
6
7
8
9
<buildtool_depend>catkin</buildtool_depend>
<!-- 添加依赖的功能包名 -->
<exec_depend>plumbing_pub_sub</exec_depend>
<exec_depend>plumbing_server_client</exec_depend>
<exec_depend>plumbing_param_server</exec_depend>
<!-- The export tag contains other, unspecified, tags -->
<export>
<metapackage />
</export>

修改 CMakeLists.txt

1
2
3
4
cmake_minimum_required(VERSION 3.0.2)
project(plumbing_my)
find_package(catkin REQUIRED)
catkin_metapackage()

不能出现换行以及注释

catkin/package.xml - ROS Wiki

launch文件

在功能包下添加 launch目录, 目录下新建 xxxx.launch 文件,编辑 launch 文件

1
2
3
4
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" />
<node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" />
</launch>

调用 launch 文件

1
roslaunch 包名 xxx.launch

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

launch标签

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

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

1
<launch deprecated="此文件可能过时!">

node标签

<node>标签用于指定 ROS 节点,是最常见的标签,但 roslaunch 命令不能保证按照 node 的声明顺序来启动节点(节点的启动是多进程的)

  • pkg=“包名”

    节点所属的功能包

  • type=“nodeType”

    节点类型,即与之相同名称的可执行文件,python文件需要带上.py,cpp文件不需要,类似CMakeList配置

  • name=“nodeName”

    节点名称,在 ROS 网络拓扑中节点的名称

  • machine=“机器名”

    在指定机器上启动节点

  • respawn=“true | false” (可选)

    如果节点退出,是否自动重启

  • respawn_delay=" N" (可选)

    如果 respawn 为 true,那么延迟 N 秒后启动节点

  • required=“true | false” (可选)

    如果为 true,那么该节点退出,将杀死整个 roslaunch,不能和respawn一起用,会报错

  • ns=“xxx” (可选)

    在指定命名空间 xxx 中启动节点 ,主要用来避免重名

    node list -> /命名空间/name

  • output=“log | screen” (可选)

    默认log,但一般改成screen

1
<node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen"/>

include标签

用于将另一个 xml 格式的 launch 文件导入到当前文件

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

1
2
3
4
5
<launch>
<!-- 包含 -->
<include file = "$(find launch01_basic)/launch/start_turtle.launch"/>
<!-- 其他节点 -->
</launch>

用于复用

remap标签

用于话题重命名

1
2
3
4
5
<node pkg="turtlesim" type="turtlesim_node"  name="myTurtle" output="screen">
<remap from="/turtle1/cmd_vel" to = "/cmd_vel" />
</node>
<!-- 在launch中的语法是每个标签结束后才标斜杠,没结束还有子标签的话就不标 -->

from = “原始话题名称” to = “目标话题名称”

此时原来的控制失效,因为turtle_teleop_key 是以/turtle1/cmd_vel 发布

1
<node pkg="turtlesim" type="turtle_teleop_key"  name="myTurtleContro" output="screen" />

这里需要下载一个控制包

1
sudo apt-get install ros-noetic-teleop-twist-keyboard

param标签

<param>标签主要用于在参数服务器上设置参数

可以通过外部文件加载,在<node>标签中时,相当于私有命名空间

  • name=“[命名空间]/参数名”

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

  • value=“xxx” (可选)

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

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

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

    • 如果包含 ‘.’ 的数字解析未浮点型,否则为整型
    • “true” 和 “false” 是 bool 值(不区分大小写)
    • 其他是字符串
1
2
3
4
5
<param name="param_A" type = "int" value ="100" />
<node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen">
<remap from="/turtle1/cmd_vel" to = "/cmd_vel" />
<param name="param_B" type = "double" value ="3.14" />
</node>

rosparam标签

<rosparam>标签可以从 YAML 文件导入参数,或将参数导出到 YAML 文件,也可以用来删除参数

  • command=“load | dump | delete” (可选,默认 load)
  • file=“$(find 功能包)/xxx/yyy…”
  • param=“参数名称”
  • ns=“命名空间” (可选)
1
2
<!-- 导入参数 -->
<rosparam command = "load" file = "$(find launch01_basic)/launch/param.yaml"/>

由于launch文件也不是按顺序进行,导出虽然写在launch文件较后的位置也可能无法正常导出,所以最好分两个launch文件,在完成后再进行导出

1
2
3
4
<launch>
<!-- 导出参数 -->
<rosparam command = "dump" file = "$(find launch01_basic)/launch/param_out.yaml"/>
</launch>

删除操作:

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

group标签

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

  • clear_params=“true | false” (可选)

    启动前,是否删除组名称空间的所有参数(慎用…此功能危险)

这个时候即使在node中name属性相同也不会报错,因为group使得前面加上前缀,不会产生相同的节点名称

1
2
3
4
5
6
7
8
9
10
<launch >
<group ns = "first">
<node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" />
<node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" />
</group>
<group ns = "second">
<node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" />
<node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" />
</group>
</launch>

arg标签

<arg>标签是用于动态传参,类似于函数的参数

  • name=“参数名称”

  • default=“默认值” (可选)

  • value=“数值” (可选)

    不可以与 default 并存

  • doc=“描述”

命令行调用传参

1
roslaunch hello.launch xxx:=值

工作空间覆盖

所谓工作空间覆盖,是指不同工作空间中,存在重名的功能包的情形

比如:自定义工作空间A存在功能包 turtlesim,自定义工作空间B也存在功能包 turtlesim,当然系统内置空间也存在turtlesim,如果调用turtlesim包,会调用哪个工作空间中的呢

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

    1
    2
    3
    4
    5
    6
    7
    8
    #include "ros/ros.h"

    int main(int argc, char *argv[])
    {
    ros::init(argc,argv,"hello_ws1");
    ROS_INFO("demo01_ws");
    return 0;
    }
  2. 为了在任何工作空间下调用demo01_ws和demo02_ws,需要在根目录下的.bashrc文件中进行修改

    1
    2
    3
    4
    source /opt/ros/noetic/setup.bash
    # 新增
    source /home/ros/demo01_ws/devel/setup.bash
    source /home/ros/demo02_ws/devel/setup.bash
  3. 刷新环境变量

    1
    source .bashrc

    运行

    1
    rosrun turtlesim [TAB]

    会发现直接跳出hello_ws2

    是因为在.bashrc中后刷新的会覆盖前面的

  4. 查看ROS环境环境变量

    1
    echo $ROS_PACKAGE_PATH

    ROS_PACKAGE_PATH 中的值,和 .bashrc 的配置顺序相反—>后配置的优先级更高

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

隐患

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

BUG 说明:

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

目前没有特别的解决方案

节点名称重名

ros::init(argc,argv,“”) rospy.init_node(“”) 定义节点名称

在ROS的网络拓扑中,是不可以出现重名的节点的,也不可以启动重名节点或者同一个节点多次

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

命名空间就是为名称添加前缀,名称重映射是为名称起别名

rosrun实现

rosrun设置命名空间

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

1
2
3
rosrun turtlesim turtlesim_node __ns:=t1
rosrun turtlesim turtlesim_node __ns:=t2
// 注意,这里=后面不可以有空格

rosnode list

1
2
/t1/turtlesim
/t2/turtlesim

rosrun名称重映射

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

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

设置命名空间同时名称重映射

1
rosrun turtlesim turtlesim_node __ns:=w1 __name:=t1

使用环境变量也可以设置命名空间,启动节点前在终端键入如下命令

1
2
export ROS_NAMESPACE=sw1
rosrun turtlesim turtlesim_node

launch实现

1
2
3
4
5
6
7
8
9
<launch >
<node pkg="turtlesim" type="turtlesim_node" name="turtlesim" output="screen"/>
<!-- 名称重映射 -->
<node pkg="turtlesim" type="turtlesim_node" name="t1" output="screen"/>
<!-- 命名空间 -->
<node pkg="turtlesim" type="turtlesim_node" name="turtlesim" output="screen" ns="p1" />
<!-- 命名空间 + 称重映射 -->
<node pkg="turtlesim" type="turtlesim_node" name="t2" output="screen" ns="p2" />
</launch>

在 node 标签中,name 属性是必须的,ns 可选

rosnode list查看节点信息

1
2
3
4
5
/p1/turtlesim
/p2/t2
/rosout
/t1
/turtlesim

编码实现

c++

重映射:(设置别名)

1
ros::init(argc,argv,"name",ros::init_options::AnonymousName);

在名称后面添加时间戳

命名空间:

1
2
3
std::map<std::string, std::string> map;
map["__ns"] = "name";
ros::init(map,"");

python

重映射:(设置别名)

1
rospy.init_node("name",anonymous=True)

话题名称重名

rosrun实现

语法: rorun 包名 节点名 话题名:=新话题名称

实现teleop_twist_keyboard与乌龟显示节点通信方案

方案1:将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel

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

这个时候rosrun turtlesim turtle_teleop_key 也能正常控制,相当于两个发布

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

1
rosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel

launch实现

在之前launch文件标签那里讲过,这里就给个代码

语法<remap from="原话题" to="新话题" />

1
2
3
4
5
6
<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>
1
2
3
4
5
6
<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>

编码实现

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

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

c++

全局名称:以/开头,和节点名称无关

1
ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter",1000);

相对名称:非/开头,参考命名空间(与节点名称平级)来确定话题名称

1
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);

__ns:=设置命名空间后,则话题位于 /name/chatter

私有名称:以~开头的名称

需要在节点handle创建时候做出些改变

1
2
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);

但如果话题以/开头则话题仍为全局(全局话题优先级更高)

1
2
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter",1000);

python

全局名称:以/开头,和节点名称无关

1
pub = rospy.Publisher("/chatter",String,queue_size=1000)

相对名称:非/开头,参考命名空间(与节点名称平级)来确定话题名称

1
pub = rospy.Publisher("chatter",String,queue_size=1000)

私有名称:以~开头的名称

1
pub = rospy.Publisher("~chatter",String,queue_size=1000)