1. ROS消息机制的核心概念
在ROS(Robot Operating System)生态中,消息(messages)是节点间通信的基础单元。每个ROS消息本质上是一个数据结构,定义了节点之间传递的数据格式。当我们需要在自定义节点间传递标准消息类型无法满足的数据时,就必须掌握自定义消息的创建和使用方法。
1.1 为什么需要自定义消息
ROS虽然提供了丰富的标准消息类型(如std_msgs/String、sensor_msgs/Image等),但在实际机器人开发中经常会遇到这些场景:
- 需要打包多个基本数据类型为一个复合数据结构
- 特定传感器或执行器需要专用数据格式
- 系统架构需要传递自定义的业务逻辑数据
例如开发机械臂控制系统时,可能需要同时传递关节角度、速度和力矩信息,这时标准的Float32MultiArray就不如专门定义的ArmJointState消息来得直观和类型安全。
1.2 ROS消息的类型系统
ROS消息支持的基础数据类型包括:
- 基本类型:bool, int8/16/32/64, uint8/16/32/64, float32/64, string, time, duration
- 复合类型:数组(固定长度和可变长度),其他消息类型嵌套
- 特殊类型:Header(包含时间戳和坐标系信息)
消息定义文件(.msg)的语法类似于C语言的结构体定义,但具有更严格的类型约束。一个典型的消息文件如下:
code复制# ArmJointState.msg
Header header
string[] joint_names
float32[] positions
float32[] velocities
float32[] efforts
2. 创建自定义消息的完整流程
2.1 准备工作环境
首先确保已正确安装ROS环境(推荐使用Ubuntu 22.04 + ROS 2 Humble或Ubuntu 20.04 + ROS Noetic)。可以使用小鱼ROS的一键安装脚本(注意:国内用户建议使用鱼香ROS的国内镜像源加速安装)。
验证环境是否就绪:
bash复制source /opt/ros/[distro]/setup.bash
ros2 pkg list # 或 ros1使用 rospack list
2.2 创建功能包
自定义消息需要放在独立的功能包中,通常以_msgs后缀命名。使用以下命令创建功能包:
bash复制# ROS 1
catkin_create_pkg custom_msgs roscpp rospy std_msgs message_generation message_runtime
# ROS 2
ros2 pkg create custom_msgs --build-type ament_cmake --dependencies std_msgs
关键点说明:
message_generation:构建时生成消息代码的依赖message_runtime:运行时解析消息的依赖- ROS 2使用ament构建系统,依赖声明方式不同
2.3 定义消息文件
在功能包中创建msg目录并添加消息定义文件:
bash复制mkdir -p custom_msgs/msg
touch custom_msgs/msg/CustomMessage.msg
示例消息内容:
code复制# CustomMessage.msg
std_msgs/Header header
uint32 id
string name
float32[] data
bool is_valid
2.4 配置构建系统
对于ROS 1(Noetic),修改package.xml:
xml复制<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
修改CMakeLists.txt:
cmake复制find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_message_files(
FILES
CustomMessage.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
CATKIN_DEPENDS message_runtime std_msgs
)
对于ROS 2(Humble),修改package.xml:
xml复制<depend>std_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>
修改CMakeLists.txt:
cmake复制find_package(ament_cmake REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/CustomMessage.msg"
DEPENDENCIES std_msgs
)
ament_export_dependencies(rosidl_default_runtime)
ament_export_dependencies(std_msgs)
2.5 编译与验证
编译功能包并验证消息是否生成:
bash复制# ROS 1
catkin_make
source devel/setup.bash
rosmsg show custom_msgs/CustomMessage
# ROS 2
colcon build --packages-select custom_msgs
source install/setup.bash
ros2 interface show custom_msgs/msg/CustomMessage
3. Python实现消息发布与订阅
3.1 创建发布者节点
新建publisher.py文件:
python复制#!/usr/bin/env python3
import rclpy # ROS 2使用rclpy,ROS 1使用rospy
from rclpy.node import Node
from custom_msgs.msg import CustomMessage
from std_msgs.msg import Header
import time
class CustomPublisher(Node):
def __init__(self):
super().__init__('custom_publisher')
self.publisher_ = self.create_publisher(CustomMessage, 'custom_topic', 10)
timer_period = 1.0 # 1秒发布一次
self.timer = self.create_timer(timer_period, self.timer_callback)
self.counter = 0
def timer_callback(self):
msg = CustomMessage()
msg.header = Header()
msg.header.stamp = self.get_clock().now().to_msg()
msg.header.frame_id = 'base_link'
msg.id = self.counter
msg.name = f'Message_{self.counter}'
msg.data = [float(self.counter), float(self.counter*2)]
msg.is_valid = True
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {msg}')
self.counter += 1
def main(args=None):
rclpy.init(args=args)
publisher = CustomPublisher()
try:
rclpy.spin(publisher)
except KeyboardInterrupt:
pass
finally:
publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3.2 创建订阅者节点
新建subscriber.py文件:
python复制#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from custom_msgs.msg import CustomMessage
class CustomSubscriber(Node):
def __init__(self):
super().__init__('custom_subscriber')
self.subscription = self.create_subscription(
CustomMessage,
'custom_topic',
self.listener_callback,
10)
self.subscription # 防止未使用变量警告
def listener_callback(self, msg):
self.get_logger().info(f'Received: ID={msg.id}, Name={msg.name}')
self.get_logger().info(f'Data: {msg.data}, Valid: {msg.is_valid}')
def main(args=None):
rclpy.init(args=args)
subscriber = CustomSubscriber()
try:
rclpy.spin(subscriber)
except KeyboardInterrupt:
pass
finally:
subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3.3 配置Python节点安装
对于ROS 2,需要在setup.py中添加入口点:
python复制entry_points={
'console_scripts': [
'publisher = custom_msgs.publisher:main',
'subscriber = custom_msgs.subscriber:main',
],
}
或在package.xml中添加:
xml复制<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>custom_msgs</exec_depend>
3.4 运行测试
编译后运行节点:
bash复制# ROS 2
ros2 run custom_msgs publisher
ros2 run custom_msgs subscriber
# ROS 1
chmod +x publisher.py
chmod +x subscriber.py
./publisher.py
./subscriber.py
4. 高级应用与调试技巧
4.1 消息兼容性与版本控制
当消息结构需要变更时,应考虑:
- 添加新字段而非修改现有字段
- 废弃字段标记为deprecated而非直接删除
- 使用语义化版本控制(如v1.0.0 → v1.1.0)
示例兼容性变更:
code复制# v2.0.0
Header header
uint32 id
string name
float32[] data
bool is_valid
string description @deprecated # 标记为废弃
float64 timestamp # 新增字段
4.2 性能优化建议
-
减少消息大小:
- 使用基本类型而非字符串传输数值
- 压缩大型数组数据
- 避免在消息中包含冗余信息
-
发布频率控制:
- 对高频数据使用
throttle过滤器 - 实现条件发布(仅当数据变化时发布)
- 对高频数据使用
-
Python特定优化:
- 复用消息对象而非每次创建新对象
- 使用
numpy处理数值数组 - 避免在回调函数中进行复杂计算
4.3 常见问题排查
问题1:消息未正确接收
- 检查话题名称是否一致
- 使用
ros2 topic list/rostopic list确认话题存在 - 使用
ros2 topic echo/rostopic echo验证消息内容
问题2:消息字段不匹配
code复制[ERROR] [1677721600.000000] [custom_subscriber]:
Field 'data' must be a list or numpy array
- 确保赋值类型与消息定义一致
- 对于数组字段,即使只有一个元素也要用列表形式
问题3:Python导入错误
code复制ImportError: cannot import name 'CustomMessage' from 'custom_msgs.msg'
- 确认功能包已正确编译
- 检查Python路径是否包含工作空间install/devel目录
- 对于ROS 2,确保
colcon build --symlink-install
4.4 可视化工具使用
-
rqt_graph:
bash复制
rqt_graph可视化节点和话题的连接关系
-
PlotJuggler:
bash复制
ros2 run plotjuggler plotjuggler实时绘制消息数据曲线
-
Foxglove Studio:
跨平台的ROS数据可视化工具,支持2D/3D数据显示
5. 实际项目集成建议
5.1 与机械臂控制集成
典型机械臂控制消息示例:
code复制# ArmControl.msg
Header header
float32[] target_positions # 目标关节角度(rad)
float32[] target_velocities # 目标关节速度(rad/s)
float32 max_effort # 最大输出力矩(N·m)
uint8 control_mode # 0=位置,1=速度,2=力矩
使用时注意:
- 数组长度应与机械臂关节数一致
- 单位需与机械臂SDK保持一致
- 考虑添加安全校验字段
5.2 在SLAM中的应用
SLAM系统常用自定义消息:
code复制# SlamResult.msg
Header header
geometry_msgs/PoseWithCovariance pose
sensor_msgs/PointCloud2 key_points
nav_msgs/OccupancyGrid map
float32[36] covariance_matrix
bool is_relocalized
最佳实践:
- 使用标准消息类型组合(如PoseWithCovariance)
- 对大尺寸数据(如点云)使用零拷贝传输
- 添加系统状态标志位
5.3 无人机仿真消息设计
Gazebo仿真中的无人机状态消息:
code复制# DroneState.msg
Header header
geometry_msgs/Vector3 position # 世界坐标系位置(m)
geometry_msgs/Vector3 velocity # 机体坐标系速度(m/s)
geometry_msgs/Quaternion orientation # 四元数姿态
float32 battery_remaining # 剩余电量(%)
uint8 flight_mode # 飞行模式枚举
float32[16] covariance # 状态协方差矩阵
注意事项:
- 明确坐标系约定(世界系/机体系)
- 对枚举类型使用uint8而非string
- 添加时间戳用于数据同步
6. ROS 1与ROS 2的差异处理
6.1 消息定义差异
-
字段默认值:
- ROS 1:不支持默认值
- ROS 2:支持字段默认值定义
code复制float32 max_speed = 1.0
-
常量定义:
- ROS 1:不支持
- ROS 2:支持消息内常量
code复制uint8 STOPPED = 0 uint8 RUNNING = 1 uint8 state
6.2 Python API差异
| 功能 | ROS 1 (rospy) | ROS 2 (rclpy) |
|---|---|---|
| 节点创建 | rospy.init_node() | rclpy.create_node() |
| 发布者 | rospy.Publisher() | create_publisher() |
| 订阅者 | rospy.Subscriber() | create_subscription() |
| 日志输出 | rospy.loginfo() | node.get_logger().info() |
| 参数处理 | rospy.get_param() | node.declare_parameter() |
6.3 跨版本兼容方案
-
使用common_interfaces:
ROS 2提供的标准接口包,尽量使用这些通用消息类型 -
双版本支持构建:
在CMake中通过条件判断支持双版本编译:cmake复制if(${ROS_VERSION} EQUAL 1) # ROS 1配置 else() # ROS 2配置 endif() -
转换桥接:
对于需要ROS 1和ROS 2通信的系统,使用ros1_bridge包:bash复制
ros2 run ros1_bridge dynamic_bridge
7. 工程化实践建议
7.1 消息命名规范
-
基本规则:
- 使用驼峰命名法(如
RobotStatus) - 避免缩写(用
position而非pos) - 单位后缀(如
angle_rad、velocity_mps)
- 使用驼峰命名法(如
-
常用后缀约定:
Stamped:带时间戳的消息WithCovariance:带协方差矩阵的数据Array:同类型数据集合
7.2 文档生成
-
自动生成消息文档:
bash复制# ROS 1 rosdoc_lite custom_msgs # ROS 2 ament_cmake_python_symlink_install_to_share -
添加消息注释:
code复制# 无人机控制指令 # mode: 0=悬停,1=起飞,2=降落,3=返航 uint8 mode float32 altitude # 目标高度(m)
7.3 单元测试
Python消息测试示例:
python复制import unittest
from custom_msgs.msg import CustomMessage
class TestCustomMessage(unittest.TestCase):
def test_message_initialization(self):
msg = CustomMessage()
self.assertEqual(msg.id, 0)
self.assertEqual(msg.name, "")
self.assertEqual(len(msg.data), 0)
self.assertFalse(msg.is_valid)
def test_message_serialization(self):
from rclpy.serialization import serialize_message, deserialize_message
msg = CustomMessage()
msg.id = 42
serialized = serialize_message(msg)
deserialized = deserialize_message(serialized, CustomMessage)
self.assertEqual(deserialized.id, 42)
if __name__ == '__main__':
unittest.main()
7.4 CI/CD集成
示例GitLab CI配置:
yaml复制stages:
- test
ros2_test:
image: ros:humble
stage: test
script:
- apt-get update && apt-get install -y python3-pip
- pip3 install colcon-common-extensions
- mkdir -p ros_ws/src
- cp -r custom_msgs ros_ws/src/
- cd ros_ws
- colcon build
- source install/setup.bash
- ros2 interface show custom_msgs/msg/CustomMessage
- python3 -m pytest src/custom_msgs/test
