You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
7.2 KiB
215 lines
7.2 KiB
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<JieliDevice> 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<BleManager>();
|
|
Future.microtask(startScan);
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_bleManager.stopScan();
|
|
super.onClose();
|
|
}
|
|
|
|
Future<void> 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() : <String>[];
|
|
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<void> stopScan() async {
|
|
await _bleManager.stopScan();
|
|
}
|
|
|
|
Future<void> 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>();
|
|
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>(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<void> disconnect() async {
|
|
await _bleManager.disconnect();
|
|
}
|
|
|
|
bool isConnected(JieliDevice device) {
|
|
return _bleManager.isConnected &&
|
|
_bleManager.currentDeviceAddress == device.address;
|
|
}
|
|
|
|
Future<bool> _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;
|
|
}
|
|
}
|
|
}
|
|
|