ROS 2边学边练(17)-- 实现自定义接口(升级版)

前言

        上一篇我们实现了自定义msg和srv接口(.msg和.srv),并在具体实现代码(发布者/订阅者)中成功使用了自定义的接口。此篇将会学习关于自定义接口更多的方面。

        理想状况下我们应该在专门的接口包中只声明定义接口,然后在其他的功能包中根据业务需要使用这些专门接口包中的接口(比如上一篇中的情况,我们创建了一个包,这里面纯粹定义了msg和srv,在另外一个包中我们调用了此包定义的msg和srv),这样就显得比较低耦合了嘛,各司其职。但是在实际的操作中,我们会比较习惯在同一个功能包中定义、创建并使用此包中的接口,讲究一个方便至上。

        在之前的教程中,我们了解到接口貌似只能在支持CMake的功能包中定义,但是在支持CMake功能包中,可能同时存在python库和节点(使用ament_CMake_Python),因此我们可以在一个包中同时定义接口和python节点,为了简单描述(接口的定义和使用在同一个功能包中)此种情况,我们将以使用CMake(有CMakeLists.txt)的和C++实现节点(调用接口实现一些业务)举例(简单一句话,实现同一个功能包中集定义接口、实现节点(使用接口)于一体的情况),其中的接口仅举例msg,但是其他的接口类型流程也是一样的。

动动手

创建一个功能包

        在工作空间的src路径下(ros2_ws/src),执行下面的命令创建一个功能包more_interfaces并在该包路径下新建msg文件夹:

$ros2 pkg create --build-type ament_cmake --license Apache-2.0 more_interfaces
$mkdir more_interfaces/msg

创建一个msg文件

        在more_interfaces/msg文件夹下新建AddressBook.msg,内容如下:

uint8 PHONE_TYPE_HOME=0
uint8 PHONE_TYPE_WORK=1
uint8 PHONE_TYPE_MOBILE=2

string first_name
string last_name
string phone_number
uint8 phone_type

上面的内容大家可能会觉得有点陌生,才过了一天时间,怎么msg的定义我都看不懂了么?我简单解释一下(接口定义详细内容可以看官方原文)。

        头三行,定义了3个常量(PHONE_TYPE_HOME/PHONE_TYPE_WORK/PHONE_TYPE_MOBILE,对于常量,接口中我们要用大写)并且进行了初始化赋值,最后四行大家应该都熟悉了,first_name、last_name、phone_number为string类型的变量,phone_type为uint8型变量,在后面使用中其指代的应该是头三行中的某一个常量。

构建msg文件

        打开此包的package.xml文件,添加下面的内容:

<buildtool_depend>rosidl_default_generators</buildtool_depend>

<exec_depend>rosidl_default_runtime</exec_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

这三行内容的解释在上一篇也提到了,此处不再多言。接着打开CMakeLists.txt,添加下面的内容:

#发现能从msg/srv文件生成代码的功能包
find_package(rosidl_default_generators REQUIRED)
#罗列出我们想要生成代码的msg
set(msg_files
  "msg/AddressBook.msg"
)
#生成msg接口代码
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
)
#导出运行时依赖
ament_export_dependencies(rosidl_default_runtime)

先不构建这个msg包,留在后面。

设置多接口(扩展)

        如果我们要使用多个msg和srv呢,我们可以在CMakeLists.txt中做如下调整:

set(msg_files
  "msg/Message1.msg"
  "msg/Message2.msg"
  # etc
  )

set(srv_files
  "srv/Service1.srv"
  "srv/Service2.srv"
   # etc
  )
...
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
  ${srv_files}
)

在同个包中使用一个接口

        下面我们就需要在more_interfaces/src路径下编写我们的调用msg程序了,写一个发布者节点,publish_address_book.cpp,内容如下:

#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"//自定义msg的头文件(自动生成的)

using namespace std::chrono_literals;

class AddressBookPublisher : public rclcpp::Node
{
public:
  AddressBookPublisher()
  : Node("address_book_publisher")
  {
    //创建一个发布者节点名字为address_book
    address_book_publisher_ =
      this->create_publisher<more_interfaces::msg::AddressBook>("address_book", 10);

    //发布者节点的回调函数
    auto publish_msg = [this]() -> void {
        //创建一个AddressBook类型的消息变量
        auto message = more_interfaces::msg::AddressBook();
        
        //给消息变量赋值
        message.first_name = "John";
        message.last_name = "Doe";
        message.phone_number = "1234567890";
        message.phone_type = message.PHONE_TYPE_MOBILE;

        std::cout << "Publishing Contact\nFirst:" << message.first_name <<
          "  Last:" << message.last_name << std::endl;
        
        //发布者节点发布消息
        this->address_book_publisher_->publish(message);
      };

    //创建一个1s周期的定时器(绑定了回调函数publish_msg)
    timer_ = this->create_wall_timer(1s, publish_msg);
  }

private:
  rclcpp::Publisher<more_interfaces::msg::AddressBook>::SharedPtr address_book_publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};


int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<AddressBookPublisher>());
  rclcpp::shutdown();

  return 0;
}

解释见代码中的注释。

构建节点

        CMakeLists.txt中增加如下内容(我们需要新增一个目标):

find_package(rclcpp REQUIRED)

add_executable(publish_address_book src/publish_address_book.cpp)
ament_target_dependencies(publish_address_book rclcpp)

install(TARGETS
    publish_address_book
  DESTINATION lib/${PROJECT_NAME})
支持使用同包msg

        同样是在CMakeLists.txt中,我们添加如下内容,使得同包中的节点程序能够使用同包中定义的msg:

rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)

target_link_libraries(publish_address_book "${cpp_typesupport_target}")

        如果像之前那样,调用的是其他包中的msg或srv,上面的内容是不需要添加的,上面内容只针对同一包中己定义了msg等接口又实现了节点(并使用其自定义的接口)情况。

最终的CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(more_interfaces)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(publish_address_book src/publish_address_book.cpp)
ament_target_dependencies(publish_address_book rclcpp)

install(TARGETS
    publish_address_book
  DESTINATION lib/${PROJECT_NAME})

set(msg_files
  "msg/AddressBook.msg"
)

rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
)

rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)

target_link_libraries(publish_address_book "${cpp_typesupport_target}")

ament_export_dependencies(rosidl_default_runtime)

ament_package()

最终的package.xml

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>more_interfaces</name>
  <version>0.0.0</version>
  <description>learn interfaces</description>
  <maintainer email="mike@qq.com">mike</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
  
  <buildtool_depend>rosidl_default_generators</buildtool_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

测试

        我们进入到工作空间的根路径,开始构建我们的more_interfaces包:

$cd ~/ros2_ws
$colcon build --packages-up-to more_interfaces

source一下环境变量然后启动一下发布者节点:

$source install/local_setup.bash
$ros2 run more_interfaces publish_address_book

我们有两种方式可以验证看看发布者节点发布的内容,ros2 topic echo <topic_name>,

$source install/setup.bash
$ros2 topic echo /address_book

还可以生成一个订阅者节点直接打印出来(这个可以自己尝试一下,正好练练手,熟悉一下)。

使用一个存在着的接口定义(扩展)

        举例解释一下标题的意思,在ROS 2中有另外一个独立的包(比如rosidl_tutotial_msgs),其中有定义Contact.msg消息文件,内容与我们上面自定义的AddressBook.msg一样,我们在发布者节点中不再使用我们自定义的AddressBook,而是使用这Contact消息,针对这种情况,我们需要对package.xml和CMakeLists.txt作出修改。

package.xml

<build_depend>rosidl_tutorials_msgs</build_depend>

<exec_depend>rosidl_tutorials_msgs</exec_depend>

CMakeLists.txt

find_package(rosidl_tutorials_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
  DEPENDENCIES rosidl_tutorials_msgs
)

publish_address_book.cpp

#include "rosidl_tutorials_msgs/msg/contact.hpp"

回调函数

#下面回调函数内容是针对消息数组来赋值的,使用创建了两次Contact赋值了两次
#rosidl_tutorials_msgs/Contact[] address_book

auto publish_msg = [this]() -> void {
   auto msg = std::make_shared<more_interfaces::msg::AddressBook>();
   {
     rosidl_tutorials_msgs::msg::Contact contact;
     contact.first_name = "John";
     contact.last_name = "Doe";
     contact.phone_number = "1234567890";
     contact.phone_type = message.PHONE_TYPE_MOBILE;
     msg->address_book.push_back(contact);
   }
   {
     rosidl_tutorials_msgs::msg::Contact contact;
     contact.first_name = "Jane";
     contact.last_name = "Doe";
     contact.phone_number = "4254242424";
     contact.phone_type = message.PHONE_TYPE_HOME;
     msg->address_book.push_back(contact);
   }

   std::cout << "Publishing address book:" << std::endl;
   for (auto contact : msg->address_book) {
     std::cout << "First:" << contact.first_name << "  Last:" << contact.last_name <<
       std::endl;
   }

   address_book_publisher_->publish(*msg);//注意参数为指针
 };

本篇完。

--------------------------------------------------------------------------------------------------------------------------------

附录

最后贴出下订阅者节点代码及CMakeLists.txt:

subscrib_address_book.cpp

/* subscrib_address_book.cpp */

#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"

using std::placeholders::_1;

class AddressBookSubscriber : public rclcpp::Node
{
public:
  AddressBookSubscriber()
  : Node("address_book_subscriber")
  {
    address_book_subscriber_ =
      this->create_subscription<more_interfaces::msg::AddressBook>(
      "address_book", 10, std::bind(&AddressBookSubscriber::topic_callback, this, _1));
  }

private:
  void topic_callback(const more_interfaces::msg::AddressBook & msg) const  
  {
    RCLCPP_INFO_STREAM(this->get_logger(), "I read: '" << msg.first_name << msg.last_name <<"'"); 
    RCLCPP_INFO_STREAM(this->get_logger(), "I read: '" << msg.phone_number << msg.phone_type <<"'");     
  }
  rclcpp::Subscription<more_interfaces::msg::AddressBook>::SharedPtr address_book_subscriber_;  

};


int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<AddressBookSubscriber>());
  rclcpp::shutdown();

  return 0;
}

CMakeLists.txt 

cmake_minimum_required(VERSION 3.8)
project(more_interfaces)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(publish_address_book src/publish_address_book.cpp)
ament_target_dependencies(publish_address_book rclcpp)

add_executable(subscrib_address_book src/subscrib_address_book.cpp)
ament_target_dependencies(subscrib_address_book rclcpp)

install(TARGETS
    publish_address_book
    subscrib_address_book
  DESTINATION lib/${PROJECT_NAME})

set(msg_files
  "msg/AddressBook.msg"
)

rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
)

rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)

target_link_libraries(publish_address_book "${cpp_typesupport_target}")
target_link_libraries(subscrib_address_book "${cpp_typesupport_target}")

ament_export_dependencies(rosidl_default_runtime)

ament_package()

package.xml无需修改。

效果如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值