Files
tuiche/lib/pages/device_bind/blueteeth_device_page.dart
2026-04-07 14:49:31 +08:00

847 lines
35 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:io';
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:permission_handler/permission_handler.dart'; // 引入permission_handler
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/BluetoothHelper.dart';
import 'package:vbvs_app/common/util/CommonVariables.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
import 'package:vbvs_app/controller/main_bottom/global_controller.dart';
import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
import 'package:vbvs_app/controller/user_info_controller.dart';
import 'package:vbvs_app/model/BleDeviceData.dart';
import 'package:vbvs_app/pages/common/selectDialog.dart';
import 'package:vbvs_app/pages/device_bind/componnet/SingleBlueteethDeviceCompoentWidget.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:flutter/services.dart';
class BlueteethDevicePage extends StatefulWidget {
int tid = -1;
BlueteethDevicePage({super.key, this.tid = -1});
@override
State<BlueteethDevicePage> createState() => _BlueteethDevicePageState();
}
class _BlueteethDevicePageState extends State<BlueteethDevicePage> {
GlobalController globalController = Get.find();
UserInfoController userInfoController = Get.find();
BlueteethBindController blueteethBindController = Get.find();
ThemeController themeController = Get.find();
late FlutterBluePlus flutterBlue; // 声明 flutterBlue 实例
List<ScanResult> scanResults = []; // 存储扫描到的设备
bool isScanning = false; // 扫描状态控制
Timer? _timer; // 定时器,用于重复扫描
bool _isDialogShowing = false;
var currentConnectedDeviceProp;
var connectDeviceCurrent = null;
List bleDevice = [];
String currentMsg = "寻找设备中...";
Timer? connectTimer;
bool isFind = false;
List bindArrBackup = [];
List bindArr = ["", "", ""];
@override
void initState() {
super.initState();
blueteethBindController.model.devicelist = [];
blueteethBindController.model.betDevicelist = [];
flutterBlue = FlutterBluePlus(); // 初始化flutterBlue实例
_checkBluetoothPermission(); // 检查蓝牙权限
Get.find<BlueteethBindController>().startStatusPolling();
blueteethBindController.search.value = "";
blueteethBindController.currentDeviceMac?.value = "";
BluetoothHelper.listenBluetoothState((isOn) async {
blueteethBindController.model.bluetooth = isOn;
if (isOn) {
isScanning = false;
_startScanning();
}
blueteethBindController.updateAll();
if (!isOn && !_isDialogShowing) {
await Future.delayed(Duration(seconds: 2));
bool isReallyOn = await FlutterBluePlus.isOn;
if (!isReallyOn) {
_isDialogShowing = true;
blueteethBindController.model.devicelist = [];
blueteethBindController.model.betDevicelist = [];
blueteethBindController.updateAll();
_showBluetoothNotEnabledDialog().then((_) {
_isDialogShowing = false;
});
}
}
});
}
// 检查蓝牙权限
Future<void> _checkBluetoothPermission() async {
PermissionStatus bluetoothStatus = await Permission.bluetooth.status;
PermissionStatus locationStatus = await Permission.location.status;
if (bluetoothStatus.isGranted && locationStatus.isGranted) {
// 权限已授予,开始扫描
_startScanning();
_startPeriodicScan(); // 开始定时扫描
} else {
// 权限未授予,请求权限
_requestBluetoothPermission(context);
}
}
Future<void> _requestBluetoothPermission(BuildContext context) async {
Map<Permission, PermissionStatus> statuses = {};
bool dialogShown = false; // 标记是否弹过权限提示弹窗
if (Platform.isIOS) {
PermissionStatus isBleGranted = await Permission.bluetooth.request();
print('checkBlePermissions-ios, isBleGranted=$isBleGranted');
if (isBleGranted.isGranted) {
// startBluetoothScanning();
_startScanning();
_startPeriodicScan();
} else {
// showToast("蓝牙开关或蓝牙权限未开启,请开启蓝牙开关与蓝牙权限".tr, closeTime: 7);
try {
_startScanning();
_startPeriodicScan();
} catch (e) {
TopSlideNotification.show(context,
text: "蓝牙权限未开启,请在设置中开启蓝牙权限".tr,
textColor: themeController.currentColor.sc9);
}
}
} else if (Platform.isAndroid) {
try {
// 检查是否已授权
bool alreadyGranted = await Permission.bluetoothScan.isGranted &&
await Permission.bluetoothConnect.isGranted &&
await Permission.location.isGranted;
if (!alreadyGranted) {
// 弹出自定义提示
showPermissionInfoDialog(
Get.context!, CommonVariables().bluetoothpermissionInfo);
dialogShown = true;
await Future.delayed(const Duration(milliseconds: 300));
// 请求权限
statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.location,
].request();
ef.log("权限状态: $statuses");
} else {
statuses = {
Permission.bluetoothScan: PermissionStatus.granted,
Permission.bluetoothConnect: PermissionStatus.granted,
Permission.location: PermissionStatus.granted,
};
}
} catch (e) {
ef.log("申请权限出错: $e");
} finally {
// 只有真的弹过提示才关闭
if (dialogShown && Get.context != null) {
Navigator.of(Get.context!, rootNavigator: true).pop();
}
}
bool allGranted = statuses[Permission.bluetoothScan]?.isGranted == true &&
statuses[Permission.bluetoothConnect]?.isGranted == true &&
statuses[Permission.location]?.isGranted == true;
if (allGranted) {
_startScanning();
_startPeriodicScan();
} else {
_showPermissionDeniedDialog(context);
}
} else {
TopSlideNotification.show(context,
text: "当前系统不支持蓝牙,无法使用此功能".tr,
textColor: themeController.currentColor.sc9);
}
}
// 显示权限被拒绝的提示
// void _showPermissionDeniedDialog(BuildContext context) {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return AlertDialog(
// title: Text("权限提示".tr),
// content: Text("应用需要蓝牙和位置权限才能扫描设备。请授予权限。".tr),
// actions: [
// TextButton(
// onPressed: () {
// Navigator.of(context).pop();
// },
// child: Text("确定".tr),
// ),
// ],
// );
// },
// );
// }
void _showPermissionDeniedDialog(BuildContext context) {
TopSlideNotification.show(context,
text: "应用需要蓝牙和位置权限才能扫描设备。请授予权限。".tr,
textColor: themeController.currentColor.sc9);
}
// 开始扫描蓝牙设备
void _startScanning() async {
if (!mounted || isScanning) return;
_scanSubscription?.cancel();
// var bluetoothState = await FlutterBluePlus.isOn;
// if (!bluetoothState && !_isDialogShowing) {
// _isDialogShowing = true;
// blueteethBindController.model.blelist = [];
// blueteethBindController.updateAll();
// await _showBluetoothNotEnabledDialog();
// _isDialogShowing = false;
// return;
// }
if (!isScanning) {
setState(() {
isScanning = true;
});
await FlutterBluePlus.startScan(timeout: Duration(seconds: 10));
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
if (!mounted) return;
final signalThreshold = blueteethBindController.model.singal!;
final searchKey =
blueteethBindController.search.value.trim().toLowerCase();
final filteredResults = results.where((r) {
final isTarget = r.rssi > signalThreshold &&
(r.advertisementData.localName == "AITH-V2" ||
r.advertisementData.localName == "SLAVE") &&
r.advertisementData.manufacturerData.containsKey(0xFFED);
if (!isTarget) return false;
// 搜索关键字过滤(根据名称和 MAC 地址模糊匹配)
final name = r.advertisementData.advName.toLowerCase();
final mac = r.device.remoteId.str.replaceAll(':', '').toLowerCase();
final search = searchKey.trim().replaceAll(':', '').toLowerCase();
if (search.isNotEmpty &&
!name.contains(search) &&
!mac.contains(search)) {
return false;
}
return true;
}).toList();
// 解析数据
final parsedDeviceList = <BleDeviceData>[];
for (var r in filteredResults) {
try {
List<int> rawData = r.advertisementData.manufacturerData[0xFFED]!;
BleDeviceData deviceData = parseBleData(rawData);
deviceData.name = r.advertisementData.advName;
deviceData.rssi = r.rssi;
deviceData.mac = deviceData.deviceId.replaceAll(':', '');
parsedDeviceList.add(deviceData);
// if (deviceData.mac!.toLowerCase() == 'b43a45c3dfa0') {
// print('匹配设备数据: ${deviceData.mac}-->sn:${deviceData.sn}');
// }
} catch (e) {
print("设备数据解析失败: $e");
}
}
// 判断是否更新
setState(() {
bool hasChanges = false;
if (blueteethBindController.model.devicelist?.length !=
parsedDeviceList.length) {
hasChanges = true;
} else {
for (int i = 0;
i < blueteethBindController.model.devicelist!.length;
i++) {
if (blueteethBindController.model.devicelist![i].mac !=
parsedDeviceList[i].mac ||
blueteethBindController.model.devicelist![i].rssi !=
parsedDeviceList[i].rssi) {
hasChanges = true;
break;
}
}
}
if (hasChanges) {
blueteethBindController.model.devicelist = parsedDeviceList;
blueteethBindController.model.blelist = filteredResults;
}
});
});
// 等待扫描完成
await Future.delayed(Duration(seconds: 10));
// await Future.delayed(Duration(minutes: 30));
await FlutterBluePlus.stopScan();
if (mounted) {
setState(() {
isScanning = false;
});
}
// print("扫描完成");
}
}
// 定时每10秒进行一次扫描
void _startPeriodicScan() {
_timer = Timer.periodic(Duration(seconds: 10), (timer) {
if (!isScanning) {
_startScanning(); // 调用扫描函数
}
});
}
// 停止扫描
void _stopScanning() {
if (isScanning) {
FlutterBluePlus.stopScan();
_scanSubscription?.cancel(); // 取消订阅
if (mounted) {
setState(() {
isScanning = false;
});
}
}
}
// 停止定时扫描
void _stopPeriodicScan() {
_timer?.cancel();
}
StreamSubscription<List<ScanResult>>? _scanSubscription; // 添加扫描订阅变量
@override
void dispose() {
_stopPeriodicScan(); // 停止定时扫描
_stopScanning(); // 停止扫描
_scanSubscription?.cancel(); // 取消扫描订阅
connectTimer?.cancel(); // 取消连接定时器
blueteethBindController.stopStatusPolling(); // 停止状态轮询
blueteethBindController.model.blelist = [];
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, boxConstraints) => GestureDetector(
// onTap: () => FocusScope.of(context).unfocus(),,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(getBackgroundImageNoImage()), // 本地图片
fit: BoxFit.fill, // 填满整个 Container
),
),
child: Scaffold(
backgroundColor: Colors.transparent, // 加上这一行
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, // 状态栏背景色
statusBarIconBrightness: Brightness.light, // 图标颜色Android
statusBarBrightness: Brightness.light, // 图标颜色iOS
),
iconTheme: IconThemeData(color: themeController.currentColor.sc3),
backgroundColor: themeController.currentColor.sc5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Container(
width: double.infinity,
height: 180.rpx,
child: Stack(
alignment: Alignment.center,
children: [
Text(
'蓝牙绑定.标题'.tr,
style: TextStyle(
fontFamily: 'Readex Pro',
color: themeController.currentColor.sc3,
letterSpacing: 0,
fontSize: 30.rpx,
),
),
Positioned(
left: 0,
child: returnIconButtom,
),
],
),
),
actions: [],
centerTitle: false,
),
body: SafeArea(
top: true,
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(0, 30.rpx, 0, 0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(20.rpx),
),
child: Align(
alignment: AlignmentDirectional(0, 0),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 30.rpx, 0, 30.rpx),
child: Text(
'蓝牙绑定.扫描'.tr,
style: TextStyle(
fontFamily: 'Inter',
color: Color(0xFFE8EEF3),
fontSize: 26.rpx,
letterSpacing: 0.0,
),
),
),
),
),
),
Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(20.rpx),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
21.rpx, 5.rpx, 21.rpx, 5.rpx),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'最小信号强度'.tr,
style: TextStyle(
fontFamily: 'Inter',
// color: stringToColor("#003058"),
color: Colors.white,
fontSize: 26.rpx,
letterSpacing: 0.0,
),
),
Expanded(
child: Obx(() {
return Slider(
activeColor: Color(0xFF1FCC9B),
inactiveColor: Colors.white,
min: -100,
max: 50,
value: blueteethBindController.model.singal!,
onChanged: (newValue) {
newValue = double.parse(
newValue.toStringAsFixed(0));
blueteethBindController.model.singal =
newValue;
_startScanning();
blueteethBindController.updateAll();
},
);
}),
),
Obx(() {
return Text(
'${blueteethBindController.model.singal!.toInt()}',
style: TextStyle(
fontFamily: 'Inter',
color: Color(0xFFE4E8EB),
fontSize: 26.rpx,
letterSpacing: 0.0,
),
);
}),
].divide(SizedBox(width: 30.rpx)),
),
),
),
Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc3,
borderRadius: BorderRadius.circular(20.rpx),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
35.rpx, 0, 35.rpx, 0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0, 0),
child: Container(
width: 25.rpx,
height: 25.rpx,
// width: double.infinity,
decoration: BoxDecoration(),
child: SvgPicture.asset(
'assets/img/icon/query.svg',
fit: BoxFit.cover,
color: stringToColor("#333333"), //固定
),
),
),
Expanded(
child: Container(
width: 100.rpx,
height: 80.rpx,
decoration: BoxDecoration(
color: Colors.white,
),
child: Align(
alignment: AlignmentDirectional(-1, 0),
child: TextFormField(
initialValue: blueteethBindController
.search.value,
onChanged: (Value) {
blueteethBindController
.search.value = Value;
},
autofocus: false,
obscureText: false,
decoration: InputDecoration(
isDense: true,
labelStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 26.rpx,
letterSpacing: 0.0,
),
hintText: '蓝牙绑定.搜索提示'.tr,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 26.rpx,
letterSpacing: 0.0,
color: themeController
.currentColor.sc4,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0x00000000),
width: 1.rpx,
),
borderRadius:
BorderRadius.circular(8.rpx),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0x00000000),
width: 1.rpx,
),
borderRadius:
BorderRadius.circular(8.rpx),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.rpx,
),
borderRadius:
BorderRadius.circular(8.rpx),
),
focusedErrorBorder:
OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.rpx,
),
borderRadius:
BorderRadius.circular(8.rpx),
),
filled: false,
fillColor: themeController
.currentColor.sc22,
),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 26.rpx,
letterSpacing: 0.0,
),
cursorColor:
themeController.currentColor.sc3,
),
),
),
),
].divide(SizedBox(width: 6.rpx)),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
26.rpx, 0, 0, 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(
height: 50.rpx,
child: VerticalDivider(
thickness: 2.rpx,
color: stringToColor("#333333"), //固定
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor:
themeController.currentColor.sc4,
borderRadius: 6.rpx,
padding: EdgeInsets.zero,
// onTap: () async {
// blueteethBindController
// .model.betDevicelist;
// blueteethBindController.search.value;
// blueteethBindController.updateAll();
// },
onTap: () async {
// final searchKey = blueteethBindController
// .search.value
// .trim();
// if (searchKey.isNotEmpty) {
// final filtered = blueteethBindController
// .model.betDevicelist!
// .where((device) {
// final name =
// device.name?.toLowerCase() ?? '';
// final mac =
// device.mac?.toLowerCase() ?? '';
// return name.contains(
// searchKey.toLowerCase()) ||
// mac.contains(
// searchKey.toLowerCase());
// }).toList();
// // 替换原始列表
// blueteethBindController
// .model.betDevicelist!
// ..clear()
// ..addAll(filtered);
// }
// blueteethBindController.updateAll();
_startScanning();
},
child: Text(
'搜索'.tr,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 30.rpx,
letterSpacing: 0.0,
color: stringToColor("#333333"), //固定
),
),
),
].divide(SizedBox(width: 26.rpx)),
),
),
],
),
),
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0, 60.rpx, 0, 32.rpx),
child: Container(
width: double.infinity,
decoration: BoxDecoration(),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(19.rpx, 0, 0, 0),
child: Obx(() {
return Text(
// '匹配出的外围设备'.tr +
// "${blueteethBindController.model.betDevicelist!.length}",
'匹配出的外围设备'.tr +
"${blueteethBindController.model.betDevicelist!.length}",
style: TextStyle(
fontFamily: 'Inter',
fontSize: 30.rpx,
letterSpacing: 0.0,
color: themeController.currentColor.sc3,
),
);
}),
),
),
),
// Obx(() {
// if (blueteethBindController
// .model.betDevicelist!.isNotEmpty) {
// return Expanded(
// child: Container(
// width: double.infinity,
// child: SingleChildScrollView(
// child: Column(
// mainAxisSize: MainAxisSize.max,
// children: [
// ...blueteethBindController.model.blelist!
// .map((device) {
// return SingleBlueteethDeviceCompoentWidget(
// // device: device,
// bleDevice: device,
// );
// })
// .toList()
// .divide(SizedBox(height: 30.rpx))
// .addToEnd(SizedBox(height: 30.rpx)),
// ],
// ),
// ),
// ),
// );
// }
// return Container();
// }),
Obx(() {
if (blueteethBindController
.model.betDevicelist!.isNotEmpty) {
// 对 blelist 进行排序(按 rssi 降序)
final sortedList = [
...blueteethBindController.model.blelist!
];
sortedList.sort((a, b) {
final rssiA = a.rssi ?? -999; // 防止空值
final rssiB = b.rssi ?? -999;
return rssiB.compareTo(rssiA); // rssi 大的排前面
});
return Expanded(
child: Container(
width: double.infinity,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
...sortedList
.map((device) {
return SingleBlueteethDeviceCompoentWidget(
bleDevice: device,
);
})
.toList()
.divide(SizedBox(height: 30.rpx))
.addToEnd(SizedBox(height: 30.rpx)),
],
),
),
),
);
}
return Container();
}),
].divide(SizedBox(height: 30.rpx)),
),
),
),
),
),
),
);
}
_showBluetoothNotEnabledDialog() async {
await showTipDialog(
context,
Column(
children: [
Text(
"蓝牙未开启".tr,
style: TextStyle(
fontSize: AppConstants().title_text_fontSize,
color: themeController.currentColor.sc3),
),
SizedBox(
height: 20.rpx,
),
Text(
"请先打开蓝牙在进行设备扫描".tr,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: themeController.currentColor.sc3),
),
],
));
}
}
BleDeviceData parseBleData(List<int> data) {
if (data.length < 18) {
throw Exception('BLE广播数据长度不足18字节');
}
int type = data[0];
int sn = data[1];
// 设备唯一ID (6字节),格式化为 MAC 地址样式
String deviceId =
List.generate(6, (i) => data[2 + i].toRadixString(16).padLeft(2, '0'))
.join(":")
.toUpperCase();
int bre = data[8];
int ht = data[9];
int active = data[10];
int flag = data[11];
// version 是4字节 uint大端字节序
int version =
(data[12] << 24) | (data[13] << 16) | (data[14] << 8) | data[15];
// qsn 是2字节 ushort大端字节序
int qsn = (data[16] << 8) | data[17];
return BleDeviceData(
type: type,
sn: sn,
deviceId: deviceId,
bre: bre,
ht: ht,
active: active,
flag: flag,
version: version,
qsn: qsn,
);
}