ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java实现MAVLink协议解析与封包上传实战指南

Java实现MAVLink协议解析与封包上传实战指南 做无人机飞控调试、写地面站、或者让后台Java服务去读取Pixhawk/ArduPilot的遥测数据这些事情绕不开同一个协议MAVLink。MAVLink是一套轻量级的二进制通信协议航模圈里几乎所有主流飞控都认它飞控、数传、地面站、机载电脑之间就是靠这一帧一帧的二进制数据说话。本文就用Java完整实现一遍MAVLink协议解析与封包上传说清楚一帧数据是怎么从串口/网络字节流里被“抠”出来、校验通过、转成业务对象以及反向怎么把一条指令、一个航点封好包发出去。这个需求我在好几个项目里都撞上过一个是给测绘无人机写配套地面站另一个是给农业植保机做航线管理后台都需要Java侧直接和飞控交互。做完之后我才发现MAVLink的资料虽然多但大部分是C语言和Python的Java实现要么太零散要么直接依赖大而全的mavlink-java库出问题时根本不知道底层在做什么。这篇文章把完整实现拆开讲代码可以直接抄适合两种人一种是刚接触MAVLink、想搞懂协议细节的Java开发另一种是项目里只用核心功能、不想引一堆冗余依赖的朋友。先说明我以MAVLink 1.0为主讲解2.0的差异会在关键位置点出来。1. MAVLink协议设计与Java实现思路1.1 先搞懂一帧MAVLink数据长什么样MAVLink的帧结构非常紧凑这也是它在低带宽数传链路上能存活的原因。MAVLink 1.0一帧数据从起始符到CRC结束总长度是8加负载长度公式是LEN 8字节其中负载部分最长为255字节。固定头一共6字节然后是可变长的payload最后是2字节校验码。字段顺序是固定的STX标记、LEN负载长度、SEQ发送序号、SYSID系统编号、COMPID组件编号、MSGID消息编号。我用一个真实场景解释这串字段。假设一台Pixhawk飞控定时给地面站发心跳包那么系统编号SYSID通常是1代表飞行器自身组件编号COMPID也会是1代表飞控主控地面站这边通常用SYSID255COMPID190。SEQ是发送方自己维护的序号从0开始每发一条消息加1最多加到255再回到0用来检测丢包。LEN告诉你后面跟着的payload具体多少字节注意它不包含两个CRC字节。MSGID则是一条消息的“身份证号”比如0代表HEARTBEAT心跳、33代表GLOBAL_POSITION_INT全球定位、76代表COMMAND_LONG长指令。MAVLink 2.0的帧头会长一些STX从0xFE变成了0xFD并且多了incompat_flags、compat_flags两个标志字节MSGID从1字节扩展成3字节还支持可选的签名区。但解析和封包的思路完全一致先找到STX读出长度再按长度切payload。掌握了1.0这套基本功升到2.0只是多几个字段的事。1.2 CRC_EXTRA最容易踩坑的设计MAVLink的校验不是简单地对整帧做CRC16它在计算前必须把该消息ID对应的CRC_EXTRA先喂进去。这个CRC_EXTRA是什么它是由消息定义XML里的字段类型、字段顺序通过算法生成的一个固定字节。换句话说每条消息都有一个专属的“种子”。比如HEARTBEAT消息ID为0它对应的CRC_EXTRA是50COMMAND_LONG消息ID为76对应152MISSION_COUNT消息ID为44对应221。这些值都写在官方common.xml里或者由mavlink生成器自动算出来。为什么这么设计官方解释是为了防止不同类型的消息在payload恰好一样时出现碰撞本质上是给协议加了一层“身份证核验”。但这种设计也坑了不少初学者你拿着通用CRC16算法去算校验永远不过因为没有先update这个CRC_EXTRA字节。我在实际项目中见过有人为了绕过CRC问题直接关校验这个习惯很危险——数传链路本身就可能丢包错位关了CRC会导致飞控执行到错误指令。CRC计算的范围需要精确理解从LEN字段开始经过SEQ、SYSID、COMPID、MSGID再到整个payload最后还要先更新一次CRC_EXTRA。注意STX起始符本身不参与CRC计算两个CRC字节自然也不参与。CRC本身是两个字节低字节在前、高字节在后这个字节序问题也让很多人栽了跟头。1.3 Java解析器的整体模块划分我实现Java端MAVLink交互时把代码分成了几个独立模块串口或网络接收层、帧切割器、CRC校验器、payload编解码器、业务处理层。这样做的好处是每一层都能单独测试。串口层只负责往一个字节缓冲区里丢数据帧切割器负责从缓冲区里找STX、切出完整一帧校验器验证这一帧的CRC业务层再去解析payload。上行数据链路则是反向的业务层构造payload封包时计算CRC最后交到串口或网络层发送。很多新手会犯的错是把接收逻辑和解析逻辑写在一起readBytes读一次就直接当一帧处理。但底层链路是不可能保证“读一次就是一帧”的串口可能一次收到半帧也可能一次收到好几帧粘在一起这就是粘包/半包问题。所以模块化第一步就是做一个可靠的帧切割状态机这也是本文第2节要重点展开的内容。只有把“从字节流中提取完整帧”这一步做扎实后面的解析才不用反复回溯调试。2. 解析一帧MAVLink数据从字节流到结构化对象2.1 先解决粘包从串口字节流里精确裁出完整帧先写一个帧切割的通用逻辑。假设我们从串口读到了一个字节数组内部维护一个ByteArrayOutputStream做累积缓冲。每次有新的字节进来就把它追加进缓冲区然后尝试从缓冲区头部开始解析。解析的第一步是检查第一个字节是不是0xFE不是就直接丢弃这个字节继续找是的话再检查缓冲区长度是否已经达到了LEN 8没达到就继续等达到了就切出这一整帧交给下一步。这段逻辑用代码写出来非常直白private final ByteArrayOutputStream buffer new ByteArrayOutputStream(); public void onBytesReceived(byte[] data, int len) { buffer.write(data, 0, len); processBuffer(); } private void processBuffer() { byte[] buf buffer.toByteArray(); int offset 0; while (offset buf.length) { if ((buf[offset] 0xFF) ! 0xFE) { offset; continue; } if (offset 1 buf.length) break; int frameLen (buf[offset 1] 0xFF) 8; if (offset frameLen buf.length) break; byte[] frame Arrays.copyOfRange(buf, offset, offset frameLen); handleFrame(frame); offset frameLen; } if (offset 0) { byte[] remaining Arrays.copyOfRange(buf, offset, buf.length); buffer.reset(); buffer.write(remaining, 0, remaining.length); } }这段逻辑里有一个非常关键的设计细节帧长度是由第一个STX后面的LEN字段决定的不是固定值。所以切割器不能按固定长度去切必须先读LEN再判断当前缓冲区够不够一整帧。如果缓冲区里是半帧就什么都不做直接返回等下一次串口数据到达后再继续累积。如果缓冲区里混着几帧这个循环会一帧一帧地消费掉直到剩下不足一帧为止。在实际调试中我用真实的Pixhawk遥测日志验证过这个切割器。它每秒会输出几十条消息其中有些消息payload只有几字节有些比如GPS_RAW_INT有30字节混在一起传输时经常出现前一条消息的后半段和后一条消息的前半段同时到达。用了这个“先找STX、再按LEN等齐”的策略之后切帧成功率是100%。你如果不用这种机制而是直接按固定长度或按read返回值切大概率会收到一堆错位的垃圾数据。2.2 CRC校验为什么总失败X25算法与CRC_EXTRA的关系帧已经切出来了但它是不是一条合法完整的MAVLink消息还得过CRC这一关。MAVLink用的CRC算法不是Java标准库里那个CRC32而是一种基于CRC-16/X25的变体初始值是0xFFFF多项式是0x1021。但它和标准X25有个区别不做最终异或并且要先对CRC_EXTRA做一次更新。很多网上的代码直接套用标准X25实现导致怎么算都对不上原因就在这里。我直接给出一个与官方C库完全一致的Java实现这是从mavlink-java项目里提炼出来的核心逻辑我在多个固件版本上验证过public class MavlinkCRC { private int crc 0xFFFF; public void update(int data) { int tmp (data 0xFF) ^ (crc 0xFF); tmp ^ (tmp 4) 0xFF; int tmp2 ((tmp 3) 0xFF) | ((tmp 5) 0xFF); crc ((crc 8) 0xFF) ^ tmp2 ^ ((tmp2 4) 0xFF00) ^ ((tmp 3) 0xFF00); crc 0xFFFF; } public void update(byte[] data, int offset, int len) { for (int i offset; i offset len; i) { update(data[i] 0xFF); } } public int getCrc() { return crc; } }用这个类校验一帧MAVLink 1.0消息的完整步骤是先创建一个MavlinkCRC实例然后按顺序update以下内容CRC_EXTRA对应字节、LEN、SEQ、SYSID、COMPID、MSGID、从第6个字节开始的整个payload。计算完成后得到一个16位整数它的低8位和高8位应该分别等于帧尾两个CRC字节。例如收到的帧尾是0x37 0x62那么计算得到的crc值就应该是0x6237也就是低位0x37、高位0x62。这里有个容易搞混的点很多人在代码里把CRC_EXTRA当成byte类型直接传入update方法。如果把负数byte传进去由于Java里有符号数转换的问题计算结果会跟C语言不一致。所以传参时一律用byteValue 0xFF转成0到255的整数。CRC_EXTRA我一般放在一个静态数组里映射关系按MSGID索引写起来简洁也方便查表。实际项目中如果用到大量消息建议直接从common.xml用代码生成器生成这张表手写很容易抄错。2.3 Payload转对象Little-Endian字节序与类型映射校验通过后payload才是可信的。MAVLink协议规定所有多字节数字类型都是小端字节序也就是低字节在前。这一点和Java ByteBuffer默认的大端模式相反所以不能直接拿ByteBuffer一套了之要么把ByteBuffer设为LITTLE_ENDIAN要么干脆手写读取方法。考虑到性能以及代码可读性我习惯手写一组工具方法。以一个很常见的GLOBAL_POSITION_INT消息为例它的MSGID是33payload固定28字节标准定义是这样的time_boot_ms占用4字节uint32lat占用4字节int32lon占用4字节int32alt占用4字节int32relative_alt占用4字节int32vx、vy、vz各占用2字节int16hdg占用2字节uint16。顺序不能乱。我写了一个通用方法把payload塞进一个Map或者直接塞进一个自定义Java对象public static int getUint8(byte[] p, int o) { return p[o] 0xFF; } public static int getUint16(byte[] p, int o) { return (p[o] 0xFF) | ((p[o 1] 0xFF) 8); } public static int getInt16(byte[] p, int o) { return (short) ((p[o] 0xFF) | (p[o 1] 8)); } public static long getUint32(byte[] p, int o) { return (p[o] 0xFFL) | ((p[o 1] 0xFFL) 8) | ((p[o 2] 0xFFL) 16) | ((p[o 3] 0xFFL) 24); } public static int getInt32(byte[] p, int o) { return (p[o] 0xFF) | ((p[o 1] 0xFF) 8) | ((p[o 2] 0xFF) 16) | ((p[o 3] 0xFF) 24); }解析GLOBAL_POSITION_INT时按照字段偏移连续读取就行。需要注意的是lat和lon单位是1e7度也就是说返回的整数值需要除以10000000才是实际经纬度。比如lat返回637000000那实际纬度就是63.7度。很多没接触过MAVLink的开发者直接拿这个整数去算距离最后出来的结果差了十万八千里。alt和relative_alt单位是毫米也需要换算成米。在这条消息里vx/vy/vz是地速分量单位是cm/s。读出来的数据记得换算后再用于业务判断。我通常会在解析器里建一个switch分支按MSGID分发到不同的解析方法然后把结果push进一个事件订阅器业务模块只需要关注感兴趣的消息。比如地面站要显示航向订阅GLOBAL_POSITION_INT要显示飞行模式订阅HEARTBEAT。这种发布订阅模式让代码结构很干净后面新增消息只需要加一个case和对应解析方法。3. 封包上传从结构化消息到字节流3.1 封包通用流程构造Payload、计算CRC、拼接成帧上行链路就是下行链路的逆过程。要发一条消息你先构造出payload再把LEN、SEQ、SYSID、COMPID、MSGID这些头部字段依次填好然后计算CRC最后把整帧写进发送缓冲区。封包的通用方法我封装成了一个静态工具public static byte[] buildFrame(int seq, int sysid, int compid, int msgid, byte[] payload, int crcExtra) { int len payload.length; byte[] frame new byte[len 8]; frame[0] (byte) 0xFE; frame[1] (byte) len; frame[2] (byte) seq; frame[3] (byte) sysid; frame[4] (byte) compid; frame[5] (byte) msgid; System.arraycopy(payload, 0, frame, 6, len); MavlinkCRC crcObj new MavlinkCRC(); crcObj.update(crcExtra 0xFF); crcObj.update(frame, 1, len 5); int crc crcObj.getCrc(); frame[len 6] (byte) (crc 0xFF); frame[len 7] (byte) ((crc 8) 0xFF); return frame; }拼帧的时候我踩过一个很隐蔽的坑CRC计算时要更新的范围是“从帧索引1开始长度是len5”这刚好覆盖了LEN到payload末尾全部字节。如果你手滑把更新起点从0开始把STX也算进去飞控就再也收不到这条消息了。因为STX不参与CRC是协议硬性规定任何实现都必须遵守。另外发送序号SEQ每个发送方自己维护收到一个发送一个循环使用即可不要从0重复发太多否则接收端难以检测丢包。封包里写入float类型我专门写了putFloat方法先把浮点转成IEEE 754的int位模式再按小端字节序拆成4个字节。注意不要直接用ByteBuffer.putFloat因为默认是大端除非显式指定LITTLE_ENDIAN。我建议在基础设施层统一处理好所有基本类型的字节序这样可以避免业务代码到处处理。3.2 封一条心跳消息HEARTBEAT完整Java代码心跳消息是MAVLink体系里最基础、也最重要的消息地面站和飞控之间靠它互相确认“我还活着”。ArduPilot等飞控默认要求地面站周期性发送心跳否则会认为链接已断开很多指令不会响应。心跳消息MSGID为0payload固定9字节CRC_EXTRA是50。它的payload结构是type占用1字节代表飞行器类型比如四旋翼是2autopilot占用1字节代表飞控固件类型ArduPilot是3base_mode占用1字节是模式标志位custom_mode占用4字节uint32是具体飞行模式编号system_status占用1字节代表系统状态正常工作状态是4mavlink_version占用1字节固定为3。public static byte[] buildHeartbeat(int seq, int sysid, int compid) { byte[] payload new byte[9]; payload[0] 2; // MAV_TYPE_QUADROTOR payload[1] 3; // MAV_AUTOPILOT_ARDUPILOTMEGA payload[2] (byte) 0x81; // base_mode: 已解锁自定义模式启用 payload[3] 0; payload[4] 0; payload[5] 0; payload[6] 0; // custom_mode 0 payload[7] 4; // MAV_STATE_STANDBY payload[8] 3; // mavlink_version return buildFrame(seq, sysid, compid, 0, payload, 50); }如果你要我给一个最容易验证整条链路的测试场景那就是让Java程序通过串口连接飞控数传然后每秒发一条HEARTBEAT。用飞控地面站软件连接同一个数传在无线电状态里能看到来自你系统的ID说明封包和发送通了。我第一次做这个测试时怎么发飞控都没反应后来用逻辑分析仪抓串口发现CRC算错了折腾了半天。后来我总结了一个经验写好封包代码后先别急着连飞控用一个能回显串口数据的串口工具把发出去的帧抓下来手动拆开核对每个字节是否符合帧格式再跑一个本地回环测试——把封好的帧直接送给自己的解析器去解析这样能在连真机前就发现至少80%的问题。3.3 上传指令与航点COMMAND_LONG与MISSION握手流程除了心跳地面站最常用的上行消息是指令和航点。指令用COMMAND_LONGMSGID为76payload固定33字节CRC_EXTRA是152。它的结构是7个float参数每个4字节共28字节然后command字段2字节uint16代表具体指令编号比如起飞是400开始航线是300接着target_system、target_component各1字节指定要发给谁最后confirmation 1字节设为0表示首次发送。我封装了这样一个发指令的方法public static byte[] buildCommandLong(int seq, int sysid, int compid, int targetSys, int targetComp, int command, float p1, float p2, float p3, float p4, float p5, float p6, float p7) { byte[] payload new byte[33]; int off 0; off putFloat(payload, off, p1); off putFloat(payload, off, p2); off putFloat(payload, off, p3); off putFloat(payload, off, p4); off putFloat(payload, off, p5); off putFloat(payload, off, p6); off putFloat(payload, off, p7); off putShort(payload, off, command); payload[off] (byte) targetSys; payload[off] (byte) targetComp; payload[off] 0; // confirmation return buildFrame(seq, sysid, compid, 76, payload, 152); }航点上传就比单条指令复杂一些它是一个多轮握手流程ArduPilot要求地面站和飞控之间必须严格按状态机来。基本流程是先发MISSION_COUNT告诉飞控“我准备传N个航点”飞控收到后会回复MISSION_REQUEST里面带一个seq字段表示“把第几个航点发给我”地面站根据这个seq发MISSION_ITEM_INT然后飞控再回复下一个MISSION_REQUEST直到所有航点传完最后飞控回MISSION_ACK带上MAV_MISSION_ACCEPTED表示接受成功。这个流程里没收到REQUEST绝对不能盲目继续发否则飞控会认为链路不可靠直接中止上传。MISSION_COUNT的payload是4字节count占用2字节uint16然后target_system、target_component各1字节MSGID为44CRC_EXTRA是221。MISSION_ITEM_INT稍微复杂payload有37字节第4个字段之前有4个float参数第5个字段是x坐标int32第6个字段是y坐标int32第7个字段是z坐标float再往后是seq、command、目标系统、目标组件、坐标系等字段。航点通信本身就能写一整篇这里核心是让读者理解MAVLink上行链路的握手模式请求、应答、确认缺一不可。3.4 串口连接用jSerialComm打通Java与飞控串口库我推荐jSerialComm相比老的RXTX它维护活跃、API设计也简单支持跨平台。连接飞控时最重要的一步是波特率必须和飞控数传设置一致Pixhawk的TELEM口常见配置是57600有些purpose-built数传模块可能用115200。连接代码非常短SerialPort sp SerialPort.getCommPort(/dev/ttyUSB0); // Windows下用COM7类似名称 sp.setBaudRate(57600); sp.setNumDataBits(8); sp.setParity(SerialPort.NO_PARITY); sp.setNumStopBits(SerialPort.ONE_STOP_BIT); sp.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 100, 0); sp.openPort();发送封好的帧用sp.writeBytes(frame, frame.length)。接收则开一个后台线程循环调用readBytes把读到的数据喂给帧切割器。这里要特别注意我给串口设置了TIMEOUT_READ_SEMI_BLOCKING超时时间100毫秒这样readBytes不会永久阻塞程序退出时能及时响应。我见过不少人在生产代码里用阻塞读然后关串口时线程卡死最后只能进程强杀。串口超时设置虽然是个小细节但能避免很多运维痛苦。还有一点如果你不是在本地串口而是通过网络连接数传模块其实封包解析逻辑完全一样只是收发变成TCP/UDP。MAVLink本身就是基于字节流的协议不关心底层是串口、TCP、UDP还是USB虚拟串口。所以这套解析与封包模块可以直接复用只需要替换最底层的收发实现这也是模块化带来的一个直接好处。4. 数据交互与踩坑实录4.1 收发线程模型与心跳保活MAVLink数据交互本质上是两个方向同时跑读线程负责接收飞控上报的状态和应答写线程负责发送心跳和业务指令。这里我强烈推荐用两个独立线程配合一个共享队列读线程解析完消息后put进一个BlockingQueue业务线程从队列里poll取消息写线程则维护一个待发送帧的队列。千万不要在读线程里直接处理业务因为串口数据的到达频率可能远高于业务处理速度读线程一旦被业务拖累接收缓冲区溢出会丢包。心跳保活建议单独起一个定时任务比如每秒发送一个HEARTBEAT。这个方法可以用ScheduledExecutorService实现发心跳时带上维护好的seq自增变量。在ArduPilot地面站协议里地面站心跳的SYSID一般约定是255但这不是硬性规定主要看飞控的MAVLink参数如何配置。我自己的项目里为了避开可能和其他地面站冲突的ID会把它做成配置项部署时按需调整。写线程队列里还有个优先级问题需要处理。业务指令比如COMMAND_LONG属于高优先级应该立即发送而一些循环上报的调试数据属于低优先级。我的做法是维护两个队列紧急队列和普通队列写线程先取紧急队列取不到再取普通队列。否则在高频遥测报文中一条用户点击的“起飞”指令可能排在几百条日志后面那种卡顿感在实际使用中非常明显。4.2 应答处理从COMMAND_ACK到MISSION_ACK上行消息中最常出问题的就是“发出去没反应”。其实飞控收到指令后几乎都会回一条对应的ACK消息。比如COMMAND_LONG的应答是COMMAND_ACKMSGID为77。COMMAND_ACK的payload包含4个字段command是2字节uint16表示应答哪条指令result是1字节0表示MAV_RESULT_ACCEPTED接受成功1表示暂时拒绝2表示系统不支持3表示临时失败4表示固件出错。另外还有progress和result_param2两个字段1.0里通常为0。所以在Java解析器里我单独对COMMAND_ACK做了一层订阅处理。当用户点击“起飞”按钮不是发完就完事而是进入一个“等待ACK”状态设置一个3秒超时。如果等到COMMAND_ACK并且result为0才在界面上显示指令已执行如果超时或者result非0就提示用户查看飞控状态。这套机制对排查问题是质的提升否则你根本分不清“指令没发出去”和“飞控拒绝了指令”。航点上传承接的是MISSION_ACKMSGID为47payload里有一个type字段0表示成功其他值是各种失败原因。我在实际调试时遇到过MISSION_ITEM_INT发到一半飞控一直不回MISSION_REQUEST的情况最后定位到是因为我发MISSION_COUNT时count填的和后续实际发送数量不一致导致飞控在等待过程中超时。所有涉及到“计数”的字段必须和后续动作严格一致这是航点上传最容易犯的错误。4.3 我踩过的坑MAVLink数据交互常见问题速查做完整套交互链路我把实际中高频出现的问题整理成下面这张表每一行都是真金白银换来的经验。问题现象根本原因解决方法飞控收不到任何数据CRC计算时忘了先更新CRC_EXTRA或把STX也算进了CRC严格按LEN开始计算并先更新对应MSGID的CRC_EXTRA解析出来全是乱码Java里byte有符号读取时没做 0xFF所有字节读取统一做无符号化处理收到半包后整条链路错乱没有做帧切割直接按read长度处理使用STX定位、按LEN判断完整帧的状态机航点上传被中途终止MISSION_COUNT的count和实际数量不一致发送前统计好航点数量循环发送时严格自增序号起飞指令无响应没有持续发送HEARTBEAT飞控视为离线建立独立定时任务每500ms到1s发送一次心跳解析经纬度偏差巨大忘了lat/lon是1e7度直接当作整数用除以10000000后再参与业务计算串口关闭时线程卡死readBytes阻塞模式没有超时设置TIMEOUT_READ_SEMI_BLOCKING并指定超时时间最后一个关于字节序的问题我单独拿出来再说一句。很多人在网络编程里习惯了大端字节序转过来做MAVLink时下意识地用了ByteBuffer.allocate().order(ByteOrder.BIG_ENDIAN)结果payload里的float解析出来全是天文数字。MAVLink和地面站生态基本都是小端顺着协议走别自创。可以用ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN)统一处理多字节读取效率也不差。再补充一个调试技巧解析阶段把所有收到的MAVLink消息打印一行摘要格式类似[seq12] [msgid33] latitude63.7000000 longitude151.2000000。这样你在跑业务之前可以先看日志判断链路是否正常。这个习惯帮我定位过无数次“协议栈对但业务错”的问题。真正复杂的项目里建议在打印时也带上帧原始hex方便用Wireshark或者Mavlink Inspector做二次比对。根据我个人的项目经验做Java与MAVLink对接最大的难点不是语法和API而是对协议字节级别的敏感性。你盯着帧里每一个字节搞清楚它为什么在这里、CRC到底覆盖了哪一段、应答超时怎么处理这套能力是可以复用的。后面哪怕接触同样基于CRC的串口私有协议比如工业设备里常见的645协议或者其他半双工通信协议你会发现套路高度相似找帧头、按长度字段切包、校验、解析字节序。这也是我写这篇长文的原因——把MAVLink这个经典样例吃透再去碰其他二进制协议会顺手很多。最后再分享一个小技巧。如果你在项目里不想引整套mavlink-java依赖只想快速支持心跳、GPS、指令、航点这几类核心消息完全可以按照本文的思路手写一个精简的解析器再用官方common.xml生成CRC_EXTRA表。大约几百行代码就能搞定大部分场景。以后就算飞控固件升级、消息定义有调整你也能很容易地修复和扩展。
返回列表