Files
feature-tenant/lib/core/bluetooth/ble_manager.dart

473 lines
16 KiB
Dart
Raw Normal View History

2026-08-07 08:49:29 +08:00
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;
2026-09-18 16:59:32 +08:00
/// 🔥 已连接设备的名称(连接时从扫描结果或设备属性保存,断开时清空)
String? _connectedDeviceName;
2026-08-07 08:49:29 +08:00
/// 有状态的协议解析器(支持 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;
2026-09-18 16:59:32 +08:00
/// 🔥 获取已连接设备的名称(连接时缓存,断开后为 null)
String? get connectedDeviceName => _connectedDeviceName;
/// 🔥 获取当前蓝牙扫描结果列表(供外部查询使用)
List<ScanResult> getScanResults() {
return _scanResults.values.toList();
}
2026-08-07 08:49:29 +08:00
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 {
2026-09-18 16:59:32 +08:00
// 🔥 连接前先保存设备名(从扫描结果或设备属性中获取)
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',
);
2026-08-07 08:49:29 +08:00
// 标记正在连接,通知所有监听者
_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;
2026-09-18 16:59:32 +08:00
_connectedDeviceName = null; // 🔥 清空已连接设备名
2026-08-07 08:49:29 +08:00
_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();
}
}