4. 自定义uORB消息:定义测绘任务消息结构体、发布与订阅机制

好,咱们进入第四章。这一章要解决一个核心问题:怎么让PX4知道你要执行什么样的测绘任务?

默认的PX4固件里,有位置指令、速度指令,但没有“测绘任务”这个概念。你想让它飞一个矩形航线、设置重叠率、指定拍照间隔——这些信息,原生的uORB消息里一个都没有。

所以,我们要自己造一个。

4.1 为什么需要自定义uORB消息?

uORB是PX4内部模块之间通信的“总线”。你可以把它理解成一套发布-订阅的消息系统。一个模块发布消息,其他模块订阅它,就能拿到数据。

我刚开始接触PX4时,犯过一个低级错误:想把测绘参数塞进vehicle_command里。结果发现那个结构体根本装不下复杂的航线定义,硬塞进去还会导致协议解析出错。后来我才明白——不要试图改造别人的消息,自己定义一个更干净。

自定义uORB消息的好处很明显:

  • 解耦:测绘模块和飞控模块各管各的,接口清晰
  • 可扩展:以后加新字段,不影响已有逻辑
  • 调试方便:用uorb top命令就能看到消息内容

核心原则:uORB消息是模块间契约。定义得越清晰,后期联调越省心。

4.2 定义测绘任务消息结构体

我们先在msg/目录下新建一个文件:SurveyTask.msg。这个文件定义了测绘任务的完整信息。

我个人习惯把消息分成三部分:任务元信息、航线参数、相机控制参数。这样结构清晰,也方便后续扩展。

# SurveyTask.msg
# 测绘任务消息定义

# 任务元信息
uint64 timestamp            # 时间戳(us)
uint8 task_id               # 任务ID
uint8 TASK_STATE_IDLE = 0   # 空闲
uint8 TASK_STATE_RUNNING = 1 # 执行中
uint8 TASK_STATE_FINISHED = 2 # 已完成
uint8 task_state            # 任务状态

# 航线参数
float32[2] start_lat_lon    # 起点经纬度 [deg]
float32[2] end_lat_lon      # 终点经纬度 [deg]
float32 altitude            # 飞行高度 [m]
float32 overlap_ratio       # 重叠率 [0.0-1.0]
float32 side_overlap_ratio  # 旁向重叠率 [0.0-1.0]
uint8 direction             # 航线方向 0:水平 1:垂直

# 相机控制参数
float32 shutter_interval    # 拍照间隔 [s]
uint8 camera_trigger_mode   # 触发模式 0:距离 1:时间
float32 trigger_distance    # 触发距离 [m]

# 执行结果
uint8 RESULT_SUCCESS = 0
uint8 RESULT_FAILURE = 1
uint8 result                # 执行结果

嗯,这里要注意几个细节:

  • 经纬度用float32数组:精度够用,而且节省带宽
  • 重叠率用0-1浮点数:比百分比更直观,计算时直接乘
  • 状态枚举值从0开始:这是PX4的惯例,避免默认值歧义

4.3 注册消息到uORB框架

定义完.msg文件,还要告诉编译系统:嘿,这里有个新消息!

打开msg/CMakeLists.txt,在合适位置加上一行:

add_custom_target(generate_uorb_messages
    ...
    SurveyTask.msg
    ...
)

然后运行编译命令:

make px4_fmu-v5_default

编译系统会自动生成C/C++头文件。你可以在build/px4_fmu-v5_default/uORB/topics/目录下找到survey_task.h。里面包含了结构体定义、发布函数、订阅函数等。

小技巧:如果只改了msg文件,可以用make px4_fmu-v5_default uorb单独编译uORB部分,省时间。

4.4 发布消息:测绘模块的实现

消息定义好了,谁来发布?当然是我们的测绘任务管理器。

假设我们有一个模块叫survey_manager,它在收到地面站的航线指令后,需要把任务信息发布出去。

发布消息的代码套路很固定,我直接贴出来:

#include <uORB/uORB.h>
#include <uORB/topics/survey_task.h>

// 在模块初始化时
struct survey_task_s task_msg = {};
orb_advert_t task_pub = orb_advertise(ORB_ID(survey_task), &task_msg);

// 当需要发布任务时
task_msg.timestamp = hrt_absolute_time();
task_msg.task_id = 1;
task_msg.task_state = survey_task_s::TASK_STATE_RUNNING;
task_msg.start_lat_lon[0] = 39.9042f;
task_msg.start_lat_lon[1] = 116.4074f;
task_msg.end_lat_lon[0] = 39.9142f;
task_msg.end_lat_lon[1] = 116.4174f;
task_msg.altitude = 100.0f;
task_msg.overlap_ratio = 0.7f;
task_msg.side_overlap_ratio = 0.6f;
task_msg.shutter_interval = 2.0f;

orb_publish(task_pub, &task_msg);

这里有个坑,我曾经踩过:发布前一定要给timestamp赋值。否则订阅方拿到的可能是旧数据,导致时间戳判断出错。

4.5 订阅消息:飞控模块的接收

另一边,飞控模块(比如导航滤波器或位置控制器)需要订阅这个消息,才能知道当前要执行什么任务。

订阅的代码同样模式化:

#include <uORB/uORB.h>
#include <uORB/topics/survey_task.h>

// 在模块初始化时
int task_sub = orb_subscribe(ORB_ID(survey_task));
// 设置队列大小,防止消息丢失
orb_set_interval(task_sub, 200); // 200ms轮询一次

// 在主循环中
struct survey_task_s task_msg;
bool updated = false;
orb_check(task_sub, &updated);

if (updated) {
    orb_copy(ORB_ID(survey_task), task_sub, &task_msg);
    // 处理任务数据
    if (task_msg.task_state == survey_task_s::TASK_STATE_RUNNING) {
        // 开始执行航线
        process_survey_waypoints(task_msg);
    }
}

为什么用orb_check而不是直接orb_copy?因为orb_check是非阻塞的,不会让主循环卡住。这在飞控这种实时系统里很重要。

4.6 消息的生命周期与同步

uORB消息是有状态的。发布者更新消息后,订阅者下一次轮询就能拿到最新数据。但如果订阅者处理得太慢,中间的消息会被覆盖。

我建议的做法是:

  • 发布者:只在任务状态变化时发布,不要高频重复发
  • 订阅者:用队列模式(orb_subscribe_multi)缓存最近几条消息

下面这张图展示了消息的流转过程:

uORB消息发布-订阅流程 Survey Manager 发布者模块 uORB 消息总线 SurveyTask 消息 消息字段 timestamp task_id task_state start_lat_lon altitude overlap_ratio Navigation 订阅者:导航模块 Camera Trigger 订阅者:相机触发 Logger 订阅者:日志记录 orb_publish orb_copy orb_copy orb_copy 💡 一个发布者,多个订阅者。每个订阅者独立轮询,互不影响。 订阅者通过 orb_check 判断是否有新数据,避免阻塞主循环。

4.7 调试与验证

消息定义好了,代码写完了,怎么验证它跑通了?

我最常用的方法是:

  1. 编译烧录:把固件刷到飞控板上
  2. 连接Mavlink终端:用QGroundControl或直接串口
  3. 输入命令:uorb top survey_task

如果看到类似下面的输出,说明消息在正常发布:

TOPIC: survey_task
#0: 50 Hz, 1 publishers, 3 subscribers
   timestamp: 1234567890
   task_id: 1
   task_state: 1
   altitude: 100.00

如果publishers是0,说明没人发布。如果subscribers是0,说明没人订阅。这两个数字能帮你快速定位问题。

避坑指南:我曾经在调试时发现订阅者收不到消息,查了半天才发现是orb_subscribe和orb_advertise的调用顺序反了。记住:先发布后订阅,或者订阅时指定队列大小,否则订阅者可能拿不到第一条消息。

4.8 本章小结

这一章我们干了三件事:

  • 定义了SurveyTask.msg:包含了测绘任务需要的所有字段
  • 实现了发布者:在测绘管理模块中发布任务消息
  • 实现了订阅者:在飞控模块中接收并处理任务数据

说白了,uORB就是PX4的“消息高速公路”。你定义好消息格式,模块之间就能自由通信。这个机制非常灵活,后面我们要扩展测绘功能,比如加一个“地形跟随”模式,只需要在消息里加几个字段就行,完全不影响现有逻辑。