4. 自定义ROS消息与服务
好,咱们进入第4章。前面几章我们把ROS通信的骨架搭起来了,但光用现成的标准消息类型,就像盖房子只用标准砖——能盖,但总有些特殊形状的墙角填不上。这时候就需要自己烧制“异形砖”了。
说白了,自定义消息(msg)和服务(srv) 就是让你能定义自己的数据结构。我个人的习惯是,只要一个话题要传递超过两个基础类型字段,我就直接定义一个新msg,而不是用 std_msgs/String 拼个JSON字符串。为什么?因为类型安全,编译期就能发现错误,而不是等到运行时解析JSON报错。
4.1 msg文件定义:从零开始造一个“数据包”
msg文件,其实就是个文本文件,里面一行一个字段。格式很简单:类型 名称。我刚开始学的时候,觉得这玩意儿跟C语言的结构体定义差不多,确实,本质就是序列化协议。
咱们拿一个实际项目举例。我在做机器人底盘驱动时,需要把电机的状态、电流、温度打包成一个消息。标准库里没有现成的,那就自己造。
在功能包目录下创建 msg/ 文件夹,然后新建 MotorStatus.msg:
# MotorStatus.msg
# 电机状态消息
# 时间戳
std_msgs/Header header
# 电机编号
int8 motor_id
# 当前转速 (rpm)
float32 rpm
# 当前电流 (A)
float32 current
# 电机温度 (摄氏度)
float32 temperature
# 是否过载
bool overload
注意几点:
- 首行惯例:我习惯把
std_msgs/Header放在第一个字段。这样消息自带时间戳和坐标系信息,很多工具(比如rqt_bag)能自动识别。 - 类型支持:ROS支持int8/16/32/64、float32/64、string、bool、time、duration,以及嵌套的其他msg类型。
- 注释用#:跟Python一样,
#后面是注释。写清楚每个字段的含义和单位,这是好习惯。我曾经接手过一个项目,里面全是float32 a、float32 b这种字段,看得我血压飙升。
MotorStatus.msg,而不是 motor_status.msg。ROS社区约定俗成的规矩,跟着走就行。
4.2 srv文件定义:请求-响应的“契约”
服务(Service)是同步的远程过程调用。srv文件比msg文件多了一个 --- 分隔线,上面是请求(Request),下面是响应(Response)。
举个例子,机器人需要执行一个“设置目标位置”的服务。客户端发一个坐标,服务端返回是否成功。
在功能包下创建 srv/ 文件夹,新建 SetTarget.srv:
# SetTarget.srv
# 请求:目标位置
float32 x
float32 y
float32 theta
---
# 响应:执行结果
bool success
string message
你看,结构一目了然。请求字段和响应字段可以完全不同类型。我见过有人把响应写成 --- 下面空着,只返回一个空消息——虽然语法上允许,但实践中强烈不推荐。服务调用方总得知道调用结果吧?
int32 id,响应里也有个 int32 id。编译能过,但运行时会混淆。建议请求字段加 req_ 前缀,响应字段加 res_ 前缀,或者干脆用不同的名字。
4.3 编译与使用:让自定义类型“活”起来
定义好文件只是第一步。要让ROS知道这些新类型,需要修改 CMakeLists.txt 和 package.xml。这一步很多人容易漏,编译报错半天找不到原因。
第一步:修改 package.xml
添加编译依赖和运行依赖:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
第二步:修改 CMakeLists.txt
这里有几个关键点,我按顺序列出来:
- find_package 里加上
message_generation:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
- add_message_files 声明你的msg文件:
add_message_files(
FILES
MotorStatus.msg
)
- add_service_files 声明你的srv文件:
add_service_files(
FILES
SetTarget.srv
)
- generate_messages 生成代码:
generate_messages(
DEPENDENCIES
std_msgs
)
- catkin_package 里导出
message_runtime:
catkin_package(
CATKIN_DEPENDS message_runtime
)
嗯,步骤有点多,但别怕。我整理了一个对照表,方便你检查:
| 文件 | 必须添加的内容 | 常见遗漏 |
|---|---|---|
| package.xml | build_depend: message_generation exec_depend: message_runtime |
只加了build,忘了exec |
| CMakeLists.txt | find_package + message_generation add_message_files / add_service_files generate_messages catkin_package + message_runtime |
漏了generate_messages |
配置好之后,catkin_make 编译。如果一切顺利,你会看到类似 Generating messages for package xxx 的日志。然后就可以在代码里 #include 或者 import 了。
C++ 示例:
#include <ros/ros.h>
#include <your_package/MotorStatus.h>
void callback(const your_package::MotorStatus::ConstPtr& msg)
{
ROS_INFO("Motor %d: rpm = %.2f", msg->motor_id, msg->rpm);
}
Python 示例:
import rospy
from your_package.msg import MotorStatus
def callback(msg):
rospy.loginfo(f"Motor {msg.motor_id}: rpm = {msg.rpm}")
rospy.init_node('listener')
sub = rospy.Subscriber('motor_status', MotorStatus, callback)
4.4 最佳实践:架构师的“内功心法”
定义消息类型看似简单,但用好了能极大提升系统可维护性。我总结了几个原则:
- 单一职责:一个消息只描述一个实体。别把电机状态和电池电量塞到同一个msg里。拆开,各自独立发布。
- 避免嵌套过深:msg里可以嵌套其他msg,但别超过两层。比如
A.msg包含B.msg,B.msg又包含C.msg。调试的时候你会疯的。 - 字段带单位:在注释里写清楚单位。是米还是毫米?是弧度还是度?我吃过一次亏,两个模块一个用度一个用弧度,机器人转圈转得跟抽风一样。
- 版本兼容:消息定义一旦发布,尽量别删字段。可以新增,但别改类型或删旧字段。否则旧bag文件回放时会崩溃。
common_msgs 包中,其他功能包都依赖它。这样消息定义统一管理,避免每个包都定义一套“轮子”。
最后,咱们用一张图来总结本章的知识体系:
这张图把整个流程串起来了。从定义msg/srv开始,到配置编译,再到代码调用和命令行验证,最后用最佳实践收尾。你想想看,是不是每一步都有章可循?
rosmsg show your_package/MotorStatus 和 rossrv show your_package/SetTarget 检查定义是否正确。如果命令行能打印出字段,说明编译和依赖都对了。