☰
WPF无人机地面站从零搭建:MAVLink协议、MVVM架构与避坑全攻略
2026/9/28 20:38:30 网站建设 项目流程

简介:这是一份基于WPF的无人机地面站控制系统完整工程包,面向无人机开发者及毕设学生,用于解决飞行状态监控、任务控制与数据记录等需求。系统提供飞行参数实时监控、任务规划与执行、数据采集分析等功能,可显示飞行状态与GPS位置;采用C#与XAML构建桌面界面,通过MAVLink协议与无人机通信,后端集成数据库保存飞行记录。压缩包共243个文件,整体约34.83MB,涵盖C#源码、XAML界面布局、DLL依赖库、可执行程序、XML/JSON/config配置文件、png/jpg界面图片、SQLite数据文件及项目说明文档,目录结构清晰,便于按模块研读。资源内含可直接运行的exe程序与完整工程,由Visual Studio解决方案组织,分数据层、通信层、界面层等模块,可帮助读者理解WPF数据绑定、MVVM分层、多线程刷新界面、串口/网络通信、实时数据可视化等关键实现路径,也可作为飞控算法开发或地面站功能扩展的基础。已有83人学习下载,适合用于课程设计、毕业设计或实际地面站项目的起步。

1. WPF地面站:毕设之外,这套代码能让你少走半年弯路

如果你是带着"无人机地面站"这个题目在找代码参考,大概率见过两种东西:一种是Matlab画几个仪表盘糊弄答辩,另一种是WinForm连个串口就敢叫地面站。这个UAV_WPF项目不太一样——它用WPF做界面层、C#做逻辑层、MAVLink协议对接飞控,工程目录里留着DesignTimeResolveAssemblyReferences.cache、MarkupCompile.cache这类编译缓存文件,说明它是从Visual Studio里实实在在编译运行过的工程,不是贴出来截图的空壳。它解决的核心问题是三件事:飞行参数实时监控、航点任务规划下发、飞行日志采集与回放。适合正在做毕设的学生,也适合刚入职想快速摸清无人机上位机开发套路的工程师。读完这篇文章,你能知道这套代码怎么分层、串口和MAVLink数据链路怎么打通、换台机器编译翻车了去哪排查。

2. 架构拆解:WPF + MVVM + MAVLink 的地面站骨架怎么立起来

2.1 为什么是WPF而不是WinForm:地面站界面复杂度决定的选型

先给结论:地面站这种软件,界面复杂度决定了用WPF比WinForm合适得多。做无人机地面站,界面上至少要同时放飞行姿态仪表盘、GPS地图区域、串口连接状态条、航点任务列表、日志输出面板。WinForm做这个不是不行,但WinForm的布局是绝对坐标加固定大小控件,窗口一缩放或者屏幕分辨率一变,整个界面就乱了。WPF的Grid、DockPanel、ItemsControl配合数据绑定,能做到"数据一变,界面自动跟着变"。很多WPF面试题第一题就会问WPF和WinForm的区别,标准答案是渲染机制、模板样式和绑定机制,放到地面站场景里这个区别更具体:飞控每秒吐10到20帧MAVLink消息,每帧带姿态角、GPS坐标、电压电流十几个字段,WinForm需要手动给TextBox逐个赋值,WPF只要把ViewModel属性一绑,数据到位界面自动刷新。这个差异在写实时监控模块的时候会非常明显。

选型上还有一层考虑:WPF的样式系统可以做到白天夜间两套主题一键切换,状态卡片、仪表盘这些视觉元素能用Style和Template统一管理,不用像WinForm那样每个控件单独设置颜色字体。对无人机地面站这种需要长时间盯着屏幕的软件,界面观感和信息密度直接影响操作效率。而且WPF生态里HandyControl、MaterialDesignInXaml这类开源控件库非常成熟,仪表盘、进度条、卡片样式都能直接套用,省掉大量手写XAML的苦力活。

2.2 项目分层:View / ViewModel / Model / Service 四层职责划分

很多毕设代码的共性问题是全部逻辑堆在MainWindow.xaml.cs里,串口接收、数据解析、界面刷新全在一个文件,窗体后台两三千行代码,能跑但每次改需求都胆战心惊,改一个变量不知道会影响哪条逻辑链。这个UAV_WPF项目按MVVM模式拆成四层:Model层放无人机状态实体,比如FlightStatus包含横滚角、俯仰角、航向角、高度、GPS经纬度这些飞控发来的原始数据映射;ViewModel层放界面状态和命令,比如连接按钮的可用状态、航点列表、当前选中任务项;View层是XAML窗口,只负责呈现ViewModel暴露出来的属性,不写业务逻辑;Service层放串口通信、MAVLink解析、数据库读写这些跟界面无关的底层操作。一个可参考的目录结构长这样:

UAV_WPF/ ├── App.xaml ├── MainWindow.xaml ├── Models/ │ ├── FlightStatus.cs │ ├── Waypoint.cs │ └── LogRecord.cs ├── ViewModels/ │ ├── ViewModelBase.cs │ ├── MainViewModel.cs │ └── FlightViewModel.cs ├── Services/ │ ├── MavlinkService.cs │ ├── SerialPortService.cs │ └── DatabaseService.cs └── Views/ └── FlightDashboard.xaml

这个结构是地面站工程最常用的组织方式。Models放数据实体,ViewModels放绑定属性和命令,Services放协议和硬件操作,Views放UserControl形式的子页面,MainWindow用ContentControl按功能切换页面,避免一个窗口堆太多控件导致XAML几千行。这样拆的好处是,串口服务只关心收发字节,MAVLink服务只关心协议解析,ViewModel只关心怎么把解析结果暴露给界面,每层职责边界干净。实际开发时有一个很实际的收益:排查问题不用整个工程翻,串口没数据直接定位SerialPortService,协议解析不对只看MavlinkService,界面不刷新只查ViewModel的属性通知。

2.3 MVVM落地的三个关键类:ViewModelBase、RelayCommand、消息总线

搭建MVVM骨架,绕不开三个基础类。ViewModelBase继承INotifyPropertyChanged,实现属性变更通知;RelayCommand实现ICommand接口,把按钮点击事件转成命令绑定;再加一个几十行的消息总线,用来在ViewModel之间传递"串口已连接""解析出错"这类事件。ViewModelBase的典型实现:

public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!Equals(field, value)) { field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }

SetProperty方法建议直接用CallerMemberName自动获取调用属性名,调用处不用手写字符串,重构时改了属性名也不会漏掉绑定通知。RelayCommand则负责把界面上"连接串口""断开连接""上传任务"这些按钮事件转成ViewModel里的方法,CommandParameter可以带上参数区分具体动作,一个命令方法处理多个相似操作。消息总线不需要上Prism那么重的框架,自己写一个静态事件容器就够:串口服务收到一帧完整MAVLink消息后广播出去,FlightViewModel订阅事件并更新FlightStatus属性,界面上的仪表盘和数字自动刷新。这一套下来,MainWindow.xaml.cs里的代码量能压缩到几十行,只做窗口初始化和ViewModel装配。

3. 把飞行数据搬到界面上:实时监控模块的实现路径

3.1 MAVLink协议接入:串口参数、消息帧与心跳检测

MAVLink是无人机领域最常用的通信协议,Pixhawk、APM这类开源飞控都原生支持。地面站通过串口或者UDP/TCP跟飞控连接,飞控以固定频率向外广播消息帧。连地面站的第一步是串口参数设置,这里最容易踩坑的就是波特率不匹配,表现是地面站完全收不到数据或者收到一堆乱码。飞控常见的波特率是57600和115200,具体看飞控固件配置,Pixhawk默认一般是57600,部分数传模块会改成115200。串口初始化常用做法:

public bool Connect(string portName, int baudRate = 57600) { try { _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _serialPort.Handshake = Handshake.None; _serialPort.ReadTimeout = 1000; _serialPort.DataReceived += OnDataReceived; _serialPort.Open(); return _serialPort.IsOpen; } catch (UnauthorizedAccessException) { // 端口被其他程序占用,常见场景是Mission Planner没关干净 return false; } }

串口参数里数据位8位、停止位1位、无校验是绝大多数飞控数传的默认配置,Handshake必须设为None,飞控串口不做硬件流控。连接成功不等于链路通了,连接后第一件事是发HEARTBEAT心跳并统计回包,2秒内没收到任何有效心跳,直接提示用户检查波特率和接线。很多新手在串口助手能收到数据就认为协议层没问题,实际接进地面站才发现解析器根本识别不了帧头,这类问题在5.4里单独展开。

3.2 从字节流到飞行状态:消息解析与字段映射

串口DataReceived事件每次提供的是一段不定长的字节流,可能半帧、一帧带半帧,也可能好几帧黏在一起。合格的解析器必须做帧同步:按MAVLink格式找帧头0xFE(MAVLink 1.0)或0xFD(2.0),读长度字段,等够一帧完整字节后做CRC校验,校验通过才把整帧交给上层的业务解析。MAVLink 1.0帧格式解析的核心逻辑:

private int _bufferOffset = 0; private byte[] _rxBuffer = new byte[4096]; public MavlinkFrame TryParseFrame(byte[] data, int count) { for (int i = 0; i < count; i++) { _rxBuffer[_bufferOffset++] = data[i]; // 帧头同步:0xFE表示MAVLink 1.0帧开始 if (_rxBuffer[0] != 0xFE) { _bufferOffset = 0; continue; } // 长度字段在第2个字节,需要凑齐 payloadLen + 帧头1 + 长度1 + 序号1 + 系统ID1 + 组件ID1 + 消息ID1 + CRC2 if (_bufferOffset >= 2) { int payloadLen = _rxBuffer[1]; int totalLen = payloadLen + 8; if (_bufferOffset >= totalLen) { if (CheckCrc(_rxBuffer, totalLen)) { MavlinkFrame frame = DecodeFrame(_rxBuffer, totalLen); Array.Copy(_rxBuffer, totalLen, _rxBuffer, 0, _bufferOffset - totalLen); _bufferOffset -= totalLen; return frame; } _bufferOffset = 0; } } } return null; }

这段代码体现串口解析的通用思路:单字节往缓冲区填,找不到0xFE就重置,找到帧头后根据长度字段判断还需要多少字节,凑齐整帧做CRC校验。细节注意两处:帧结构里MAVLink 1.0的消息ID只有1字节,取值范围0到255,2.0版本扩展为3字节,如果要做协议层最好两套帧格式都支持,因为飞控固件版本不同,实际跑起来的协议版本也不同;解析器必须保证一次调用只处理一次数据到达,剩余字节留在缓冲区里拼接下一条消息。

3.3 界面刷新不卡顿:Dispatcher与属性绑定的性能取舍

实时数据绑定跑起来后,最容易翻车的是"界面刷不过来"。飞控每秒往外发10到20帧数据,每帧解析完都去更新UI属性,WPF的UI线程会被高频绑定通知淹没,表现是界面卡顿、鼠标发飘,严重时窗口直接白屏无响应。原因在于串口的DataReceived事件在后台线程触发,直接修改ViewModel属性会抛跨线程异常,常规做法是借助Dispatcher把UI更新调度到UI线程:

Application.Current.Dispatcher.BeginInvoke(new Action(() => { FlightStatusViewModel.UpdateAttitude( roll: msg.Roll, pitch: msg.Pitch, yaw: msg.Yaw ); }), DispatcherPriority.Background);

DispatcherPriority.Background是个容易被忽略但很实用的参数。数据刷新不需要最高优先级,用Background级别让UI线程优先处理鼠标拖动、按钮点击这些交互事件,再处理数据刷新,观感上比Normal级别顺滑很多。另一个习惯是合并UI更新,不要每帧都触发绑定,而是设置变化阈值,比如航向角变化小于0.1度就不刷新。界面元素上,姿态仪表盘可以用WPF的Polygon旋转模拟,也可以用HandyControl这类控件库现成的仪表盘控件,状态卡片配FontAwesome.Sharp图标库,比手画矢量图形省力气。日志输出区域用ItemsControl绑定消息列表,比反复清空TextBox再Append性能好很多,长时间运行也不会有内存越攒越大的问题。

4. 任务规划与数据落库:地图交互和SQLite存储的设计取舍

4.1 航点任务的数据结构:从地面站到飞控的指令序列

实时监控跑通之后,第二个核心功能是任务规划。最常见的形态是用户在界面上点几个航点,设置每个航点的高度和动作,地面站把任务包下发到飞控,飞控按顺序执行。航点任务在MAVLink体系里用MISSION_COUNT、MISSION_ITEM系列消息完成,地面站需要维护有序的航点列表,逐个发送并等待飞控确认。航点模型字段设计:

public class Waypoint { public int Seq { get; set; } public float Lat { get; set; } // 纬度,单位度 public float Lng { get; set; } // 经度,单位度 public float Alt { get; set; } // 相对起飞点高度,单位米 public WaypointAction Action { get; set; } // 到达后的动作:悬停、拍照、降落 public float Param1 { get; set; } // 动作参数,比如悬停时长(秒) }

航点下发有个顺序问题,第一次写容易踩:不能把整个列表一次性发给飞控。MAVLink的传输机制是先发MISSION_COUNT告诉飞控"我要传10个航点",飞控回MISSION_REQUEST请求第0号航点,地面站收到请求才发第0号航点,再等飞控请求第1号航点,如此一问一答往复。这个机制是为了防止串口丢包导致任务中断,代码里必须实现成状态机,不能简单for循环往下发。实际调试时经常出现发送到一半飞控不回包的情况,此时需要加超时重发逻辑,重发3次仍无响应就中止上传并提示用户检查链路。

4.2 地图与航点绘制的两层方案:嵌入地图控件与自绘坐标系

地图显示是地面站里工作量和坑最多的模块。业界常见做法分两层:嵌入现成地图控件,或者自绘坐标系。嵌入方案在.NET生态里最常用的是GMap.NET,支持离线瓦片和在线瓦片,缩放平移都成熟;缺点是瓦片源授权问题,不同服务商对桌面程序访问的限制策略不一样,有的需要API Key,有的对请求频率有限制。自绘方案是另一个极端:不引入地图控件,用一张静态底图,把用户点击的屏幕坐标换算成经纬度:

public Waypoint OnMapClick(Point screenPoint) { double lat = _mapTopLeftLat + (screenPoint.Y / _mapHeight) * _mapLatSpan; double lng = _mapTopLeftLng + (screenPoint.X / _mapWidth) * _mapLngSpan; return new Waypoint { Lat = lat, Lng = lng, Alt = 50 }; }

线性换算只适合底图范围很小的场景,比如固定区域的航拍任务。涉及大范围飞行就必须考虑墨卡托投影换算,线性映射会有明显误差。对毕设来说,先自绘一个可点击的网格坐标系,把航点管理的交互流程跑通,比花一周时间去折腾地图瓦片授权更划算。注意地图切换和窗口尺寸变化时要重新计算底图范围,否则会出现航点绘制偏移。地图区域建议单独封装成UserControl,后续要换GMap.NET只改这一个控件,不影响上层航点逻辑。

4.3 SQLite存储:飞行日志与配置文件,不是说上就上

很多人在功能没跑通时就急着给地面站上MySQL,这个选择要掂量一下。飞机飞完一次,飞行日志也就是几十MB的文本量级,CSV也能装下。SQLite的好处是单文件、不需要安装服务、C#用Microsoft.Data.Sqlite就能连,适合"地面站自己产生的数据自己管"的场景。数据库接入的常用做法:

CREATE TABLE IF NOT EXISTS flight_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, flight_id TEXT, timestamp INTEGER, roll REAL, pitch REAL, yaw REAL, gps_lat REAL, gps_lng REAL, altitude REAL, voltage REAL ); CREATE INDEX idx_flight_timestamp ON flight_log(flight_id, timestamp);

建表时给flight_id和timestamp建联合索引是关键一步。飞行日志按flight_id分组、按timestamp排序查询,没有索引,上万条数据查起来会有肉眼可见的延迟。C#端插入建议用参数化SQL批量提交,每攒够50帧或者500毫秒统一写入一次,避免每帧开一次事务导致磁盘IO吃紧。日志回放界面用DataGrid绑定大量行数据时性能会明显下滑,超过几百行滚动就会卡顿,常用技巧是开启VirtualizingStackPanel虚拟化,或者把日志查询做成按时间段分页加载。DataGrid行显示异常时优先检查列宽和RowHeight设置,直接赋固定行高能省掉不少兼容性问题。

报表导出这块提一句:想用RDLC ReportViewer做复杂格式报表,先确认报表结构是不是常规的分组汇总,地面站日志这类带自定义字段的长表格,RDLC做起来成本挺高,直接DataGrid导出CSV或生成Excel更省事。想跨平台用.NET MAUI重写这套界面的话,WPF的XAML不能直接复用,但数据绑定和MVVM的模型思维可以平移过去,MAVLink服务层本来就是纯C#代码,迁移成本集中在UI层,这也侧面反映分层设计对后续演进的价值。

5. 避坑与排查:跑通WPF地面站的5个高频翻车点

5.1 XAML编译缓存报错:DesignTimeResolveAssemblyReferences.cache导致的假崩溃

现象:从网上下载的WPF工程,双击.sln打开后编译随机报某个XAML文件找不到资源,或者提示DesignTimeResolveAssemblyReferences.cache文件被占用,清理解决方案重新生成还是报错。

原因:工程目录里遗留的DesignTimeResolveAssemblyReferences.cache、MarkupCompile.cache、GenerateResource.cache这批文件,是Visual Studio编译期和设计器预览阶段生成的缓存。不同版本的VS(2017/2019/2022)生成的缓存格式不完全兼容,换台机器后旧缓存会让编译器误判引用状态,产生各种"这台机器能编译、换台机器就废"的假报错。很多WPF毕设代码传到网上都带着这类缓存,下载后第一件事不是打开工程,而是先把它们清掉。

解决:打开工程根目录,按扩展名筛选出所有.cache文件,包括DesignTimeResolveAssemblyReferencesInput.cache、CoreCompileInputs.cache,全部删除,再重新生成解决方案。顺手把bin和obj目录也删掉,这两个目录里同样存着旧编译产物。这个操作能解决大半"下载的WPF工程编译不过"的问题,值得养成习惯。

5.2 Dispatcher跨线程访问界面:后台线程改UI属性引发的崩溃

现象:串口一连接,界面直接崩溃,异常信息提示"调用线程无法访问此对象,因为另一个线程拥有该对象"。从WinForm转过来的人觉得用Invoke就能解决,照搬到WPF偶尔还是崩。

原因:串口DataReceived事件在后台线程触发,直接给XAML里的TextBlock或自定义控件赋值,违反了WPF的线程模型。UI元素只能在UI线程操作,后台线程更新必须通过Dispatcher调度。WinForm的Control.Invoke和WPF的Dispatcher.Invoke用法相似但细节不同,最常见的手误是把DispatcherPriority参数漏掉或者用成了Normal,导致高频刷新时UI线程被数据更新占满,界面表现为假死。

解决:所有UI属性更新统一走Dispatcher.BeginInvoke,优先级固定用Background。写一个UpdateUi(Action action)的封装方法放在ViewModelBase里,Service层调用时不直接接触Dispatcher,减少样板代码的同时也避免遗漏调度。测试时用数据发生器定时往串口服务喂假数据,让界面稳定运行几分钟不崩,线程问题才算过。这里有个实用技巧:串口服务和界面层之间加一层队列,后台线程只往队列里塞数据,UI线程按固定间隔从队列取数据刷新,削峰效果比单纯依赖Dispatcher好很多。

5.3 串口第一帧乱码:波特率和数据位不匹配的排查顺序

现象:地面站连上飞控,数据窗口偶尔能识别出几条消息,大多数时间是乱码,或者完全没有数据。飞控端用Mission Planner连得好好的,换到自研地面站就废。

原因:波特率不匹配是第一嫌疑,飞控端和地面站端任何一边设置不一致,收进来的就是错位字节流。第二嫌疑是数据位停止位校验位设置不对,常见是8数据位1停止位无校验,个别数传模块默认加了校验位。第三是USB转串口芯片驱动问题,CH340、CP2102这类芯片的驱动没装好,串口号在设备管理器里都看不到,软件层面自然连不上。

解决:先用串口助手排除硬件和驱动问题,打开设备管理器确认串口号,用串口助手按57600波特率连接飞控观察是否有可读输出;再切到软件侧,把初始化时实际传入的串口参数打印到日志窗口,确认和界面下拉框里选的一致。很多"第一帧正常、后面全乱"的场景是USB转串口芯片在长时间高频率收发后缓冲溢出,串口服务里要做读缓冲区的循环读取,读到多少处理多少,不要依赖单次DataReceived事件的数据完整性。

5.4 MAVLink CRC校验失败:只做帧头同步不做校验的解析盲区

现象:能收到消息,大部分字段能解析出来,但偶尔姿态数据显示为0或明显异常,回放日志里能翻出一些半截帧。重新连接后又恢复正常,看起来像偶发故障。

原因:解析逻辑只做了帧头同步没做CRC校验,错位字节里恰好出现0xFE就会误判为新帧头,后续解析全乱。还有一种情况是MAVLink 1.0和2.0的CRC算法混用,两个版本的CRC种子不同,校验必然失败。串口的电磁干扰在电机启动瞬间会加剧,丢帧和错位不可避免,解析器必须把CRC当作第一道防线。

解决:帧解析严格走"找帧头→读长度→凑齐整帧→CRC校验→上抛业务层"五步,不放宽容忍度。CRC校验失败的帧直接丢弃,不进入业务解析。每累计100次连续校验失败,在日志输出一次"链路数据异常"报警,辅助定位是线缆屏蔽问题还是波特率误差偏大。日志窗口对校验失败帧单开一个计数器显示,排查时直接看这个数字是否持续增长,比肉眼盯数据输出效率高很多。

5.5 地图控件黑屏或空白:瓦片源访问策略与坐标系初始化

现象:嵌入的地图控件在开发机上正常显示瓦片,打包到另一台电脑运行变成全黑或者灰色空白网格,也没有报错弹窗。航点绘制功能在开发机正常,换机器后坐标错乱。

原因:一类是瓦片源访问策略限制,不少在线瓦片服务不允许桌面程序直接批量访问,服务商会按UserAgent和请求频次做风控,IP被临时限制后就拉不到瓦片。另一类是坐标系未初始化,控件加载时没有设置初始经纬度和缩放级别,地图控件没有基准点,瓦片自然无法定位。

解决:离线瓦片方案把常用缩放级别(12到18级)的瓦片预先缓存到本地,加载时拦截网络请求读本地文件。在线方案换用明确允许桌面程序访问的瓦片源,并控制请求频率。初始化时固定先设置中心点经纬度和缩放级别,再加载瓦片。地图上的航点绘制,用控件提供的屏幕坐标和经纬度互转方法,不要自己写线性映射,除非底图范围固定且做过严格校准。出现黑屏时先打开瓦片请求日志,确认是网络层被拒还是本地缓存没命中,再对症处理。

6. 仿真联动验证:不飞真机也能完整跑一遍地面站流程

拿到这份代码,别急着往飞控上接,先跑一遍仿真全流程。ArduPilot提供软件在环(SITL)模拟环境,可以在一台Windows电脑上模拟完整的飞控行为,地面站通过UDP或虚拟串口跟它通信,效果和真机几乎一样。先把SITL启动起来:

sim_vehicle.py -v ArduCopter --map --console

之后流程是:在地面站的串口设置里选UDP模式,连接SITL监听的端口;观察连接状态是否变为绿色,心跳计数器是否持续增长;切换到实时监控页,确认姿态角、高度、电池电压这些字段有数值变动;进入任务规划页,在地图上点几个航点,设置高度50米,上传任务,切到飞行日志页确认航点下发记录完整落库。这套流程跑通了,再换真机,只需要把通信方式从UDP改回串口,其他逻辑不用动。

验证时给自己列一个检查清单:心跳2秒内建立,姿态数据刷新延迟低于500毫秒,航点上传和飞控请求顺序一致,日志写入量跟飞行时间对得上,界面长时间运行内存不持续增长。仿真环境最大的价值是能故意制造异常——把链路断开再重连,看地面站是否自动恢复;在飞行中突然拔掉模拟串口,看日志是否记录断连事件。我在第一次接真机前就是靠这套仿真流程发现了消息重发机制的问题,避免了一次实飞丢任务的尴尬。从那以后,我每次拿到一套地面站代码,都会强制自己先跑一遍仿真全流程,再考虑碰硬件。这份UAV_WPF代码也一样,先删掉缓存文件打开工程,跑通仿真链路,再带着对数据流的理解去看每个类,你会比直接连飞控快得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询