473 lines
16 KiB
Dart
473 lines
16 KiB
Dart
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();
|
||
}
|
||
}
|