ros

image-20240627122745164

编译工作空间 CMake

image-20240627140005928

首先在工作空间 catkin_ws/src/ 下递归的查找其中每一个ROS的package。

2.package中会有 package.xml 和 CMakeLists.txt 文件,Catkin(CMake)编译系统依据 CMakeLists.txt 文件,从而生成 makefiles (放在 catkin_ws/build/)。

3.然后 make 刚刚生成的 makefiles 等文件,编译链接生成可执行文件(放在 catkin_ws/devel )。

也就是说,Catkin就是将 cmake 与 make 指令做了一个封装从而完成整个编译过程的工具。catkin有比较突出的优点,主要是:操作更加简单一次配置,多次使用跨依赖项目编译。

要用catkin编译一个工程或软件包,只需要用 catkin_make 指令。一般当我们写完代码,执行一次 catkin_make 进行编译,调用系统自动完成编译和链接过程,构建生成目标文件

cd ~/ros_workspace/
catkin_make
source ~/ros_workspace/devel/setup.bash

任何ROS程序只有组织成package功能包才能编译

Package结构:

一个package下常见的文件、路径有:

├── CMakeLists.txt #package的编译规则(必须)

├── package.xml #package的描述信息(必须)

├── src/ #源代码文件

├── include/ #C++头文件

├── scripts/ #可执行脚本

├── msg/ #自定义消息

├── srv/ #自定义服务

├── models/ #3D模型文件

├── urdf/ #urdf文件

├── launch/ #launch文件

其中定义package的是 CMakeLists.txt 和 package.xml ,这两个文件是package中必不可少的。catkin编译系统在编译前,首先就要解析这两个文件。这两个文件就定义了一个package。

  1. CMakeLists.txt: 定义package的包名、依赖、源文件、目标文件等编译规则,是package不可少的成分;
  2. package.xml: 描述package的包名、版本号、作者、依赖等信息,是package不可少的成分;
  3. src/: 存放ROS的源代码,包括C++的源码和(.cpp)以及Python的module(.py);
  4. include/: 存放C++源码对应的头文件;
  5. scripts/: 存放可执行脚本,例如shell脚本(.sh)、Python脚本(.py);
  6. msg/: 存放自定义格式的消息(.msg);
  7. srv/: 存放自定义格式的服务(.srv);
  8. models/: 存放机器人或仿真场景的3D模型(.sda, .stl, .dae等);
  9. urdf/: 存放机器人的模型描述(.urdf或.xacro);
  10. launch/: 存放launch文件(.launch或.xml);

Package功能包创建

catkin_create_pkg package depends
cd ~/ros_workspace /src #切到功能包创建目录
catkin_create_pkg test_pkg roscpp rospy std_msgs #创建名字为test_pkg 的功能包,依赖了roscpp,rospy,std_msgs

这样就会在当前路径下新建 test_pkg 软件包,包括:

├── CMakeLists.txt

├── include

│ └── test_pkg

├── package.xml

└── src

catkin_create_pkg 帮你完成了软件包的初始化,填充好了 CMakeLists.txt 和 package.xml ,并且将依赖项填进了这两个文件中。

Package相关命令

rospack

rospack是对package管理的工具,命令的用法如下:

表2.1 rospack命令

rospack命令 作用
rospack help 显示rospack的用法
rospack list 列出本机所有package
rospack depends [package] 显示package的依赖包
rospack find [package] 定位某个package
rospack profile 刷新所有package的位置记录

roscd

roscd 命令类似与Linux系统的 cd ,改进之处在于 roscd 可以直接 cd 到ROS的软件包。

表2.2 roscd命令

roscd命令 作用
roscd [pacakge] cd到ROS package所在路径

rosls:

rosls 也可以视为Linux指令 ls 的改进版,可以直接 ls ROS软件包的内容。

表2.3 rosls命令

rosls命令 作用
rosls [pacakge] 列出ROS package下的文件

rosdep:

rosdep 是用于管理ROS package依赖项的命令行工具,用法如下:

表2.4 rosdep命令

img

一个较常使用的命令是 rosdep install –from-paths src –ignore-src –rosdistro=kinetic -y ,用于安装工作空间中 src 路径下所有package的依赖项(由pacakge.xml文件指定)。

node

最小的进程单元就是节点(可执行文件在运行之后就成了一个进程(process),这个进程在ROS中就叫做节点)

从程序角度来说,node就是一个可执行文件(通常为C++编译生成的可执行文件、Python脚本)被执行,加载到了内存之中;

当执行一个ROS程序,就被加载到了内存中,就成为了一个进程,在ROS里叫做节点

分布式

节点管理器master

ROS为机器人开发者们提供了不同语言的编程接口,比如C++接口叫做roscpp,Python接口叫做rospy,Java接口叫做rosjava。

image-20240627141840726

cpp编写

#include "ros/ros.h"
#include "std_msgs/String.h"
#include "sstream"
int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "talker"
  ros::init(argc, argv, "talker");// 创建一个节点句柄
  ros::NodeHandle n;// 创建一个 Publisher,发布到话题 "chatter",消息类型为 std_msgs::String,队列大小为 1000
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);// 设置循环频率为 10Hz
  ros::Rate loop_rate(10);
  int count = 0;// 当 ROS 节点正常运行时,进入循环
  while (ros::ok())
  {// 创建一个 std_msgs::String 类型的消息
    std_msgs::String msg;
    std::stringstream ss;// 将 "hello world" 和当前计数值 count 写入字符串流
    ss << "hello world " << count;
    msg.data = ss.str();// 打印消息内容到终端
    ROS_INFO("%s", msg.data.c_str());// 发布消息到 "chatter" 话题
    chatter_pub.publish(msg);// 调用一次回调函数,处理所有已经到来的消息
    ros::spinOnce();// 按照设定的频率休眠
    loop_rate.sleep();// 计数值加 1
    ++count;
  }
  return 0;
}

每秒发布 10 条内容为 “hello world x” 的消息,其中 x 是从 0 开始递增的计数值。

CMakeLists.txt文件修改

add_executable(${PROJECT_NAME}_node  src/test_pkg_node.cpp){PROJECT_NAME}_node为生成可执行程序的名字,该名字可以任意指定。
src/node.cpp:为编译要使用的源码的文件名。
target_link_libraries(${PROJECT_NAME}_node ${catkin_LIBRARIES})
{PROJECT_NAME}_node是在add_executable中生成的可执行文件

image-20240627142349193

编译

roscore//打开Master
rosrun test_pkg test_pkg_node

rosnode命令

image-20240627142527534

头文件引用

打开终端输入以下命令进入要创建头文件的目录:

cd ~/ros_workspace/src/test_pkg/include/test_pkg

输入以下命令创建头文件:

gedit test_pkg.h

输入以下代码:

#ifndef _TEST_PKG_
#define _TEST_PKG_
#define TEST_PKG_VER "1.0.0"
#define INIT_COUNT 100
int addTwoNum(int a,int b);
#endif

完成后保存关闭头文件,修改test.cpp,如下代码

#include "ros/ros.h"
#include "std_msgs/String.h"
#include "test_pkg/test_pkg.h" //自定义头文件
#include <sstream>
int addTwoNum(int a,int b)
{
return a+b;
}//头文件函数实现部分
int main(int argc, char *argv[])
{
ros::init(argc, argv, "talker");//这个名字是给系统看的
ros::NodeHandle n;
ros::Publisher chatter_pub = n.advertise <std_msgs::String >("chatter", 1000);
ros::Rate loop_rate(10);
int count = INIT_COUNT;//调用了头文件中的宏定义
ROS_INFO("test_pkg version:%s",TEST_PKG_VER);
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
ROS_INFO("%s", msg.data.c_str());chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}

需要修改修改test_pkg 中的 CMakeLists.txt
image-20240627142818770

此处的include是一个相对路径,指的是当前功能包test_pkg下的include

引用同一工作空间内其他软件包的头文件

image-20240627143034917

my_pkg的目的是要引用test_pkg中的自定义头文件,因此这里创建my_pkg的时候要对test_pkg进行依赖。

my_pkg/src路径下创建源码文件my_pkg.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
#include "test_pkg/test_pkg.h" //自定义头文件
int main(int argc, char *argv[])
{
ros::init(argc, argv, "my_pkg");
ros::NodeHandle n;
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("my_chatter", 1000);
ros::Rate loop_rate(10);
int count = INIT_COUNT;//调用了头文件中的宏定义
ROS_INFO("test_pkg version:%s,init count:%d",TEST_PKG_VER,INIT_COUNT);//将其他软件包头文件中声明的宏定义打印出来
while (ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss << "my_pkg " << count;
msg.data = ss.str();
ROS_INFO("%s", msg.data.c_str());
chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}

image-20240627143210185

image-20240627143238451

对工作空间进行编译:

cd ~/ros_workspace/

catkin_make

编译完成之后可以运行节点查看结果。

roscore

rosrun my_pkg my_pkg_node

image-20240627143307049

launch文件编写

roslaunch工具来调用launch文件,执行这个脚本文件就一次性启动所有的节点程序。

launch文件同样也遵循着xml格式规范,是一种标签文本。


每个XML文件都必须要包含一个根元素,根元素由一对launch标签定义:

标签的上一级根标签为标签,用于启动一个ROS节点。启动一个节点的写法如下:

标签下有若干属性,至少要包含三个属性:pkg,type,name。Pkg属性指定了要运行的节点属于哪个功能包,type是指要运行节点的可执行文件的名称,name属性给节点指派了名称,它将覆盖任何通过调用 ros::int来赋 予节点的名称。

<launch>
<node pkg="test_pkg" type="test_pkg_node" name="test_pkg_node" output="screen"/>
<node pkg="my_pkg" type="my_pkg_node" name="my_pkg_node" output="screen"/>
</launch>

其中,output=“screen”属性可以将单个节点的标准输出到终端而不是存储在日志文件中。

image-20240627143727913
标签

<launch>
<node pkg="test_pkg" type="test_pkg_node" name="test_pkg_node" respawn="true" output="screen"/>
<node pkg="my_pkg" type="my_pkg_node" name="my_pkg_node" required="true" output="screen"/>
<include file="$(find third_pkg)/launch/third_pkg.launch"/>
</launch>
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "third_pkg"
  ros::init(argc, argv, "third_pkg");
  // 创建一个节点句柄
  ros::NodeHandle n;
  // 创建一个 Publisher,发布到话题 "third_pkg",消息类型为 std_msgs::String,队列大小为 1000
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("third_pkg", 1000);
  // 设置循环频率为 10Hz
  ros::Rate loop_rate(10);
  int count = 0;
  // 当 ROS 节点正常运行时,进入循环
  while (ros::ok())
  {// 创建一个 std_msgs::String 类型的消息
    std_msgs::String msg;
    std::stringstream ss;
    // 将 "third pkg:" 和当前计数值 count 写入字符串流
    ss << "third pkg: " << count;
    msg.data = ss.str();
    // 打印消息内容到终端
    ROS_INFO("%s", msg.data.c_str());
    // 发布消息到 "third_pkg" 话题
    chatter_pub.publish(msg);
    // 调用一次回调函数,处理所有已经到来的消息
    ros::spinOnce();
    // 按照设定的频率休眠
    loop_rate.sleep();
    // 计数值加 1
    ++count;
  }
  return 0;
}

image-20240627144220030

话题通讯 -话题

topic通讯原理

topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息

topic要经历下面几步的初始化过程:

首先publisher节点和subscriber节点都要到节点管理器进行注册;

然后publisher会发布topic,subscriber在master的指挥下会订阅该topic;

从而建立起sub-pub之间的通信。

注意整个过程是单向的。其结构示意图如下。
image-20240627145943528

Subscriber接收消息会进行处理,一般这个过程叫做回调(Callback)。所谓回调就是提前定义好了一个处理函数(写在代码中),当有消息来就会触发这个处理函数,函数会对消息进行处理。

topic通信属于一种异步的通信方式 各司其责,不存在协同工作,我们称这样的通信方式是异步的

ROS是一种分布式的架构,一个topic可以被多个节点同时发布,也可以同时被多个节点接收

分布式系统通信的好处:扩展性好、软件复用率高

  1. topic通信方式是异步的,发送时调用publish()方法,发送完成立即返回,不用等待反馈。
  2. subscriber通过回调函数的方式来处理消息。
  3. topic可以同时有多个subscribers,也可以同时有多个publishers。

通讯示例

输入以下命令在ros_workspace中创建第四个功能包subscribe_pkg。

cd ~/ros_workspace/src/
catkin_create_pkg subscribe_pkg rospy roscpp std_msgs
#include "ros/ros.h"
#include "std_msgs/Int32.h"
void chatterCallback(const std_msgs::Int32::ConstPtr& msg)
  {
ROS_INFO("I heard name:[%d]",msg->data);
}
int main(int argc,char **argv)
{
  ros::init(argc,argv,"subscribe_node");
  ros::NodeHandle n;
  ros::Subscriber sub = n.subscribe("third_pkg_topic",1000,chatterCallback);
  ros::spin();
  return 0;
}

当有消息发布到该话题时,调用回调函数 chatterCallback 并打印消息的内容

#include "ros/ros.h"
#include "std_msgs/Int32.h"
int main(int argc, char **argv)
{
  // 初始化 ROS 节点,节点名为 "third_pkg"
  ros::init(argc, argv, "third_pkg");
  // 创建一个节点句柄
  ros::NodeHandle n;
  // 创建一个 Publisher,发布到话题 "third_pkg_topic",队列大小为 1000
  ros::Publisher chatter_pub = n.advertise<std_msgs::Int32>("third_pkg_topic", 1000);
  // 设置循环频率为 2Hz
  ros::Rate loop_rate(2);
  // 初始化计数器
  int count = 0;
  // 当 ROS 仍在运行时,循环执行以下操作
  while (ros::ok())
  {
    // 创建一个消息对象
    std_msgs::Int32 msg;
    // 将计数器的值赋给消息的数据字段
    msg.data = count;
    // 打印消息的数据
    ROS_INFO("%d", msg.data);
    // 将消息发布到话题 "third_pkg_topic"
    chatter_pub.publish(msg);
    // 处理回调函数(如果有的话)
    ros::spinOnce();
    // 按照设定的频率休眠
    loop_rate.sleep();
    // 计数器递增,且在 0 到 4 之间循环
    count = (++count) % 5;
  }
  return 0;
}

(0 到 4)到 “third_pkg_topic” 话题,并且每秒发布 2 次。
image-20240627152313341

理解自定义消息类型

话题是ROS提供的一种重要通讯方式,话题上传输的信息内容即为消息

话题的类型由消息的数据类型决定,ROS提供了组成消息的基本数据类型。

int8, int16, int32, int64 (plus uint*)

float32, float64

string

time, duration

other msg files

variable-length array[] and fixed-length array[C]

image-20240627153012684

在third_pkg功能包下创建msg文件夹并在msg文件夹下新建myTestMsg.msg文件。

打开myTestMsg.msg文件输入如图4.20内容。

img

图4.20 myTestMsg.msg文件内容

img

img

img

图4.21 CMakeLists.txt文件修改内容

img

图4.22 package.xml文件修改内容

image-20240627153248478

修改third_pkg.cpp

#include "ros/ros.h"
#include "third_pkg/myTestMsg.h"//添加自定义消息的头文件
int main(int argc,char **argv)
{
ros::init(argc,argv,"third_pkg");
ros::NodeHandle n;// 更新话题的消息格式为自定义的消息格式
ros::Publisher chatter_pub = n.advertise<third_pkg::myTestMsg>("third_pkg_topic",1000);
ros::Rate loop_rate(2);
while(ros::ok())
{
third_pkg::myTestMsg msg;//声明一个自定义消息的对象
msg.name = "corvin";//填充消息内容
msg.age = 20;
msg.handsome = true;
msg.salary = 123.45;
chatter_pub.publish(msg);//将消息发布到话题中
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}

修改订阅端代码subscribe.cpp如下,黑色字体为修改部分要格外注意:

#include "ros/ros.h"
#include "third_pkg/myTestMsg.h"
// 回调函数,当接收到消息时调用
void chatterCallback(const third_pkg::myTestMsg::ConstPtr& msg)
{
  // 打印接收到的消息内容
  ROS_INFO("I heard - name: %s, age: %d, isHandsome: %d, salary: %f", 
           msg->name.c_str(), msg->age, msg->handsome, msg->salary);
}
int main(int argc, char **argv)
{
  // 初始化 ROS 节点,节点名为 "subscribe_node"
  ros::init(argc, argv, "subscribe_node");
  // 创建一个节点句柄
  ros::NodeHandle n;
  // 创建一个 Subscriber,订阅 "third_pkg_topic" 话题,队列大小为 1000,回调函数为 chatterCallback
  ros::Subscriber sub = n.subscribe("third_pkg_topic", 1000, chatterCallback);
  // 进入循环等待回调
  ros::spin();
  return 0;
}

image-20240627153615293

ROS通讯机制-服务

service方式在通信模型上与topic做了区别。Service通信是双向的,它不仅可以发送消息,同时还会有反馈。所以service包括两部分,一部分是请求方(Clinet),另一部分是应答方/服务提供方(Server)。这时请求方(Client)就会发送一个request,要等待server处理,反馈回一个reply,这样通过类似“请求-应答”的机制完成整个服务通信。

Node B是server(应答方),提供了一个服务的接口,叫做 /Service ,我们一般都会用string类型来指定service的名称,类似于topic。Node A向Node B发起了请求,经过处理后得到了反馈。

image-20240627155508120
Service是同步通信方式,所谓同步就是说,此时Node A发布请求后会在原地等待reply,直到Node B处理完了请求并且完成了reply,Node A才会继续执行。Node A等待过程中,是处于阻塞状态的成通信。这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。

表5.1 topic与service的对比

img

注意:远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。

示例

在third_pkg功能包下创建srv工作目录并在该目录下创建myTestSrv.srv文件

打开myTestSrv.srv文件输入以下内容:

int32 index

---

int32 result

两个部分用“—”分开修改third_pkg功能包的CMakeLists.txt

这里和自定义消息的修改方法类似:

  1. 添加message_generation依赖

img

  1. 添加.srv服务文件

img

  1. 添加消息生成依赖

img

修改third_pkg.cpp

int main(int argc, char **argv)
{// 初始化 ROS 节点,节点名为 "third_pkg"
    ros::init(argc, argv, "third_pkg");// 创建一个节点句柄
    ros::NodeHandle n;// 创建一个 Publisher,发布自定义消息到 "third_pkg_topic" 话题,队列大小为1000
    ros::Publisher chatter_pub = n.advertise<third_pkg::myTestMsg>("third_pkg_topic", 1000);// 定义并初始化服务端,服务名为 "batteryStatus",服务回调函数为 checkBattery
    ros::ServiceServer service = n.advertiseService("batteryStatus", checkBattery);// 设置发送话题的频率为 2Hz,即每0.5秒发送一条消息
    ros::Rate loop_rate(2);// 循环运行,直到 ROS 关闭
    while (ros::ok())
    {// 电池电量减少
        battery--;// 如果电池电量小于10,则重置为100
        if (battery < 10) battery = 100;// 声明一个自定义消息对象
        third_pkg::myTestMsg msg;// 填充消息数据
        msg.name = "corvin";
        msg.age = 20;
        msg.handsome = true;
        msg.salary = 123.45;// 将消息发布到话题中
        chatter_pub.publish(msg);// 处理回调函数
        ros::spinOnce();// 休眠以维持循环频率
        loop_rate.sleep();
    }
    return 0;
}

修改订阅者subscribe.cpp

#include "ros/ros.h"
#include "third_pkg/myTestMsg.h"  // 自定义消息的头文件
#include "third_pkg/myTestSrv.h"  // 自定义服务的头文件
// 订阅者的回调函数,用于处理接收到的消息
void chatterCallback(const third_pkg::myTestMsg::ConstPtr& msg)
{
    ROS_INFO("I heard-name:%s,age:%d,ishandsome:%d,salary:%f", 
             msg->name.c_str(), msg->age, msg->handsome, msg->salary);}
int main(int argc, char **argv)
{// ROS 初始化,节点名为 "subscribe_node"
    ros::init(argc, argv, "subscribe_node");// 创建节点句柄
    ros::NodeHandle n;// 设置循环频率为 1Hz
    ros::Rate loop_rate(1);// 创建一个订阅者,订阅 "third_pkg_topic" 话题,队列大小为 1000,回调函数为 chatterCallback
    ros::Subscriber sub = n.subscribe("third_pkg_topic", 1000, chatterCallback);
    // 创建一个服务客户端,连接到 "batteryStatus" 服务
    ros::ServiceClient client = n.serviceClient<third_pkg::myTestSrv>("batteryStatus");// 创建一个服务请求对象
    third_pkg::myTestSrv mySrv;
    mySrv.request.index = 1;  // 设置服务请求的参数
    while (ros::ok())
    {// 调用服务并处理响应
        if (client.call(mySrv))
        {
            ROS_WARN("Client Get Battery: %d", mySrv.response.result);  // 打印获得的电
        }
        else
        {
            ROS_ERROR("Failed to call batteryStatus service");  // 打印错误信息
            return 1;  // 返回错误码
        }// 处理回调函数
        ros::spinOnce();// 休眠以保持循环频率
        loop_rate.sleep();}
    return 0;}

1****服务端的定义与初始化

ros::ServiceServer service = n.advertiseService(“batteryStatus”,checkBattery);

参数1:”batteryStatus”:发布服务的名称,类型:字符串,可以任意指定;

参数2:checkBattery:指定回调函数,类型:函数指针;

2 服务回调函数写法

bool checkBattery(third_pkg::myTestSrv::Request &req,third_pkg::myTestSrv::Response &res)

参数1:third_pkg::myTestSrv::Request &req,请求部分参数.

img

如上图所示:

1部分:服务的类型,写上要发布的服务类型。

2部分:固定写法,表征此部分为请求部分;

3部分:变量名称,可以随便写;

在此处,由于服务类型third_pkg::myTestSrv的定义如下:

文本框: int32 index --- int32 result

因此,req的成员变量只有一个为index,即:req.index

参数2:third_pkg::myTestSrv::Response &res,响应部分参数。

img

1部分:服务的类型,写上要发布的服务类型。

2部分:固定写法,表征此部分为响应部分;

3部分:变量名称,可以随便写;

在此处,由于服务类型third_pkg::myTestSrv的定义如下:

文本框: int32 index --- int32 result

因此res的成员变量只有一个为:result,即:res.result。

3 客户端定义

ros::ServiceClient client = n.serviceClient<third_pkg::myTestSrv>(“batteryStatus”);

img

其中:

ros::ServiceClient为客户端定义类;

1部分:客户端初始化函数;

2部分:服务类型,这里为:third_pkg::myTestSrv;

3部分:要调用的服务名字,类型为字符串;

4 服务调用:

third_pkg::myTestSrv mySrv;//定义变量

mySrv.request.index = 1;//填充数据

client.call(mySrv);//调用服务

移动机器人

四大组成部分:执行机构 驱动系统 传感系统 控制系统

控制 - 驱动 -执行 -对象 -传感器 - 控制

| |

传感器

ros组成

ros = 通信机制 + 开发工具 + 应用功能 + 生态系统

分布式通信框架 功能独立又相互联系

节点 功能的执行单元

节点 节点管理器

话题和消息

服务

参数

topic通信属于一种异步的通信方式 各司其责,不存在协同工作,我们称这样的通信方式是异步的

ROS是一种分布式的架构,一个topic可以被多个节点同时发布,也可以同时被多个节点接收

分布式系统通信的好处:扩展性好、软件复用率高

  1. topic通信方式是异步的,发送时调用publish()方法,发送完成立即返回,不用等待反馈。
  2. subscriber通过回调函数的方式来处理消息。
  3. topic可以同时有多个subscribers,也可以同时有多个publishers。

ros常用工具

  1. rostopic 话题相关
  2. rosservice 服务
  3. rosnode 节点
  4. rosparam 参数
  5. rosmsg 话题消息
  6. rossrv 服务消息