Files
tuiche/lib/pages/mh_page/device/mht_blueteeth_device_page.dart
2025-09-02 14:03:00 +08:00

815 lines
30 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 'dart:typed_data';
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/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/TopSlideNotification.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 = "";
BluetoothHelper.listenBluetoothState((isOn) {
mhtBlueToothController.model.bluetooth = isOn;
mhtBlueToothController.updateAll();
if (!isOn && !_isDialogShowing) {
_isDialogShowing = true;
mhtBlueToothController.model.blueRawData = [];
mhtBlueToothController.model.deviceDataStatus = [];
mhtBlueToothController.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();
}
}
Future<void> _requestBluetoothPermission() 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) {
TopSlideNotification.show(context,
text: "应用需要蓝牙和位置权限才能扫描设备。请授予权限。".tr,
textColor: themeController.currentColor.sc9);
}
void _startScanning() async {
try {
if (!mounted || isScanning || !mhtBlueToothController.shouldScan.value)
return;
_scanSubscription?.cancel();
// var bluetoothState = await FlutterBluePlus.isOn;
// bool isOn = await BluetoothHelper.isBluetoothOn();
// mhtBlueToothController.model.bluetooth = isOn;
mhtBlueToothController.updateAll();
if (!mhtBlueToothController.model.bluetooth!) 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.map((r) {
Map<String, dynamic> d = {
"updateTime": DateTime.now().millisecondsSinceEpoch,
"name": r.advertisementData.localName?.trim() ??
r.advertisementData.advName?.trim() ??
r.device.name ??
"",
"rssi": r.rssi,
"device": r.device,
"connectable": r.advertisementData.connectable
};
// 从 manufacturerData 解析设备唯一 ID
Map<int, List<int>> m_d = r.advertisementData.manufacturerData;
String? deviceId;
m_d.forEach((key, value) {
if (value == null || value.isEmpty) return;
if (key == 65517) {
List<int> a = [0, 0, ...value];
advertisDataFormatter(a, d); // 你原来的处理
if (d['adData']?['deviceId'] != null)
deviceId = d['adData']['deviceId'];
} else if (key == 11125 && value.length == 8) {
deviceId = ab2str(value.sublist(2, 8)).toUpperCase();
} else if (value.length == 8 && isQuanShiDevice(d["name"])) {
deviceId = ab2str(value.sublist(2, 8)).toUpperCase();
} else if ((value.length == 4 || value.length == 6) &&
isMHTSWES(d["name"])) {
List<int> a;
if (value.length == 4) {
ByteData bd = ByteData(2);
bd.setUint16(0, key, Endian.little);
a = [bd.getUint8(0), bd.getUint8(1), ...value];
} else {
a = [...value];
}
deviceId = ab2str(a).toUpperCase();
}
});
d['id'] = deviceId ?? r.device.remoteId.str; // fallback UUID
return BlueToothDataModel.fromScanResult(
r,
widget.deviceType['type']?.toInt(),
bind: false,
name: d['name'],
mac: d['id'],
);
}).where((d) {
// 信号强度过滤
if (d.scanResult.rssi <= signalThreshold) return false;
// 搜索关键字过滤
if (searchKey.isNotEmpty) {
String name = d.name.toLowerCase();
String mac = d.mac.toLowerCase();
if (!name.contains(searchKey) && !mac.contains(searchKey))
return false;
}
// 名称过滤规则,必须包含 deviceType['reg'] 列表中的某个字符串
List<String> regList =
widget.deviceType['reg']?.cast<String>() ?? [];
if (regList.isNotEmpty) {
String lowerName = d.name.toLowerCase();
bool match =
regList.any((reg) => lowerName.contains(reg.toLowerCase()));
if (!match) return false;
}
return true;
}).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 = [];
BluetoothHelper.cancelListener();
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)),
),
),
),
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;
});
}
}
String getDeviceId(ScanResult result) {
// AndroidremoteId 就是 MAC
if (Platform.isAndroid) {
return result.device.remoteId.str.replaceAll(':', '').toUpperCase();
}
// iOS尝试从 manufacturerData 里解析
Map<int, List<int>> mData = result.advertisementData.manufacturerData;
for (var key in mData.keys) {
List<int>? bytes = mData[key];
if (bytes != null && bytes.length == 6) {
// 假设这 6 个字节就是 MAC
return bytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('')
.toUpperCase();
}
}
return result.device.remoteId.str.toUpperCase();
}
}
void advertisDataFormatter(var a, item) {
Map<String, dynamic> obj = {};
try {
if (a[2] == 1) {
obj['sn'] = a[3];
obj['deviceId'] = ab2str(a.sublist(4, 10)).toUpperCase();
obj['b'] = a[10];
obj['h'] = a[11];
obj['t'] = a[12];
item['adData'] = obj;
} else if (a[2] == 2) {
obj['sn'] = a[3];
obj['deviceId'] = ab2str(a.sublist(4, 10)).toUpperCase();
obj['b'] = a[10];
obj['h'] = a[11];
obj['t'] = a[12];
obj['net'] = (a[13] & 1) == 1 ? '在线' : '离线';
obj['flag'] = (a[13] & 2) == 2 ? '异常' : '正常';
ByteData byteData = ByteData.sublistView(
Uint8List.fromList(a.sublist(14, 18).reversed.toList()));
obj['version'] = byteData.getUint32(0);
item['adData'] = obj;
} else if (a[2] == 3) {
List<String> otherstr = [];
obj['sn'] = a[3];
obj['deviceId'] = ab2str(a.sublist(4, 10)).toUpperCase();
obj['b'] = a[10];
obj['h'] = a[11];
obj['t'] = a[12];
obj['net'] = (a[13] & 1) == 1 ? '在线' : '离线';
obj['flag'] = (a[13] & 2) == 2 ? '异常' : '正常';
if ((a[13] & 4) == 4) {
otherstr.add('呼吸暂停');
}
if ((a[13] & 8) == 8 && (a[13] & 1) == 1) {
obj['isbed'] = '在床';
} else {
obj['isbed'] = '离床';
}
if ((a[13] & 16) == 16) {
otherstr.add('授权过期');
}
if ((a[13] & 64) == 64) {
otherstr.add('设备休眠');
}
obj['other'] = otherstr.join('');
ByteData byteData = ByteData.sublistView(
Uint8List.fromList(a.sublist(14, 18).reversed.toList()));
obj['version'] = byteData.getUint32(0);
ByteData qsnData =
ByteData.sublistView(Uint8List.fromList(a.sublist(17, 19)));
obj['qsn'] = qsnData.getUint16(0) * 256 + obj['sn'];
item['adData'] = obj;
} else if (a.length > 17) {
obj['sn'] = a[3];
obj['deviceId'] = ab2str(a.sublist(4, 10)).toUpperCase();
obj['b'] = a[10];
obj['h'] = a[11];
obj['t'] = a[12];
obj['net'] = (a[13] & 1) == 1 ? '在线' : '离线';
obj['flag'] = (a[13] & 2) == 2 ? '异常' : '正常';
ByteData byteData = ByteData.sublistView(
Uint8List.fromList(a.sublist(14, 18).reversed.toList()));
obj['version'] = byteData.getUint32(0);
item['adData'] = obj;
}
} catch (e) {
print(e);
}
}
bool isQuanShiDevice(name) {
return "$name".contains("S4-ZM-M94-4") ||
"$name".contains("S4-ZM-N94-4") ||
"$name".contains("MHT-SWES-D");
}
bool isMHTSWES(name) {
return "$name".contains("MHT-") ||
"$name".contains("MHT-SWES-M") ||
"$name".contains("MHT-SWES-S");
}