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.

460 lines
15 KiB

import 'dart:async';
import 'dart:io';
import 'package:app_settings/app_settings.dart';
import 'package:device_jieli/device_jieli.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
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/models/user_Info.dart';
import '/data/services/ble_manager.dart';
import '/data/services/network/api.dart';
import '/data/utils/jieli_adv_parser.dart';
import '/routes/app_routes.dart';
/// 设备页控制器:扫描杰理设备、绑定(首次)、连接、断开。
class DevicesController extends GetxController {
static const String _tag = 'DevicesController';
late final BleManager _bleManager;
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();
/// 正在调用绑定接口的目标地址(用于 tile UI 反馈)
final RxnString bindingAddress = RxnString();
/// 当 [User.instance.devices] 变化时触发 UI 重建。
/// 直接读 user 对象不响应式,所以加一个 tick。
final RxInt boundDevicesTick = 0.obs;
/// 蓝牙适配器是否已开启(用于 UI 提示)。
final RxBool isBluetoothOn = true.obs;
/// 蓝牙开关状态监听。
StreamSubscription<fbp.BluetoothAdapterState>? _adapterStateSub;
@override
void onInit() {
super.onInit();
_bleManager = Get.find<BleManager>();
_init();
}
@override
void onClose() {
_adapterStateSub?.cancel();
_bleManager.stopScan();
super.onClose();
}
Future<void> _init() async {
// 1. 已存在已连接的设备 → 不进扫描流程,直接进入主界面。
try {
if (await _bleManager.hasActiveConnection()) {
Logger.i(_tag, '已存在已连接设备,直接进入主界面');
Get.offAllNamed(Routes.home);
return;
}
} catch (e) {
Logger.w(_tag, '检查已连接设备失败: $e');
}
// 2. 监听蓝牙开关:关闭→停扫并刷新提示;从关到开→自动重新扫描。
_listenAdapterState();
// 3. 开始扫描(内部包含蓝牙开关 / 权限检查与引导)。
await startScan();
}
void _listenAdapterState() {
_adapterStateSub?.cancel();
_adapterStateSub = fbp.FlutterBluePlus.adapterState.listen((state) {
final on = state == fbp.BluetoothAdapterState.on;
final wasOn = isBluetoothOn.value;
isBluetoothOn.value = on;
if (!on) {
_bleManager.stopScan();
} else if (!wasOn && !isScanning.value) {
// 蓝牙刚被打开 → 自动重新扫描。
startScan();
}
});
}
Future<void> startScan() async {
Logger.i(_tag, 'startScan 被调用');
// 1. 蓝牙是否开启,未开启则弹引导对话框。
if (!await _ensureBluetoothOn()) {
Logger.w(_tag, '蓝牙未开启,已弹出开启引导');
return;
}
// 2. 蓝牙相关权限,未授权则弹引导对话框。
if (!await _ensureBluetoothPermission()) {
Logger.w(_tag, '蓝牙权限不足,已弹出权限引导');
return;
}
final uuids =
AppConfig.isInitialized() ? AppConfig.allproductUuids() : <String>[];
final ok = await _bleManager.startScan(uuids: uuids);
if (!ok) {
final err = _bleManager.lastScanError ?? 'unknown';
Logger.w(_tag, '扫描启动失败: $err');
Get.snackbar('error'.tr, '${'startScanFailed'.tr}: $err');
}
}
Future<void> stopScan() async {
await _bleManager.stopScan();
}
/// 当前用户已绑定的设备里是否包含这个 BLE 设备(按 MAC 匹配)。
bool isDeviceBound(JieliDevice device) {
if (!User.isLoggedIn()) return false;
return _findBoundDevice(device) != null;
}
UserDevice? _findBoundDevice(JieliDevice device) {
final addr = device.address.replaceAll(':', '').toUpperCase();
final edr = (device.edrAddr ?? '').replaceAll(':', '').toUpperCase();
for (final d in User.instance.devices) {
final dm = d.devicemac.replaceAll(':', '').toUpperCase();
if (dm.isEmpty) continue;
if (dm == addr || (edr.isNotEmpty && dm == edr)) return d;
}
return null;
}
/// 点击"连接"或"绑定后自动连接"时调用。
Future<void> connect(JieliDevice device) async {
if (connectingAddress.value != null) {
Logger.i(_tag, '连接进行中,忽略重复点击: ${device.address}');
return;
}
if (_bleManager.isConnected &&
_bleManager.currentDeviceAddress == device.address) {
Logger.i(_tag, '设备已处于连接状态,直接进入首页: ${device.address}');
Get.offAllNamed(Routes.home);
return;
}
Logger.i(_tag, '连接设备: ${device.name} ${device.address}');
connectingAddress.value = device.address;
await _bleManager.stopScan();
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.e(_tag, '连接异常: $e');
if (Get.isDialogOpen ?? false) Get.back();
Get.snackbar('error'.tr, 'connectionFailure'.tr);
} finally {
await evtSub.cancel();
connectingAddress.value = null;
}
}
/// 点击"绑定"按钮:解析广播 → 调用 binddevice → 成功后自动 connect。
Future<void> bindAndConnect(JieliDevice device) async {
if (bindingAddress.value != null || connectingAddress.value != null) {
Logger.i(_tag, '已有绑定/连接进行中,忽略重复点击: ${device.address}');
return;
}
if (!User.isLoggedIn()) {
Get.snackbar('tip'.tr, 'pleaseLoginFirst'.tr);
return;
}
final info = JieliAdvParser.parse(device);
if (info == null || info.code.isEmpty) {
Logger.w(_tag, '无法从广播包解析绑定信息: ${device.address}');
Get.snackbar('error'.tr, 'deviceUnauthorized'.tr);
return;
}
bindingAddress.value = device.address;
try {
final response = await Api.binddevice({
'pid': info.pid,
'code': info.code,
'devicename': device.name,
'devicemac': device.address,
});
Logger.i(_tag, '绑定接口返回: $response');
if (response == null) {
Get.snackbar('error'.tr, 'deviceUnauthorized'.tr);
Get.offAllNamed(Routes.devices);
return;
}
final deviceJson = response['device'];
if (deviceJson == null || deviceJson == '') {
Get.snackbar('error'.tr, 'deviceUnauthorized'.tr);
Get.offAllNamed(Routes.devices);
return;
}
final newUserDevice =
UserDevice.fromJson(deviceJson as Map<String, dynamic>);
User.instance.addUserDevice(newUserDevice);
boundDevicesTick.value++;
// 首次绑定奖励
if (response['isgiveaway'] == true) {
_applyGiveaway(response);
}
// 绑定成功后自动连接进入主界面
await connect(device);
} catch (e) {
Logger.e(_tag, '绑定异常: $e');
Get.snackbar('error'.tr, 'deviceUnauthorized'.tr);
} finally {
bindingAddress.value = null;
}
}
void _applyGiveaway(Map<String, dynamic> response) {
User.instance.viplv = (response['viplv'] as num?)?.toInt() ?? User.instance.viplv;
User.instance.vipexptime =
(response['vipexptime'] as num?)?.toInt() ?? User.instance.vipexptime;
User.instance.aichatintegral =
(response['aichatintegral'] as num?)?.toInt() ?? User.instance.aichatintegral;
User.instance.aichattotalintegral =
(response['aichattotalintegral'] as num?)?.toInt() ??
User.instance.aichattotalintegral;
User.instance.tradeintegral =
(response['tradeintegral'] as num?)?.toInt() ?? User.instance.tradeintegral;
User.instance.tradetotalintegral =
(response['tradetotalintegral'] as num?)?.toInt() ??
User.instance.tradetotalintegral;
User.instance.meetintegral =
(response['meetintegral'] as num?)?.toInt() ?? User.instance.meetintegral;
User.instance.meettotalintegral =
(response['meettotalintegral'] as num?)?.toInt() ??
User.instance.meettotalintegral;
Get.dialog(
AlertDialog(
title: Text('bindRewardTitle'.tr),
content: Text('bindRewardSimpleDesc'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('ok'.tr),
),
],
),
barrierDismissible: true,
);
}
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;
}
/// 检查蓝牙是否开启。未开启则弹出引导对话框(Android 可直接开启,
/// iOS 引导去系统设置),并返回 false。
Future<bool> _ensureBluetoothOn() async {
var state = fbp.FlutterBluePlus.adapterStateNow;
// 首帧可能还是 unknown,等首个确定状态(带超时兜底)。
if (state == fbp.BluetoothAdapterState.unknown) {
try {
state = await fbp.FlutterBluePlus.adapterState
.firstWhere((s) => s != fbp.BluetoothAdapterState.unknown)
.timeout(const Duration(seconds: 2));
} catch (_) {
state = fbp.FlutterBluePlus.adapterStateNow;
}
}
isBluetoothOn.value = state == fbp.BluetoothAdapterState.on;
if (isBluetoothOn.value) return true;
_showEnableBluetoothDialog();
return false;
}
void _showEnableBluetoothDialog() {
if (Get.isDialogOpen ?? false) return;
Get.dialog(
AlertDialog(
title: Text('needEnableBluetooth'.tr),
content: Text('enableBluetoothDialogContent'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () async {
Get.back();
// Android 可直接请求系统开启蓝牙;iOS 不允许,引导去系统设置。
if (Platform.isAndroid) {
try {
await fbp.FlutterBluePlus.turnOn();
return;
} catch (e) {
Logger.w(_tag, 'turnOn 失败,转去系统蓝牙设置: $e');
}
}
await AppSettings.openAppSettings(
type: AppSettingsType.bluetooth);
},
child: Text('enableBluetooth'.tr),
),
],
),
barrierDismissible: false,
);
}
Future<bool> _ensureBluetoothPermission() async {
// iOS: CoreBluetooth 首次扫描时由系统自动弹出蓝牙权限对话框
// (依赖 Info.plist 的 NSBluetoothAlwaysUsageDescription),
// permission_handler 的 bluetoothScan/bluetoothConnect 是 Android 专属,
// 在 iOS 上调用永远返回 denied,会误拦截扫描。
if (Platform.isIOS) return true;
try {
final results = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.locationWhenInUse,
].request();
final scanOk = results[Permission.bluetoothScan]?.isGranted ?? false;
final connectOk = results[Permission.bluetoothConnect]?.isGranted ?? false;
if (scanOk && connectOk) return true;
// 被拒(含永久拒绝)→ 弹引导对话框,带用户去系统设置手动开启。
_showPermissionDialog();
return false;
} catch (e) {
Logger.error('请求蓝牙权限失败: $e');
return false;
}
}
void _showPermissionDialog() {
if (Get.isDialogOpen ?? false) return;
Get.dialog(
AlertDialog(
title: Text('needPermission'.tr),
content: Text('blePermissionDialogContent'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () {
Get.back();
AppSettings.openAppSettings();
},
child: Text('goToSettings'.tr),
),
],
),
barrierDismissible: false,
);
}
}