Files
ShowenV2/clients/flutter/lib/services/ble_service.dart
showen d30c111c71 feat: M1.1 完成 + M1.2 启动 — 全量更新
M1.1 收尾:
- 24项 P0/P1/P2 bug 修复 (Rust 107 tests + Flutter 15 tests)
- Flutter App v0.3: cupertino_icons 修复, 单元测试, 调试面板, APK 52.6MB
- 示例插件完善: manifest.json + 请求/响应示范 + 7个测试
- API 文档重写 (以 routes.rs 为唯一权威)
- MILESTONES.md 更新至 100%

M1.2 启动:
- P0: 插件管理 API 闭环 (handle_manager_message Custom 分支 + broadcast_plugin_states)
- ServiceManager 集成测试 8/8 (tests/m1_2_service_manager.rs)
- M1.2 测试计划 (docs/M1.2_TEST_PLAN.md, 18个E2E场景)
- 动态插件系统: auto_rollback + version_manager GC + 路径穿越防护

总计: Rust 115/115 测试, Flutter 15/15 测试, 零 warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:12:42 +08:00

239 lines
7.1 KiB
Dart

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<List<ScanResult>>? _scanSubscription;
Stream<List<BleDevice>> scanForShowenDevices({
Duration timeout = const Duration(seconds: 6),
}) {
final controller = StreamController<List<BleDevice>>();
final seen = <String, BleDevice>{};
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<void>.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<void> 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<Stream<BleStatus>> subscribeToStatus() async {
final characteristic = _statusCharacteristic;
if (characteristic == null) {
return Stream<BleStatus>.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<BleStatus> 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<BleStatus>();
late final StreamSubscription<BleStatus> 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<void> 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<void> play() => sendCommand('play');
Future<void> pause() => sendCommand('pause');
Future<void> next() => sendCommand('next');
Future<void> previous() => sendCommand('prev');
Future<void> 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<void> dispose() => disconnect();
Future<void> _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;
}
}