import 'dart:async'; import 'dart:convert'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../models/ble_models.dart'; class BleService { static final Guid _provisionServiceUuid = Guid('12345678-1234-5678-1234-56789abcdef0'); static final Guid _ssidCharacteristicUuid = Guid('12345678-1234-5678-1234-56789abcdef1'); static final Guid _passwordCharacteristicUuid = Guid('12345678-1234-5678-1234-56789abcdef2'); static final Guid _commandCharacteristicUuid = Guid('12345678-1234-5678-1234-56789abcdef3'); static final Guid _statusCharacteristicUuid = Guid('12345678-1234-5678-1234-56789abcdef4'); BluetoothDevice? _connectedDevice; BluetoothCharacteristic? _ssidCharacteristic; BluetoothCharacteristic? _passwordCharacteristic; BluetoothCharacteristic? _commandCharacteristic; BluetoothCharacteristic? _statusCharacteristic; StreamSubscription>? _scanSubscription; Stream> scanForShowenDevices({ Duration timeout = const Duration(seconds: 6), }) { final controller = StreamController>(); final seen = {}; unawaited(_scanSubscription?.cancel()); _scanSubscription = FlutterBluePlus.scanResults.listen((results) { for (final result in results) { final name = _deviceName(result); if (name.isEmpty || !name.toLowerCase().contains('showen')) { continue; } final id = result.device.remoteId.str; seen[id] = BleDevice(name: name, id: id, rssi: result.rssi); } final devices = seen.values.toList(growable: false) ..sort((a, b) => b.rssi.compareTo(a.rssi)); if (!controller.isClosed) { controller.add(devices); } }); unawaited(FlutterBluePlus.startScan(timeout: timeout)); Future.delayed(timeout, () async { await FlutterBluePlus.stopScan(); if (!controller.isClosed) { await controller.close(); } }); controller.onCancel = () async { await FlutterBluePlus.stopScan(); await _scanSubscription?.cancel(); _scanSubscription = null; }; return controller.stream; } Future connectToDevice(BleDevice device) async { await disconnect(); final bluetoothDevice = BluetoothDevice.fromId(device.id); await bluetoothDevice.connect(timeout: const Duration(seconds: 12)); _connectedDevice = bluetoothDevice; await _discoverCharacteristics(bluetoothDevice); } Future> subscribeToStatus() async { final characteristic = _statusCharacteristic; if (characteristic == null) { return Stream.value( const BleStatus( ok: false, action: 'status', error: '未发现 BLE 状态特征值', ), ); } await characteristic.setNotifyValue(true); return characteristic.lastValueStream .where((value) => value.isNotEmpty) .map((value) { final raw = utf8.decode(value, allowMalformed: true); try { return BleStatus.fromRawJson(raw); } catch (_) { return BleStatus( ok: true, action: 'status', state: raw, ); } }); } Future provisionWifi( String ssid, String password, { Duration timeout = const Duration(seconds: 30), }) async { final ssidChar = _ssidCharacteristic; final passwordChar = _passwordCharacteristic; final command = _commandCharacteristic; if (_connectedDevice == null) { throw StateError('未连接 BLE 设备'); } if (ssidChar == null || passwordChar == null || command == null) { throw StateError('未发现 BLE 配网特征值'); } final statusStream = await subscribeToStatus(); final completer = Completer(); late final StreamSubscription subscription; subscription = statusStream.listen((status) { if (!status.isQueued && !completer.isCompleted) { completer.complete(status); } }, onError: (Object error, StackTrace stackTrace) { if (!completer.isCompleted) { completer.completeError(error, stackTrace); } }); await ssidChar.write( utf8.encode(ssid), withoutResponse: ssidChar.properties.writeWithoutResponse, ); await passwordChar.write( utf8.encode(password), withoutResponse: passwordChar.properties.writeWithoutResponse, ); await command.write( utf8.encode('connect'), withoutResponse: command.properties.writeWithoutResponse, ); try { return await completer.future.timeout(timeout); } finally { await subscription.cancel(); } } Future sendCommand(String command) async { final characteristic = _commandCharacteristic; if (_connectedDevice == null) { throw StateError('未连接 BLE 设备'); } if (characteristic == null) { throw StateError('未发现 BLE 命令特征值'); } await characteristic.write( utf8.encode(command), withoutResponse: characteristic.properties.writeWithoutResponse, ); } Future play() => sendCommand('play'); Future pause() => sendCommand('pause'); Future next() => sendCommand('next'); Future previous() => sendCommand('prev'); Future disconnect() async { await _scanSubscription?.cancel(); _scanSubscription = null; final device = _connectedDevice; _connectedDevice = null; _ssidCharacteristic = null; _passwordCharacteristic = null; _commandCharacteristic = null; _statusCharacteristic = null; if (device != null) { await device.disconnect(); } } Future dispose() => disconnect(); Future _discoverCharacteristics(BluetoothDevice device) async { final services = await device.discoverServices(); BluetoothCharacteristic? ssid; BluetoothCharacteristic? password; BluetoothCharacteristic? command; BluetoothCharacteristic? status; for (final service in services) { final shouldInspect = service.uuid == _provisionServiceUuid || services.length == 1 || service.characteristics.isNotEmpty; if (!shouldInspect) { continue; } for (final characteristic in service.characteristics) { if (characteristic.uuid == _ssidCharacteristicUuid) { ssid = characteristic; } else if (characteristic.uuid == _passwordCharacteristicUuid) { password = characteristic; } else if (characteristic.uuid == _commandCharacteristicUuid) { command = characteristic; } else if (characteristic.uuid == _statusCharacteristicUuid) { status = characteristic; } } } _ssidCharacteristic = ssid; _passwordCharacteristic = password; _commandCharacteristic = command; _statusCharacteristic = status; } String _deviceName(ScanResult result) { final platformName = result.device.platformName.trim(); if (platformName.isNotEmpty) { return platformName; } final advertisedName = result.advertisementData.advName.trim(); return advertisedName; } }