- 新增 Flutter 跨平台客户端项目 (clients/flutter/)
- 29 个 Dart 文件: 服务层/状态管理/5个页面/BLE配网
- BLE 蓝牙配网: 扫描设备、写入WiFi凭据、配网状态监听
- HTTP API 客户端: 覆盖全部端点 (播放/场景/WiFi/视频/配置/文件/插件)
- WebSocket 实时通信: 事件流 + 自动重连
- 暗色主题 Material 3 UI, 中文界面
- Android 配置: minSdkVersion 21, BLE/网络权限
- PRD 产品需求文档 + 开发任务看板
- Web UI 添加 APK 下载入口 (routes.rs)
- 下载弹窗 + 二维码 + /download/{filename} 静态文件路由
- BLE 插件增加自动重连循环 (ble/mod.rs)
- BLE 默认设备名修正为 'Showen' (config.rs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
197 lines
5.6 KiB
Dart
197 lines
5.6 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
|
||
import '../models/ble_models.dart';
|
||
import '../services/ble_service.dart';
|
||
|
||
class BleProvider extends ChangeNotifier {
|
||
BleProvider({BleService? bleService}) : _bleService = bleService ?? BleService();
|
||
|
||
final BleService _bleService;
|
||
|
||
StreamSubscription<List<BleDevice>>? _scanSubscription;
|
||
StreamSubscription<BleStatus>? _statusSubscription;
|
||
|
||
List<BleDevice> _devices = const <BleDevice>[];
|
||
BleDevice? _selectedDevice;
|
||
BleStatus? _latestStatus;
|
||
ProvisioningState _provisioningState = ProvisioningState.scanning;
|
||
String? _errorMessage;
|
||
bool _isScanning = false;
|
||
bool _isConnecting = false;
|
||
bool _isProvisioning = false;
|
||
bool _isConnected = false;
|
||
bool _isDisposed = false;
|
||
|
||
List<BleDevice> get devices => _devices;
|
||
BleDevice? get selectedDevice => _selectedDevice;
|
||
BleStatus? get latestStatus => _latestStatus;
|
||
ProvisioningState get provisioningState => _provisioningState;
|
||
String? get errorMessage => _errorMessage;
|
||
bool get isScanning => _isScanning;
|
||
bool get isConnecting => _isConnecting;
|
||
bool get isProvisioning => _isProvisioning;
|
||
bool get isConnected => _isConnected;
|
||
|
||
Future<void> startScan() async {
|
||
_errorMessage = null;
|
||
_selectedDevice = null;
|
||
_isConnected = false;
|
||
_provisioningState = ProvisioningState.scanning;
|
||
_isScanning = true;
|
||
_notifySafely();
|
||
|
||
await _scanSubscription?.cancel();
|
||
_scanSubscription = _bleService
|
||
.scanForShowenDevices()
|
||
.listen((List<BleDevice> scannedDevices) {
|
||
_devices = scannedDevices;
|
||
_notifySafely();
|
||
}, onError: (Object error, StackTrace stackTrace) {
|
||
_errorMessage = error.toString();
|
||
_isScanning = false;
|
||
_provisioningState = ProvisioningState.failed;
|
||
_notifySafely();
|
||
});
|
||
|
||
Future<void>.delayed(const Duration(seconds: 6), () {
|
||
if (_isScanning) {
|
||
_isScanning = false;
|
||
_notifySafely();
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> connectToDevice(BleDevice device) async {
|
||
_selectedDevice = device;
|
||
_errorMessage = null;
|
||
_isConnecting = true;
|
||
_isScanning = false;
|
||
_provisioningState = ProvisioningState.connecting;
|
||
_notifySafely();
|
||
|
||
try {
|
||
await _bleService.connectToDevice(device);
|
||
await _subscribeToStatus();
|
||
_isConnected = true;
|
||
} catch (error) {
|
||
_isConnected = false;
|
||
_errorMessage = error.toString();
|
||
_provisioningState = ProvisioningState.failed;
|
||
rethrow;
|
||
} finally {
|
||
_isConnecting = false;
|
||
_notifySafely();
|
||
}
|
||
}
|
||
|
||
Future<void> provisionWifi(String ssid, String password) async {
|
||
_errorMessage = null;
|
||
_latestStatus = null;
|
||
_isProvisioning = true;
|
||
_provisioningState = ProvisioningState.writingCredentials;
|
||
_notifySafely();
|
||
|
||
try {
|
||
final Future<BleStatus> operation = _bleService.provisionWifi(
|
||
ssid,
|
||
password,
|
||
timeout: const Duration(seconds: 30),
|
||
);
|
||
|
||
Future<void>.delayed(const Duration(milliseconds: 400), () {
|
||
if (_isProvisioning &&
|
||
_provisioningState == ProvisioningState.writingCredentials) {
|
||
_provisioningState = ProvisioningState.connectingWifi;
|
||
_notifySafely();
|
||
}
|
||
});
|
||
|
||
final BleStatus result = await operation;
|
||
_latestStatus = result;
|
||
_provisioningState = result.ok
|
||
? ProvisioningState.success
|
||
: ProvisioningState.failed;
|
||
if (!result.ok) {
|
||
_errorMessage = result.error ?? 'WiFi provisioning failed';
|
||
}
|
||
} on TimeoutException {
|
||
_errorMessage = 'BLE 配网超时(30 秒)';
|
||
_provisioningState = ProvisioningState.failed;
|
||
rethrow;
|
||
} catch (error) {
|
||
_errorMessage = error.toString();
|
||
_provisioningState = ProvisioningState.failed;
|
||
rethrow;
|
||
} finally {
|
||
_isProvisioning = false;
|
||
_notifySafely();
|
||
}
|
||
}
|
||
|
||
Future<void> disconnect() async {
|
||
await _scanSubscription?.cancel();
|
||
await _statusSubscription?.cancel();
|
||
_scanSubscription = null;
|
||
_statusSubscription = null;
|
||
await _bleService.disconnect();
|
||
_isConnected = false;
|
||
_isConnecting = false;
|
||
_isProvisioning = false;
|
||
_selectedDevice = null;
|
||
_notifySafely();
|
||
}
|
||
|
||
Future<void> retryScan() async {
|
||
await disconnect();
|
||
_devices = const <BleDevice>[];
|
||
_latestStatus = null;
|
||
_errorMessage = null;
|
||
_provisioningState = ProvisioningState.scanning;
|
||
_notifySafely();
|
||
await startScan();
|
||
}
|
||
|
||
Future<void> _subscribeToStatus() async {
|
||
await _statusSubscription?.cancel();
|
||
final Stream<BleStatus> stream = await _bleService.subscribeToStatus();
|
||
_statusSubscription = stream.listen((BleStatus status) {
|
||
_latestStatus = status;
|
||
if (!status.ok) {
|
||
_errorMessage = status.error ?? 'BLE status returned an error';
|
||
}
|
||
if (status.action == 'connect') {
|
||
if (!status.ok) {
|
||
_provisioningState = ProvisioningState.failed;
|
||
} else if (!status.isQueued) {
|
||
_provisioningState = ProvisioningState.success;
|
||
} else if (_isProvisioning) {
|
||
_provisioningState = ProvisioningState.connectingWifi;
|
||
}
|
||
}
|
||
_notifySafely();
|
||
}, onError: (Object error, StackTrace stackTrace) {
|
||
_errorMessage = error.toString();
|
||
_provisioningState = ProvisioningState.failed;
|
||
_notifySafely();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_isDisposed = true;
|
||
unawaited(_scanSubscription?.cancel());
|
||
unawaited(_statusSubscription?.cancel());
|
||
unawaited(_bleService.dispose());
|
||
super.dispose();
|
||
}
|
||
|
||
void _notifySafely() {
|
||
if (_isDisposed) {
|
||
return;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
}
|