简介:这是一套面向计算机专业毕设与无人机开发初学者的WPF地面站控制系统完整工程源码,基于.NET Framework与C#构建,通过MAVLink协议实现与无人机的通信,可用于飞行参数实时监控、任务规划执行与数据采集分析等场景。压缩包共243个文件,约34.83MB,其中59个cs源文件承载核心业务逻辑,12个xaml文件定义界面布局,另有53个dll依赖库、17个xml配置、8个nupkg包及png、jpg等界面素材,并附带sqlite数据库文件用于存储飞行数据与用户配置,目录结构完整、层次清晰。目前已有84人学习下载。项目涵盖实时数据显示、GPS位置跟踪、任务规划与数据存储等模块,适合作为毕设参考或WPF上位机入门练手,读者可据此理解地面站整体架构、MAVLink通信流程与界面数据绑定思路,并在此基础上进行二次开发与功能扩展。
1. 拆开这个 WPF 无人机地面站压缩包:它到底能跑起来吗
如果你手头正好有一个UAV_WPF的压缩包,解压后第一眼看到的不是熟悉的App.xaml、MainWindow.xaml,而是一堆DesignTimeResolveAssemblyReferences.cache、UAV_WPF_MarkupCompile.cache、UAV_WPF.csproj.CoreCompileInputs.cache,先别急着删。这些.cache文件是 WPF 编译管线在obj目录里留下的中间产物,它们的存在恰恰说明这个工程被 Visual Studio 真正编译过,而不是一个只放了源码、从没打开过的空壳。对做毕设或者接手别人半成品的人来说,这一点很关键——能编译过,意味着依赖链、XAML 标记编译、资源生成这几条路至少走通过一次。
这个资源是一个基于 WPF 的无人机地面站控制系统,用 C# 和 XAML 写界面,后端走 .NET Framework,通信层用 MAVLink 协议跟飞控对话,数据落地一般配 SQLite 或 MySQL。它要解决的问题很具体:把飞行状态、GPS 位置、任务执行情况实时显示在一个桌面上,同时让操作者能下发航点任务、回放采集数据。适合谁?做毕设需要一套能演示完整链路的同学,以及想找一个 WPF + MAVLink 通信骨架的桌面端开发者。下面我按「先看懂结构、再动手跑、最后避坑」的顺序拆一遍。
2. 工程结构与编译链路:从 csproj 到可执行文件
2.1 先认清目录里哪些是源码、哪些是缓存
解压之后,根目录通常能看到UAV_WPF.sln和UAV_WPF.csproj,这两个是入口。真正要关心的源码分布在几个位置:App.xaml/App.xaml.cs管应用生命周期,MainWindow.xaml是主界面,Views或Pages目录放各个功能页,ViewModels放 MVVM 的绑定逻辑,Models放数据实体,通信相关的类一般在Services或Communication目录。而obj和bin两个目录是编译产物,obj里就是你在文件列表里看到的那堆.cache。
这些.cache文件的作用值得说清楚,因为很多人误以为它们是垃圾。UAV_WPF.csprojAssemblyReference.cache记录的是程序集引用的解析结果,UAV_WPF_MarkupCompile.cache记录 XAML 标记编译的输入输出映射,UAV_WPF.csproj.CoreCompileInputs.cache是增量编译的输入指纹。它们让第二次编译变快,但也会在你换了机器、改了引用路径之后变成「幽灵错误」的来源。我的习惯是:拿到别人的工程,先删掉obj和bin,让它从零编译一次,这样能暴露真实的依赖问题,而不是被旧缓存掩盖。
判断工程是否完整,看三个点:.csproj里<TargetFrameworkVersion>写的是哪个 .NET Framework 版本;packages.config或.csproj的<Reference>里有没有 MAVLink 相关的库;Properties目录下有没有AssemblyInfo.cs。这三样齐了,基本可以判断是个能编译的完整工程。
2.2 用命令行还原并编译,比直接双击 sln 更可控
很多人拿到工程直接双击.sln,然后被 Visual Studio 的「无法加载项目」拦住。更稳的做法是先用命令行确认编译链路。假设你装了 Visual Studio 的 MSBuild,打开「Developer Command Prompt」,进到工程目录:
:: 先清掉旧缓存,避免幽灵错误 rmdir /s /q obj rmdir /s /q bin :: 还原 NuGet 包(如果工程用 packages.config) nuget restore UAV_WPF.sln :: 用 MSBuild 编译,Release 配置 msbuild UAV_WPF.sln /p:Configuration=Release /p:Platform="Any CPU" /v:minimal这段命令的逻辑是:先删缓存保证干净,再还原包,最后编译。/p:Configuration=Release指定发布配置,/v:minimal让输出精简,只报错误和警告。如果编译报「找不到 MAVLink 程序集」,说明包没还原成功,检查nuget.config里的源地址是否可达。如果报 XAML 相关的MC错误,多半是MarkupCompile阶段的问题,通常是某个x:Class和后台代码的命名空间对不上。
编译成功后,bin\Release下会生成UAV_WPF.exe。双击能起来,说明 UI 层没问题;如果起来后界面空白或者报绑定错误,那是数据层或通信层的事,下一章再说。
2.3 关键引用与运行时依赖清单
WPF 工程对 .NET Framework 版本敏感,不同版本对 XAML 特性和异步 API 的支持不一样。下面这张表是我整理这个工程时常用的核对项,你可以对着自己的.csproj逐条看:
| 检查项 | 常见值 | 不匹配时的表现 |
|---|---|---|
| TargetFrameworkVersion | v4.7.2 / v4.8 | 编译报 API 不存在 |
| MAVLink 库引用 | 第三方 MAVLink 包 | 找不到Mavlink命名空间 |
| 数据库驱动 | SQLite / MySQL Connector | 运行时抛连接异常 |
| 平台目标 | Any CPU / x64 | 串口或驱动加载失败 |
| 输出类型 | WinExe | 控制台窗口一闪而过 |
这里有个血泪经验:如果工程引用了串口通信库,而你的平台目标设成Any CPU,在 64 位系统上跑 32 位串口驱动时可能直接崩。改成x64或者x86明确指定,能省掉半天排查。另外,MAVLink 的库版本要和飞控固件支持的协议版本对上,老版本 MAVLink 1.0 和新版 2.0 在消息帧格式上有差异,连不上飞控时先查这个。
3. MAVLink 通信与实时数据绑定:让界面动起来
3.1 MAVLink 心跳、消息解析与串口/UDP 通道
地面站的核心是通信。MAVLink 是一种轻量级的二进制协议,飞控和地面站之间靠它交换心跳、姿态、GPS、电池等消息。这个工程里,通信层一般会有一个MavlinkService或类似的类,负责打开串口或 UDP 端口、收发字节流、按 MAVLink 帧格式解析。
MAVLink 帧的结构是:起始标志0xFE(v1.0)或0xFD(v2.0),然后是长度、序列号、系统 ID、组件 ID、消息 ID,接着是 payload,最后两位是 CRC 校验。解析时最容易翻车的地方是粘包和半包——串口一次读到的字节数不固定,可能一条消息读了一半,也可能两条消息粘在一起。常见做法是维护一个缓冲区,每次读到数据就追加,然后循环尝试从缓冲区头部解析完整帧,解析成功就消费掉,不成功就等下一批数据。
// 简化的 MAVLink 缓冲区解析逻辑 private readonly List<byte> _buffer = new List<byte>(); private void OnSerialDataReceived(object sender, SerialDataReceivedEventArgs e) { int count = _serialPort.BytesToRead; byte[] temp = new byte[count]; _serialPort.Read(temp, 0, count); _buffer.AddRange(temp); // 追加到缓冲区 // 循环解析,直到缓冲区里没有完整帧 while (TryParseFrame(out MavlinkMessage msg)) { DispatchMessage(msg); // 分发到对应的处理器 } } private bool TryParseFrame(out MavlinkMessage msg) { msg = null; // 找起始标志 int startIndex = _buffer.FindIndex(b => b == 0xFE || b == 0xFD); if (startIndex < 0 || _buffer.Count - startIndex < 8) return false; // 读长度字段,判断是否收齐 int payloadLen = _buffer[startIndex + 1]; int frameLen = payloadLen + 8; // v1.0 帧头6 + payload + CRC2 if (_buffer.Count - startIndex < frameLen) return false; // 截取完整帧并解析 byte[] frame = _buffer.GetRange(startIndex, frameLen).ToArray(); _buffer.RemoveRange(0, startIndex + frameLen); msg = MavlinkParser.Parse(frame); return true; }这段代码的关键参数:0xFE和0xFD是两种协议版本的起始标志,payloadLen从帧头第二个字节读,frameLen的计算要区分 v1.0 和 v2.0(v2.0 帧头多一个字节,且支持签名)。_buffer.RemoveRange那一步是消费已解析的数据,防止缓冲区无限增长。如果你发现界面数据更新一顿一顿的,先看这里的循环是不是每次只解析一帧就返回了。
3.2 WPF 数据绑定:INotifyPropertyChanged 与 Dispatcher
通信层解析出数据后,要送到 UI 上显示。WPF 的绑定机制依赖INotifyPropertyChanged接口,属性值变了要主动通知界面。但这里有个线程问题:串口回调是在后台线程触发的,而 WPF 的 UI 元素只能在 UI 线程上更新。直接跨线程改绑定属性,轻则数据不刷新,重则抛InvalidOperationException。
正确做法是用Dispatcher把更新操作切回 UI 线程:
public class FlightStatusViewModel : INotifyPropertyChanged { private double _altitude; public double Altitude { get => _altitude; set { _altitude = value; OnPropertyChanged(nameof(Altitude)); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } // 从通信层回调里调用,切回 UI 线程 public void UpdateAltitude(double value) { Application.Current.Dispatcher.Invoke(() => { Altitude = value; }); } }Dispatcher.Invoke是同步等待,BeginInvoke是异步不等待。高频数据(比如姿态每秒几十帧)建议用BeginInvoke,避免阻塞通信线程。XAML 那边绑定写{Binding Altitude, StringFormat={}{0:F1} m},StringFormat控制显示精度,别小看这个,高度显示成123.456789和123.5对操作者的体验差别很大。
3.3 任务规划与航点下发的数据流
任务规划模块一般是这样一条链路:界面上在地图控件里点几个点,生成航点列表,每个航点有经纬度、高度、速度、悬停时间等参数,然后通过 MAVLink 的MISSION_ITEM消息逐条下发给飞控。下发流程是:先发MISSION_COUNT告诉飞控一共几条,飞控回MISSION_REQUEST要第几条,地面站发对应的MISSION_ITEM,循环到全部发完,最后飞控回MISSION_ACK。
这里常见的坑是超时重传没做。如果某条MISSION_ITEM丢了,飞控会一直等,地面站也一直等,界面就卡在「上传中」。稳妥的做法是给每条消息设一个超时定时器,比如 500ms 没收到MISSION_REQUEST就重发当前条,重试三次还失败就报错退出。参数上,MISSION_COUNT的 count 字段要和实际航点数一致,多一个少一个都会导致飞控拒绝整个任务。
4. 避坑与排查:那些让地面站连不上的原因
4.1 现象:编译通过但运行报「找不到 MAVLink 程序集」
原因通常是 NuGet 包还原到了全局缓存,但工程的HintPath指向的是旧机器的绝对路径。解决方法是打开.csproj,把所有<HintPath>里的绝对路径改成相对路径,或者干脆删掉packages目录重新nuget restore。如果用的是 PackageReference 格式,检查obj\project.assets.json是否生成,没有就说明还原没跑。
4.2 现象:串口打开成功但收不到任何数据
先确认波特率。MAVLink 常用 57600 或 115200,飞控端和地面站端必须一致,差一个档位就是收不到。其次确认串口号没被其他软件占用,有些飞控调参工具会独占串口。再检查数据位、停止位、校验位,标准配置是 8-N-1。如果用的是 UDP 转发,确认目标 IP 和端口,以及防火墙有没有拦。我一般会先用一个独立的串口助手抓一下原始字节,看有没有0xFE开头的帧,有帧说明物理链路通,问题在解析;没帧说明链路本身有问题。
4.3 现象:界面数据不刷新,但后台日志显示收到消息
这是典型的跨线程绑定问题。检查PropertyChanged事件是不是在后台线程触发的,如果是,必须用Dispatcher切回 UI 线程。另一个可能是绑定的DataContext没设对,XAML 里{Binding Altitude}找不到源属性时不会报错,只是静默不显示。排查方法是在输出窗口看绑定错误,Visual Studio 的「输出」面板会打印System.Windows.Data Error,顺着路径找就能定位。
4.4 现象:任务上传到一半卡死
多半是超时重传没做,或者MISSION_REQUEST的消息 ID 匹配逻辑写错了。MAVLink 的MISSION_REQUEST里有个seq字段表示要第几条,地面站要按这个 seq 发对应的航点。如果地面站自己维护的索引和飞控请求的 seq 对不上,就会死循环。解决方法是每次收到MISSION_REQUEST都按它的 seq 去航点列表里取,而不是用自增计数器。
4.5 现象:换台电脑后 XAML 设计器报错但能编译
这是obj里的MarkupCompile.cache残留导致的。设计器读的是缓存里的旧路径,编译时又重新生成,所以出现「设计器报错、编译通过」的割裂现象。解决办法很简单:关掉 Visual Studio,删掉obj和.vs隐藏目录,重新打开。这个操作我每次换机器都强制走一遍,能省掉很多莫名其妙的红波浪线。
5. 进阶:把地面站数据落库并做飞行回放
5.1 用 SQLite 存飞行遥测,按时间戳建索引
实时显示只是第一步,真正有价值的是把遥测数据存下来做回放和分析。这个工程如果配了 SQLite,一般会有一张Telemetry表,字段包括时间戳、经纬度、高度、速度、姿态角、电池电压等。建表时给时间戳加索引,回放时按时间范围查询才快。
CREATE TABLE Telemetry ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Timestamp INTEGER NOT NULL, -- Unix 毫秒时间戳 Latitude REAL, Longitude REAL, Altitude REAL, GroundSpeed REAL, Roll REAL, Pitch REAL, Yaw REAL, BatteryVoltage REAL ); CREATE INDEX idx_telemetry_ts ON Telemetry(Timestamp);写入时不要每条消息都开一次事务,那样磁盘 I/O 会把通信线程拖垮。常见做法是攒一批,比如 50 条或者 1 秒,批量插入。Timestamp用 Unix 毫秒,跨时区回放不会乱。回放时按Timestamp排序读出来,喂给一个模拟的「数据源」,界面绑定不变,只是数据来源从串口换成了数据库。
5.2 回放速度控制与地图轨迹绘制
回放的核心是时间轴控制。简单实现是维护一个回放索引,按倍速定时器推进,比如 1 倍速就是每 100ms 读一条对应时间的数据。地图轨迹用Polyline或者第三方地图控件的路径图层,把经纬度点依次加进去。这里有个细节:经纬度直接画在地图控件上需要投影转换,不同地图控件的坐标系不一样,GCJ-02 和 WGS-84 混用会导致轨迹偏移几百米。确认你的地图控件用哪个坐标系,MAVLink 出来的是 WGS-84,如果控件要 GCJ-02,得做转换。
5.3 一个我常做的验证习惯
每次改完通信层或者绑定逻辑,我不会直接连飞控测,而是先写一个「模拟数据源」——用定时器每 100ms 生成一条假的姿态和 GPS 数据,走和真实通信一样的解析、分发、绑定链路。这样能快速验证 UI 刷新、线程切换、数据落库这几段逻辑对不对,不用反复插拔串口。等模拟数据跑通了,再切到真实串口,问题范围就缩小到物理链路和协议匹配上。从那以后我每次动通信相关的代码,都强制先跑一遍模拟数据源,确认界面和数据流没问题再上真机。希望帮到你。
本文还有配套的精品资源,点击获取