自定义话题消息:msg文件定义、编译规则、Python中调用自定义消息

好,咱们接着聊。上一章我们搞定了标准消息类型,但说实话,实际项目里你很快就会发现——标准消息根本不够用。

举个例子。我在做一个六轴机械臂的力控项目时,需要同时传递关节角度、末端位姿、力矩值、时间戳。用标准消息?要么拆成四五个话题发,要么自己拼字符串。两种方式都让人头疼。后来我学乖了——自己定义消息结构,一劳永逸。

为什么需要自定义消息?

说白了,标准消息是ROS官方给你搭的积木。但你的机器人不是积木,它有自己独特的数据结构。

  • 数据聚合:把多个相关字段打包成一个整体
  • 类型安全:编译时就能发现类型错误,不用等到运行时崩溃
  • 代码可读性:看到消息名就知道里面装了什么
  • 跨语言支持:C++、Python、Java都能用同一个msg文件

核心原则:一个msg文件只描述一种数据结构。别把关节状态和电机温度塞到同一个消息里,那是给自己挖坑。

msg文件长什么样?

msg文件其实就是个文本文件,后缀是.msg。放在功能包里的msg文件夹下。语法极其简单:

# 这是注释,用#号开头
# 格式:字段类型 字段名

# 机械臂关节状态消息
string robot_name        # 机器人名字,比如"arm_01"
uint8 joint_count        # 关节数量
float32[] joint_angles   # 关节角度数组,单位弧度
float32[] joint_velocities # 关节速度数组
float32[] joint_torques  # 关节力矩数组
bool is_moving           # 是否正在运动
time stamp               # 时间戳

嗯,这里要注意:字段类型必须是ROS支持的基本类型。我列个表给你看:

类型 说明 C++对应 Python对应
bool 布尔值 uint8_t bool
int8/uint8 8位整数 int8_t/uint8_t int
int16/uint16 16位整数 int16_t/uint16_t int
int32/uint32 32位整数 int32_t/uint32_t int
float32 32位浮点 float float
float64 64位浮点 double float
string 字符串 std::string str
time ROS时间 ros::Time rospy.Time
duration ROS时长 ros::Duration rospy.Duration
uint8[] 可变长数组 std::vector<uint8_t> list

我的习惯:数组类型尽量用固定长度。比如关节数量是6,就写成float32[6] joint_angles。这样内存分配更高效,调试时也更容易发现问题。

编译规则配置

msg文件写好了,但ROS还不知道它的存在。你得告诉编译系统:嘿,帮我生成代码!

首先,在CMakeLists.txt里做三件事:

# 1. 添加message_generation依赖
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation  # 这行是关键
)

# 2. 声明你要编译的msg文件
add_message_files(
  FILES
  JointState.msg
  # 如果有多个msg,继续往下写
  # EndEffectorPose.msg
)

# 3. 生成消息代码
generate_messages(
  DEPENDENCIES
  std_msgs  # 如果你的msg里用了std_msgs的类型
)

然后,在package.xml里也要声明依赖:

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

我曾经犯过一个低级错误——只加了build_depend,忘了加exec_depend。结果编译通过,运行时却报找不到消息。排查了半天,最后发现是运行时依赖没加。嗯,这种坑踩过一次就记住了。

编译与验证

配置好之后,回到工作空间根目录:

cd ~/catkin_ws
catkin_make
# 或者用 catkin build,看你习惯

编译成功后,用rosmsg命令验证:

rosmsg show your_package_name/JointState

如果看到你定义的字段列表,说明消息生成成功了。我个人习惯每次改完msg都跑一下这个命令,确认没写错。

Python中调用自定义消息

消息编译好了,怎么在Python里用?其实和标准消息一模一样。

发布者示例

#!/usr/bin/env python3
import rospy
from your_package_name.msg import JointState

def publish_joint_state():
    pub = rospy.Publisher('joint_states', JointState, queue_size=10)
    rospy.init_node('joint_state_publisher', anonymous=True)
    rate = rospy.Rate(10)  # 10Hz

    while not rospy.is_shutdown():
        msg = JointState()
        msg.robot_name = "arm_01"
        msg.joint_count = 6
        msg.joint_angles = [0.0, -0.5, 1.2, 0.0, 0.8, 0.0]
        msg.joint_velocities = [0.1, 0.0, -0.2, 0.0, 0.0, 0.0]
        msg.joint_torques = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        msg.is_moving = True
        msg.stamp = rospy.Time.now()

        pub.publish(msg)
        rate.sleep()

if __name__ == '__main__':
    try:
        publish_joint_state()
    except rospy.ROSInterruptException:
        pass

订阅者示例

#!/usr/bin/env python3
import rospy
from your_package_name.msg import JointState

def callback(msg):
    rospy.loginfo(f"收到 {msg.robot_name} 的状态")
    rospy.loginfo(f"关节角度: {msg.joint_angles}")
    rospy.loginfo(f"运动状态: {'运动中' if msg.is_moving else '静止'}")

def listen_joint_state():
    rospy.init_node('joint_state_listener', anonymous=True)
    rospy.Subscriber('joint_states', JointState, callback)
    rospy.spin()

if __name__ == '__main__':
    listen_joint_state()

注意:Python中访问消息字段时,变量名必须和msg文件里定义的一模一样。大小写敏感,下划线也不能少。我见过有人把joint_angles写成jointAngles,结果运行时AttributeError,一脸懵。

消息嵌套与复用

有时候一个消息不够用,你可以在一个msg里引用另一个msg。比如:

# RobotArmState.msg
JointState arm_joints
float64[3] end_effector_pose
float64 gripper_force

前提是JointState.msg也在同一个包或者依赖包里。编译时ROS会自动处理这种嵌套关系。

我的建议:消息嵌套别超过两层。三层以上,代码可读性急剧下降。你想想看,调试的时候要一层层剥开消息,多痛苦。

本章知识体系

下面这张图帮你理清自定义消息的完整流程:

自定义消息完整流程 1. 编写msg文件 msg/JointState.msg 2. 配置编译规则 CMakeLists.txt + package.xml 3. catkin_make 生成代码 验证:rosmsg show your_package/JointState 确认字段类型和名称正确 Python发布者 from pkg.msg import JointState pub.publish(msg) Python订阅者 def callback(msg): msg.joint_angles[0] 话题通信:/joint_states

从定义到编译,再到Python调用,每一步都环环相扣。你可能会问:为什么非要这么麻烦?直接用字典传数据不行吗?

行,但不行。字典没有类型检查,你传个字符串进去,订阅方以为是浮点数,运行时才报错。自定义消息在编译阶段就能发现这种问题。我在一个协作机器人项目里吃过这个亏——调试了三天,最后发现是字段类型写错了。从那以后,我坚持用自定义消息,再也不用字典凑合了。

总结一下:自定义消息是ROS里最实用的技能之一。它让你的代码更健壮、更可读、更专业。花十分钟定义好msg,能省下后面十小时的调试时间。

可可精品资料库