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 createState() => _BlueteethDevicePageState(); } class _BlueteethDevicePageState extends State { GlobalController globalController = Get.find(); UserInfoController userInfoController = Get.find(); BlueteethBindController blueteethBindController = Get.find(); ThemeController themeController = Get.find(); late FlutterBluePlus flutterBlue; // 声明 flutterBlue 实例 List 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().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 _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 _requestBluetoothPermission(BuildContext context) async { Map 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 = []; for (var r in filteredResults) { try { List 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>? _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 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, ); }