Files
flutterApp/lib/core/bluetooth/ble_manager.dart

522 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io' show Platform;
import 'dart:typed_data';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'bytes_util.dart';
import 'protocol_parser.dart';
class BleManager {
static final BleManager instance = BleManager._internal();
BleManager._internal();
BluetoothDevice? _connectedDevice;
BluetoothDevice? _connectingDevice;
BluetoothCharacteristic? _writeCharacteristic;
BluetoothCharacteristic? _readCharacteristic;
/// 🔥 已连接设备的名称(连接时从扫描结果或设备属性保存,断开时清空)
String? _connectedDeviceName;
/// 有状态的协议解析器(支持 BLE 分片)
final ProtocolParser _parser = ProtocolParser();
final Map<DeviceIdentifier, ScanResult> _scanResults = {};
final StreamController<List<ScanResult>> _scanController =
StreamController.broadcast();
final StreamController<BlePacket> _packetController =
StreamController.broadcast();
final StreamController<BluetoothDevice?> _connectionController =
StreamController.broadcast();
final StreamController<BluetoothDevice?> _connectingController =
StreamController.broadcast();
StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<List<int>>? _readSubscription;
StreamSubscription<BluetoothAdapterState>? _adapterStateSubscription;
StreamSubscription<BluetoothConnectionState>? _deviceConnectionSubscription;
StreamSubscription<int>? _mtuSubscription;
/// 防止异步竞态:stopScan() 后 in-flight 的 startScan() 不应生效
int _scanGen = 0;
/// 协商后的 MTU 值,用于分片写入
int _negotiatedMtu = 23;
/// 已收到的数据包存储(跨页面持久化)
final List<BlePacket> receivedPacketStore = [];
static const int _maxStoredPackets = 100;
void clearReceivedPackets() {
receivedPacketStore.clear();
}
bool get isConnected => _connectedDevice != null;
BluetoothDevice? get connectedDevice => _connectedDevice;
BluetoothDevice? get connectingDevice => _connectingDevice;
/// 🔥 获取已连接设备的名称(连接时缓存,断开后为 null)
String? get connectedDeviceName => _connectedDeviceName;
Stream<List<ScanResult>> get scanResults => _scanController.stream;
Stream<BlePacket> get packetStream => _packetController.stream;
Stream<BluetoothAdapterState> get adapterState =>
FlutterBluePlus.adapterState;
/// 🔥 获取当前蓝牙扫描结果列表(供外部查询使用)
List<ScanResult> getScanResults() {
return _scanResults.values.toList();
}
/// 连接状态变化流:连接成功时发出 device,断开时发出 null
Stream<BluetoothDevice?> get connectionStream => _connectionController.stream;
/// 连接中状态流:开始连接时发出 device,连接完成/失败时发出 null
Stream<BluetoothDevice?> get connectingStream => _connectingController.stream;
Future<bool> checkBluetooth() async {
final state = await FlutterBluePlus.adapterState.first;
return state == BluetoothAdapterState.on;
}
Future<bool> requestPermissions() async {
if (Platform.isAndroid) {
final connStatus = await Permission.bluetoothConnect.status;
developer.log(
'[BLE] bluetoothConnect status: $connStatus',
name: 'BleManager',
);
if (connStatus.isDenied) {
await Permission.bluetoothConnect.request();
}
final scanStatus = await Permission.bluetoothScan.status;
developer.log(
'[BLE] bluetoothScan status: $scanStatus',
name: 'BleManager',
);
if (scanStatus.isDenied) {
await Permission.bluetoothScan.request();
}
final locStatus = await Permission.locationWhenInUse.status;
developer.log(
'[BLE] locationWhenInUse status: $locStatus',
name: 'BleManager',
);
if (locStatus.isDenied) {
await Permission.locationWhenInUse.request();
}
final allGranted =
await Permission.bluetoothConnect.isGranted &&
await Permission.bluetoothScan.isGranted;
developer.log(
'[BLE] all permissions granted: $allGranted',
name: 'BleManager',
);
return allGranted;
}
// iOS: 蓝牙权限由 Info.plist 声明,系统弹窗授权,无需主动请求
developer.log('[BLE] iOS platform, skipping Android permissions', name: 'BleManager');
return true;
}
Future<void> openBluetooth() async {
try {
await FlutterBluePlus.turnOn();
} catch (e) {
await _goToSystemSettings();
}
}
Future<void> _goToSystemSettings() async {
await openAppSettings();
}
Future<void> _openLocationSettings() async {
await openAppSettings();
}
Future<bool> _checkLocationService() async {
final locStatus = await Permission.location.serviceStatus;
developer.log(
'[BLE] location service status: $locStatus',
name: 'BleManager',
);
return locStatus == ServiceStatus.enabled;
}
Future<void> startScan({bool continuous = true}) async {
final int myGen = ++_scanGen;
developer.log(
'[BLE] startScan called, gen=$myGen, continuous=$continuous',
name: 'BleManager',
);
// 先停止任何残留扫描,确保干净启动
try { await FlutterBluePlus.stopScan(); } catch (_) {}
_scanResults.clear();
_scanController.add([]);
final isOn = await checkBluetooth();
if (myGen != _scanGen) return;
developer.log('[BLE] Bluetooth adapter on: $isOn', name: 'BleManager');
if (!isOn) {
developer.log(
'[BLE] Bluetooth is OFF, aborting scan',
name: 'BleManager',
);
return;
}
final granted = await requestPermissions();
if (myGen != _scanGen) return;
developer.log('[BLE] Permissions granted: $granted', name: 'BleManager');
if (!granted) {
developer.log(
'[BLE] Permissions not granted, aborting scan',
name: 'BleManager',
);
return;
}
if (Platform.isAndroid) {
final locEnabled = await _checkLocationService();
if (myGen != _scanGen) return;
if (!locEnabled) {
developer.log(
'[BLE] Location service OFF, directing to settings',
name: 'BleManager',
);
await _openLocationSettings();
return;
}
}
_scanSubscription?.cancel();
_scanSubscription = FlutterBluePlus.scanResults.listen(
(results) {
for (final result in results) {
_scanResults[result.device.remoteId] = result;
}
developer.log(
'[BLE] 📡 扫描到 ${results.length} 个设备: ${results.map((r) => '${r.device.platformName.isNotEmpty ? r.device.platformName : r.device.advName.isNotEmpty ? r.device.advName : r.device.remoteId} (${r.rssi}dBm)').join(', ')}',
name: 'BleManager',
);
_scanController.add(_scanResults.values.toList());
},
onError: (e) {
developer.log('[BLE] scanResults error: $e', name: 'BleManager');
},
);
try {
if (continuous) {
developer.log('[BLE] Starting continuous scan...', name: 'BleManager');
await FlutterBluePlus.startScan(
timeout: const Duration(days: 1),
androidUsesFineLocation: true,
androidScanMode: AndroidScanMode.lowLatency,
androidLegacy: true,
);
} else {
developer.log('[BLE] Starting 10s scan...', name: 'BleManager');
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: 10),
androidUsesFineLocation: true,
androidScanMode: AndroidScanMode.lowLatency,
androidLegacy: true,
);
}
developer.log('[BLE] startScan completed', name: 'BleManager');
} catch (e, stackTrace) {
developer.log(
'[BLE] startScan error: $e\n$stackTrace',
name: 'BleManager',
);
}
}
Future<void> refreshScan() async {
await stopScan();
await Future.delayed(const Duration(milliseconds: 200));
await startScan(continuous: true);
}
Future<void> stopScan() async {
_scanGen++;
developer.log('[BLE] stopScan, gen=$_scanGen', name: 'BleManager');
_scanSubscription?.cancel();
await FlutterBluePlus.stopScan();
}
/// 连接设备,返回 null 表示成功,返回错误信息字符串表示失败
Future<String?> connect(BluetoothDevice device) async {
// 🔥 连接前先保存设备名(从扫描结果或设备属性中获取)
final scanResult = _scanResults[device.remoteId];
final advName = scanResult?.device.advName ?? '';
final platformName = device.platformName;
_connectedDeviceName = platformName.isNotEmpty
? platformName
: (advName.isNotEmpty ? advName : device.remoteId.toString());
developer.log(
'[BLE] 🔥 保存已连接设备名: $_connectedDeviceName',
name: 'BleManager',
);
// 标记正在连接,通知所有监听者
_connectingDevice = device;
_connectingController.add(device);
try {
await device
.connect(license: License.nonprofit, mtu: 512)
.timeout(const Duration(seconds: 15));
_connectedDevice = device;
// 连接成功,立即停止扫描(避免扫描射频干扰导致连接断开)
await stopScan();
// 清除连接中状态
_connectingDevice = null;
_connectingController.add(null);
// 监听设备连接状态,任何一方断开都能感知
_deviceConnectionSubscription?.cancel();
_deviceConnectionSubscription = device.connectionState.listen((state) {
developer.log(
'[BLE] device connectionState: $state',
name: 'BleManager',
);
if (state == BluetoothConnectionState.disconnected) {
_onDeviceDisconnected();
}
});
_connectionController.add(device);
await _discoverServices(device);
// 确认协商后的 MTU(Android 主动请求;iOS 由 CoreBluetooth 自动协商)
await _setupMtu(device);
return null; // 成功
} catch (e) {
// 连接失败,清除连接中状态
_connectingDevice = null;
_connectingController.add(null);
// 返回具体的错误原因
if (e is TimeoutException) {
return '连接超时(15秒),请确认设备在附近且已开启';
}
return '连接失败: $e';
}
}
/// 平台差异化的 MTU 协商
/// - Android: 主动 requestMtu(512),一般协商到 247/517
/// - iOS: CoreBluetooth 自动协商,requestMtu 会抛异常;读 mtuNow 并订阅 mtu 流
Future<void> _setupMtu(BluetoothDevice device) async {
if (Platform.isAndroid) {
try {
final mtu = await device.requestMtu(512);
_negotiatedMtu = mtu;
developer.log('[BLE] Android MTU negotiated: $mtu', name: 'BleManager');
} catch (e) {
developer.log('[BLE] requestMtu failed: $e', name: 'BleManager');
}
return;
}
if (Platform.isIOS) {
// iOS 上 connect 完成后系统会异步协商 MTU,稍等一拍再读取实际值
try {
await Future.delayed(const Duration(milliseconds: 300));
final mtu = device.mtuNow;
// iOS 保底 185(现代 iPhone CoreBluetooth 常见协商值),避免默认 23 把长帧切成 9 片
_negotiatedMtu = mtu > 23 ? mtu : 185;
developer.log(
'[BLE] iOS MTU (auto-negotiated): mtuNow=$mtu, using=$_negotiatedMtu',
name: 'BleManager',
);
// 订阅后续 MTU 变化(部分外设会在服务发现后再次协商)
_mtuSubscription?.cancel();
_mtuSubscription = device.mtu.listen((m) {
if (m > 23 && m != _negotiatedMtu) {
_negotiatedMtu = m;
developer.log('[BLE] iOS MTU updated: $m', name: 'BleManager');
}
});
} catch (e) {
_negotiatedMtu = 185;
developer.log(
'[BLE] iOS MTU setup failed, fallback to 185: $e',
name: 'BleManager',
);
}
}
}
void _onDeviceDisconnected() {
_connectedDevice = null;
_connectingDevice = null;
_connectedDeviceName = null; // 🔥 清空已连接设备名
_writeCharacteristic = null;
_readCharacteristic = null;
_negotiatedMtu = 23;
_readSubscription?.cancel();
_deviceConnectionSubscription?.cancel();
_mtuSubscription?.cancel();
_connectionController.add(null);
_connectingController.add(null);
// 清空协议解析器缓冲区
_parser.clear();
}
Future<void> disconnect() async {
final device = _connectedDevice;
if (device == null) return;
developer.log('[BLE] 发起断开连接...', name: 'BleManager');
await device.disconnect();
// 等待连接状态流确认已断开,设置超时防止无限等待
try {
await device.connectionState
.firstWhere((s) => s == BluetoothConnectionState.disconnected)
.timeout(const Duration(seconds: 3));
developer.log('[BLE] 连接已确认断开', name: 'BleManager');
} catch (_) {
developer.log('[BLE] 等待断开超时,强制清理', name: 'BleManager');
}
_onDeviceDisconnected();
}
Future<void> _discoverServices(BluetoothDevice device) async {
final services = await device.discoverServices();
for (final service in services) {
for (final characteristic in service.characteristics) {
if (characteristic.properties.write) {
_writeCharacteristic = characteristic;
}
if (characteristic.properties.notify) {
_readCharacteristic = characteristic;
await characteristic.setNotifyValue(true);
_readSubscription?.cancel();
_readSubscription = characteristic.lastValueStream.listen((value) {
_onDataReceived(value);
});
}
}
}
}
void _onDataReceived(List<int> data) {
developer.log(
'[BLE] 📩 收: ${data.length}B ${_bytesToHex(data)}',
name: 'BleManager',
);
_parser.append(data);
final packets = _parser.parse();
developer.log(
'[BLE] 📦 解析 ${packets.length}包 (buf ${_parser.bufferLength}B)',
name: 'BleManager',
);
if (packets.isEmpty && data.isNotEmpty) {
developer.log(
'[BLE] ⚠️ 非标准帧 raw(${data.length}B)',
name: 'BleManager',
);
final packet = BlePacket(
command: 0x00,
payload: Uint8List.fromList(data),
);
_storePacket(packet);
_packetController.add(packet);
return;
}
for (final packet in packets) {
developer.log(
'[BLE] cmd=0x${packet.command.toRadixString(16).toUpperCase().padLeft(2, '0')} '
'payload(${packet.payload.length}B): ${_bytesToHex(packet.payload)}',
name: 'BleManager',
);
_storePacket(packet);
_packetController.add(packet);
}
}
void _storePacket(BlePacket packet) {
receivedPacketStore.insert(0, packet);
if (receivedPacketStore.length > _maxStoredPackets) {
receivedPacketStore.removeLast();
}
}
String _bytesToHex(List<int> bytes) {
if (bytes.isEmpty) return '';
return bytes
.map((b) => b.toRadixString(16).toUpperCase().padLeft(2, '0'))
.join(' ');
}
Future<void> sendCommand(int command, List<int> payload) async {
final char = _writeCharacteristic;
if (char == null) {
developer.log('[BLE] ❌ 写特征为空,无法发送', name: 'BleManager');
return;
}
final frame = ProtocolParser.pack(command, payload);
final crc = frame.length >= 5
? 'CRC16=0x${frame[frame.length - 4].toRadixString(16).padLeft(2, '0')}${frame[frame.length - 3].toRadixString(16).padLeft(2, '0')}'
: '';
developer.log(
'[BLE] 📤 发送: 0x${command.toRadixString(16).toUpperCase().padLeft(2, '0')}, '
'帧(${frame.length}B) $crc\n'
' ${_bytesToHex(frame)}',
name: 'BleManager',
);
// 分片写入:每次最多写 (MTU - 3) 字节,全部走 withResponse 确保可靠性
final maxWriteLen = _negotiatedMtu - 3;
final totalChunks = (frame.length + maxWriteLen - 1) ~/ maxWriteLen;
if (totalChunks > 1) {
developer.log(
'[BLE] 🔀 分${totalChunks}片写入 (platform=${Platform.isIOS ? "iOS" : "Android"}, MTU=$_negotiatedMtu, 每片≤${maxWriteLen}B)',
name: 'BleManager',
);
}
for (int offset = 0; offset < frame.length; offset += maxWriteLen) {
final end = (offset + maxWriteLen <= frame.length)
? offset + maxWriteLen
: frame.length;
final chunk = frame.sublist(offset, end);
if (totalChunks > 1) {
final chunkIdx = offset ~/ maxWriteLen + 1;
developer.log(
'[BLE] 片$chunkIdx/$totalChunks: offset=$offset len=${chunk.length}B ${_bytesToHex(chunk)}',
name: 'BleManager',
);
}
await char.write(chunk, withoutResponse: false);
// iOS 分片间加小延时,给 MCU 侧协议解析器留出帧间处理时间,
// 避免长帧(如 0x06 写配置 176B)被拆多片时因间隔过短触发丢帧
if (Platform.isIOS &&
totalChunks > 1 &&
offset + maxWriteLen < frame.length) {
await Future.delayed(const Duration(milliseconds: 20));
}
}
developer.log('[BLE] ✅ 写入完成', name: 'BleManager');
}
Future<void> sendRawBytes(List<int> bytes) async {
if (_writeCharacteristic == null) return;
await _writeCharacteristic!.write(bytes, withoutResponse: false);
}
Future<void> sendHeartbeat() async {
await sendCommand(0xFF, []);
}
void dispose() {
_scanSubscription?.cancel();
_readSubscription?.cancel();
_adapterStateSubscription?.cancel();
_deviceConnectionSubscription?.cancel();
_mtuSubscription?.cancel();
_scanController.close();
_packetController.close();
_connectionController.close();
_connectingController.close();
}
}