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

如何使用这个Manufacturer
Dart
在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连接后通讯
协议部分

Dart
这部分的协议规定了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; //存储-接收特征值
}
}
协议部分
帧格式

解析帧协议
Dart
第一个字节和第二个字节都是固定的
第三个字节是命令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` 执行;未收齐不执行。
代码如何解析帧
Dart
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}");
}
}
}
分片如何合并
Dart
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监听拿到数据
Dart
_valueState = _rx?.onValueReceived.listen((event) {
Log.d("rx: ${Log.listToHex(event)}");
if(event.isNotEmpty){
_parse(event);//解析接收到的数据,十进制数,数组
Log.d("rx收到数据");
}
});
2.解析数据的方法
Dart
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.是否完成分包的判断
Dart
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个字节为设备信息头