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)缓存最近几条消息
下面这张图展示了消息的流转过程:
4.7 调试与验证
消息定义好了,代码写完了,怎么验证它跑通了?
我最常用的方法是:
- 编译烧录:把固件刷到飞控板上
- 连接Mavlink终端:用QGroundControl或直接串口
- 输入命令:
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的“消息高速公路”。你定义好消息格式,模块之间就能自由通信。这个机制非常灵活,后面我们要扩展测绘功能,比如加一个“地形跟随”模式,只需要在消息里加几个字段就行,完全不影响现有逻辑。