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.
267 lines
7.6 KiB
267 lines
7.6 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import '../../../core/utils/permission_util.dart';
|
|
|
|
class BleController extends GetxController {
|
|
// 扫描状态
|
|
final RxBool isScanning = false.obs;
|
|
final RxBool isBluetoothOn = false.obs;
|
|
final RxBool hasPermissions = false.obs;
|
|
|
|
// 发现的设备列表
|
|
final RxList<fbp.ScanResult> scanResults = <fbp.ScanResult>[].obs;
|
|
|
|
// 连接的设备
|
|
final Rx<fbp.BluetoothDevice?> connectedDevice =
|
|
Rx<fbp.BluetoothDevice?>(null);
|
|
|
|
// 错误信息
|
|
final RxString errorMessage = ''.obs;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initBluetooth();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
stopScan();
|
|
super.onClose();
|
|
}
|
|
|
|
// 初始化蓝牙
|
|
Future<void> _initBluetooth() async {
|
|
try {
|
|
// 先检查权限
|
|
await _checkPermissions();
|
|
|
|
// 检查蓝牙适配器状态
|
|
fbp.FlutterBluePlus.adapterState.listen((state) {
|
|
isBluetoothOn.value = state == fbp.BluetoothAdapterState.on;
|
|
if (state == fbp.BluetoothAdapterState.on) {
|
|
errorMessage.value = '';
|
|
} else {
|
|
errorMessage.value = 'pleaseEnableBluetooth'.tr;
|
|
stopScan();
|
|
}
|
|
});
|
|
|
|
// 监听扫描结果
|
|
fbp.FlutterBluePlus.scanResults.listen((results) {
|
|
scanResults.value = results;
|
|
});
|
|
|
|
// 监听扫描状态
|
|
fbp.FlutterBluePlus.isScanning.listen((scanning) {
|
|
isScanning.value = scanning;
|
|
});
|
|
} catch (e) {
|
|
errorMessage.value = 'bleInitFailedWithError'.trParams({'error': e.toString()});
|
|
}
|
|
}
|
|
|
|
// 检查权限(只查状态,绝不发起申请)
|
|
//
|
|
// 合规(场景 1/2):进页面就自动弹权限框是明确禁止的。这里原来虽然叫
|
|
// "不请求",实际却调了 .request(),用户一进 BLE 页就被弹蓝牙+定位框,
|
|
// 拒绝之后每次重进页面还会再弹。现在只读状态,申请交给用户点"扫描"时触发。
|
|
Future<void> _checkPermissions() async {
|
|
try {
|
|
bool allGranted = true;
|
|
for (final p in _blePermissions) {
|
|
if (!await p.status.isGranted) {
|
|
allGranted = false;
|
|
break;
|
|
}
|
|
}
|
|
hasPermissions.value = allGranted;
|
|
|
|
if (!allGranted) {
|
|
errorMessage.value = 'needBluetoothAndLocationPermission'.tr;
|
|
}
|
|
} catch (e) {
|
|
print('检查权限失败: $e');
|
|
hasPermissions.value = false;
|
|
}
|
|
}
|
|
|
|
// 扫描需要的权限(成套,缺一个都扫不出设备)
|
|
static const List<Permission> _blePermissions = [
|
|
Permission.bluetoothScan,
|
|
Permission.bluetoothConnect,
|
|
Permission.location,
|
|
];
|
|
|
|
// 请求权限(仅由用户点击"扫描"触发)
|
|
//
|
|
// 合规(场景 1):拒绝之后不再弹任何框、不跳设置页——原来这里在检测到
|
|
// permanentlyDenied 时会立刻弹一个带"前往设置"的自定义框,正是要禁掉的行为。
|
|
// 现在统一走 PermissionUtil,拒绝即进入 48h 冷却,返回 false 后由 UI 置灰扫描按钮。
|
|
Future<bool> _requestPermissions() async {
|
|
try {
|
|
final allGranted = await PermissionUtil.instance.requestPermissions(
|
|
permissions: _blePermissions,
|
|
permissionName: 'bluetooth'.tr,
|
|
explanationText: 'needBluetoothAndLocationPermission'.tr,
|
|
);
|
|
hasPermissions.value = allGranted;
|
|
|
|
if (!allGranted) {
|
|
// 只做静态提示,不引导、不跳转
|
|
errorMessage.value = 'needBluetoothAndLocationPermission'.tr;
|
|
}
|
|
|
|
return allGranted;
|
|
} catch (e) {
|
|
print('请求权限失败: $e');
|
|
errorMessage.value = 'permissionRequestFailedWithError'.trParams({'error': e.toString()});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 权限被拒且仍在冷却期 —— 视图据此把扫描入口置灰
|
|
bool get isPermissionCoolingDown => _blePermissions
|
|
.any((p) => PermissionUtil.instance.isDenialCoolingDown(p));
|
|
|
|
// 显示蓝牙开启对话框
|
|
void _showBluetoothDialog() {
|
|
Get.dialog(
|
|
AlertDialog(
|
|
title: Text('needEnableBluetooth'.tr),
|
|
content: Text('enableBluetoothDialogContent'.tr),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: Text('cancel'.tr),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Get.back();
|
|
// 尝试开启蓝牙
|
|
fbp.FlutterBluePlus.turnOn();
|
|
},
|
|
child: Text('enableBluetooth'.tr),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 开始扫描
|
|
Future<void> startScan() async {
|
|
try {
|
|
errorMessage.value = '';
|
|
|
|
// 检查权限
|
|
if (!hasPermissions.value) {
|
|
if (!await _requestPermissions()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 检查蓝牙状态
|
|
if (!isBluetoothOn.value) {
|
|
_showBluetoothDialog();
|
|
return;
|
|
}
|
|
|
|
// 清空之前的结果
|
|
scanResults.clear();
|
|
|
|
// 开始扫描
|
|
await fbp.FlutterBluePlus.startScan(
|
|
timeout: const Duration(seconds: 30),
|
|
androidUsesFineLocation: true,
|
|
);
|
|
} catch (e) {
|
|
errorMessage.value = 'bleScanFailedWithError'.trParams({'error': e.toString()});
|
|
}
|
|
}
|
|
|
|
// 停止扫描
|
|
Future<void> stopScan() async {
|
|
try {
|
|
await fbp.FlutterBluePlus.stopScan();
|
|
} catch (e) {
|
|
print('停止扫描失败: $e');
|
|
}
|
|
}
|
|
|
|
// 连接设备
|
|
Future<void> connectToDevice(fbp.BluetoothDevice device) async {
|
|
try {
|
|
errorMessage.value = '';
|
|
|
|
// 停止扫描
|
|
await stopScan();
|
|
|
|
// 连接设备
|
|
await device.connect(timeout: const Duration(seconds: 15));
|
|
connectedDevice.value = device;
|
|
|
|
Get.snackbar(
|
|
'bleConnectionSuccess'.tr,
|
|
'bleConnectedToDevice'.trParams({'device': device.platformName.isNotEmpty ? device.platformName : 'unknownEquipment'.tr}),
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
);
|
|
} catch (e) {
|
|
errorMessage.value = 'bleConnectionFailedWithError'.trParams({'error': e.toString()});
|
|
Get.snackbar(
|
|
'connectionFailed'.tr,
|
|
e.toString(),
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 断开连接
|
|
Future<void> disconnectDevice() async {
|
|
try {
|
|
if (connectedDevice.value != null) {
|
|
await connectedDevice.value!.disconnect();
|
|
connectedDevice.value = null;
|
|
|
|
Get.snackbar(
|
|
'bleDisconnected'.tr,
|
|
'bleDeviceDisconnected'.tr,
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
errorMessage.value = 'bleDisconnectFailedWithError'.trParams({'error': e.toString()});
|
|
}
|
|
}
|
|
|
|
// 获取设备名称
|
|
String getDeviceName(fbp.ScanResult result) {
|
|
if (result.device.platformName.isNotEmpty) {
|
|
return result.device.platformName;
|
|
}
|
|
if (result.advertisementData.localName.isNotEmpty) {
|
|
return result.advertisementData.localName;
|
|
}
|
|
return 'unknownEquipment'.tr;
|
|
}
|
|
|
|
// 获取信号强度描述
|
|
String getSignalStrengthText(int rssi) {
|
|
if (rssi >= -50) return 'signalStrong'.tr;
|
|
if (rssi >= -70) return 'signalMedium'.tr;
|
|
if (rssi >= -90) return 'signalWeak'.tr;
|
|
return 'signalVeryWeak'.tr;
|
|
}
|
|
|
|
// 获取信号强度图标
|
|
IconData getSignalStrengthIcon(int rssi) {
|
|
if (rssi >= -50) return Icons.signal_wifi_4_bar;
|
|
if (rssi >= -70) return Icons.signal_wifi_4_bar;
|
|
if (rssi >= -90) return Icons.signal_wifi_4_bar;
|
|
return Icons.signal_wifi_4_bar;
|
|
}
|
|
}
|
|
|