尾灯-应用层通信协议(BLE)
2026/9/12 19:51:33 网站建设 项目流程
介绍

这份刹车灯 TAG — BLE 协议是一份完整的蓝牙低功耗(BLE)应用层通信协议,用于APP 与智能刹车灯设备之间的扫描发现、连接建立、命令交互与状态上报

第一部分:BLE广播

协议部分

如何使用这个Manufacturer

在scan_item中,传入扫描结果ScanResult,然后从中拿到厂商设定的数据。 //final manuData = result.advertisementData.manufacturerData; 协议上有5个字节,所以根据协议格式,拿到自己想要的数据 前两个字节为CID,后三个字节为MAC的后三个字节 BLE 广播里 ManufacturerData 的结构为: ┌────────────────┬─────────────────────────────┐ │ Company ID │ 厂商自定义数据 │ │ (2 字节, 小端) │ (N 字节) │ └────────────────┴─────────────────────────────┘ 参考以下的代码,理解怎么使用manufacturerData // key = Company ID(从原始字节里解析出来的整数) //value = 该厂商的私有数据字节 //manuData.forEach((key, value) { // if(key == 1602 ) { //等于0x0642 CompanyID // _company = key; // if(value.length >= 3){ // var mac4 = value[0]; // var mac5 = value[1]; // var mac6 = value[2]; // _deviceMac = '41:42:00:${_hex2(mac4)}:${_hex2(mac5)}:${_hex2(mac6)}'; // // Log.d("⭐当前设备的MAC地址为$_deviceMac"); // } // } // });
第二部分:BLE连接后通讯

协议部分

这部分的协议规定了UUID,APP通过UUID过滤得到相应的设备,通过TX,RX和设备通信 //UUID static final Guid _serviceGuid = Guid("FF20"); static final Guid _txGuid = Guid("FF21"); //写 static final Guid _rxGuid = Guid("FF22"); //读 //过滤服务代码 if(service.uuid != _serviceGuid) {//服务过滤,只处理指定服务UUID continue; } //过滤YX,RX代码 //遍历服务下的所有特征 for(var characteristic in service.characteristics) { //Log.d(characteristic.uuid.toString()); //根据 UUID 匹配特征 if(characteristic.uuid == _txGuid) { _tx = characteristic; //存储-发送特征值 } if(characteristic.uuid == _rxGuid) { _rx = characteristic; //存储-接收特征值 } }

协议部分

帧格式

解析帧协议

第一个字节和第二个字节都是固定的 第三个字节是命令ID,用来区分是哪一条命令 第四个字节是用来区分是否是分包,和是否需要回应 bit[0] = 0,本包不是分包,bit[0] = 1,本包是分包, bit[1] = 0,本条命令不需要回应,bit[1] = 1,本条命令需要回应 第五个字节是序号,seq 第六个字节+第七个字节 是payload真正数据的长度,并且是小端序显示 从第八个字节开始,到倒数第二个字节,都是数据段 最后一个字节是CRC校验 第一种情况:不分包 payload **就是命令参数**(命令表里写的字段)。无参数则 N=0。 第二种情况:分包 各包 `cmd`、`seq` 相同 此时 payload **不是**命令参数本身, 而是有一个格式,payload[0]为分包序号,payload[1]为分包的总数量,后面的字节才是真正的数据 分包序号(1) | 总包数量(1) | chunk...(N-2) len = 2 + chunk长度。 设备按分包序号把各包 `chunk` 按序拼接,得到完整命令参数后再按该 `cmd` 执行;未收齐不执行。

代码如何解析帧

BleMessage.from(List<int> data) { Log.d(" BleMessage.from() 开始解析"); // 保存原始数据 raw = List.from(data); Log.d("原始数据长度: ${data.length}"); if (data.length < 8) { Log.d("❌ 数据长度不足: ${data.length}"); return; } // 1.解析帧头 final magic = data[0] & 0xFF; //& 0xFF的目的是为了,把有符号的字节值转成无符号的 0~255,原始数据没有发生改变 final ver = data[1] & 0xFF; cmd = data[2] & 0xFF; final flags = data[3] & 0xFF; final seq = data[4] & 0xFF; //小端,所以len本质上是data[6]拼上data[5]; // <<8:左移八位,然后或运算data[5],实现拼接效果 len = (data[6] << 8) | data[5]; if (magic != 0xBF) { Log.d("❌ magic 不匹配: 0x${magic.toRadixString(16)}"); return; } if (ver != 0x01) { Log.d("❌ ver 不匹配: 0x${ver.toRadixString(16)}"); return; } // 2.提取数据 payload payload = []; if (len > 0) { final payloadStart = 7; // 帧头固定 7 字节,payload 从下标 7 开始 if (data.length >= payloadStart + len) { //判断数据长度是否正确 //偏移切包,sublist(7, 7+len),左闭右开,从下标7开始包含,一直包含到下标为7+len的前一个字节 payload = List.from(data.sublist(payloadStart, payloadStart + len)); } else { Log.d("⚠️ payload 长度不足: 需要 $len 字节,实际 ${data.length - payloadStart} 字节"); payload = List.from(data.sublist(payloadStart)); Log.d("⚠️ 实际提取: ${payload.length} 字节"); } } else { Log.d("📭 payload 为空"); } // 3.检查 flags //0x02 = 0000 0010,(flags & 0x02) 可以取出flag的bit[1],bit[1] = 1,需要回应,bit[1] = 0,不需要回应 _response = (flags & 0x02) != 0; //是否需要回应 final isFragment = (flags & 0x01) != 0; //是否是分包 // 解析分包信息 if (isFragment && payload.length >= 2) { offset = payload[0] & 0xFF; //分包的序号 size = payload[1] & 0xFF; //分包的总数量 payload.removeRange(0, 2);//删除前两个字节,得到真正的数据 } else { size = 0; offset = 0; if (isFragment) { Log.d("⚠️ 分包标志设置但 payload 长度不足 2: ${payload.length}"); } } }

分片如何合并

void contact(List<int> data) { // 保存原始数据 raw.addAll(data); //检查数据长度 if (data.length < 8) { Log.d("❌ contact 数据长度不足: ${data.length}"); return; } //检查cmd final dataCmd = data[2] & 0xFF; if (dataCmd != cmd) { Log.d("❌ cmd 不匹配: 数据cmd=0x${dataCmd.toRadixString(16)}, 期望cmd=0x${cmd.toRadixString(16)}"); return; } //检查分包标志 final flags = data[3] & 0xFF; final isFragment = (flags & 0x01) != 0; Log.d("分包标志: ${isFragment ? '是' : '否'}"); if (isFragment && data.length >= 9) { final payloadStart = 7; //包头大小 final dataLen = (data[6] << 8) | data[5]; //payload的大小 Log.d("数据长度: $dataLen 字节"); if (dataLen >= 2) { offset = data[payloadStart] & 0xFF; //分包序号 size = data[payloadStart + 1] & 0xFF;//分包的总数量 Log.d("分包信息: 分包序号为:$offset, 分包的总数量为:$size"); if (dataLen > 2) { final chunkStart = payloadStart + 2; //真实数据的第一个字节下标 final chunkEnd = payloadStart + dataLen;//真实数据的最后一个字节下标 //检查数据长度 if (data.length >= chunkEnd) { //拿到真正的数据 final chunk = data.sublist(chunkStart, chunkEnd); //拼接数据 payload.addAll(chunk); Log.d("合并后 payload 长度: ${payload.length}"); } else { Log.d("⚠️ chunk 数据不完整"); } } else { Log.d("📭 无 chunk 数据 (dataLen == 2)"); } } else { Log.d("⚠️ dataLen < 2,无法解析分包信息"); } } else { Log.d("⚠️ 不是分包或数据长度不足,跳过合并"); } }

数据分包的逻辑

1.rx监听拿到数据

_valueState = _rx?.onValueReceived.listen((event) { Log.d("rx: ${Log.listToHex(event)}"); if(event.isNotEmpty){ _parse(event);//解析接收到的数据,十进制数,数组 Log.d("rx收到数据"); } });

2.解析数据的方法

void _parse(List<int> data) { Log.d(" 收到原始数据 (${data.length} 字节): ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}"); ///===数据检查=== // 数据完整性检查 if (data.length < 8) { Log.d("❌ 数据长度不足: ${data.length},需要至少 8 字节"); return; } // 检查 magic if (data[0] != 0xBF) { Log.d("⚠️ magic 不匹配: 0x${data[0].toRadixString(16)},期望 0xBF"); return; } // 检查 ver if (data[1] != 0x01) { Log.d("⚠️ ver 不匹配: 0x${data[1].toRadixString(16)},期望 0x01"); return; } ///===解析为 BleMessage=== final msg = BleMessage.from(data); Log.d(" BleMessage 解析完成: cmd=0x${msg.cmd.toRadixString(16)}, len=${msg.len}, isAll=${msg.isAll}, payload长度=${msg.payload.length}"); ///===分片数据重组=== //是完整的消息,没有有正在拼接的信息 if (msg.isAll && _msg == null) { //_msg用来暂存“正在拼接种的分片消息” ///不分包的完整信息 Log.d("完整的消息,直接解析"); _dispatchMessage(msg); } else { ///分包逻辑 //正在拼接某条消息,传入消息的cmd与正在等待拼接的cmd是一致的 if (_msg != null && _msg!.cmd == msg.cmd) { ///除第一包的所有包 Log.d("合并分片"); _msg!.contact(data); } else { ///第一包 Log.d("开始新的分片序列: cmd=0x${msg.cmd.toRadixString(16)}"); _msg = msg; } //_msg!.contact(data);这里拼完最后一包,进入if逻辑,走消息分发的方法 if (_msg != null && _msg!.isAll) { Log.d("✅ 分片接收完成"); _dispatchMessage(_msg!); _msg = null; } else { Log.d("等待更多分片,当前进度: ${_msg?.offset}/${_msg?.size}"); } } }

3.是否完成分包的判断

bool get isAll { if(size == 0){ //不分包 return true; }else{ //分包,当前分包 + 1 >= 分包的总数量 ;offset从0开始,所以这里要+1 final result = size > 0 && offset + 1 >= size; Log.d(" isAll: $result (size=$size, offset=$offset)"); return result; } }

协议部分

核心指令

1.DEV_INFO_GET

下行:无 payload。 //APP给设备发送命令 上行 Notify:同一 cmd。一条逻辑 payload(可分包),结构如下。 //设备上报信息

协议解析

前8个字节为 版本号,传递的格式是ASCLL格式 第9个字节是 充电相关 bit[7] = 1;表示在充电,bit[7] = 0;表示未在充电 bit[6]~bit[0]表示设备当前的电量 第10个字节表示 电量格数 第11个字节表示 智能启停 第12+第13字节表示 智能启停的时间 第14个字节表示 刹车感应 第15个字节表示 刹车的时间 第16个字节表示 刹车的灵敏度 第17个字节表示 组队同频 第18个字节表示 当前被点亮的槽位 从第19个字节开始到最后一个字节表示 当前槽位上的动画 所以前18个字节为设备信息头

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

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

立即咨询