一、创建自定义ROS 2接口(msg与srv文件)
目标:定义自定义接口文件(.msg和.srv),并在Python和C++节点中使用它们。
教程级别:初学者
预计时间:20分钟
目录
- 背景
- 前提条件
- 任务
- 创建新功能包
- 定义自定义接口
- 配置
CMakeLists.txt - 配置
package.xml - 构建
tutorial_interfaces功能包 - 验证msg和srv文件创建结果
- 测试新接口
- 总结
背景
在之前的教程中,你通过消息(message)和服务(service)接口学习了话题(topics)、服务(services),以及简单的发布者/订阅者(C++/Python)和服务端/客户端(C++/Python)节点。这些教程中使用的接口都是预先定义好的。
虽然使用预定义接口是推荐做法,但有时你仍需定义自己的消息和服务。本教程将介绍创建自定义接口定义的最简方法。
前提条件
- 已创建ROS 2工作空间(如
ros2_ws)。 - 已完成发布者/订阅者(C++和Python)、服务端/客户端(C++和Python)教程,并保留了当时创建的功能包(用于测试新自定义接口)。
任务
1. 创建新功能包
本教程中,自定义.msg和.srv文件需放在独立的功能包中,再在其他功能包中调用。两个功能包需位于同一工作空间。
由于将使用之前教程创建的发布/订阅、服务/客户端功能包,需确保当前处于这些功能包所在的工作空间(ros2_ws/src),执行以下命令创建新功能包:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
tutorial_interfaces是新功能包的名称。注意,该功能包必须是ament_cmake类型,但这并不限制其接口的使用场景——你可以在ament_cmake功能包中创建自定义接口,再在C++或Python节点中使用(最后一节将介绍具体方法)。
.msg和.srv文件需分别放在名为msg和srv的目录中。在ros2_ws/src/tutorial_interfaces目录下创建这两个目录:
mkdir msg srv
2. 定义自定义接口
2.1 定义msg文件
在刚创建的tutorial_interfaces/msg目录下,新建名为Num.msg的文件,添加以下代码定义其数据结构:
int64 num
这是一个自定义消息,用于传输一个名为num的64位整数。
同样在tutorial_interfaces/msg目录下,新建名为Sphere.msg的文件,添加以下内容:
geometry_msgs/Point center
float64 radius
该自定义消息引用了其他消息功能包中的类型(此处为geometry_msgs/Point)。
2.2 定义srv文件
在tutorial_interfaces/srv目录下,新建名为AddThreeInts.srv的文件,定义请求和响应结构:
int64 a
int64 b
int64 c
---
int64 sum
这是一个自定义服务:请求包含三个名为a、b、c的整数,响应返回一个名为sum的整数(即三个请求整数的和)。
3. 配置CMakeLists.txt
为将你定义的接口转换为特定语言的代码(如C++和Python)以便在对应语言中使用,需在CMakeLists.txt中添加以下代码:
find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"msg/Sphere.msg"
"srv/AddThreeInts.srv"
DEPENDENCIES geometry_msgs # 声明上述消息依赖的功能包(此处Sphere.msg依赖geometry_msgs)
)
注意:
rosidl_generate_interfaces中的第一个参数(库名称)必须以功能包名称开头,例如直接使用${PROJECT_NAME}或${PROJECT_NAME}_suffix。详情可参考rosidl官方说明。
4. 配置package.xml
由于接口需要依赖rosidl_default_generators生成特定语言的代码,需声明其为构建工具依赖;rosidl_default_runtime是运行时依赖,用于后续调用接口;rosidl_interface_packages是依赖组名称,需通过<member_of_group>标签将你的功能包tutorial_interfaces关联到该组。
在package.xml的<package>元素内添加以下代码:
<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
5. 构建tutorial_interfaces功能包
自定义接口功能包的所有配置已完成,可开始构建。在工作空间根目录(~/ros2_ws)执行以下命令:
Linux/macOS
colcon build --packages-select tutorial_interfaces
Windows
colcon build --merge-install --packages-select tutorial_interfaces
构建完成后,其他ROS 2功能包即可发现并使用这些自定义接口。
6. 验证msg和srv文件创建结果
打开新终端,进入工作空间(ros2_ws)并配置环境:
Linux
source install/setup.bash
macOS
. install/setup.bash
Windows
call install/setup.bat
使用ros2 interface show命令验证接口创建是否成功,终端输出应与以下内容类似:
# 查看Num.msg
ros2 interface show tutorial_interfaces/msg/Num
int64 num
# 查看Sphere.msg
ros2 interface show tutorial_interfaces/msg/Sphere
geometry_msgs/Point center
float64 x
float64 y
float64 z
float64 radius
# 查看AddThreeInts.srv
ros2 interface show tutorial_interfaces/srv/AddThreeInts
int64 a
int64 b
int64 c
---
int64 sum
7. 测试新接口
此步骤将使用之前教程创建的功能包,通过对节点、CMakeLists.txt和package.xml进行简单修改,即可使用新自定义接口。由于将标准字符串消息改为数值类型,输出结果会与之前略有不同。
7.1 使用Num.msg测试发布/订阅功能
对之前创建的发布者/订阅者功能包(C++或Python)进行如下修改,即可验证Num.msg的功能。
发布者(Publisher)
C++
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // 改动:导入自定义Num.msg
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
// 改动:创建发布者时使用自定义消息类型tutorial_interfaces::msg::Num
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10);
auto timer_callback = [this](){
// 改动:初始化自定义消息对象
auto message = tutorial_interfaces::msg::Num();
// 改动:为消息的num字段赋值
message.num = this->count_++;
// 改动:打印发布的数值
RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'");
publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
// 改动:发布者类型改为自定义消息类型
rclcpp::Publisher<tutorial_interfaces::msg::Num>::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;
}
Python
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from tutorial_interfaces.msg import Num # 改动:导入自定义Num.msg
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
# 改动:创建发布者时使用自定义消息类型Num
self.publisher_ = self.create_publisher(Num, 'topic', 10)
timer_period = 0.5 # 秒
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
# 改动:初始化自定义消息对象
msg = Num()
# 改动:为消息的num字段赋值
msg.num = self.i
self.publisher_.publish(msg)
# 改动:打印发布的数值
self.get_logger().info('Publishing: "%d"' % msg.num)
self.i += 1
def main(args=None):
try:
with rclpy.init(args=args):
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
订阅者(Subscriber)
C++
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // 改动:导入自定义Num.msg
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
// 改动:回调函数参数类型改为自定义消息类型
auto topic_callback = [this](const tutorial_interfaces::msg::Num & msg){
// 改动:打印接收的数值
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'");
};
// 改动:创建订阅者时使用自定义消息类型
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>(
"topic", 10, topic_callback);
}
private:
// 改动:订阅者类型改为自定义消息类型
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
Python
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from tutorial_interfaces.msg import Num # 改动:导入自定义Num.msg
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
Num, # 改动:使用自定义消息类型Num
'topic',
self.listener_callback,
10)
self.subscription # 防止未使用变量警告
def listener_callback(self, msg):
# 改动:打印接收的数值
self.get_logger().info('I heard: "%d"' % msg.num)
def main(args=None):
try:
with rclpy.init(args=args):
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
配置文件修改
CMakeLists.txt(仅C++)
# ...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # 改动:添加自定义接口功能包依赖
add_executable(talker src/publisher_member_function.cpp)
# 改动:链接自定义接口目标
target_link_libraries(talker PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})
add_executable(listener src/subscriber_member_function.cpp)
# 改动:链接自定义接口目标
target_link_libraries(listener PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
package.xml
| 语言 | 添加内容 |
|---|---|
| C++ | <depend>tutorial_interfaces</depend> |
| Python | <exec_depend>tutorial_interfaces</exec_depend> |
构建与运行
完成上述修改并保存后,构建功能包:
C++
- Linux/macOS:
colcon build --packages-select cpp_pubsub - Windows:
colcon build --merge-install --packages-select cpp_pubsub
Python
- Linux/macOS:
colcon build --packages-select py_pubsub - Windows:
colcon build --merge-install --packages-select py_pubsub
打开两个新终端,在每个终端中配置ros2_ws环境,然后运行:
C++
ros2 run cpp_pubsub talker
ros2 run cpp_pubsub listener
Python
ros2 run py_pubsub talker
ros2 run py_pubsub listener
由于Num.msg仅传输整数,发布者将只发布整数值(而非之前的字符串),示例输出如下:
[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'
7.2 使用AddThreeInts.srv测试服务/客户端功能
对之前创建的服务端/客户端功能包(C++或Python)进行如下修改,即可验证AddThreeInts.srv的功能。由于将原本接收两个整数的服务改为接收三个整数,输出结果会与之前略有不同。
服务端(Service)
C++
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // 改动:导入自定义AddThreeInts.srv
#include <memory>
// 改动:请求和响应类型改为自定义服务类型
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,
std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response)
{
// 改动:计算三个整数的和
response->sum = request->a + request->b + request->c;
// 改动:打印三个请求整数
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld",
request->a, request->b, request->c);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
// 改动:节点名称改为"add_three_ints_server"
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server");
// 改动:服务类型和名称改为自定义的AddThreeInts和"add_three_ints"
rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =
node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add);
// 改动:日志提示改为"Ready to add three ints."
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints.");
rclcpp::spin(node);
rclcpp::shutdown();
}
Python
from tutorial_interfaces.srv import AddThreeInts # 改动:导入自定义AddThreeInts.srv
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
# 改动:服务类型和名称改为AddThreeInts和"add_three_ints"
self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback)
# 改动:回调函数计算三个整数的和,并打印三个请求值
def add_three_ints_callback(self, request, response):
response.sum = request.a + request.b + request.c
self.get_logger().info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c))
return response
def main(args=None):
try:
with rclpy.init(args=args):
minimal_service = MinimalService()
rclpy.spin(minimal_service)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
客户端(Client)
C++
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // 改动:导入自定义AddThreeInts.srv
#include <chrono>
#include <cstdlib>
#include <memory>
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
// 改动:节点名称改为"add_three_ints_client"
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");
// 改动:客户端类型和服务名称改为自定义的AddThreeInts和"add_three_ints"
rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =
node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");
// 改动:请求对象类型改为自定义服务的Request
auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();
request->a = 41;
request->b = 1;
request->c = 1; // 改动:添加第三个请求参数c
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = client->async_send_request(request);
// 等待结果返回
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
// 改动:错误提示中的服务名称改为"add_three_ints"
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints");
}
rclcpp::shutdown();
return 0;
}
Python
from tutorial_interfaces.srv import AddThreeInts # 改动:导入自定义AddThreeInts.srv
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
# 改动:客户端类型和服务名称改为AddThreeInts和"add_three_ints"
self.cli = self.create_client(AddThreeInts, 'add_three_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
# 改动:请求对象类型改为AddThreeInts.Request
self.req = AddThreeInts.Request()
def send_request(self):
self.req.a = 41
self.req.b = 1
self.req.c = 1 # 改动:添加第三个请求参数c
return self.cli.call_async(self.req)
def main(args=None):
try:
with rclpy.init(args=args):
minimal_client = MinimalClientAsync()
future = minimal_client.send_request()
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()
# 改动:日志提示改为打印三个整数的和
minimal_client.get_logger().info(
'Result of add_three_ints: for %d + %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum))
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
配置文件修改
CMakeLists.txt(仅C++)
# ...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # 改动:添加自定义接口功能包依赖
add_executable(server src/add_two_ints_server.cpp)
# 改动:链接自定义接口目标
target_link_libraries(server PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})
add_executable(client src/add_two_ints_client.cpp)
target_link_libraries(client PUBLIC rclcpp::rclcpp ${tutorial_interfaces_TARGETS})
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
package.xml
| 语言 | 添加内容 |
|---|---|
| C++ | <depend>tutorial_interfaces</depend> |
| Python | <exec_depend>tutorial_interfaces</exec_depend> |
构建与运行
完成上述修改并保存后,构建功能包:
C++
- Linux/macOS:
colcon build --packages-select cpp_srvcli - Windows:
colcon build --merge-install --packages-select cpp_srvcli
Python
- Linux/macOS:
colcon build --packages-select py_srvcli - Windows:
colcon build --merge-install --packages-select py_srvcli
打开两个新终端,在每个终端中配置ros2_ws环境,然后运行:
C++
ros2 run cpp_srvcli server
ros2 run cpp_srvcli client
Python
ros2 run py_srvcli service
ros2 run py_srvcli client
总结
在本教程中,你学习了如何在独立功能包中创建自定义接口,以及如何在其他功能包中调用这些接口。
本教程仅介绍了自定义接口定义的基础内容,更多细节可参考ROS 2接口详解。
二、实现自定义接口
目标:学习在ROS 2中实现自定义接口的更多方法。
教程级别:初学者
预计时间:15分钟
目录
- 背景
- 前提条件
- 任务
- 创建功能包
- 创建msg文件
- 在同一功能包中使用接口
- 尝试运行
- (额外内容)使用现有接口定义
- 总结
- 下一步
背景
在之前的教程中,你已经学会了如何创建自定义的msg和srv接口。
尽管最佳实践是在专门的接口功能包中声明接口,但有时将接口的声明、创建和使用都放在同一个功能包中会更方便。
需要注意的是,目前接口只能在CMake功能包中定义。不过,CMake功能包中可以包含Python库和节点(通过ament_cmake_python实现),因此你可以在一个功能包中同时定义接口和Python节点。为简化操作,本教程将使用CMake功能包和C++节点。
本教程重点介绍msg类型接口,但所学步骤适用于所有接口类型。
前提条件
- 已学习《创建自定义msg和srv文件》教程的基础内容。
- 已安装ROS 2,创建了工作空间,并了解功能包的创建方法。
- 与往常一样,在打开的每个新终端中,都需配置ROS 2环境。
任务
1. 创建功能包
在工作空间的src目录下,创建名为more_interfaces的功能包,并在其中新建用于存放msg文件的目录:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 more_interfaces
mkdir more_interfaces/msg
2. 创建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
该消息包含以下字段:
first_name:字符串(string)类型last_name:字符串(string)类型phone_number:字符串(string)类型phone_type:无符号8位整数(uint8)类型,包含多个预定义的命名常量值
注意:可以在消息定义中为字段设置默认值。更多接口自定义方式可参考《ROS 2接口详解》。
接下来,需确保msg文件能被转换为C++、Python及其他语言的源代码。
2.1 构建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>注意:构建阶段需要
rosidl_default_generators,而运行阶段只需rosidl_default_runtime。 -
打开
CMakeLists.txt,添加以下代码:- 查找用于从msg/srv文件生成消息代码的功能包:
find_package(rosidl_default_generators REQUIRED) - 声明需要生成代码的消息列表:
手动添加.msg文件,可确保在添加其他.msg文件后,CMake能知晓需要重新配置项目。set(msg_files "msg/AddressBook.msg" ) - 生成消息代码:
rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} ) - 确保导出消息运行时依赖:
ament_export_dependencies(rosidl_default_runtime)
- 查找用于从msg/srv文件生成消息代码的功能包:
现在已准备好从msg定义生成源代码。暂时跳过编译步骤,将在步骤4中统一完成。
3. 在同一功能包中使用接口
现在可以开始编写使用该消息的代码了。
在more_interfaces/src目录下,创建文件publish_address_book.cpp,粘贴以下代码:
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"
using namespace std::chrono_literals;
class AddressBookPublisher : public rclcpp::Node
{
public:
AddressBookPublisher()
: Node("address_book_publisher")
{
address_book_publisher_ =
this->create_publisher<more_interfaces::msg::AddressBook>("address_book", 10);
auto publish_msg = [this]() -> void {
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);
};
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;
}
3.1 代码解析
-
导入新创建的
AddressBook.msg的头文件:#include "more_interfaces/msg/address_book.hpp" -
创建节点和
AddressBook类型的发布者:using namespace std::chrono_literals; class AddressBookPublisher : public rclcpp::Node { public: AddressBookPublisher() : Node("address_book_publisher") { 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(); -
为
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); -
创建一个1秒周期的定时器,每秒调用一次
publish_msg函数:timer_ = this->create_wall_timer(1s, publish_msg);
3.2 构建发布者节点
需在CMakeLists.txt中为该节点创建新目标:
find_package(rclcpp REQUIRED)
add_executable(publish_address_book src/publish_address_book.cpp)
target_link_libraries(publish_address_book rclcpp::rclcpp)
install(TARGETS
publish_address_book
DESTINATION lib/${PROJECT_NAME})
3.3 链接接口
要使用同一功能包中生成的消息,需添加以下CMake代码:
rosidl_get_typesupport_target(cpp_typesupport_target
${PROJECT_NAME} rosidl_typesupport_cpp)
target_link_libraries(publish_address_book "${cpp_typesupport_target}")
这段代码会找到从AddressBook.msg生成的相关C++代码,并允许目标节点链接该代码。
你可能已经注意到,当使用来自独立构建的其他功能包的接口时,无需此步骤。只有当你想在定义接口的同一功能包中使用该接口时,才需要这段CMake代码。
4. 尝试运行
返回工作空间根目录,构建功能包:
Linux/macOS
cd ~/ros2_ws
colcon build --packages-up-to more_interfaces
Windows
cd /ros2_ws
colcon build --merge-install --packages-up-to more_interfaces
配置工作空间环境并运行发布者节点:
Linux
source install/local_setup.bash
ros2 run more_interfaces publish_address_book
macOS
. install/local_setup.bash
ros2 run more_interfaces publish_address_book
Windows(命令提示符)
call install/local_setup.bat
ros2 run more_interfaces publish_address_book
Windows(PowerShell)
install/local_setup.ps1
ros2 run more_interfaces publish_address_book
你将看到发布者节点输出你定义的消息内容,包括在publish_address_book.cpp中设置的值。
为确认消息正在address_book话题上发布,可打开另一个终端,配置工作空间环境后,执行topic echo命令:
Linux
source install/setup.bash
ros2 topic echo /address_book
macOS
. install/setup.bash
ros2 topic echo /address_book
Windows(命令提示符)
call install/setup.bat
ros2 topic echo /address_book
Windows(PowerShell)
install/setup.ps1
ros2 topic echo /address_book
本教程不创建订阅者节点,但你可以尝试自行编写(可参考《编写简单的C++发布者和订阅者》教程)。
5. (额外内容)使用现有接口定义
注意:可以在新的接口定义中使用现有的接口定义。例如,假设存在一个名为
Contact.msg的消息,它属于已有的ROS 2功能包rosidl_tutorials_msgs,且其定义与我们之前自定义的AddressBook.msg接口完全相同。
在这种情况下,你可以将(节点所在功能包中的)AddressBook.msg接口定义为Contact类型(来自另一个功能包的接口)。甚至可以将AddressBook.msg定义为Contact类型的数组,如下所示:
rosidl_tutorials_msgs/Contact[] address_book
要生成该消息,需在package.xml中声明对Contact.msg所在功能包rosidl_tutorials_msgs的依赖:
<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
)
此外,还需在发布者节点中导入Contact.msg的头文件,才能将contacts添加到address_book中:
#include "rosidl_tutorials_msgs/msg/contact.hpp"
你可以将回调函数修改为如下形式:
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 = contact.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 = contact.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);
};
构建并运行修改后的代码,将看到预期的消息输出,包括上面定义的消息数组。
总结
在本教程中,你尝试了使用不同字段类型定义接口,并在定义接口的同一功能包中使用该接口。
你还学会了如何将另一个接口用作字段类型,以及实现该功能所需的package.xml配置、CMakeLists.txt配置和#include语句。
下一步
接下来,你将创建一个带有自定义参数的简单ROS 2功能包,并学习如何通过启动文件设置该参数。同样,你可以选择使用C++或Python编写。

被折叠的 条评论
为什么被折叠?



