Files
feature-tenant/lib/core/bluetooth/ble_manager.dart
2026-09-18 16:59:32 +08:00

473 lines
16 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: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;
/// 防止异步竞态: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;
/// 🔥 获取当前蓝牙扫描结果列表(供外部查询使用)
List<ScanResult> getScanResults() {
return _scanResults.values.toList();
}
Stream<List<ScanResult>> get scanResults => _scanController.stream;
Stream<BlePacket> get packetStream => _packetController.stream;
Stream<BluetoothAdapterState> get adapterState =>
FlutterBluePlus.adapterState;
/// 连接状态变化流:连接成功时发出 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 {
final connStatus = await Permission.bluetoothConnect.status;
developer.log(
'[BLE] bluetoothConnect status: $connStatus',
name: 'BleManager',
);
if (connStatus.isDenied) {
developer.log('[BLE] requesting bluetoothConnect...', name: 'BleManager');
await Permission.bluetoothConnect.request();
}
final scanStatus = await Permission.bluetoothScan.status;
developer.log(
'[BLE] bluetoothScan status: $scanStatus',
name: 'BleManager',
);
if (scanStatus.isDenied) {
developer.log('[BLE] requesting bluetoothScan...', name: 'BleManager');
await Permission.bluetoothScan.request();
}
final locStatus = await Permission.locationWhenInUse.status;
developer.log(
'[BLE] locationWhenInUse status: $locStatus',
name: 'BleManager',
);
if (locStatus.isDenied) {
developer.log(
'[BLE] requesting locationWhenInUse...',
name: 'BleManager',
);
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;
}
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;
}
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
try {
final mtu = await device.requestMtu(512);
_negotiatedMtu = mtu;
developer.log('[BLE] MTU: $mtu', name: 'BleManager');
} catch (e) {
developer.log('[BLE] requestMtu failed: $e', name: 'BleManager');
}
return null; // 成功
} catch (e) {
// 连接失败,清除连接中状态
_connectingDevice = null;
_connectingController.add(null);
// 返回具体的错误原因
if (e is TimeoutException) {
return '连接超时(15秒),请确认设备在附近且已开启';
}
return '连接失败: $e';
}
}
void _onDeviceDisconnected() {
_connectedDevice = null;
_connectingDevice = null;
_connectedDeviceName = null; // 🔥 清空已连接设备名
_writeCharacteristic = null;
_readCharacteristic = null;
_negotiatedMtu = 23;
_readSubscription?.cancel();
_deviceConnectionSubscription?.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}片写入 (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);
}
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();
_scanController.close();
_packetController.close();
_connectionController.close();
_connectingController.close();
}
}