0%

1.launch启动文件

基本元素

启动文件Launch File是ROS中一种同时启动多个节点的途径。可以自动启动ROS Master并实现多个节点的各种配置,为多个节点的操作提供了很大便利。

先来看一个简单的.launch文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<launch>
<!-- Turtlesim Node,注意注释的格式-->
<node pkg="turtlesim" type="turtlesim_node" name="sim"/>

<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
<!-- Axes -->
<param name="scale_linear" value="2" type="double"/>
<param name="scale_angular" value="2" type="double"/>

<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle1" name="turtle1_tf_broadcaster" />
<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle2" name="turtle2_tf_broadcaster" />

</launch>

其中:

  • <launch>...</launch>为XML语法中的根元素,XML语法要求每个文件必须包含一个根元素,文件中其他内容都必须包含在该标签内。

  • <node.../>为启动ROS节点的标签元素,由上述定义规则可知,在launch文件中启动一个节点需要三个属性:

    • pkg:该节点所在功能包的名字
    • type:该节点的可执行文件名字,可执行文件是在为CMake_Lists中通过add_executable定义的
    • name:用于定义节点运行中的名字,会覆盖掉init()中定义的节点名字

    此外,还有可能用到:

    • output=”screen”:将节点的标准输出打印到终端屏幕
    • respawn=”ture”:当节点停止时会自动重启,默认false
    • require=”ture”:必要节点,当该节点终止时launch中的其他节点也终止
    • ns=”namespace”:为节点内的相对名称添加命名空间前缀
    • args=”arguments”:节点需要的输入参数

参数设置

变量声明,由param和arg两种标签元素,它们的作用是不同的

  • <param.../>:设置ros中运行的参数:

    1
    <param name="scale_linear" value="2" type="double"/>

    作用显而易见:名为scale_linear的参数(parameter)的值被设置为了2,类型为double

  • <arg.../>设置仅限于launch内部使用的局部变量

    1
    <arg name="aca" default="value"/>

重映射机制(重要)

ROS提供一种重映射机制,便于将社区中其他人的功能包提供的接口进行重映射,而不是直接修改。

例如乌龟控制节点发布的控制话题可能是/turtlebot/cmd_vel,但是我们机器人订阅的话题是/cmd_vel,此时只需要重映射,我们的机器人就能收到话题消息了

1
<remap from="/turtlebot/cmd_vel" to="/com_vel"/>

2.TF变换

TF功能包使用树形数据结构,可以让用户跟踪多个坐标系,以时间为轴跟踪这些坐标系(默认10s内),并允许开发者请求如下数据:

  • 5s前,机器人头部坐标系相对于全局坐标系是怎样的?
  • 机器人夹取的物体相对于机器人中心坐标系的位置在哪?
  • 机器人中心坐标系相对于全局坐标系的位置在哪?

想要使用TF功能包,总体来说要以下两个步骤:

  • 监听TF变换,接收并缓存系统中发布的所有坐标数据,并查询需要的坐标变换关系
  • 广播TF变换,向系统广播坐标变换关系

乌龟例程中的TF

该环境若出现问题可看linux、ROS杂 | 小董的BLOG

  • 准备库环境:sudo apt-get install ros-noetic-turtle-tf
  • 启动launch文件进行实验:roslaunch turtle_tf turtle_tf_dmeo.launch
  • 开启键盘控制:rosrun turtlesim turtle_teleop_key

可得到两只乌龟,一只会跟踪另一只

  • rosrun tf view_frames后可在用户根目录下找到frames.pdf。可得当前系统中存在三个坐标系:world、turtle1、turtle2,其中world为TF树的根节点。
  • rosrun tf tf_echo turtle1 turtle2可实时查看当前turtle2跟随turtle1所需的坐标变换

得到坐标变换后,便可以计算两乌龟间的距离和角度。

创建TF广播器

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
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>

std::string turtle_name;

void poseCallback(const turtlesim::PoseConstPtr& msg)
{
// tf广播器
static tf::TransformBroadcaster br;

// 根据乌龟当前的位姿,设置相对于世界坐标系的坐标变换
tf::Transform transform;
transform.setOrigin( tf::Vector3(msg->x, msg->y, 0.0) );//平移矩阵,因为为平面移动,第三项z为0
tf::Quaternion q;
q.setRPY(0, 0, msg->theta);//旋转矩阵,平面上只有yaw有值
transform.setRotation(q);

// 发布坐标变换
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "world", turtle_name));
}

int main(int argc, char** argv)
{
// 初始化节点
ros::init(argc, argv, "my_tf_broadcaster");
if (argc != 2)//argc为输入字符串个数
{
ROS_ERROR("need turtle name as argument");
return -1;
};
turtle_name = argv[1];//输入的参数从argv[1]开始存放

// 订阅乌龟的pose信息
ros::NodeHandle node;
ros::Subscriber sub = node.subscribe(turtle_name+"/pose", 10, &poseCallback);

ros::spin();

return 0;
};

创建TF监听器

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
55
56
#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>

int main(int argc, char** argv)
{
// 初始化节点
ros::init(argc, argv, "my_tf_listener");

ros::NodeHandle node;

// 通过服务调用,产生第二只乌龟turtle2
ros::service::waitForService("spawn");
ros::ServiceClient add_turtle =
node.serviceClient<turtlesim::Spawn>("spawn");
turtlesim::Spawn srv;
add_turtle.call(srv);

// 定义turtle2的速度控制发布器
ros::Publisher turtle_vel =
node.advertise<geometry_msgs::Twist>("turtle2/cmd_vel", 10);

// tf监听器,创建后自动接收TF树的消息并缓存10s
tf::TransformListener listener;

ros::Rate rate(10.0);
while (node.ok())
{
tf::StampedTransform transform;
try
{
// 查找turtle2与turtle1的坐标变换
listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));//给定目标坐标系和源坐标系,等待两个坐标系之间一定周期下的变换关系,第四个参数为超时时间(该函数会产生堵塞)
listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform);//给定目标坐标系和源坐标系,得到两个坐标系之间一定周期下的坐标变换(存入第四个参数中),ros::Time(0)表示想要的是最新的一次坐标变换
}
catch (tf::TransformException &ex)
{
ROS_ERROR("%s",ex.what());
ros::Duration(1.0).sleep();
continue;
}

// 根据turtle1和turtle2之间的坐标变换,计算turtle2需要运动的线速度和角速度
// 并发布速度控制指令,使turtle2向turtle1移动
geometry_msgs::Twist vel_msg;
vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(),
transform.getOrigin().x());
vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) +
pow(transform.getOrigin().y(), 2));
turtle_vel.publish(vel_msg);

rate.sleep();
}
return 0;
};

通过launch文件运行这些节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<launch>
<!-- 海龟仿真器 -->
<node pkg="turtlesim" type="turtlesim_node" name="sim"/>

<!-- 键盘控制 -->
<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>

<!-- 两只海龟的tf广播,用同一可执行文件创建两个广播节点 -->
<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle1" name="turtle1_tf_broadcaster" />
<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle2" name="turtle2_tf_broadcaster" />

<!-- 监听tf广播,并且控制turtle2移动 -->
<node pkg="learning_tf" type="turtle_tf_listener"
name="listener" />

</launch>

1.话题中的发布者与订阅者

  • 使用rqt_graph可以查看当前的节点关系图,如图为乌龟例程的键盘输入控制节点图

    其中teleop_turtle节点创建了一个发布者,turtlesim节点创建了一个订阅者;一个发布键盘控制的命令,一个订阅命令实现🐢的移动,此时的话题是/turtlel/cmd_vel。

创建Publisher(发布者)

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
 #include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"

int main(int argc, char **argv)
{
// ROS节点初始化,一个cpp对应一个节点,节点名为talker,在当前运行的ROS中独一无二
ros::init(argc, argv, "talker");

// 创建节点句柄,方便对talker节点的使用
ros::NodeHandle n;

/* 创建一个Publisher,发布名为chatter(chatter才是话题名字!!)的话题,消息类型为std_msgs::String
ros::声明命名空间,chatter_pub为话题变量(?自己的理解),其发布的内容为String类型消息,1000为队列大小,advertise类似cpp容器之类的东西?*/

ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

// 设置循环的频率,单位Hz
ros::Rate loop_rate(10);

int count = 0;
while (ros::ok())//节点未发送异常则持续循环
{
// 初始化std_msgs::String类型的消息
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();//将流的内容全部返回到msg的data中,std_msgs::String对象只有data这一个成员

//
ROS_INFO("%s", msg.data.c_str());//打印内容,只是打印而已
chatter_pub.publish(msg);//发布消息,发布后Master会找订阅该话题的节点

// 循环等待回调函数
ros::spinOnce();

// 按照循环频率延时
loop_rate.sleep();//节点休眠,时长与前设置的循环频率有关
++count;
}

return 0;
}

创建Subscriber(订阅者)

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
/**
* 该例程将订阅chatter话题,消息类型String
*/

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

// 接收到订阅的消息后,会进入消息回调函数,传入的参数为一个消息指针(记住吧,感觉形式怪怪的)
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
// 将接收到的消息打印出来
ROS_INFO("I heard: [%s]", msg->data.c_str());//注意形式
}

int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "listener");

// 创建节点句柄
ros::NodeHandle n;

// 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback,注意这里要注册回调函数,消息类型与订阅者无关
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

// 循环等待回调函数
ros::spin();

return 0;
}

然后在该功能包中的CmakeLsit.txt中:

1
2
3
4
5
6
7
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
##add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)不需

add_executable(listener src/talker.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
##add_dependencies(listener ${PROJECT_NAME}_generate_messages_cpp)不需

其中:

  • add_ex...:为设置需编译的代码和可执行文件。第一个参数为期望生成可执行文件的名字,一般与节点名相同,方便使用;第二个参数为要编译的文件。
  • target...:设置链接库。第一个参数为需链接的可执行文件名(同上的名字);第二个为要链接的库
  • add_de...:设置依赖。为可执行文件添加能动态产生消息代码的依赖。但是先版本好像已不需要添加这个

自定义消息类型

前两节中使用的消息类型为ROS元功能包定义的std_msgs(标准数据类型)中预定义的String类型,除此之外,用户可以自定义msg文件,使用自定义的类型,流程如下:

1.编写msg文件

在本功能包中创建msg文件夹(与src平行),并创建person.msg

1
2
3
4
5
6
7
string name
uint8 sex
uint8 age

uint8 unknown=0
uint8 male=1
uint8 female=2

2.修改本功能包下的package.xml

添加:

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

保证msg文件能转化为cpp,py等语言的源文件,第一行为编译依赖;第二行为执行依赖

3.修改本功能包下的CMakeLists.txt

  • 已有的find_package中:

    1
    2
    3
    4
    5
    6
    find_package(catkin REQUIRED COMPONENTS
    roscpp
    rospy
    std_msgs
    message_generation
    )
  • 已有的catkin_package中增加:

    1
    2
    3
    4
    catkin_package(
    ...
    CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
    ...)
  • 找到被注释或已有的add_message_files()语句:

    1
    2
    3
    4
    add_message_files(
    FILES
    person.msg
    )
  • 找到被注释或已有的genreate_message()语句:

    1
    2
    3
    4
    generate_messages(
    DEPENDENCIES
    std_msgs
    )

3.编译

编译成功后,对于C++而言,编译器帮我们自动编写一个头文件:sensor.h,文件位于workspace/devel/include中,通过引用头文件就可以使用这个自定义数据了。

  • 引用格式:#include "learning_communication/person.h"/前为该msg文件(不是.h文件)所在的功能包的名字

  • 使用格式:learning_communication::person

    1
    2
    3
    4
    learning_communication::person msg;
    std::stringstream ss;
    ss << "dhk";
    msg.name=ss.str();

将自定义消息用于刚刚的话题中

发布者

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
#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "learning_communication/person.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "talker");

// 创建节点句柄
ros::NodeHandle n;

// 创建一个Publisher,发布名为chatter的topic,消息类型为自定义消息类型
ros::Publisher chatter_pub = n.advertise<learning_communication::person>("chatter", 1000);

// 设置循环的频率
ros::Rate loop_rate(10);

int count = 0;
while (ros::ok())
{
// 初始化std_msgs::String类型的消息
learning_communication::person msg;
std::stringstream ss;
ss << "dhk" << count;
msg.name = ss.str();

// 发布消息
ROS_INFO("%s", msg.name.c_str());//c_str()通用于string类型
chatter_pub.publish(msg);

// 循环等待回调函数
ros::spinOnce();

// 按照循环频率延时
loop_rate.sleep();
++count;
}

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
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "learning_communication/person.h"
// 接收到订阅的消息后,会进入消息回调函数
//ConstPtr&为通用类型,直接cv就行
void chatterCallback(const learning_communication::person::ConstPtr& msg)
{
// 将接收到的消息打印出来
ROS_INFO("I heard: [%s]", msg->name.c_str());
}

int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "listener");

// 创建节点句柄
ros::NodeHandle n;

// 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

// 循环等待回调函数
ros::spin();

return 0;
}

如图,talker节点发布了名为chatter的话题,被listener节点订阅。

2.服务中的服务端与客户端

服务(service)为节点之间通信的一种方式,由客户端(Client)发布请求(Request),服务端(Server)处理后返回应答(Response)

自定义服务数据

1.编写srv文件

同话题中的自定义消息一样,服务数据可以通过srv文件进行定义,且同msg文件一样放置在具体功能包文件夹中,但由于文件包含请求应答两个数据域,因此要特别分割一下:

1
2
3
4
int64 a
int64 b
---
int64 sum

上为请求域,下为应答域

2.修改package.xml

内容同自定义订阅消息中的操作

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

3.修改CMakeLists.txt

找到被注释或已有的add_service_files()语句:

1
2
3
4
add_service_files(
FILES
AddTwoInts.srv
)

其他操作同自定义订阅消息

创建Server

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"
#include "learning_communication/AddTwoInts.h"//记得添加头文件

// service回调函数,第一参数为请求域req,第二参数为应答域res,不同域要分别声明
bool add(learning_communication::AddTwoInts::Request &req,
learning_communication::AddTwoInts::Response &res)
{
// 将输入参数中的请求数据相加,结果放到应答变量中
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);

return true;
}

int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_server");

// 创建节点句柄
ros::NodeHandle n;

// 创建一个名为add_two_ints的server,注册回调函数add()
ros::ServiceServer service = n.advertiseService("add_two_ints", add);

// 等待回调函数
ROS_INFO("Ready to add two ints.");
ros::spin();

return 0;
}

创建Client

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
#include <cstdlib>
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"

int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_client");

// 从终端命令行获取两个加数,为main的输入参数
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}

// 创建节点句柄
ros::NodeHandle n;

// 创建一个client,请求add_two_int的service,两文件在此处联系
// service消息类型是learning_communication::AddTwoInts
ros::ServiceClient client = n.serviceClient<learning_communication::AddTwoInts>("add_two_ints");

// 创建learning_communication::AddTwoInts类型的service消息
learning_communication::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);

// 发布service请求,等待加法运算的应答结果
//这里输入参数为1个,但是回调是两个,回调的写法应该是固定格式?
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}

return 0;
}

编译功能包

在CMakeLists.txt中添加相关内容

1
2
3
4
5
add_executable(add_two_ints_server src/server.cpp)
target_link_libraries(add_two_ints_server ${catkin_LIBRARIES})

add_executable(add_two_ints_client src/client.cpp)
target_link_libraries(add_two_ints_client ${catkin_LIBRARIES})

这里要注意add_executable第一个参数为期望的可执行文件名,可以不与节点名相同,但我喜欢相同(哼)

运行

这里要注意

  • 运行节点时不要运行成cpp文件
  • Client节点需添加2个初始参数

话题与服务的区别

  • 话题中建立两节点关系的阶段是在listener中订阅相关话题并注册回调函数;而服务中建立两节点关系的阶段是在client中请求名为xx的服务。因此顺序应该为:
    • 话题:先编写talker,再写listener
    • 服务:先编写server,再写client
  • 两种通信的具体顺序:
  • 话题
    • 创建发布者,话题,并规定话题的消息形式
    • 发布消息
    • 创建订阅者,并订阅话题,注册回调函数,等待回调函数运行
  • 服务
    • 创建服务端,服务名,并注册服务回调函数
    • 创建客户端,并请求相关服务名的服务,此时规定服务的消息形式
    • 客户端请求服务并输入相关内容,服务端回调函数进行处理时客户端堵塞,等待服务完成,服务完成后客户端内与回调函数相关的参数已改变
  • 需要注意的是,话题的回调函数只在订阅文件生效,不是反馈;而服务的回调函数会生效于客户端,是真正的反馈

ROS命名规则

  • 基础名称:dhk
  • 全局名称:/xx/dhk
  • 相对名称:xx/dhk
  • 私有名称:~xx/dhk

起始为/的都为全局名称

1.关键概念

节点Node

执行运算任务的进程,一个系统一般由多个节点组成。

消息Message

节点之间的通信机制就是基于发布/订阅模型的消息通信,消息有多种数据结构。

话题Topic

消息的传递方式。

一个节点可以针对一个给定的话题发布消息,也可以关注某个话题并订阅特定类型的数据。

服务Service

双向同步传输模式,两节点一个用于请求,一个用于应答。ROS只允许一个节点提供指定命名的服务。

节点管理器ROS Master

管理节点

2.文件系统

功能包

ROS软件的基本单元,包含节点、库、配置文件等

对应文件夹内容

  • config:功能包的配置文件

  • include:功能包需要用到的头文件

  • scripts:可以直接运行的py脚本

  • src:需要编译的cpp代码

  • launch:所有启动文件

  • msg:功能包自定义的消息类型

  • srv:自定义的服务类型

  • action:自定义的动作指令

  • CMakeLists.txt:编译器编一功能包的规则

  • package.xml:功能包清单,包含该功能包名称、版本号等信息。

    定义了代码编译所依赖的其他功能包

    定义了功能包中可执行程序运行时所依赖的其他功能包

1
2
3
4
5
6
7
8
9
10
针对功能包的常用命令:
catkin_create_pkg 创建功能包
rospack 获取功能包信息
catkin_make 编译工作空间中的功能包
rosdep 自动安装功能包依赖的其他包
roscd 功能包目录跳转
roscp 拷贝功能包中的文件
rosed 编辑功能包中的文件
rosrun 运行功能包中的可执行文件
roslaunch 运行启动文件

元功能包

只包含一个package.xml,将多个功能包整合成一个逻辑上的独立功能包

与功能包中的类似,需额外包含一个

1
2
3
<export>
<metapackage/>
</export>

3.ROS的通信机制

话题通信

  • Talker/Listener注册
  • ROS Master进行信息匹配,根据Listener的订阅信息从注册列表找Talker,没找到则等
  • Listener发送连接请求
  • Talker确认连接请求
  • Listener尝试与Talker建立网络连接
  • Talker向Listener发布数据

服务通信

与话题相比减少了RPC通信,即匹配后直接进行网络连接

服务是一种带应答的通信,最后一步为Talker接收到Listener的请求和参数后开始执行服务功能,完成后Talker发送应答数据。

区别

  • 异步;同步
  • 无反馈;有反馈
  • 有缓冲区;无缓冲区
  • 多对多;一对一

话题适用于不断更新的数据通信;服务适用于逻辑处理复杂的数据同步交换

4.小乌龟仿真

  • roscore为运行ROS Master

  • rosrun … … 启动…功能包中的…节点

    1
    2
    3
    rosrun turtlesim ...	启动turtlesim功能包中的某个节点
    rosrun turtlesim turtlesim_node 启动turtlesim仿真器节点
    rosrun turtlesim turtle_teleop_key 运行键盘控制节点

5.创建工作空间和功能包

工作空间是存放工程开发相关文件的文件夹,现默认使用Catkin编译系统。

一个典型的工作空间包含以下目录空间:

  • src:代码空间,储存所有ROS功能包的源码
  • build:编译空间,用于存储工作空间编译过程中产生的缓存信息和中间文件
  • devel:开发空间,放置编译生成的可执行文件
  • install:安装空间,编译成功后,可以使用make install命令将可执行文件安装到当前工作空间。运行该空间中的环境变量脚本即可在终端中运行这些可执行文件。该空间非必要。

创建工作空间

1
2
3
4
5
mkdir catkin_ws/src
进入src后
catkin_init_workspace 创建工作空间
cd .. 回到工作空间
catkin_make 编译

编译成功后,自动生成build和devel。devel中生成几个setup.*sh形式的环境变量设置脚本,可使用source运行。

运行后该工作空间环境变量生效。可在工作空间外使用?

1
2
source devel/setup.bash
该命令设置的环境变量只能在当前终端中剩下

创建功能包

1
catkin_create_pkg <package_name> [depend1] [depend2]...
  • 为功能包名字
  • depend为当前创建的功能包编译所依赖的其他功能包c’d
  • ROS不允许功能包嵌套,所有功能包平行放置在src
  • 任何添加操作完后都应回到根目录source添加其环境变量

工作空间覆盖

ROS允许多个工作空间并存,当遇到工作空间中名字相同(内容不一定相同)的功能包时,新设置的路径会自动放到最前端,在运行时,ROS也会优先查找最前端的工作空间是否存在指定的功能包。

1
2
rospack find ... 查找...功能包的位置
/opt/ros为ROS的默认工作空间

如果一个工作空间下的b功能包依赖同空间的a功能包,而a功能包又被另一工作空间下的新a功能包覆盖,该新a功能包名字与a功能包相同,但内容可能不同,因此可能导致b功能包存在潜在风险。

6.vocode配置ROS环境

1打开工作空间

在工作空间的根目录下输入:

1
code .

因为安装了ROS插件,VScode会直接识别catkin环境,并且自动生成.vscode文件夹,里面保含c_cpp_properties.json、settings.json 两个文件。

2创建功能包

在vscode资源管理中右键src选择create catkin package

3配置相关文件

1.在.vscoce下的task.json(记得加逗号和引号)

1
2
3
4
5
6
"args": [
"--directory",
"/home/dhk/catkin_ws",
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
],

2.在c_cpp_properties.json中添加 (记得逗号和引号)

“compileCommands”: “${workspaceFolder}/build/compile_commands.json”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"configurations": [
{
"browse": {
"databaseFilename": "${default}",
"limitSymbolsToIncludedHeaders": false
},
"includePath": [
"/opt/ros/noetic/include/**",
"/home/dhk/catkin_ws/src/learning_communication/include/**",
"/usr/include/**"
],
"name": "ROS",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "c++14",
"compileCommands": "${workspaceFolder}/build/compile_commands.json"
}

若后续对节点提示找不到ros头文件且确实无法编译,应该与这两项有关

4编写测试节点

在新功能包的src中创建helloworld.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");//节点名为hello
//创建 ros 节点句柄(非必须)
ros::NodeHandle n;
//控制台输出 hello world
ROS_INFO("hello world!");

return 0;
}

新功能包中的的CmakeLsit.txt添加:

1
2
3
4
5
6
7
add_executable(hello src/helloworld.cpp)
target_link_libraries(hello ${catkin_LIBRARIES})


其中
add_executable(节点名 src/文件名)
target_link_libraries(节点名 ${catkin_LIBRARIES})

vscode编译,效果同catkin_make

1
执行快捷键:ctrl+shift+b

5运行ROS Master

建议还是用roscore

vscode中 c+s+p

ros:start和ros:stop对应开关,但是运行后没有提示?

6运行节点

执行快捷键ctrl + shfit + p输入ROS:Run a Ros executable, 依次输入你创建的功能包的名称以及节点名称(即编译成功后二进制文件的名称)

问题

当工作空间可以正常编译但却找不到功能包中的某个节点时:

将第4节中的

1
2
add_executable(hello src/helloworld.cpp)
target_link_libraries(hello ${catkin_LIBRARIES})

一定要放在该功能包中的CmakeLsit.txt的末尾处!!

快捷键

ctrl+shift+p:调出用于执行命令的输入框
ctrl+shift+b:编译