一、说明
不知是何原因,ROS2居然没有集成开发环境,因此工程管理、编译等是全手工活。本文将详细讲述工程构建、编译、topic节点具体内容。让初学者直接进入战场环境。结合图文,尽量看清开发过程。
二、目标实现
我们这里就是要手工构建一个Publisher和一个Subscriber让它们实现简单通信。
三、开发过程
3.1 新建一个工作空间
新建工作空间,叫mytry_ws:
mkdir -p mytry_ws/src
cd mytry_ws/src
mkdir -p
: 递归创建目录,即使上级目录不存在,会按目录层级自动创建目录
注意:这里的src不是源代码所在地,而是包所在地。
3.2 创建工作包
1)进入包的目录中:
cd mytry_ws/src
2)创建名称为mytopic的包
ros2 pkg create mytopic --build-type ament_cmake --dependencies rclcpp std_msgs
3)语法解释
ros2 pkg
create mytopic \ # 建立一个包,名称 mytopic
--build-type ament_cmake \ # 构建工具是 ament_cmake,这类似于cmake
--dependencies rclcpp std_msgs # 依赖的基类,rclcpp是c++基础类,std_msgs是消息类
以上实质性参数有三个:
- mytopic: 包名称
- --build-type ament_cmake: 编译器是ament_cmake
- --dependencies rclcpp std_msgs: 依赖包指定为rclcpp、std_msgs;
4)执行结果
- 创建固定工程路径,以下图所示,基本上是固定的框架。
- CMakeLists。txt是编译用文件
- package.xml是包的说明文件
- include下的mytopic包名,是在编译的时候与其它包的include区别。
- mytopic/src目录下,是真正的节点源文件。
- 一个工程可以多个包
- 一个包内可以多个节点
结果图如下,(注意这里用链表表示目录层次关系)
3.3 publisher节点编写
在包内建立节点程序,在mytry_ws/src/mytopic/src
下创建lambda.cpp
和sublambda.cpp俩个节点源代码。
3.3.1 定义一个发布方节点,源代码lambda.cpp
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses a fancy C++11 lambda
* function to shorten the callback syntax, at the expense of making the
* code somewhat more difficult to understand at first glance. */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
auto timer_callback =
[this]() -> void {
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(this->count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
this->publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
3.3.2 程序解释
- 1) 时间类
#include <chrono>此处为调用时间类变量做准备。时间类变量这里举个例子:
- 2)你的节点类 MinimalPublisher继承了基类rclcpp::Node方可成为正式节点
class MinimalPublisher : public rclcpp::Node
- 3) MinimalPublisher 的构造函数中,初始化两个变量
MinimalPublisher(): Node("minimal_publisher"), count_(0)
- Node::name = "minimal_publisher" , 直接用父类定义节点名称;
- 和计数器属性,这里计数器 count_(n)是发送缓存长度
- 4)定义一个发布者
先定义了一个string类型的发布者,然后初始化:初始内容为“topic”,历史数据深度为10
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
private:
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
- 5) 关于auto的用法
auto xxx = [ function ];
此处的auto是自动变量,意思是说,xxx和=后面的函数返回变量一致。(这里避免写具体的类型,让编译器自己看着处理)
- 6)关于函数指针
auto timer_callback =
[this]() -> void {}
此处 timer_callback是个函数指针,指向 [this]() -> void {},这相当于定义:
void MinimalPublisher::timer_callback() { }
7)定义字符串对象
auto message = std_msgs::msg::String();
这里定义一个字符串调用了函数值得吗,值得:1)可以用auto指针,2)确保了变量初始化。
8) 把信息打印到屏幕
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
9)发布信息
this->publisher_->publish(message);
10)定时器间歇性调用
timer_ = this->create_wall_timer(500ms, timer_callback);
11)主函数入口
注意,这里构造函数就按以前的一般函数用了,在循环中重复调用。
3.4 Subscriber节点创建
3.4.1 消息订阅方代码
节点程序名称:sublambda.cpp,位置
在mytry_ws/src/mytopic/src
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic",
10,
[this](std_msgs::msg::String::UniquePtr msg) {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
});
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
3.4.2 代码解释
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic",
10,
[this](std_msgs::msg::String::UniquePtr msg) { ... } );
此处回调函数获得数据指针,如获取数值,可以:
MsgT::SharedPtr my_msg;
auto sub = node->create_subscription<MsgT>(topic,
[&](const MsgT::SharedPtr msg){my_msg = msg;},
QoS,
callback_group);
四、编译前准备
4.1 修改Cmakelist.txt
1)依赖项追加
如果新建功能包的时候没有加--dependencies rclcpp std_msgs等功能包, 则需要手动添加: (任意位置均可)
- find_package(rclcpp REQUIRED)
- find_package(std_msgs REQUIRED)
2)可执行节点定义
- add_executable()
让编译器编译Customer.cpp和KFC.cpp这两个文件. 并生成可执行文件Customer_node和KFC_node
- ament_target_dependencies
添加编译的依赖
add_executable(publisher_lambda lambda.cpp)
ament_target_dependencies(publisher_lambda rclcpp std_msgs)
add_executable(subscriber_lambda sublambda.cpp)
ament_target_dependencies(subscriber_lambda rclcpp std_msgs)
3)将编译好的文件安装到正确路径
install(TARGETS
publisher_lambda
subscriber_lambda
DESTINATION lib/${PROJECT_NAME}
)
4.2 修改package.xml
同样地, 新建功能包的时候没有加--dependencies rclcpp std_msgs等功能包, 则需要手动添加, 放置于<package>标签下
<depend>rclcpp</depend>
<depend>std_msgs</depend>
也可自行修改下面这些声明, 与实现功能无关, 但是最好写全
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="fanziqi@fanziqi.site">fanziqi</maintainer>
<license>TODO: License declaration</license>
五、 编译
5.1 编译指定包名称
- 注意:编译的时候以包为单位,指定哪个包,就编译哪个。
--packages-select指定编译customer_and_kfc功能包
colcon build --packages-select mytopic
5.2 刷新环境
- 在ubuntu下:
echo "source /mytry_ws/install/setup.zsh" >> ~/.bashrc
source ~/.bashrc
六、 运行
新建一个终端窗口, 运行发布节点
ros2 run mytopic publisher_lambda
再另新建一个终端, 运行收听节点
ros2 run mytopic subscriber_lambda
此时应该可以看见: