import 'dart:async'; import 'package:EasyDartModule/EasyDartModule.dart' as edm; import 'package:easydevice/easydevice.dart'; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/color/ServiceConstant.dart'; import 'package:vbvs_app/common/color/app_uri_status.dart'; import 'package:vbvs_app/common/util/DailyLogUtils.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; import 'package:vbvs_app/common/util/requestWithLog.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/component/tool/CustomCard.dart'; import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/model/BleDeviceData.dart'; import 'package:vbvs_app/model/api_response.dart'; import 'package:vbvs_app/pages/mh_page/component/mht_bind_dialog.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/homepage/controller/mht_home_controller.dart'; import 'package:vbvs_app/pages/mh_page/test/WebviewTestModel.dart'; class DeviceComponentWidget extends StatefulWidget { BlueToothDataModel bleDevice; var deviceType; DeviceComponentWidget({ super.key, required this.bleDevice, required this.deviceType, }); @override State createState() => _DeviceComponentWidgetState(); } class _DeviceComponentWidgetState extends State { ThemeController themeController = Get.find(); MHTBlueToothController blueteethBindController = Get.find(); MHTHomeController homeController = Get.find(); var lisObj; @override Widget build(BuildContext context) { Map device = { "name": widget.bleDevice.name, "mac": widget.bleDevice.mac, "rssi": widget.bleDevice.scanResult.rssi, "bind": widget.bleDevice.bind, }; return ClickableContainer( backgroundColor: Colors.white, highlightColor: Colors.white, borderRadius: 20.rpx, padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 36.rpx, 55.rpx, 52.rpx), onTap: () async {}, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ Row( children: [ Text( "${device['name']}", style: TextStyle( color: stringToColor("#333333"), fontSize: 30.rpx), ), SizedBox( width: 10.rpx, ), Obx(() { if (blueteethBindController.currentDeviceMac.value == device['mac']) { return SizedBox( width: 24.rpx, height: 24.rpx, child: CircularProgressIndicator( strokeWidth: 1, valueColor: AlwaysStoppedAnimation( stringToColor("#929699")), ), ); } return Container(); }), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: MediaQuery.sizeOf(context).width * 0.14, constraints: BoxConstraints(minWidth: 106.rpx), child: Text( "MAC".tr, style: TextStyle( color: stringToColor("#929699"), fontSize: 26.rpx, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Text( "${device['mac']}", style: TextStyle( fontSize: 26.rpx, color: stringToColor("#333333")), ), ].divide(SizedBox(width: 33.rpx)), ), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: MediaQuery.sizeOf(context).width * 0.14, constraints: BoxConstraints(minWidth: 106.rpx), child: Text( "信号强度".tr, style: TextStyle( color: stringToColor("#929699"), fontSize: 26.rpx, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Text( "${device['rssi']}" + "dBm".tr, style: TextStyle( fontSize: 26.rpx, color: stringToColor("#333333")), ), ].divide(SizedBox(width: 33.rpx)), ), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: MediaQuery.sizeOf(context).width * 0.14, constraints: BoxConstraints(minWidth: 106.rpx), child: Text( "设备状态".tr, style: TextStyle( color: stringToColor("#929699"), fontSize: 26.rpx, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Text( "${device['bind'] == true ? "已被绑定" : "可绑定"}", style: TextStyle( fontSize: 26.rpx, color: device['bind'] == true ? stringToColor("#FF7159") : stringToColor("#6BFDAC"), ), ), ].divide(SizedBox(width: 33.rpx)), ), ].divide(SizedBox(height: 37.rpx)), ), ), CustomCard( borderRadius: 16.rpx, onTap: () async { try { //连接前暂停扫描 blueteethBindController.pauseScanning(); if (blueteethBindController.currentDeviceMac?.value != null && blueteethBindController .currentDeviceMac!.value.isNotEmpty) { if (blueteethBindController.currentDeviceMac?.value != device['mac']) { showConfirmDialog( context, Container(), "其他设备正在绑定中,是否终止其他设备绑定?".tr, onConfirm: () { blueteethBindController.currentDeviceMac.value = ""; blueteethBindController.updateAll(); }, onCancel: () { blueteethBindController.resumeScanning(); }); } } blueteethBindController.currentDeviceMac?.value = device['mac']!; blueteethBindController.updateAll(); if (device['bind'] == true) { await showHaveBindDialog(context); blueteethBindController.currentDeviceMac.value = ""; blueteethBindController.resumeScanning(); } else { showConfirmDialog( context, Container(), '是否确认绑定?'.tr, onConfirm: () async { //连接蓝牙 ,获取设备信息 blueteethBindController.currentDeviceMac.value = widget.bleDevice.mac; blueteethBindController.updateAll(); String mac = await getBindTHMAC( context, widget.bleDevice, widget.deviceType); if (mac != null && mac.isNotEmpty) { bool flag = await fillTHMac(mac, widget.bleDevice, context); if (!flag) { return; } } blueteethBindController.currentFullDevice = widget.bleDevice; ApiResponse response = await blueteethBindController .bindDeviceAndMAC(widget.bleDevice, context); TopSlideNotification.show(context, text: response.msg!); if (response.code == HttpStatusCodes.ok) { try { WebviewTestController webviewTestController = Get.find(); webviewTestController.web.jsbridge?.dart .unBindDevice(); } catch (e) { ef.log("[h5]通知列表更新报错:$e"); } homeController.getPersonList(); //请求绑定设备列表 // homeController.getSleepReport(); homeController.getDeviceNum().then((apiResponse) { if (apiResponse.code != HttpStatusCodes.ok) { TopSlideNotification.show( Get.context!, text: apiResponse.msg!, textColor: themeController.currentColor.sc9, ); } }); homeController .getDeviceList(group: 'room') .then((apiResponse) { if (apiResponse.code != HttpStatusCodes.ok) { TopSlideNotification.show( Get.context!, text: apiResponse.msg!, textColor: themeController.currentColor.sc9, ); } else { //请求睡眠报告 // deviceController.getSleepReport(); } }); //更新设备绑定流程 Get.toNamed("/mHTwifiPage", arguments: widget.bleDevice); THapp bledevice = THapp( device: widget.bleDevice.scanResult.device); blueteethBindController.currentDevice = bledevice; blueteethBindController.currentDeviceMac.value = ""; } else { blueteethBindController.resumeScanning(); blueteethBindController.currentDeviceMac.value = ""; blueteethBindController.updateAll(); TopSlideNotification.show( context, text: response.msg ?? "蓝牙绑定.连接异常".tr, textColor: themeController.currentColor.sc9, ); } blueteethBindController.resumeScanning(); }, onCancel: () { print('用户点击了取消'); blueteethBindController.currentDeviceMac.value = ""; blueteethBindController.resumeScanning(); blueteethBindController.updateAll(); }, ); } } catch (e) { Navigator.pop(context); TopSlideNotification.show( context, text: "连接异常".tr, textColor: themeController.currentColor.sc9, ); edm.EasyDartModule.logger.info("连接异常: $e"); DailyLogUtils.writeLog("连接异常: $e"); } finally { // 确保在任何情况下都恢复扫描 // if (blueteethBindController // .currentDeviceMac.value.isEmpty) { // blueteethBindController.resumeScanning(); // } blueteethBindController.resumeScanning(); } }, colors: [stringToColor("1592AA"), stringToColor("006FA3")], child: Container( width: 150.rpx, height: 90.rpx, alignment: Alignment.center, child: Text( "添加".tr, style: TextStyle(color: Colors.white, fontSize: 26.rpx), ), ), ), ], ), ].divide(SizedBox(height: 37.rpx)), ), ); } //更新设备绑定状态 void updateDeviceBindStatus(BleDeviceData device) { String serviceAddress = ServiceConstant.service_address; String serviceName = ServiceConstant.server_service; String serviceApi = ServiceConstant.user_setting; String mac = device.mac!; String type = "device_bind_status_${mac}"; String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; Map data = { "type": type, "mac": mac, "wifi": false, "celibration": false, "person_info": false, "time": DateTime.now().millisecondsSinceEpoch, }; requestWithLog( logTitle: "更新用户绑定流程", method: MyHttpMethod.put, queryUrl: queryUrl, data: data, onSuccess: (res) {}, onFailure: (res) {}, ); } Widget _buildDeviceInfoSection(device, BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( device.name ?? '蓝牙绑定.默认设备名称'.tr, style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFF6FAFD), fontSize: 30.rpx, letterSpacing: 0.0, ), ), Obx(() { if (blueteethBindController.currentDeviceMac.value == device.mac) { return SizedBox( width: 24.rpx, height: 24.rpx, child: CircularProgressIndicator( strokeWidth: 1, valueColor: AlwaysStoppedAnimation(Colors.white), ), ); } return Container(); }), ], ), ), Row( children: [ Text( "蓝牙绑定.信号强度".tr + ':${device.rssi ?? '-'}dBm', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFEBF2F8), fontSize: 26.rpx, letterSpacing: 0.0, ), ), SizedBox(width: 40.rpx), Text( "蓝牙绑定.SN".tr + ':${device.sn ?? '-'}', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFF5F9FD), fontSize: 26.rpx, letterSpacing: 0.0, ), ), ], ), Text( "蓝牙绑定.蓝牙地址".tr + ':${device.deviceId ?? '-'}', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFF6FAFD), fontSize: 26.rpx, letterSpacing: 0.0, ), ), Text( "蓝牙绑定.mac".tr + ':${device.mac ?? '-'}', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFF6FAFD), fontSize: 26.rpx, letterSpacing: 0.0, ), ), Row( children: [ Text( "蓝牙绑定.网络".tr + ':${device.isOnline == true ? '蓝牙绑定.在线'.tr : '蓝牙绑定.离线'.tr}', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFEBF2F8), fontSize: 26.rpx, letterSpacing: 0.0, ), ), SizedBox(width: 145.rpx), Row( children: [ Text( "蓝牙绑定.传感器".tr + ":", style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFEBF2F8), fontSize: 26.rpx, letterSpacing: 0.0, ), ), Text( device.bind == false ? '蓝牙绑定.可绑定'.tr : '蓝牙绑定.已被绑定'.tr, style: TextStyle( fontFamily: 'Inter', color: device.bind == false ? const Color(0xFF1AD2B5) : themeController.currentColor.sc9, fontSize: 26.rpx, letterSpacing: 0.0, ), ), ], ), ], ), Text( "版本".tr + '${device.version ?? '-'}', style: TextStyle( fontFamily: 'Inter', color: const Color(0xFFF6FAFD), fontSize: 26.rpx, letterSpacing: 0.0, ), ), ].divide(SizedBox( height: 37.rpx, )), ); } //获取智能床/床垫mac Future getBindTHMAC( BuildContext context, BlueToothDataModel device, Map deviceType) async { const int maxRetries = 2; const Duration timeout = Duration(seconds: 5); String? macAddress; try { // 连接设备 THapp bledevice = THapp(device: device.scanResult.device); await bledevice.connect(); var res2 = bledevice.isConnected; if (!res2) { edm.EasyDartModule.logger.error("蓝牙连接失败"); DailyLogUtils.printLog("蓝牙连接失败"); TopSlideNotification.show( context, text: "蓝牙连接失败".tr, textColor: themeController.currentColor.sc9, ); throw Exception("蓝牙连接失败"); } blueteethBindController.blueConnectFlag.value = 2; blueteethBindController.currentDevice = bledevice; await Future.delayed(Duration(seconds: 2)); if (deviceType['type'] == 3) { //智能床垫 macAddress = await getMacFromType3(bledevice, timeout); } else if (deviceType['type'] == 2) { //智能床 macAddress = await getMacFromType2(bledevice, timeout); } else { throw Exception("不支持的设备类型"); } if (macAddress == null) { throw Exception("未能获取到MAC地址"); } // device.macA = macAddress; print('MAC地址: $macAddress'); return macAddress; } catch (e) { blueteethBindController.currentDeviceMac.value = ""; edm.EasyDartModule.logger.error("蓝牙获取MAC失败:$e"); DailyLogUtils.printLog("蓝牙获取MAC失败:$e"); TopSlideNotification.show( context, text: "设备连接失败,请重试".tr, textColor: themeController.currentColor.sc9, ); rethrow; } } fillTHMac( String mac, BlueToothDataModel bleDevice, BuildContext context) async { bool flag = false; String serviceAddress = ServiceConstant.service_address; String serviceName = ServiceConstant.server_service; String serviceApi = ServiceConstant.get_bluetooth_device_status; String queryUrl = "$serviceAddress$serviceName$serviceApi"; String macParam = "mac=${Uri.encodeQueryComponent(mac.replaceAll(':', ''))}"; if (queryUrl.contains('?')) { queryUrl += '&$macParam'; } else { queryUrl += '?$macParam'; } await requestWithLog( logTitle: "获取设备状态", method: MyHttpMethod.get, queryUrl: queryUrl, onSuccess: (res) { flag = true; if (res.code != HttpStatusCodes.ok) return; if (res.data != null && res.data is List) { List responseList = res.data; // 查找当前MAC对应的数据 String macKey = mac.replaceAll(':', '').toUpperCase(); for (var item in responseList) { if (item['mac'].toString().toUpperCase() == macKey) { // 更新 bleDevice 的状态 //如果传感器已经绑定 暂时不处理 // bleDevice.bind = item['bind'] ?? bleDevice.bind; bleDevice.macA = item['mac']; if (item['bindMac'] != null && item['bindMac'].toString().isNotEmpty) { bleDevice.macB = item['bindMac']; } break; } } } }, onFailure: (res) { flag = false; TopSlideNotification.show(context, text: "获取设备状态失败".tr, textColor: themeController.currentColor.sc9); }, ); return flag; } Future getMacFromType3(THapp bledevice, Duration timeout) async { final read = bledevice.getresource('fff0/fff1'); await read!.characteristic.setNotifyValue(true); final write = bledevice.getresource('fff0/fff2'); const int maxRetries = 2; for (int attempt = 0; attempt < maxRetries; attempt++) { final completer = Completer(); final subscription = read.characteristic.onValueReceived.listen((data) { if (data.length >= 14) { completer.complete(parseMacFromBleResponse(data)); } }); final order = [ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0x40, 0x01, 0x01, 0x00, 0x45, 0xFD ]; await write!.characteristic.write(order); try { final mac = await completer.future.timeout(timeout); await subscription.cancel(); return mac; } catch (_) { await subscription.cancel(); if (attempt == maxRetries - 1) rethrow; } } throw Exception("获取MAC超时"); } Future getMacFromType2(THapp bledevice, Duration timeout) async { try { final read = bledevice.getresource('ffe0/ffe1'); await read!.characteristic.setNotifyValue(true); final write = bledevice.getresource('ffe0/ffe1'); // 与 read 同 characteristic const int maxRetries = 2; for (int attempt = 0; attempt < maxRetries; attempt++) { final completer = Completer(); final subscription = read.characteristic.onValueReceived.listen((data) { if (data.length >= 17) { completer.complete(parseMacFromTH2Response(data)); } }); final order = [0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x0C, 0x0B, 0x0A]; int checksum = order.reduce((a, b) => a + b) & 0xFFFF; order.add(checksum & 0xFF); // 低位 order.add((checksum >> 8) & 0xFF); // 高位 await write!.characteristic.write(order); try { final mac = await completer.future.timeout(timeout); await subscription.cancel(); return mac; } catch (_) { await subscription.cancel(); if (attempt == maxRetries - 1) rethrow; } } } catch (e) { ef.log("[获取设备 MAC]:失败:$e"); } throw Exception("获取MAC超时"); } } String parseMacFromBleResponse(List data) { // 先做简单的合法性判断 if (data.length >= 17 && data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF && data[6] == 0x40 && data[7] == 0x01) { // 取出 Byte8 ~ Byte13 List macBytes = data.sublist(8, 14); String macAddress = macBytes .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) .join(''); return macAddress; } else { throw Exception("BLE返回数据格式不正确"); } } String parseMacFromTH2Response(List data) { if (data.length < 17) { throw Exception("数据长度不足,无法解析MAC"); } int status = data[8]; if (status != 0x03 && status != 0x04) { throw Exception("未连接心率带"); } // 提取9~14字节的MAC地址 List macBytes = data.sublist(9, 15); return macBytes .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(":") .toUpperCase(); }