面试被问:"你自定义过ROS2的消息类型吗?流程是怎样的?"

我说"写过.msg文件"。面试官说:"CMakeLists.txt里怎么配置?自定义的消息怎么在Python和C++之间互操作?接口设计有什么原则?"

自定义消息是ROS2开发的基本功,但很多人只停留在"会写.msg文件"的层面。今天把整个流程和接口设计的原则讲清楚。

为什么需要自定义消息

ROS2的标准消息类型覆盖了大部分常见场景。但你的项目总有一些特殊需求:自定义的传感器数据格式、特定的控制指令结构、业务逻辑相关的状态信息。

标准类型满足不了的时候,就需要自定义消息类型。

创建消息类型

第一步,在包的msg/目录下创建.msg文件:

# RobotStatus.msg
std_msgs/Header header
string robot_name
float32 battery_level
bool is_charging
float32[] joint_positions
float32[] joint_velocities
uint8 state
uint8 IDLE=0
uint8 MOVING=1
uint8 ERROR=2

消息类型支持的字段:基本类型(bool、int8-64、uint8-64、float32/64、string)、数组(固定长度和可变长度)、其他消息类型、常量定义。

第二步,在CMakeLists.txt中添加依赖和配置:

find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RobotStatus.msg"
  DEPENDENCIES std_msgs
)

第三步,在package.xml中添加依赖:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<depend>std_msgs</depend>

编译之后,ROS2会自动生成Python和C++的代码。Python中通过from my_package.msg import RobotStatus使用,C++中通过#include "my_package/msg/robot_status.hpp"使用。

自定义Service和Action类型

同样的流程可以用于Service和Action:

# CalculatePath.srv
geometry_msgs/Pose start
geometry_msgs/Pose goal
---
bool success
geometry_msgs/Pose[] path
float32 total_distance
# PatrolRobot.action
geometry_msgs/Pose[] waypoints
float32 speed
---
bool all_reached
float32 total_time
---
uint32 current_waypoint_index
float32 distance_to_next

Service用.srv文件,Action用.action文件。格式都是请求和响应(或目标、反馈、结果)用---分隔。

接口设计原则

自定义消息不是随便定义字段就行。好的接口设计能让你的系统更易维护、更易扩展。

单一职责。一个消息类型只描述一个概念。RobotStatus描述机器人状态,SensorReading描述传感器读数。不要把不相关的信息塞到同一个消息里。

使用标准类型。能用geometry_msgs/Pose就不要自己定义float64 x, y, z, qx, qy, qz, qw。标准类型有现成的工具支持,和其他包对接也方便。

包含Header。如果你的消息会通过Topic传输,加上std_msgs/Header字段。Header提供时间戳和frame_id,这在调试和多传感器融合时至关重要。

版本号。如果消息类型可能会变化,在名字或注释中标注版本。RobotStatusV2比悄悄修改RobotStatus的字段要安全得多。

避免过大的消息。一个消息里塞太多数据会导致序列化开销大、传输延迟高。大数据(比如图像、点云)用标准类型或者考虑分块传输。

跨语言使用

ROS2的消息类型定义是语言无关的。rosidl工具链会自动生成Python、C++(甚至其他语言)的绑定代码。

Python中:

from my_package.msg import RobotStatus

msg = RobotStatus()
msg.robot_name = "robot_01"
msg.battery_level = 85.5
msg.state = RobotStatus.MOVING

C++中:

#include "my_package/msg/robot_status.hpp"

auto msg = my_package::msg::RobotStatus();
msg.robot_name = "robot_01";
msg.battery_level = 85.5;
msg.state = my_package::msg::RobotStatus::MOVING;

注意C++中的命名转换:msg文件中的RobotStatus在C++中变成robot_status.hpp,字段名从下划线变成驼峰。rosidl自动处理这些转换。

接口版本兼容

项目迭代中消息类型难免要改。加个字段、改个类型,看似小事,处理不好就出大问题。

ROS2的消息类型兼容性有一套规则:新增字段默认有初始值,老代码不填这个字段也不会报错,这叫向后兼容。但如果你改了已有字段的类型或者删掉了某个字段,老代码就编译不过了,这叫不兼容变更。

实际项目中的经验是:优先通过添加新字段来扩展消息,避免修改或删除已有字段。如果确实需要不兼容变更,就创建一个新的消息类型而不是改旧的。比如RobotStatus不够用了,新建一个RobotStatusV2,让老节点继续用旧版本,新节点用新版本,平滑过渡。

在大型项目中,接口变更应该走代码评审流程,所有依赖方都要被通知到。ROS2提供了ros2 interface相关命令可以查看消息定义,对比不同版本的差异。这种接口治理的习惯在面试中很加分。

面试中怎么聊

面试官问自定义消息,你可以说:"自定义消息需要在msg目录下定义.msg文件,CMakeLists.txt中配置rosidl_generate_interfaces,package.xml中添加生成器和运行时依赖。编译后自动生成Python和C++绑定。接口设计遵循单一职责、使用标准类型、包含Header、避免过大消息等原则。Service和Action类型也可以用同样的方式自定义。"

自定义消息的版本管理

在长期维护的项目中,自定义消息接口的版本管理很重要。修改消息字段时,要考虑到向后兼容性,旧的bag文件可能用的是旧格式。一个实用的做法是:只添加新字段,不删除或修改已有字段;新字段设置默认值;在消息定义中用注释标注版本号和变更历史。这样即使消息格式演进,旧数据仍然可以正确读取。

给你的建议

在你的项目里创建一个自定义消息类型,编译通过,然后在Python和C++中分别使用它。跑通整个流程之后,你对rosidl工具链的理解会上一个台阶。

然后审视一下项目中现有的消息类型定义,看看有没有违反接口设计原则的地方。这种代码审查能力在面试中很加分。

最后提醒一点:自定义消息类型修改之后,所有依赖它的节点都要重新编译。在大型项目中,消息类型的变更要谨慎,提前通知团队成员。


上一篇:第121篇 ROS2消息类型——std_msgs/sensor_msgs/geometry_msgs详解

下一篇预告:第123篇 URDF建模入门——从link和joint开始描述一个机器人

更多推荐