Add packet handling and logging functionality for Bluetooth data transfer
This commit is contained in:
@ -39,6 +39,7 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: routerConfig,
|
||||
theme: ThemeData.light().copyWith(
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
|
5
bluetooth_low_energy/example/lib/utils/common.dart
Normal file
5
bluetooth_low_energy/example/lib/utils/common.dart
Normal file
@ -0,0 +1,5 @@
|
||||
/// 辅助函数:将数字转换为格式化的十六进制字符串(带0x前缀且至少两位数)
|
||||
String toHexString(int value) {
|
||||
String hex = value.toRadixString(16).padLeft(2, '0');
|
||||
return '0x$hex';
|
||||
}
|
230
bluetooth_low_energy/example/lib/utils/logger.dart
Normal file
230
bluetooth_low_energy/example/lib/utils/logger.dart
Normal file
@ -0,0 +1,230 @@
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:logger/logger.dart' as log_show;
|
||||
|
||||
class logger {
|
||||
static void info(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
_logger.i(formattedText);
|
||||
}
|
||||
|
||||
static void i(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
_loggerSm.i(formattedText);
|
||||
}
|
||||
|
||||
static void w(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
_loggerSm.w(formattedText);
|
||||
}
|
||||
|
||||
static void e(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
_loggerSm.e(formattedText);
|
||||
}
|
||||
|
||||
static void s(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
loggerSuccess.i(formattedText);
|
||||
}
|
||||
|
||||
static void error(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
_logger.e(formattedText);
|
||||
}
|
||||
|
||||
static void warn(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
_logger.w(formattedText);
|
||||
}
|
||||
|
||||
static void debug(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
|
||||
_logger.d(formattedText);
|
||||
}
|
||||
|
||||
static void success(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
var formattedText =
|
||||
getString(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
loggerSuccess.i(formattedText);
|
||||
}
|
||||
|
||||
static dynamic getString(
|
||||
[dynamic arg1,
|
||||
dynamic arg2,
|
||||
dynamic arg3,
|
||||
dynamic arg4,
|
||||
dynamic arg5,
|
||||
dynamic arg6,
|
||||
dynamic arg7,
|
||||
dynamic arg8,
|
||||
dynamic arg9,
|
||||
dynamic arg10]) {
|
||||
List<dynamic> args = [
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
arg5,
|
||||
arg6,
|
||||
arg7,
|
||||
arg8,
|
||||
arg9,
|
||||
arg10
|
||||
];
|
||||
// 过滤掉 null
|
||||
args.removeWhere((element) => element == null);
|
||||
if (args.isEmpty) return "";
|
||||
if (args.length == 1) return args[0];
|
||||
|
||||
String formattedText = "";
|
||||
for (var arg in args) {
|
||||
int index = args.indexOf(arg);
|
||||
if (arg == null) {
|
||||
break;
|
||||
}
|
||||
// 如果是最后一个不加 \n
|
||||
if (index == args.length - 1) {
|
||||
formattedText += "$arg";
|
||||
} else {
|
||||
formattedText += "$arg\n";
|
||||
}
|
||||
}
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
}
|
||||
|
||||
var _logger = log_show.Logger(
|
||||
printer: PrettyPrinter(
|
||||
stackTraceBeginIndex: 1,
|
||||
methodCount: 3,
|
||||
dateTimeFormat: DateTimeFormat.dateAndTime,
|
||||
), // SimplePrinter PrefixPrinter LogfmtPrinter HybridPrinter PrettyPrinter
|
||||
);
|
||||
var _loggerSm = log_show.Logger(
|
||||
printer: PrettyPrinter(
|
||||
stackTraceBeginIndex: 0,
|
||||
methodCount: 0,
|
||||
// dateTimeFormat: DateTimeFormat.dateAndTime,
|
||||
), // SimplePrinter PrefixPrinter LogfmtPrinter HybridPrinter PrettyPrinter
|
||||
);
|
||||
|
||||
var loggerSuccess = log_show.Logger(
|
||||
printer: PrettyPrinter(
|
||||
stackTraceBeginIndex: 1,
|
||||
methodCount: 3,
|
||||
dateTimeFormat: DateTimeFormat.dateAndTime,
|
||||
levelColors: {
|
||||
Level.trace: AnsiColor.fg(AnsiColor.grey(0.5)),
|
||||
Level.debug: const AnsiColor.none(),
|
||||
Level.info: const AnsiColor.fg(41),
|
||||
Level.warning: const AnsiColor.fg(208),
|
||||
Level.error: const AnsiColor.fg(196),
|
||||
Level.fatal: const AnsiColor.fg(199),
|
||||
},
|
||||
), // SimplePrinter PrefixPrinter LogfmtPrinter HybridPrinter PrettyPrinter
|
||||
);
|
18
bluetooth_low_energy/example/lib/view_models/crc.dart
Normal file
18
bluetooth_low_energy/example/lib/view_models/crc.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// 使用crc16
|
||||
import 'dart:typed_data';
|
||||
|
||||
int crc16(Uint8List data) {
|
||||
int crc = 0;
|
||||
for (int byte in data) {
|
||||
crc = (crc ^ byte) & 0xFFFF;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((crc & 0x0001) != 0) {
|
||||
crc = ((crc >> 1) ^ 0xA001) & 0xFFFF;
|
||||
} else {
|
||||
crc = (crc >> 1) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
112
bluetooth_low_energy/example/lib/view_models/packet.dart
Normal file
112
bluetooth_low_energy/example/lib/view_models/packet.dart
Normal file
@ -0,0 +1,112 @@
|
||||
// 包结构
|
||||
import 'dart:typed_data';
|
||||
import 'crc.dart';
|
||||
|
||||
class Packet {
|
||||
PacketHeader header; // 包头
|
||||
Uint8List data; // 数据
|
||||
int crc; // 2字节,CRC校验码
|
||||
|
||||
Packet(this.header, this.data, this.crc);
|
||||
|
||||
PacketType get type => header.type;
|
||||
|
||||
/// 将数据包序列化为字节数组(包头+数据+CRC)
|
||||
Uint8List toBytes() {
|
||||
// 创建包头字节(5字节)
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value; // 类型
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF; // 序列号高字节
|
||||
headerBytes[2] = header.seqNum & 0xFF; // 序列号低字节
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF; // 数据长度高字节
|
||||
headerBytes[4] = header.dataLen & 0xFF; // 数据长度低字节
|
||||
|
||||
// 创建CRC字节(2字节)
|
||||
Uint8List crcBytes = Uint8List(2);
|
||||
crcBytes[0] = (crc >> 8) & 0xFF; // CRC高字节
|
||||
crcBytes[1] = crc & 0xFF; // CRC低字节
|
||||
|
||||
// 合并所有部分:包头 + 数据 + CRC
|
||||
return Uint8List.fromList([...headerBytes, ...data, ...crcBytes]);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
// 包类型、序列号、数据长度、CRC
|
||||
return 'Packet{type=${header.type}, seqNum=${header.seqNum}, dataLen=${header.dataLen}, crc=${crc.toRadixString(16)},\nallData=${toBytes().map((e) => e.toRadixString(16)).join(",")}}';
|
||||
}
|
||||
}
|
||||
|
||||
// 包头结构
|
||||
class PacketHeader {
|
||||
PacketType type; // 1字节,包类型
|
||||
int seqNum; // 2字节,序列号
|
||||
int dataLen; // 2字节,数据长度
|
||||
|
||||
PacketHeader(this.type, this.seqNum, this.dataLen);
|
||||
}
|
||||
|
||||
/// 数据包类型
|
||||
enum PacketType {
|
||||
// 用于传输开始时发送,包含文件大小和分块大小等信息
|
||||
start(0x01, '开始包'),
|
||||
// 用于传输实际的文件数据块
|
||||
data(0x02, '数据包'),
|
||||
// 用于确认某个数据包已正确接收
|
||||
ack(0x03, '确认包'),
|
||||
// 用于指示某个数据包接收失败,请求重传
|
||||
nak(0x04, '重传请求包'),
|
||||
// 用于标识文件传输结束
|
||||
end(0x05, '结束包'),
|
||||
// 错误
|
||||
error(0x06, '错误包'),
|
||||
;
|
||||
|
||||
const PacketType(this.value, this.label);
|
||||
|
||||
final int value;
|
||||
final String label;
|
||||
|
||||
static PacketType fromValue(int value) {
|
||||
switch (value) {
|
||||
case 0x01:
|
||||
return start;
|
||||
case 0x02:
|
||||
return data;
|
||||
case 0x03:
|
||||
return ack;
|
||||
case 0x04:
|
||||
return nak;
|
||||
case 0x05:
|
||||
return end;
|
||||
case 0x06:
|
||||
return error;
|
||||
// 默认为error
|
||||
default:
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打包函数
|
||||
Packet packPacket(PacketType type, int seqNum, Uint8List? data) {
|
||||
int dataLen = data?.length ?? 0;
|
||||
PacketHeader header = PacketHeader(type, seqNum, dataLen);
|
||||
Uint8List packetData = data ?? Uint8List(0);
|
||||
|
||||
// 创建包头字节(5字节)
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value; // 类型
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF; // 序列号高字节
|
||||
headerBytes[2] = header.seqNum & 0xFF; // 序列号低字节
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF; // 数据长度高字节
|
||||
headerBytes[4] = header.dataLen & 0xFF; // 数据长度低字节
|
||||
|
||||
// 合并包头和数据
|
||||
Uint8List fullData = Uint8List.fromList(headerBytes + packetData);
|
||||
|
||||
// 计算CRC
|
||||
int crc = crc16(fullData);
|
||||
|
||||
return Packet(header, packetData, crc);
|
||||
}
|
282
bluetooth_low_energy/example/lib/view_models/parser.dart
Normal file
282
bluetooth_low_energy/example/lib/view_models/parser.dart
Normal file
@ -0,0 +1,282 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'packet.dart';
|
||||
|
||||
// 检查数据长度是否合理 (可以设置一个最大值以防止异常)
|
||||
const int maxDataLength = 1024 + 7;
|
||||
|
||||
/// 处理接收蓝牙数据的解析器,将字节流解析为数据包结构
|
||||
|
||||
class Parser {
|
||||
/// 接收数据缓冲区
|
||||
final List<int> _buffer = [];
|
||||
|
||||
final Function(Packet) onParser; // 回调函数
|
||||
|
||||
Parser(this.onParser);
|
||||
|
||||
|
||||
/// 添加数据到缓冲区并尝试解析
|
||||
void add(List<int> data) {
|
||||
// 添加数据到缓冲区
|
||||
_buffer.addAll(data);
|
||||
// 尝试解析数据包
|
||||
_processBuffer();
|
||||
}
|
||||
|
||||
/// 处理缓冲区,尝试解析出完整的数据包
|
||||
void _processBuffer() {
|
||||
// 包头需要至少5个字节
|
||||
while (_buffer.length >= 7) {
|
||||
// 检查包类型是否有效
|
||||
int typeValue = _buffer[0];
|
||||
if (typeValue < 0x01 || typeValue > 0x06) {
|
||||
// 无效包类型,丢弃当前字节并继续
|
||||
_buffer.removeAt(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取包类型
|
||||
PacketType type = PacketType.fromValue(typeValue);
|
||||
|
||||
// 解析序列号和数据长度
|
||||
int seqNum = (_buffer[1] << 8) | _buffer[2];
|
||||
int dataLen = (_buffer[3] << 8) | _buffer[4];
|
||||
|
||||
// 检查数据长度是否合理 (可以设置一个最大值以防止异常)
|
||||
if (dataLen < 0 || dataLen > maxDataLength) {
|
||||
// 假设最大数据长度为1KB
|
||||
// 数据长度异常,丢弃当前字节并继续
|
||||
_buffer.removeAt(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查缓冲区是否有足够的数据 (包头5字节 + 数据长度 + CRC 2字节)
|
||||
int totalPacketLength = 5 + dataLen + 2;
|
||||
if (_buffer.length < totalPacketLength) {
|
||||
// 数据不完整,等待更多数据
|
||||
break;
|
||||
}
|
||||
|
||||
// 提取完整的数据包
|
||||
List<int> packetBytes = _buffer.sublist(0, totalPacketLength);
|
||||
|
||||
// 验证CRC
|
||||
int expectedCrc = (packetBytes[totalPacketLength - 2] << 8) | packetBytes[totalPacketLength - 1];
|
||||
List<int> dataToCheck = packetBytes.sublist(0, totalPacketLength - 2);
|
||||
int calculatedCrc = calculateCrc16(Uint8List.fromList(dataToCheck));
|
||||
|
||||
if (calculatedCrc != expectedCrc) {
|
||||
// CRC校验失败,丢弃当前字节并继续
|
||||
_buffer.removeAt(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// CRC校验通过,创建数据包对象
|
||||
PacketHeader header = PacketHeader(type, seqNum, dataLen);
|
||||
Uint8List data = Uint8List.fromList(_buffer.sublist(5, 5 + dataLen));
|
||||
Packet packet = Packet(header, data, expectedCrc);
|
||||
|
||||
// 发送解析出的数据包
|
||||
onParser(packet); // 回调函数
|
||||
|
||||
// 从缓冲区移除已处理的数据
|
||||
_buffer.removeRange(0, totalPacketLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算CRC16校验值
|
||||
int calculateCrc16(Uint8List data) {
|
||||
int crc = 0;
|
||||
for (int byte in data) {
|
||||
crc = (crc ^ byte) & 0xFFFF;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((crc & 0x0001) != 0) {
|
||||
crc = ((crc >> 1) ^ 0xA001) & 0xFFFF;
|
||||
} else {
|
||||
crc = (crc >> 1) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/// 创建开始包
|
||||
Packet createStartPacket(int fileSize, int blockSize, String fileName) {
|
||||
// 创建数据: [文件大小(4字节) + 块大小(2字节) + 文件名(n字节)]
|
||||
// 文件大小: 高位在前
|
||||
List<int> data = [
|
||||
(fileSize >> 24) & 0xFF,
|
||||
(fileSize >> 16) & 0xFF,
|
||||
(fileSize >> 8) & 0xFF,
|
||||
fileSize & 0xFF,
|
||||
(blockSize >> 8) & 0xFF,
|
||||
blockSize & 0xFF,
|
||||
];
|
||||
|
||||
// 添加文件名的ASCII码
|
||||
data.addAll(fileName.codeUnits);
|
||||
|
||||
// 创建包头
|
||||
PacketHeader header = PacketHeader(PacketType.start, 0, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes, ...data]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, Uint8List.fromList(data), crc);
|
||||
}
|
||||
|
||||
/// 创建数据包
|
||||
Packet createDataPacket(int seqNum, Uint8List data) {
|
||||
// 创建包头
|
||||
PacketHeader header = PacketHeader(PacketType.data, seqNum, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes, ...data]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, data, crc);
|
||||
}
|
||||
|
||||
/// 创建确认包
|
||||
Packet createAckPacket(int seqNum) {
|
||||
// 确认包的数据部分包含被确认的包序号
|
||||
Uint8List data = Uint8List(2);
|
||||
data[0] = (seqNum >> 8) & 0xFF;
|
||||
data[1] = seqNum & 0xFF;
|
||||
|
||||
// 创建包头
|
||||
PacketHeader header = PacketHeader(PacketType.ack, seqNum, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes, ...data]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, data, crc);
|
||||
}
|
||||
|
||||
/// 创建重传请求包
|
||||
Packet createNakPacket(int seqNum) {
|
||||
// 重传请求包的数据部分包含需要重传的包序号
|
||||
Uint8List data = Uint8List(2);
|
||||
data[0] = (seqNum >> 8) & 0xFF;
|
||||
data[1] = seqNum & 0xFF;
|
||||
|
||||
// 创建包头
|
||||
PacketHeader header = PacketHeader(PacketType.nak, seqNum, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes, ...data]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, data, crc);
|
||||
}
|
||||
|
||||
/// 创建结束包
|
||||
Packet createEndPacket(int seqNum) {
|
||||
// 结束包的数据部分为空
|
||||
Uint8List data = Uint8List(0);
|
||||
|
||||
// 创建包头
|
||||
PacketHeader header = PacketHeader(PacketType.end, seqNum, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, data, crc);
|
||||
}
|
||||
|
||||
/// 创建错误包
|
||||
Packet createErrorPacket(int errorCode) {
|
||||
// 错误包的数据部分包含错误码
|
||||
Uint8List data = Uint8List(1);
|
||||
data[0] = errorCode & 0xFF;
|
||||
|
||||
// 创建包头 (错误包序号默认为0)
|
||||
PacketHeader header = PacketHeader(PacketType.error, 0, data.length);
|
||||
|
||||
// 计算CRC
|
||||
Uint8List headerBytes = Uint8List(5);
|
||||
headerBytes[0] = header.type.value;
|
||||
headerBytes[1] = (header.seqNum >> 8) & 0xFF;
|
||||
headerBytes[2] = header.seqNum & 0xFF;
|
||||
headerBytes[3] = (header.dataLen >> 8) & 0xFF;
|
||||
headerBytes[4] = header.dataLen & 0xFF;
|
||||
|
||||
Uint8List dataToCheck = Uint8List.fromList([...headerBytes, ...data]);
|
||||
int crc = calculateCrc16(dataToCheck);
|
||||
|
||||
return Packet(header, data, crc);
|
||||
}
|
||||
|
||||
/// 解析开始包中的数据
|
||||
Map<String, dynamic> parseStartPacketData(Uint8List data) {
|
||||
// 文件大小: 4字节
|
||||
int fileSize = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
|
||||
|
||||
// 块大小: 2字节
|
||||
int blockSize = (data[4] << 8) | data[5];
|
||||
|
||||
// 文件名: 剩余字节
|
||||
String fileName = String.fromCharCodes(data.sublist(6));
|
||||
|
||||
return {
|
||||
'fileSize': fileSize,
|
||||
'blockSize': blockSize,
|
||||
'fileName': fileName,
|
||||
};
|
||||
}
|
||||
|
||||
/// 解析确认包和重传请求包中的序列号
|
||||
int parseAckNakPacketData(Uint8List data) {
|
||||
// 序列号: 2字节
|
||||
return (data[0] << 8) | data[1];
|
||||
}
|
||||
|
||||
/// 解析错误包中的错误码
|
||||
int parseErrorPacketData(Uint8List data) {
|
||||
// 错误码: 1字节
|
||||
return data[0];
|
||||
}
|
||||
|
||||
/// 重置解析器
|
||||
void reset() => _buffer.clear();
|
||||
}
|
@ -4,14 +4,34 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:bluetooth_low_energy_example/models.dart';
|
||||
import 'package:bluetooth_low_energy_example/utils/logger.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'packet.dart';
|
||||
import 'parser.dart';
|
||||
import 'receiver.dart';
|
||||
|
||||
class PeripheralManagerViewModel extends ViewModel {
|
||||
final PeripheralManager _manager;
|
||||
final List<Log> _logs;
|
||||
bool _advertising;
|
||||
|
||||
late Parser parser = Parser(handleParser);
|
||||
|
||||
late Receiver receiver = Receiver(manager: _manager);
|
||||
|
||||
void handleParser(Packet packet) {
|
||||
logger.i(
|
||||
'收到 ${packet.type}',
|
||||
'序号: ${packet.header.seqNum}',
|
||||
'总长度: ${packet.toBytes().length},数据长度: ${packet.data.length}',
|
||||
);
|
||||
|
||||
// 将接收到的数据包传递给 Receiver 处理
|
||||
receiver.handlePacket(packet);
|
||||
}
|
||||
|
||||
late final StreamSubscription _stateChangedSubscription;
|
||||
late final StreamSubscription _characteristicReadRequestedSubscription;
|
||||
late final StreamSubscription _characteristicWriteRequestedSubscription;
|
||||
@ -22,14 +42,12 @@ class PeripheralManagerViewModel extends ViewModel {
|
||||
_logs = [],
|
||||
_advertising = false {
|
||||
_stateChangedSubscription = _manager.stateChanged.listen((eventArgs) async {
|
||||
if (eventArgs.state == BluetoothLowEnergyState.unauthorized &&
|
||||
Platform.isAndroid) {
|
||||
if (eventArgs.state == BluetoothLowEnergyState.unauthorized && Platform.isAndroid) {
|
||||
await _manager.authorize();
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
_characteristicReadRequestedSubscription =
|
||||
_manager.characteristicReadRequested.listen((eventArgs) async {
|
||||
_characteristicReadRequestedSubscription = _manager.characteristicReadRequested.listen((eventArgs) async {
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
@ -48,24 +66,36 @@ class PeripheralManagerViewModel extends ViewModel {
|
||||
value: trimmedValue,
|
||||
);
|
||||
});
|
||||
_characteristicWriteRequestedSubscription =
|
||||
_manager.characteristicWriteRequested.listen((eventArgs) async {
|
||||
|
||||
/// 订阅写入数据请求
|
||||
int receivedTotal = 0;
|
||||
_characteristicWriteRequestedSubscription = _manager.characteristicWriteRequested.listen((eventArgs) async {
|
||||
// 收到APP写入数据请求
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
receiver.central = eventArgs.central;
|
||||
receiver.characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final offset = request.offset;
|
||||
final value = request.value;
|
||||
final log = Log(
|
||||
type: 'Characteristic write requested',
|
||||
message:
|
||||
'[${value.length}] ${central.uuid}, ${characteristic.uuid}, $offset, $value',
|
||||
message: '[${value.length}] ${central.uuid}, ${characteristic.uuid}, $offset, ${value.map((e) => e.toRadixString(16)).join(",")}',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
await _manager.respondWriteRequest(request);
|
||||
// 解析数据包
|
||||
// logger.i(
|
||||
// '解析数据包,长度${eventArgs.request.value.length}',
|
||||
// '[${eventArgs.request.value.map((e) => e.toRadixString(16)).join(",")}]',
|
||||
// );
|
||||
receivedTotal += value.length;
|
||||
logger.i("接受总量:$receivedTotal");
|
||||
parser.add(value);
|
||||
});
|
||||
_characteristicNotifyStateChangedSubscription =
|
||||
_manager.characteristicNotifyStateChanged.listen((eventArgs) async {
|
||||
_characteristicNotifyStateChangedSubscription = _manager.characteristicNotifyStateChanged.listen((eventArgs) async {
|
||||
print('Characteristic notify state changed');
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final state = eventArgs.state;
|
||||
@ -77,8 +107,7 @@ class PeripheralManagerViewModel extends ViewModel {
|
||||
notifyListeners();
|
||||
// Write someting to the central when notify started.
|
||||
if (state) {
|
||||
final maximumNotifyLength =
|
||||
await _manager.getMaximumNotifyLength(central);
|
||||
final maximumNotifyLength = await _manager.getMaximumNotifyLength(central);
|
||||
final elements = List.generate(maximumNotifyLength, (i) => i % 256);
|
||||
final value = Uint8List.fromList(elements);
|
||||
await _manager.notifyCharacteristic(
|
||||
@ -91,7 +120,9 @@ class PeripheralManagerViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
BluetoothLowEnergyState get state => _manager.state;
|
||||
|
||||
bool get advertising => _advertising;
|
||||
|
||||
List<Log> get logs => _logs;
|
||||
|
||||
Future<void> showAppSettings() async {
|
||||
|
58
bluetooth_low_energy/example/lib/view_models/receiver.dart
Normal file
58
bluetooth_low_energy/example/lib/view_models/receiver.dart
Normal file
@ -0,0 +1,58 @@
|
||||
// 接收方类
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:bluetooth_low_energy_example/utils/common.dart';
|
||||
import 'package:bluetooth_low_energy_example/utils/logger.dart';
|
||||
|
||||
import 'packet.dart';
|
||||
|
||||
class Receiver {
|
||||
List<Uint8List> receivedChunks = [];
|
||||
int expectedSeqNum = 1;
|
||||
Central? central;
|
||||
GATTCharacteristic? characteristic;
|
||||
PeripheralManager? manager;
|
||||
|
||||
Receiver({this.central, this.characteristic, this.manager});
|
||||
|
||||
void handlePacket(Packet packet) {
|
||||
if (packet.header.type == PacketType.start) {
|
||||
int fileSize = (packet.data[0] << 24) | (packet.data[1] << 16) | (packet.data[2] << 8) | packet.data[3];
|
||||
int chunkSize = (packet.data[4] << 8) | packet.data[5];
|
||||
logger.i('收到Start包: 文件大小=$fileSize, 包大小=$chunkSize');
|
||||
expectedSeqNum = 1;
|
||||
receivedChunks = [];
|
||||
sendAck(0);
|
||||
} else if (packet.header.type == PacketType.data) {
|
||||
if (packet.header.seqNum == expectedSeqNum) {
|
||||
receivedChunks.add(packet.data);
|
||||
// logger.i('收到Data包: 序号=${packet.header.seqNum}');
|
||||
sendAck(packet.header.seqNum);
|
||||
expectedSeqNum++;
|
||||
} else {
|
||||
logger.i('收到乱序Data包: 序号=${packet.header.seqNum},期望=$expectedSeqNum');
|
||||
sendNak(expectedSeqNum);
|
||||
}
|
||||
} else if (packet.header.type == PacketType.end) {
|
||||
logger.i('收到End包,传输完成');
|
||||
sendAck(packet.header.seqNum);
|
||||
}
|
||||
}
|
||||
|
||||
void sendAck(int seqNum) {
|
||||
Packet ackPacket = packPacket(PacketType.ack, seqNum, Uint8List.fromList([seqNum >> 8, seqNum & 0xFF]));
|
||||
logger.i('接收方:发送ACK: ${ackPacket.toBytes().map((e) => toHexString(e)).join(',')}');
|
||||
if (manager != null && central != null && characteristic != null) {
|
||||
manager!.notifyCharacteristic(central!, characteristic!, value: ackPacket.toBytes());
|
||||
}
|
||||
}
|
||||
|
||||
void sendNak(int seqNum) {
|
||||
Packet nakPacket = packPacket(PacketType.nak, seqNum, Uint8List.fromList([seqNum >> 8, seqNum & 0xFF]));
|
||||
logger.i('接收方:发送NAK: $seqNum');
|
||||
if (manager != null && central != null && characteristic != null) {
|
||||
manager!.notifyCharacteristic(central!, characteristic!, value: nakPacket.toBytes());
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "6.0.1"
|
||||
version: "6.0.2"
|
||||
bluetooth_low_energy_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -108,10 +108,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -234,18 +234,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.4"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -262,6 +262,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -282,10 +290,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.11.1"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -298,10 +306,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.15.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -322,10 +330,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -346,7 +354,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -359,10 +367,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -375,10 +383,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -399,10 +407,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -423,18 +431,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.3.0"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -37,6 +37,7 @@ dependencies:
|
||||
convert: ^3.1.1
|
||||
flutter_simple_treeview: ^3.0.2
|
||||
material_symbols_icons: ^4.2744.0
|
||||
logger: #日志
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
|
Reference in New Issue
Block a user