ROS2通信编程

ROS2通信编程

一、话题

话题是ROS2中最常用的通信方式之一,话题通信采取的是订阅发布模型与MQTT协议类似。

1、订阅发布模型

一个节点发布数据到某个话题上,另外一个节点就可以通过订阅话题拿到数据。

image-20240315104407861

模型类似我们订阅报纸或关注微博,换成下图也许更好理解。

image-20240315104701418

除了上述这种一个节点发布,一个节点接受的形式外,ROS2话题通信其实还可以是1对n,n对1,n对n的。

  • 1对n
image-20240315105038855
  • n对1
image-20240315105508566
  • n对n

    image-20240315105443290

2、消息接口

为了方便发送者和接收者进行数据的交换,ROS2帮我们在数据传递时做好了消息的序列化和反序列化,而且ROS2的消息序列化与反序列化通信是可以做到跨编程语言、跨平台和跨设备之间的。ROS2要求同一个话题,所有的发布者和接收者必须使用相同消息接口,以此做到跨编程语言、跨平台和跨设备之间的数据收发。

3、ROS2话题工具

3.1 RQT工具

依次打开三个终端并输入如下三条命令

ros2 run demo_nodes_py listener
ros2 run demo_nodes_cpp talker
rqt_graph
image-20240315110829947

可以看到RQT工具就是一种GUI工具,它帮助我们了解各节点、话题之间的关系,并画出系统逻辑图。

3.2 CLI工具

CLI 是 command line interface 的简称,也就是命令行界面。用户可在提示符下键入可执行指令,然后计算机执行,它通常不支持鼠标。

  • ros2支持很多强大的topic指令,可以使用下面的指令查看。
ros2 topic -h
image-20240315111440087
  • 返回系统中当前活动的所有主题的列表
ros2 topic list

image-20240315111610208

  • 增加消息类型
ros2 topic list -t

image-20240315111647464

  • 打印实时话题内容
ros2 topic echo /chatter
image-20240315111847603
  • 查看主题信息
ros2 topic info  /chatter
image-20240315112334745
  • 查看消息类型
ros2 interface show std_msgs/msg/String
image-20240315112442547

发布订阅的数据正是string类型

  • 手动发布命令
ros2 topic pub /chatter std_msgs/msg/String 'data: "123"'
image-20240315112658496

加入了一个话题发布者,对应前面的n对1订阅发布模型

4、话题RCLCPP实现

4.1创建节点
  • 创建一个控制节点和一个被控节点。

控制节点创建一个话题发布者,发布控制命令(command)话题,接口类型为字符串(string),控制接点通过发布者发布控制命令(前进、后退、左转、右转、停止)。

被控节点创建一个订阅者,订阅控制命令,收到控制命令后根据命令内容打印对应指令出来。

image-20240315121502618

依次输入下面的命令,创建workspace工作空间、example_topic_rclcpp功能包和topic_publisher_01.cpp

cd d2lros2/
mkdir -p RCLCPP/workspace/src
cd RCLCPP/workspace/src
ros2 pkg create example_topic_rclcpp --build-type ament_cmake --dependencies rclcpp
touch example_topic_rclcpp/src/topic_publisher_01.cpp

linux显示文件分支结构的目录树

ls -r

使用ls -r目录结构混乱,这里我们使用另一个工具tree,下载工具命令,并使用tree命令查看目录结构

sudo apt-get install tree
cd d2lros2/RCLCPP/
tree
image-20240315124527803
tree --help
image-20240315124843120

tree常用命令

tree -a  #显示所有目录及文件
 tree -d  #仅显示目录
tree -L n  #n表示要显示n层目录
tree -f  #显示文件完整路径
  • 采用面向对象方式写一个节点
#include "rclcpp/rclcpp.hpp"

class TopicPublisher01 : public rclcpp::Node
{
public:
    // 构造函数,有一个参数为节点名称
    TopicPublisher01(std::string name) : Node(name)
    {
        RCLCPP_INFO(this->get_logger(), "%s节点已经启动.", name.c_str());
    }

private:
    // 声明节点
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    /*创建对应节点的共享指针对象*/
    auto node = std::make_shared<TopicPublisher01>("topic_publisher_01");
    /* 运行节点,并检测退出信号*/
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

  • 修改CMakeLists.txt
add_executable(topic_publisher_01 src/topic_publisher_01.cpp)
ament_target_dependencies(topic_publisher_01 rclcpp)

install(TARGETS
  topic_publisher_01
  DESTINATION lib/${PROJECT_NAME}
)
  • 安装colcon编译工具
sudo apt-get install python3-colcon-common-extensions
  • 接着可以编译测试下,注意运行colcon的目录。
cd d2lros2/RCLCPP/workspace/
colcon build --packages-select example_topic_rclcpp
source install/setup.bash
ros2 run example_topic_rclcpp topic_publisher_01
4.2 编写发布者
  • 导入消息接口

消息接口是ROS2通信时必须的一部分,通过消息接口ROS2才能完成消息的序列化和反序列化。ROS2为我们定义好了常用的消息接口,并生成了C++和Python的依赖文件,我们可以直接在程序中进行导入。

ament_cmake类型功能包导入消息接口分为三步:

  1. CMakeLists.txt中导入,具体是先find_packagesament_target_dependencies
  2. packages.xml中导入,具体是添加depend标签并将消息接口写入。
  3. 在代码中导入,C++中是#include"消息功能包/xxx/xxx.hpp"

我们依次做完这三步后文件内容如下:

CMakeLists.txt

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(topic_publisher_01 src/topic_publisher_01.cpp)
ament_target_dependencies(topic_publisher_01 rclcpp std_msgs)

packages.xml

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

代码文件topic_publisher_01.cpp

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TopicPublisher01 : public rclcpp::Node
  • 消息接口上面我们已经导入了,是std_msgs/msg/string.h
  • 话题名称(topic_name),我们就用control_command
  • Qos,Qos支持直接指定一个数字,这个数字对应的是KeepLast队列长度。一般设置成10,即如果一次性有100条消息,默认保留最新的10个,其余的都扔掉。

接着我们可以编写发布者的代码了。

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TopicPublisher01 : public rclcpp::Node
{
public:
    // 构造函数,有一个参数为节点名称
    TopicPublisher01(std::string name) : Node(name)
    {
        RCLCPP_INFO(this->get_logger(), "大家好,我是%s.", name.c_str());
        // 创建发布者
        command_publisher_ = this->create_publisher<std_msgs::msg::String>("command", 10);
    }

private:
    // 声明话题发布者
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr command_publisher_;
};
4.3 使用定时器定时发布数据

需要通过ROS2中的定时器来设置指定的周期调用回调函数,在回调函数里实现发布数据功能。

代码

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TopicPublisher01 : public rclcpp::Node
{
public:
    // 构造函数,有一个参数为节点名称
    TopicPublisher01(std::string name) : Node(name)
    {
        RCLCPP_INFO(this->get_logger(), "大家好,我是%s.", name.c_str());
        // 创建发布者
        command_publisher_ = this->create_publisher<std_msgs::msg::String>("command", 10);
        // 创建定时器,500ms为周期,定时发布
        timer_ = this->create_wall_timer(std::chrono::milliseconds(500), std::bind(&TopicPublisher01::timer_callback, this));
    }

private:
    void timer_callback()
    {
        // 创建消息
        std_msgs::msg::String message;
        message.data = "forward";
        // 日志打印
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        // 发布消息
        command_publisher_->publish(message);
    }
    // 声名定时器指针
    rclcpp::TimerBase::SharedPtr timer_;
    // 声明话题发布者指针
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr command_publisher_;
};

  • 编译运行
cd chapt3/chapt3_ws/
colcon build --packages-select example_topic_rclcpp
source install/setup.bash
ros2 run example_topic_rclcpp topic_publisher_01

--packages-select ,仅生成单个包(或选定的包)

  • 测试
# 查看列表
ros2 topic list
# 输出内容
ros2 topic echo /command
4.4 编写订阅者
  • 创建订阅节点
cd chapt3_ws/src/example_topic_rclcpp
touch src/topic_subscribe_01.cpp
  • 代码
#include "rclcpp/rclcpp.hpp"

class TopicSubscribe01 : public rclcpp::Node
{
public:
    // 构造函数,有一个参数为节点名称
    TopicSubscribe01(std::string name) : Node(name)
    {
        RCLCPP_INFO(this->get_logger(), "大家好,我是%s.", name.c_str());
    }

private:
    // 声明节点
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    /*创建对应节点的共享指针对象*/
    auto node = std::make_shared<TopicSubscribe01>("topic_subscribe_01");
    /* 运行节点,并检测退出信号*/
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
  • CMakeLists.txt
add_executable(topic_subscribe_01 src/topic_subscribe_01.cpp)
ament_target_dependencies(topic_subscribe_01 rclcpp)

install(TARGETS
topic_subscribe_01
  DESTINATION lib/${PROJECT_NAME}
)
  • 编译测试
cd chapt3/chapt3_ws/
colcon build --packages-select example_topic_rclcpp
source install/setup.bash
ros2 run example_topic_rclcpp topic_subscribe_01
  • 订阅者代码
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TopicSubscribe01 : public rclcpp::Node
{
public:
    TopicSubscribe01(std::string name) : Node(name)
    {
        RCLCPP_INFO(this->get_logger(), "大家好,我是%s.", name.c_str());
          // 创建一个订阅者订阅话题
        command_subscribe_ = this->create_subscription<std_msgs::msg::String>("command", 10, std::bind(&TopicSubscribe01::command_callback, this, std::placeholders::_1));
    }

private:
     // 声明一个订阅者
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr command_subscribe_;
     // 收到话题数据的回调函数
    void command_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        double speed = 0.0f;
        if(msg->data == "forward")
        {
            speed = 0.2f;
        }
        RCLCPP_INFO(this->get_logger(), "收到[%s]指令,发送速度 %f", msg->data.c_str(),speed);
    }
};

依然的需要在CMakeLists.txt添加下std_msgs依赖

ament_target_dependencies(topic_subscribe_01 rclcpp std_msgs)

5、话题RCLPY实现

5.1创建功能包和节点
cd r2lros2/
mkdir -p RCLPY/workspace/src
ros2 pkg create example_service_rclpy --build-type ament_python --dependencies rclpy example_interfaces  --node-name service_server_02

只支持一个节点文件,所以我们还需要手动创建一个。

cd example_service_rclpy/example_service_rclpy/
touch service_client_02.py
  • setup.py
    entry_points={
        'console_scripts': [
            "service_client_02 = example_service_rclpy.service_client_02:main",
            "service_server_02 = example_service_rclpy.service_server_02:main"
        ],
    },
  • service_server_02
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node

class ServiceServer02(Node):
    def __init__(self,name):
        super().__init__(name)
        self.get_logger().info("节点已启动:%s!" % name)
        
def main(args=None):
    rclpy.init(args=args) # 初始化rclpy
    node = ServiceServer02("service_server_02")  # 新建一个节点
    rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
    rclpy.shutdown() # 关闭rclpy

5.2测试
colcon build --packages-select example_service_rclpy
source install/setup.bash
ros2 run example_service_rclpy service_server_02
  • 打开终端
ros2 service call /add_two_ints_srv example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"

二、服务

服务分为客户端和服务端,平时我们用的手机APP都可以成为客户端,而APP服务器对于软件来说就是服务端。

客户端发送请求给服务端,服务端可以根据客户端的请求做一些处理,然后返回结果给客户端。

image-20240315151340840

所以服务-客户端模型,也可以成为请求-响应模型。

不知道你有没有感觉到服务和话题的不同之处,话题是没有返回的,适用于单向或大量的数据传递。而服务是双向的,客户端发送请求,服务端响应请求。

同时服务还是有一些注意事项:

  • 同一个服务(名称相同)有且只能有一个节点来提供
  • 同一个服务可以被多个客户端调用

1、启动服务

ros2 run examples_rclpy_minimal_service service
  • 查看服务列表
ros2 service list
image-20240315151538529

2、手动调用服务

再启动一个终端,输入下面的命令(注意a:、b:后的空格)。

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
image-20240315151724666 image-20240315151901390

/bxs-1315075022.cos.ap-chengdu.myqcloud.com/img/image-20240315151538529.png" alt=“image-20240315151538529” style=“zoom: 50%;” />

2、手动调用服务

再启动一个终端,输入下面的命令(注意a:、b:后的空格)。

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
image-20240315151724666 image-20240315151901390
  • 服务同样可以使用RCLCPPRCLPY实现
  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Ubuntu 18.04上进行ROS通信编程,需要先安装ROS Melodic版本。安装完成后,可以使用ROS提供的通信机制,如话题(Topic)和服务(Service)等。 话题是一种发布/订阅(Publish/Subscribe)模式的通信机制,可以实现多个节点之间的异步通信。在ROS中,话题的数据类型是消息(Message),可以自定义消息类型。节点可以发布消息到话题,也可以订阅话题接收消息。 服务是一种请求/响应(Request/Response)模式的通信机制,可以实现节点之间的同步通信。在ROS中,服务的数据类型也是消息,但是服务消息包含请求消息和响应消息两部分。节点可以提供服务,也可以请求服务。 下面是一个简单的ROS话题通信示例: 1. 创建一个ROS工作空间 ``` mkdir -p ~/catkin_ws/src cd ~/catkin_ws/ catkin_make ``` 2. 创建一个ROS包 ``` cd ~/catkin_ws/src catkin_create_pkg my_package rospy ``` 3. 在my_package中创建一个发布者节点 ``` cd ~/catkin_ws/src/my_package mkdir scripts cd scripts touch talker.py chmod +x talker.py ``` 将以下代码复制到talker.py文件中: ```python #!/usr/bin/env python import rospy from std_msgs.msg import String def talker(): pub = rospy.Publisher('chatter', String, queue_size=10) rospy.init_node('talker', anonymous=True) rate = rospy.Rate(10) # 10hz while not rospy.is_shutdown(): hello_str = "hello world %s" % rospy.get_time() rospy.loginfo(hello_str) pub.publish(hello_str) rate.sleep() if __name__ == '__main__': try: talker() except rospy.ROSInterruptException: pass ``` 4. 在my_package中创建一个订阅者节点 ``` cd ~/catkin_ws/src/my_package/scripts touch listener.py chmod +x listener.py ``` 将以下代码复制到listener.py文件中: ```python #!/usr/bin/env python import rospy from std_msgs.msg import String def callback(data): rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data) def listener(): rospy.init_node('listener', anonymous=True) rospy.Subscriber('chatter', String, callback) rospy.spin() if __name__ == '__main__': listener() ``` 5. 编译ROS包 ``` cd ~/catkin_ws/ catkin_make ``` 6. 运行ROS节点 在一个终端中运行发布者节点: ``` source ~/catkin_ws/devel/setup.bash rosrun my_package talker.py ``` 在另一个终端中运行订阅者节点: ``` source ~/catkin_ws/devel/setup.bash rosrun my_package listener.py ``` 你可以看到,发布者节点会不断地发布消息到话题“chatter”中,而订阅者节点会接收到这些消息并打印出来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值