import 'dart:async'; import 'package:device_jieli/device_jieli.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import '/core/utils/logger.dart'; import '/data/models/appconfig.dart'; import '/data/services/ble_manager.dart'; import '/routes/app_routes.dart'; /// 设备页控制器:扫描杰理设备、点击连接、断开。 /// 旧的绑定 / 解绑流程已下线。 class DevicesController extends GetxController { late final BleManager _bleManager; // 扫描结果(直接绑定到 BleManager 内部 Rx 列表) RxList get scannedDevices => _bleManager.scannedDevices; RxBool get isScanning => _bleManager.isScanning; // 当前连接地址(响应式) RxInt get connectionStateRx => _bleManager.connectionStateRx; String? get currentDeviceAddress => _bleManager.currentDeviceAddress; // 正在连接的目标地址(用于 tile UI 反馈) final RxnString connectingAddress = RxnString(); @override void onInit() { super.onInit(); _bleManager = Get.find(); Future.microtask(startScan); } @override void onClose() { _bleManager.stopScan(); super.onClose(); } Future startScan() async { print('[BLE] [SCAN] DevicesController.startScan 被调用'); final granted = await _ensureBluetoothPermission(); print('[BLE] [SCAN] 权限申请结果: granted=$granted'); if (!granted) { Get.snackbar('tip'.tr, 'pleaseGrantBluetoothPermission'.tr); return; } // 设备页是通用绑定入口,扫描白名单 = AppConfig 下发的所有产品 scanuuid final uuids = AppConfig.isInitialized() ? AppConfig.allproductUuids() : []; final ok = await _bleManager.startScan(uuids: uuids); if (!ok) { final err = _bleManager.lastScanError ?? 'unknown'; print('[BLE] [SCAN] 扫描启动失败: $err'); Get.snackbar('error'.tr, '${'startScanFailed'.tr}: $err'); } } Future stopScan() async { await _bleManager.stopScan(); } Future connect(JieliDevice device) async { if (connectingAddress.value != null) { Logger.info('连接进行中,忽略重复点击: ${device.address}'); return; } // 已连上同一台设备 → 直接进首页,避免卡在「连接中」对话框 if (_bleManager.isConnected && _bleManager.currentDeviceAddress == device.address) { Logger.info('设备已处于连接状态,直接进入首页: ${device.address}'); Get.offAllNamed(Routes.home); return; } Logger.info('连接设备: ${device.name} ${device.address}'); connectingAddress.value = device.address; await _bleManager.stopScan(); // 必须 BLE 连接 OK + RCSP init 成功 才算「真正连上」。 // 任一环节失败立即判失败,避免 dialog 关掉之后又被断开。 final completer = Completer(); bool gotConnectionOk = false; final evtSub = Jielihome.instance.events.listen((evt) { if (evt is ConnectionStateEvent) { if (evt.address != device.address) return; if (evt.state == ConnectionStateEvent.connectionOk) { gotConnectionOk = true; } else if (evt.state == ConnectionStateEvent.connectionDisconnect) { if (!completer.isCompleted) completer.complete(false); } } else if (evt is RcspInitEvent) { if (evt.address != device.address) return; if (evt.success && gotConnectionOk) { if (!completer.isCompleted) completer.complete(true); } else if (!evt.success) { if (!completer.isCompleted) completer.complete(false); } } }); _showConnectingDialog(device); try { await _bleManager.connectDevice(device); final ok = await completer.future .timeout(const Duration(seconds: 20), onTimeout: () => false); if (Get.isDialogOpen ?? false) Get.back(); if (ok) { Get.offAllNamed(Routes.home); } else { Get.snackbar('error'.tr, 'connectionFailure'.tr); } } catch (e) { Logger.error('连接异常: $e'); if (Get.isDialogOpen ?? false) Get.back(); Get.snackbar('error'.tr, 'connectionFailure'.tr); } finally { await evtSub.cancel(); connectingAddress.value = null; } } void _showConnectingDialog(JieliDevice device) { Get.dialog( PopScope( canPop: false, child: Dialog( backgroundColor: Colors.transparent, elevation: 0, insetPadding: EdgeInsets.symmetric(horizontal: 40.w), child: Container( padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 28.h), decoration: BoxDecoration( color: Get.isDarkMode ? const Color(0xFF1C1C1E) : Colors.white, borderRadius: BorderRadius.circular(16.r), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 44.w, height: 44.w, child: const CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Color(0xFF0066CC)), ), ), SizedBox(height: 18.h), Text( 'connectingDevice'.tr, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, color: Get.isDarkMode ? Colors.white : Colors.black87, ), ), SizedBox(height: 6.h), Text( device.name.isEmpty ? device.address : device.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12.sp, color: Get.isDarkMode ? Colors.white54 : Colors.black54, ), ), ], ), ), ), ), barrierDismissible: false, ); } Future disconnect() async { await _bleManager.disconnect(); } bool isConnected(JieliDevice device) { return _bleManager.isConnected && _bleManager.currentDeviceAddress == device.address; } Future _ensureBluetoothPermission() async { try { // Android 12+:BLUETOOTH_SCAN 已声明 neverForLocation,所以不强制要求定位。 // 但实际不少 ROM 仍要求定位才能扫到 BLE 设备 —— 同时申请,谁拒绝就报错。 final results = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.locationWhenInUse, ].request(); print('[BLE] [SCAN] 权限申请详情: $results'); // 至少 scan + connect 必须授权;location 缺失警告但不一定阻塞 final scanOk = results[Permission.bluetoothScan]?.isGranted ?? false; final connectOk = results[Permission.bluetoothConnect]?.isGranted ?? false; return scanOk && connectOk; } catch (e) { Logger.error('请求蓝牙权限失败: $e'); return false; } } }