简介:MissionPlanner 开源地面站源码,使用 C# 编写,面向无人机开发者、飞控研究人员及桌面应用学习者。项目基于 MAVLink 协议,可与 Pixhawk 等飞控无缝对接,完整覆盖飞行任务规划、实时遥测接收、遥控器校准、参数设置、日志分析与回放等核心功能,是理解地面站软件设计与飞控通信机制的理想学习材料。压缩包约 162.8MB,内含近 2000 个文件,其中以 .cs 源文件为主,配合 .resx 资源文件、.xml/.json 工程配置、.py 辅助脚本、.js/.css 前端资源及 .bat 编译驱动脚本,目录结构基本对应完整解决方案。目前已有 1963 人学习。源码中可重点研读 Mission 类如何定义并上传航点任务、Telemetry 模块如何解析位置姿态速度数据、安全模块的故障检测与应急策略,以及 GIS 地图集成、遥控器校准、日志回放和插件系统等实现;同时也能看到 WPF+MVVM 架构在大型桌面程序中的实际运用,包内附带工程文件与构建脚本,便于直接编译、部署和二次开发。
1. 无人机地面站源码拆解:MissionPlanner-master到底能不能跑起来
很多第一次接触无人机地面站开发的开发者,拿到这份基于C#的MissionPlanner-master源码时,第一反应都是先试探着点开解决方案文件,然后被一波依赖报警劝退。我的答案是:这份源码确实能跑,但它不是那种打开即用的演示项目,而是一整套依赖MAVLink协议、串口通信、地图渲染的工程链。在本文里,我会把这份开源控制软件源码从目录结构、编译步骤、连接飞控到二次开发一条龙拆开讲,重点放在那些在文档里找不到的边界条件和踩坑点。适合正在学习地面站开发、想基于MissionPlanner做定制功能的人,也适合想理解上位机与飞控之间真实工作流程的开发者。
2. MissionPlanner源码结构:为什么C#成了地面站的事实选择
2.1 C#和.NET Framework在无人机地面站里的位置
MissionPlanner是无人机社区里流传最广的开源地面站之一。整个程序是基于C#和WinForms构建的,解决方案里默认目标是.NET Framework 4.7.2。这个技术栈在当年不是唯一选择,有团队用Electron+JS做过地面站,也有用PyQt做的,但MissionPlanner一直留在C#不是没有道理的。
第一个理由,MAVLink协议几乎所有适用飞控都自带了,C#处理字节流和二进制数据非常顺手,BitConverter、MemoryStream这类工具直接拉满。第二个理由,WinForms在Windows生态下的控件即时性非常好,画仪表盘、拖航线、刷传感器数据这种高频GUI操作,C#的GDI+和双缓冲机制能扛得住。第三个理由是生态,MissionPlanner积累了大量C#飞控库,比如MAVLink库、串口库、地图封装,你改起来不需要从零拼。
当然,它的缺点是跨平台差。想把这套源码拉上Linux或macOS,还需要处理Mono环境、串口库差异等一堆问题。我的建议是:如果你只做Windows端的二次开发,C#这套没毛病;如果你要跨平台,还不如去找基于MAVLink的Python地面站框架,别硬啃这套源码。
2.2 核心目录:谁在负责通信、地图、航线和UI
把源码包解压后(通常是MissionPlanner-master目录),你会看到这样的顶层结构:MissionPlanner.sln是入口;MissionPlanner.csproj是主项目文件;ExtLibs目录里放着各种第三方封装;GCSViews目录放着所有用户界面;MAVLink目录是通信层;Properties里是程序集信息。
其中MAVLink目录是整个程序的通信心脏。MAVLink.cs里定义了消息帧、字节解析、校验逻辑;MAVLinkInterface.cs负责发送请求、接收数据流、处理心跳;mavlink_device.cs负责抽象的设备连接。如果你要研究协议层,看这一层就够了。
GCSViews是大家最常改的地方。FlightPlanner.cs是航线规划界面,你拖拽航点、设置高度和速度,最终会通过MAVLink_MISSION_ITEM协议下发到飞控。HUD.cs是飞行姿态显示,在界面上画一个人工地平仪和速度带,它内部用Timer定时刷新。default.cs是主飞行界面,包含地图、状态栏、连接按钮等。还有ConfigFrame.cs用于调参面板,会和飞控交互参数流。
ExtLibs里比较重要的是GMap.NET——地图显示的核心封装。它还包含一个名为Arduino的库,用于对某些飞控板进行固件升级,但实际编译时不一定全部引用,全是可选的。如果你在项目中看到一些带引号的项目引用缺失,多半就来自ExtLibs里的历史遗留。
下面这张表能帮你快速定位要改的地方:
| 目录/文件 | 对应模块 | 二次开发高频改动点 |
|---|---|---|
| MAVLink 目录 | MAVLink协议解包、心跳判定、数据队列 | 增加自定义消息解析 |
| GCSViews/Default.cs | 主飞行界面、地图集成、连接管理 | 改布局、加按钮、改连接逻辑 |
| GCSViews/FlightPlanner.cs | 航线规划与航点上传 | 增加航点类型、修改上传协议 |
| GCSViews/HUD.cs | 姿态仪表、空速高度显示 | 改仪表盘样式、增加姿态图形 |
| ExtLibs 目录 | 第三方库封装:地图、日志、地面站工具 | 升级依赖版本、替换地图实现 |
如果你只是想改一个按钮的位置,去Default.cs;想动地图,就要同时看GCSViews和ExtLibs的GMap封装;想处理飞控数据,别绕开MAVLink目录。
2.3 协议层速记:MAVLink消息长这样
要想动手改功能,你得先认识MAVLink消息的结构。一个标准MAVLink v1消息帧包括:起始字节0xFE、长度、序列号、系统ID、组件ID、消息ID、载荷、校验码。在MissionPlanner源码里,所有解析都发生在MAVLink.cs的readPacket方法中。
常见消息ID:HEARTBEAT是0,SYSTEM_TIME是2,GPS_RAW_INT是24,ATTITUDE是30,VFR_HUD是74。地面站和飞控的“对话”,本质上就是不断交换这些带消息ID的二进制帧。你在源码里搜索“MAVLink.MAVLINK_MSG_ID.GPS_RAW_INT”,就能找到对应处理代码。
这个理解对避坑很重要,因为很多二次开发问题最后都发现是消息ID版本不匹配、字节偏移没对齐。比如飞控固件升级后新增了一条消息,旧源码解包就会报CRC错误。
2.4 线程与刷新机制:为什么界面代码都在Invoke
MissionPlanner的UI更新大量依赖System.Windows.Forms.Timer和BackgroundWorker。连接飞控后,sensor跟踪由BackgroundWorker线程读取缓冲区,再把解析结果交给Timer去刷新界面。因为这个原因,你几乎在所有界面类里都能看到类似:
if (this.InvokeRequired) { this.Invoke((Action)delegate { // 跨线程更新控件 label1.Text = "刷新"; }); }这段代码在C# WinForms里是标准写法,作用是跨线程安全访问控件。新手改功能时最容易犯的错,就是在后台线程里直接改Label.Text或PictureBox.Image,轻则控件闪烁,重则直接抛出跨线程访问异常。后面我还会在避坑章详细展开。
3. 从源码到可执行程序:编译环境、构建流程与三个必踩的坑
3.1 环境准备:Visual Studio版本与组件选择
我第一次编译这份源码时用的是Visual Studio 2019,后来在2022上也测试通过。如果你用VS2022,注意安装向导里勾选“.NET 桌面开发”工作负载,这会同时带上.NET Framework 4.x目标包、WinForms模板和Windows SDK。我一般还会额外勾选“使用C++的桌面开发”,因为源码里的部分原生依赖编译时可能需要C++工具集。
组件选好之后,不要着急打开解决方案。先把源码文件夹放到一个不含空格的纯英文路径下,比如D:\Develop\DroneProject\MissionPlanner-master。中文路径或含空格路径会引发一些不可描述的编译错误,这是第一个要提前避的坑。
3.2 打开解决方案并还原NuGet包
源码包不带packages目录,所有第三方依赖都靠NuGet还原。用VS打开MissionPlanner.sln后,第一次会触发还原过程。如果网络好,等待几分钟就自动完成。如果你使用的是离线环境,就得手动去NuGet配置里添加一个可用源,或者从别处移一个现成的packages文件夹过来。
这里给出一个命令行还原的示例(也可以直接在VS里操作):
# 进入源码根目录 cd MissionPlanner-master # 用NuGet还原解决方案(需要NuGet CLI或dotnet命令) nuget restore MissionPlanner.sln这段命令会读取每个项目文件里的packages.config或PackageReference,把依赖下载到本地。还原完成后,你会发现根目录出现了一个packages文件夹,里面是gmap、log4net、Newtonsoft.Json等几十个dll。参数说明:MissionPlanner.sln是解决方案入口;restore是NuGet的还原动作;如果报错,多半是NuGet源不可用,换成官方源或镜像即可。
3.3 生成解决方案并定位输出文件
还原后,在VS里把MissionPlanner项目设为启动项目,然后选择Release模式,点击“生成 > 生成解决方案”。如果你的机器上没有安装特定版本的.NET Framework SDK,编译器会在第二个层次报DK/目标框架错误。这时按照报错提示打开项目属性,把目标框架改为本机已安装的版本,比如从4.7.2改成4.8。
生成成功后,输出目录通常在bin\Release。里面最关键的是MissionPlanner.exe,还有一堆依赖dll和一个名为参数目录的文件夹。你可以直接双击exe启动,如果能出现端口选择窗口,说明构建基本成功。
3.4 但别急着连飞控:先跑一遍内置模拟器
刚编译出来的exe第一次运行时,如果没有任何飞控设备,会卡在连接端口选择界面,这很正常。我建议先不要接真机,而是运行一次内置的SITL模拟器,让地面站和模拟飞控完成一次完整握手。这既能验证二进制文件具备了通信能力,也能帮你区分将来遇到的问题是硬件连接还是软件配置。
在MissionPlanner主界面的“模拟”菜单里,选择一款机型,比如四旋翼。它会自动下载模拟器组件并启动一个本地UDP服务,地面站通过UDP 14660端口与模拟飞控通信。你不需要额外安装任何依赖,这一步跑通了,说明你的编译产物没有少模块。
3.5 常见编译报错快速对照
编译过程中如果遇到下面表格里的错误,直接对应解决:
| 报错信息 | 常见原因 | 解决方式 |
|---|---|---|
| MSB3073 调用 nuget.exe 失败 | NuGet源不可用 | 换源或离线还原 |
| The target framework ... is not installed | 缺少 .NET Framework 目标包 | 安装对应目标包或改项目目标框架 |
| The type or namespace name 'GMap.NET' could not be found | GMap引用丢失 | 还原ExtLibs里GMap相关NuGet包 |
| Error MSB3084: 无法定位自定义工具 | 资源生成工具版本问题 | 清理解决方案,重新生成一次 |
这些错误我基本都踩过一遍,前两个属于环境问题,后两个多半是还原不完整。解决顺序建议是:先清理再还原,再检查目标框架,最后才去检查代码。
4. 连接真实飞控与SITL模拟:MAVLink心跳、参数流与数据读取
4.1 认识MAVLink心跳机制
飞控和地面站之间没有任何“连接握手”的附带仪式,唯一的判断依据是心跳包。飞控上电后,会以1Hz持续广播HEARTBEAT消息,消息里包含系统类型、自动驾驶类型、飞控模式等。地面站如果在超时窗口(比如3秒)内收到心跳,就认为链路通了。
在MissionPlanner源码里,MAVLinkInterface.cs中维护了一个lastHeartbeat时间戳。所有需要在线状态的控件,都会去检查这个时间戳是否在3秒内。如果超过3秒没有收到心跳,界面上的“连接”状态就会变灰。
4.2 用源码里的MAVLink库写一个最小连接Demo
很多新手想弄清地面站是怎么连接飞控的,但直接被一堆UI代码淹没了,所以我建议你把通信层单独剥离出来。下面这个Demo是我参照MissionPlanner内核心流程写的简化版:
using MissionPlanner.MAVLink; using MissionPlanner.Utilities; // 初始化串口 var port = new MAVLinkSerialPort("COM3", 115200); port.Open(); // 初始化MAVLink收发对象 var mav = new MAVLinkInterface(); mav.BaseStream = port; // 等待心跳,超时5秒 DateTime timeout = DateTime.Now.AddSeconds(5); while (DateTime.Now < timeout) { try { var mavlink = mav.readPacket(); if (mavlink != null && mavlink.msgid == (byte)MAVLink.MAVLINK_MSG_ID.HEARTBEAT) { Console.WriteLine("收到心跳,飞控在线"); break; } } catch (Exception ex) { Console.WriteLine("解析异常: " + ex.Message); } } port.Close();这段代码的逻辑是:打开串口后,循环调用readPacket()解析MAVLink帧,一旦发现消息ID等于0的HEARTBEAT,就确认链路正常。和MissionPlanner主程序相比,这里省略了线程和界面,只保留了通信链路,目的是让你看清核心机制。参数说明:COM3是串口号,按你实际设备修改;115200是波特率,多数飞控默认是这个值;timeout是心跳等待超时,5秒足够,超过5秒还没心跳就检查接线和波特率。
4.3 读取姿态与GPS数据:从缓冲到界面
收到心跳只是开始,真正给界面用的一般是ATTITUDE姿态、GPS_RAW_INT定位、VFR_HUD速度。MissionPlanner源码中,readPacket()解析出的每条消息都会进入排队缓冲区,后台线程再把缓冲区里的数据按msgid分发给对应控件。
例如在HUD.cs里,ATTITUDE消息的roll、pitch、yaw会直接作为人工地平仪的旋转角度。GPS消息则会被转换成地图上的光标位置。如果你自己写独立程序,可以直接从readPacket返回的mavlink_attitude_t对象里取属性:
var attitude = mav.getAttitude(); Console.WriteLine($"Roll: {attitude.roll} Pitch: {attitude.pitch} Yaw: {attitude.yaw}");这里的getAttitude()在MissionPlanner库中会返回最新ATTITUDE消息的解析结果,是二次开发时取姿态的常用入口。需要注意的是,角度值是弧度制,要显示成度需要乘57.2958。
4.4 SITL模拟器的组合用法:不加硬件也能调UI
如果你的目标是修改界面或测试航线规划,完全不必每次都接真机。启动SITL模拟器后,MissionPlanner会自动将模拟飞控的MAVLink包转发到本地UDP端口,地面站和模拟器可以同时运行在同一个Windows会话里。
我常用的组合是:SITL模拟器 + 修改过的MissionPlanner二进制 + Visual Studio调试器。在VS里给工程设置断点,然后启动模拟,你就能在断点处看到来自模拟飞控的MAVLink原始字节。这种做法对学习协议非常有帮助,也能大幅减少实飞风险。
5. 二次开发避坑:编译、地图、连接和线程的六个真实问题
5.1 NuGet还原失败,ExtLibs目录缺失引用
现象:打开解决方案后,大量项目引用显示黄色感叹号,编译时提示“未能找到类型或命名空间”。 原因:源码包为了瘦身,通常不附带packages目录,所有第三方包都需要在线还原。如果你恰好在离线环境,或NuGet源配置不对,还原就会静默失败。 解决:先检查packages.config文件里列出的所有包名,然后配置本地可用的NuGet镜像源,再执行一遍nuget restore。我通常还会写一个脚本,restore后检查packages目录是否完整,缺哪个包就单独补装哪一个。
5.2 地图显示灰色或空白
现象:运行后地图区域一片空白或只有格子,无论怎么拖动都不加载。 原因:MissionPlanner的地图用了GMap.NET,本身需要访问在线地图服务。在无法连接公共服务或网络受限的环境下,图层加载就会失败。 解决:在“设置”里切换地图提供方,例如从Google切换到OpenStreetMap;或改用离线地图缓存方式,把缓存目录指到本地已有的地图瓦片。国内网络环境下,OpenStreetMap的可用性通常好一些。
5.3 编译时提示“found conflicts between different versions”
现象:生成时出现类似“MSB3277: Found conflicts between different versions of the same dependent assembly”的错误。 原因:项目中不同模块引用了同一个dll的不同版本,例如Newtonsoft.Json 11与13同时在引用树里。 解决:在VS的程序包管理控制台里执行统一版本更新,或修改相应项目的绑定重定向。我的做法是先把所有项目里的Newtonsoft.Json统一升级到同一个版本,再重新还原一遍。
5.4 USB连接后端口列表不刷新
现象:插入飞控后,MissionPlanner的串口下拉框里没有新设备,点“刷新”也看不到。 原因:MissionPlanner的串口枚举实现是启动时加载一次SerialPort.GetPortNames(),有些版本没有处理热插拔事件。 解决:手动重新打开端口刷新函数,或者卸载USB串口驱动再重装。如果你改源码,可以在主窗口的Timer事件中加入串口列表的重枚举逻辑。
5.5 自定义控件在后台线程中更新后闪退
现象:加了线程读取数据后,界面不定时闪退,报“线程间操作无效”。 原因:WinForms控件不是线程安全的,后台线程直接修改UI属性就会触发跨线程异常。 解决:所有UI更新都通过控件的Invoke/BeginInvoke包装。MissionPlanner源码里已经有大量Invoke写法可以直接抄,你自己新增界面前,先想清楚哪个线程在写,哪个线程在读。
5.6 SITL模拟器启动后看不到任何数据
现象:模拟器进程启动,但MissionPlanner连接按钮一直闪烁,收不到心跳。 原因:本地UDP端点绑定错误,或模拟器用的端口与地面站配置的端口不一致。常见的是14660端口被其他程序占用。 解决:检查端口占用,修改SITL启动参数,确保地面站的“连接”设置里选择UDP客户端并指向127.0.0.1:14660。
6. 进阶:把自定义日志导出功能写进MissionPlanner
如果你已经能编译并连接飞控,下一步大概率是想加自己的功能。我提供一个比较实用的方向:把飞行日志里的关键消息导出成CSV,方便在Excel里分析。MissionPlanner本身自带日志导出,但导出格式是自定义的,没法直接对照你自己的数据需求。我们可以自己加一个菜单项。
先找到主窗体的菜单结构,在ToolStripMenuItem里添加一个“导出CSV”的按钮。点击后调用以下逻辑:
private void ExportCsv_Click(object sender, EventArgs e) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = "CSV文件|*.csv"; if (dlg.ShowDialog() != DialogResult.OK) return; using (var writer = new StreamWriter(dlg.FileName)) { writer.WriteLine("Time, Roll, Pitch, Yaw"); foreach (var log in logfileData) { var att = log.GetMSG<MAVLink.mavlink_attitude_t>(MAVLink.MAVLINK_MSG_ID.ATTITUDE); if (att == null) continue; writer.WriteLine($"{log.timestamp}, {att.roll}, {att.pitch}, {att.yaw}"); } } MessageBox.Show("导出完成"); }这段代码的思路是:读取日志文件中的原始MAVLink包,筛出ATTITUDE消息,再把姿态数据逐行写入CSV。实际使用时,logfileData需要替换为源码里日志解析类MAVLinkLog已经解析好的消息列表。参数说明:GetMSG方法和消息ID是关键,改成GPS_RAW_INT就可以导出经纬度;timestamp字段是飞行时间戳,单位通常是毫秒。
运行这段逻辑之后,你会发现自己开始真正理解MAVLink协议与源码模块边界了。我在第一次做类似功能时,因为没注意radian与degree切换,导出的数据在图表上严重偏斜,后来才发现要乘57.2958。从那以后我每次导完都会在Excel里先画一条线检查数值范围是否正常,强制自己走一遍数据置信度校验。希望帮到你。
本文还有配套的精品资源,点击获取