尾灯-应用层通信协议(BLE)

介绍

这份 刹车灯 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个字节为设备信息头
相关推荐
恋猫de小郭5 小时前
iPhone Duo 适配详解,需要改变的不止是布局模型
前端·flutter·ios
我就是DaLing呀!8 小时前
flutter + ffmpeg_kit_extended_flutter 大视频合并下载
flutter·ffmpeg·音视频
程序员老刘1 天前
Dart Skills CLI 1.0发布,老刘年初的预言兑现了
flutter·ai编程·dart
天空之城--1 天前
Flutter UI 绘制原理浅析
flutter
yume_sibai1 天前
01-Flutter入门指南
flutter
lqj_本人2 天前
Flutter 三方库「flutter_ble_peripheral」的鸿蒙化适配指南
flutter·华为·harmonyos
lqj_本人2 天前
Flutter 三方库「flutter-dualscreen」的鸿蒙化适配指南
flutter·华为·harmonyos
●VON2 天前
鸿蒙跨平台框架怎么选?从真实需求比较 Flutter、React Native、KMP/CMP 与 Web 路线
flutter·react native·harmonyos
lqj_本人2 天前
Flutter 三方库「flutter_displaymode」的鸿蒙化适配指南
flutter·华为·harmonyos