Files
tuiche/lib/pages/mh_page/device/mht_blueteeth_device_page.dart
2025-08-12 17:22:11 +08:00

857 lines
37 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 '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';
import 'package:vbvs_app/common/color/appConstants.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/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/pages/common/selectDialog.dart';
import 'package:vbvs_app/pages/mh_page/component/mht_bind_dialog.dart';
import 'package:vbvs_app/pages/mh_page/device/component/DeviceComponentWidget.dart';
import 'package:vbvs_app/pages/mh_page/device/controller/mht_bluetooth_controller.dart';
import 'package:vbvs_app/pages/mh_page/device/model/BlueToothDataModel.dart';
import 'package:vbvs_app/pages/mh_page/searchWidget.dart';
class MHTBlueteethDevicePage extends StatefulWidget {
var deviceType;
MHTBlueteethDevicePage({super.key, required this.deviceType});
@override
State<MHTBlueteethDevicePage> createState() => _MHTBlueteethDevicePageState();
}
class _MHTBlueteethDevicePageState extends State<MHTBlueteethDevicePage> {
MHTBlueToothController mhtBlueToothController = Get.find();
GlobalController globalController = Get.find();
UserInfoController userInfoController = Get.find();
ThemeController themeController = Get.find();
late FlutterBluePlus 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 = ["", "", ""];
StreamSubscription<List<ScanResult>>? _scanSubscription;
@override
void initState() {
super.initState();
mhtBlueToothController.model.blueRawData = [];
mhtBlueToothController.model.deviceDataStatus = [];
flutterBlue = FlutterBluePlus();
_checkBluetoothPermission();
mhtBlueToothController.startStatusPolling();
mhtBlueToothController.search.value = "";
mhtBlueToothController.currentDeviceMac?.value = "";
}
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();
}
}
// Future<void> _requestBluetoothPermission() async {
// // showPermissionInfoDialog(Get.context!, CommonVariables().permissionInfo);
// // Map<Permission, PermissionStatus> statuses = await [
// // Permission.bluetoothScan,
// // Permission.bluetoothConnect,
// // Permission.location,
// // ].request();
// Map<Permission, PermissionStatus> statuses = {};
// try {
// // 1⃣ 先显示自定义提示弹窗
// showPermissionInfoDialog(Get.context!, CommonVariables().permissionInfo);
// // 2⃣ 等待一小会,让 UI 有时间渲染提示弹窗
// await Future.delayed(const Duration(milliseconds: 300));
// // 3⃣ 再去申请权限(系统权限弹窗可能会弹出)
// statuses = await [
// Permission.bluetoothScan,
// Permission.bluetoothConnect,
// Permission.location,
// ].request();
// // 这里你可以检查每个权限的状态
// ef.log("权限状态: $statuses");
// } catch (e) {
// ef.log("申请权限出错: $e");
// } finally {
// // 4⃣ 无论成功失败,最后关闭提示弹窗
// if (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();
// }
// }
Future<void> _requestBluetoothPermission() async {
Map<Permission, PermissionStatus> statuses = {};
bool dialogShown = false; // 标记是否弹过权限提示弹窗
try {
// 检查是否已授权
bool alreadyGranted = await Permission.bluetoothScan.isGranted &&
await Permission.bluetoothConnect.isGranted &&
await Permission.location.isGranted;
if (!alreadyGranted) {
// 弹出自定义提示
showPermissionInfoDialog(
Get.context!, CommonVariables().permissionInfo);
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();
}
}
void _showPermissionDeniedDialog() {
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 _startScanning() async {
try {
if (!mounted || isScanning || !mhtBlueToothController.shouldScan.value)
return;
_scanSubscription?.cancel();
var bluetoothState = await FlutterBluePlus.isOn;
mhtBlueToothController.model.bluetooth = bluetoothState;
mhtBlueToothController.updateAll();
if (!bluetoothState && !_isDialogShowing) {
_isDialogShowing = true;
mhtBlueToothController.model.blueRawData = [];
mhtBlueToothController.model.deviceDataStatus = [];
mhtBlueToothController.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 = mhtBlueToothController.model.singal!;
final searchKey =
mhtBlueToothController.search.value.trim().toLowerCase();
final filteredResults = results.where((r) {
final localName = r.advertisementData.localName;
final isTarget = r.rssi > signalThreshold &&
isTargetDevice(
localName, widget.deviceType['reg'].cast<String>());
if (!isTarget) return false;
final name = r.advertisementData.advName.toLowerCase();
String macAddress = r.device.remoteId.str;
final mac = macAddress.replaceAll(':', '');
final search = searchKey.trim().replaceAll(':', '').toLowerCase();
if (search.isNotEmpty &&
!name.contains(search) &&
!mac.replaceAll(':', '').toLowerCase().contains(search)) {
return false;
}
return true;
}).map((r) {
return BlueToothDataModel.fromScanResult(
r, widget.deviceType['type']?.toInt(),
bind: false,
name: r.advertisementData.localName,
mac: r.device.remoteId.str.replaceAll(':', ''));
}).toList();
final currentDevices = mhtBlueToothController.model.blueRawData ?? [];
final newDevices = <BlueToothDataModel>[];
for (var newDevice in filteredResults) {
// 检查设备是否已存在
final existingIndex =
currentDevices.indexWhere((d) => d.mac == newDevice.mac);
if (existingIndex >= 0) {
// 更新已有设备信息(如信号强度)
currentDevices[existingIndex] = newDevice;
} else {
// 添加新设备
newDevices.add(newDevice);
}
}
setState(() {
mhtBlueToothController.model.blueRawData = [
...currentDevices,
...newDevices
];
});
});
await Future.delayed(Duration(seconds: 10));
await FlutterBluePlus.stopScan();
if (mounted) {
setState(() {
isScanning = false;
});
}
}
} catch (e) {
ef.log("$e");
} finally {
setState(() {
isScanning = false;
});
}
}
void _startPeriodicScan() {
_timer = Timer.periodic(Duration(seconds: 3), (timer) {
if (mhtBlueToothController.shouldScan.value && !isScanning) {
_removeOldDevices(); // 先清理老旧设备
_startScanning();
}
});
}
void _stopScanning() {
if (isScanning) {
FlutterBluePlus.stopScan();
_scanSubscription?.cancel();
if (mounted) {
setState(() {
isScanning = false;
});
}
}
}
void _stopPeriodicScan() {
_timer?.cancel();
}
@override
void dispose() {
_stopPeriodicScan();
_stopScanning();
_scanSubscription?.cancel();
connectTimer?.cancel();
mhtBlueToothController.stopStatusPolling();
mhtBlueToothController.model.blueRawData = [];
mhtBlueToothController.model.deviceDataStatus = [];
super.dispose();
}
bool isTargetDevice(String? name, List<String> keywords) {
if (name == null) return false;
return keywords.any((k) => name.contains(k));
}
_showBluetoothNotEnabledDialog() async {
await showTipDialog(
backgroundColor: Colors.white,
context,
Column(
children: [
Text(
"蓝牙未开启".tr,
style: TextStyle(
fontSize: AppConstants().title_text_fontSize,
color: stringToColor("#333333"),
),
),
SizedBox(
height: 20.rpx,
),
Text(
"请先打开蓝牙在进行设备扫描".tr,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: stringToColor("#333333"),
),
),
],
));
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, boxConstraints) => GestureDetector(
// onTap: () => FocusScope.of(context).unfocus(),,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/new_background.png'),
fit: BoxFit.fill,
),
),
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
appBar: AppBar(
iconTheme: IconThemeData(color: themeController.currentColor.sc3),
backgroundColor: Colors.transparent,
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: [
Expanded(
child: Column(
children: [
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0, 30.rpx, 0, 0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
stringToColor("#003058").withOpacity(0.8),
stringToColor("#003058").withOpacity(0.8),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(20.rpx),
),
child: Align(
alignment: AlignmentDirectional(0, 0),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 30.rpx, 0, 30.rpx),
child: Obx(() {
return Text(
(mhtBlueToothController.model.bluetooth ==
null ||
mhtBlueToothController
.model.bluetooth ==
false)
? "等待扫描".tr
: '扫描中'.tr,
style: TextStyle(
fontFamily: 'Inter',
color: stringToColor("#FFFFFF"),
fontSize: 26.rpx,
letterSpacing: 0.0,
),
);
}),
),
),
),
),
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
// stringToColor("FCFCFC"),
// stringToColor("CEECE3")
Colors.transparent,
Colors.transparent,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
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("#FFFFFF"),
fontSize: 26.rpx,
letterSpacing: 0.0,
),
),
Expanded(
child: Obx(() {
return Slider(
activeColor: Color(0xFF1FCC9B),
inactiveColor: Colors.white,
min: -100,
max: 50,
value:
mhtBlueToothController.model.singal!,
onChanged: (newValue) {
newValue = double.parse(
newValue.toStringAsFixed(0));
mhtBlueToothController.model.singal =
newValue;
_startScanning();
mhtBlueToothController.updateAll();
},
);
}),
),
Obx(() {
return Text(
'${mhtBlueToothController.model.singal!.toInt()}',
style: TextStyle(
fontFamily: 'Inter',
color: stringToColor("#FFFFFF"),
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,
// // decoration: BoxDecoration(),
// // child: SvgPicture.asset(
// // 'assets/img/icon/query.svg',
// // fit: BoxFit.cover,
// // color: stringToColor("#333333"),
// // ),
// // ),
// // ),
// // Expanded(
// // child: Container(
// // width: 100.rpx,
// // height: 90.rpx,
// // decoration: BoxDecoration(
// // color: Colors.white,
// // ),
// // child: Align(
// // alignment:
// // AlignmentDirectional(-1, 0),
// // child: TextFormField(
// // initialValue:
// // mhtBlueToothController
// // .search.value,
// // onChanged: (Value) {
// // mhtBlueToothController
// // .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,
// // ),
// // 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.red,
// // width: 1.rpx,
// // ),
// // borderRadius:
// // BorderRadius.circular(
// // 8.rpx),
// // ),
// // focusedErrorBorder:
// // OutlineInputBorder(
// // borderSide: BorderSide(
// // color: Colors.red,
// // 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:
// // stringToColor("#003058"),
// // ),
// // ),
// // ),
// // ),
// // ].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 {
// // _startScanning();
// // },
// // child: Text(
// // '搜索'.tr,
// // style: TextStyle(
// // fontFamily: 'Inter',
// // fontSize: 30.rpx,
// // letterSpacing: 0.0,
// // color: stringToColor("#333333"),
// // ),
// // ),
// // ),
// // ].divide(SizedBox(width: 26.rpx)),
// // ),
// // ),
// // ],
// // ),
// // ),
// child: Padding(
// padding:
// EdgeInsetsDirectional.fromSTEB(0, 10, 0, 23),
// child: SearchWidget(
// keyword: mhtBlueToothController.search.value,
// color: Colors.red,
// hint: "检索设备",
// onChange: (d) {
// mhtBlueToothController.search.value = d;
// },
// findCallback: () {
// // controller.getDeviceList();
// },
// ),
// ),
// ),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
child: SearchWidget(
padding: EdgeInsets.all(0),
keyword: mhtBlueToothController.search.value,
color: Colors.red,
hint: "检索设备".tr,
onChange: (d) {
mhtBlueToothController.search.value = d;
},
findCallback: () {
_startScanning();
},
),
),
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 +
"${mhtBlueToothController.model.deviceDataStatus!.length}",
style: TextStyle(
fontFamily: 'Inter',
fontSize: 30.rpx,
letterSpacing: 0.0,
color: themeController.currentColor.sc3,
),
);
}),
),
),
),
Obx(() {
if (mhtBlueToothController
.model.deviceDataStatus!.isNotEmpty) {
final sortedList = mhtBlueToothController
.model.deviceDataStatus!
.toList()
..sort((a, b) => b.scanResult.rssi
.compareTo(a.scanResult.rssi));
return Expanded(
child: Container(
width: double.infinity,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
...sortedList
.map((device) {
return DeviceComponentWidget(
bleDevice: device,
deviceType: widget.deviceType,
);
})
.toList()
.divide(SizedBox(height: 30.rpx))
.addToEnd(SizedBox(height: 30.rpx)),
],
),
),
),
);
}
return Container();
}),
].divide(SizedBox(
height: 30.rpx,
)),
)),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0, 52.rpx, 0, 30.rpx),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.rpx),
border: Border.all(
color: themeController.currentColor.sc4
.withOpacity(0.5),
width: AppConstants().border_width,
),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 30.rpx, 30.rpx, 30.rpx),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 8.rpx, 0, 0),
child: Container(
width: 23.rpx,
height: 23.rpx,
decoration: BoxDecoration(),
child: SvgPicture.asset(
'assets/img/icon/tips.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
Expanded(
child: Text(
'蓝牙绑定提示'.tr,
style: TextStyle(
fontFamily: 'Inter',
color: themeController.currentColor.sc4,
fontSize: 26.rpx,
letterSpacing: 0.0,
),
),
),
].divide(SizedBox(width: 23.rpx)),
),
),
),
),
].divide(SizedBox(height: 30.rpx)),
),
),
),
),
),
),
);
}
void _removeOldDevices() {
final now = DateTime.now();
final currentDevices = mhtBlueToothController.model.blueRawData ?? [];
// 移除30秒内未出现的设备
final updatedDevices = currentDevices.where((device) {
return now.difference(device.lastSeen) < Duration(seconds: 30);
}).toList();
if (updatedDevices.length != currentDevices.length) {
setState(() {
mhtBlueToothController.model.blueRawData = updatedDevices;
});
}
}
}