import 'dart:async'; import 'dart:convert'; import 'package:fis_common/logger/logger.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:vitalapp/architecture/app_parameters.dart'; import 'package:vitalapp/global.dart'; import 'package:vitalapp/managers/device_controller_manager.dart'; import 'package:vitalapp/managers/interfaces/data_convert.dart'; import 'package:vitalapp/managers/interfaces/models/device.dart'; import 'package:vitalapp/pages/medical/widgets/exam_card.dart'; import 'package:vitalapp/pages/medical/controller.dart'; import 'package:vitalapp/pages/medical/models/worker.dart'; import 'package:vitalapp/pages/medical/widgets/device_status.dart'; import 'package:vitalapp/pages/medical/widgets/device_status_position.dart'; import 'package:vitalapp/pages/medical/widgets/ecg_view/index.dart'; import 'package:vitalapp/pages/medical/widgets/side_bar.dart'; import 'package:vnote_device_plugin/consts/types.dart'; import 'package:vnote_device_plugin/devices/heart.dart'; import 'package:vnote_device_plugin/models/exams/heart.dart'; import 'package:http/http.dart' as http; class HeartRate extends StatefulWidget { const HeartRate({ super.key, }); @override State<HeartRate> createState() => _HeartRateState(); } class _HeartRateState extends State<HeartRate> { bool get isPureSoftwareMode => AppParameters.data.isPureSoftwareMode; final MedicalController medicalController = Get.find<MedicalController>(); final IDataConvertManager dataConvertManager = Get.find<IDataConvertManager>(); DeviceControllerManager? heart; late HeartDeviceWorker worker; WorkerStatus connectStatus = WorkerStatus.connecting; List<int> ecgPoint = []; int errorCount = 0; bool isConnectFail = false; late String _heart = medicalController.diagnosisDataValue['Heart']?['HEART']?.toString() ?? ''; late String _assess = medicalController.diagnosisDataValue['Heart']?['ASSESS'] ?? ''; late final heartRate = medicalController.diagnosisDataValue['Heart']?['HEART'] ?? 0; /// 是否显示重试按钮 bool showResetButton = false; /// 初始时的心电初始数据 List<int>? initEcgData; String _deviceError = ''; @override Widget build(BuildContext context) { return Stack( children: [ ExamCard( titleText: const SizedBox(), // bottomPadding: 10, content: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ const SideBar( title: '心率', value: '', unit: '', ), const Expanded(child: SizedBox()), Text( _heart.isEmpty ? '--' : _heart, style: const TextStyle( fontSize: 60, color: Colors.black, ), ), const Text( " bpm", style: TextStyle(fontSize: 25), ), const SizedBox( width: 15, ) ], ), if (_deviceError.isNotEmpty) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 30, ), child: const Text( '心率测量出错', style: TextStyle( fontSize: 25, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 50, ), child: Text( _deviceError, style: const TextStyle( fontSize: 24, ), ), ), ], ), if (_assess.isNotEmpty) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 30, ), child: const Text( '心率评估', style: TextStyle( fontSize: 25, ), ), ), Container( padding: const EdgeInsets.only( right: 15, ), child: Text( dataConvertManager.heartRateConversion( int.parse(heartRate.toString())), style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox( height: 10, ), SizedBox( height: 240, child: LayoutBuilder(builder: (context, constraints) { if (initEcgData == null) { return Container(); } else { return EcgView( width: constraints.maxWidth, height: constraints.maxHeight, initData: initEcgData!, ); } }), ), Container( // !! 这一层 Container 不能删 ,否则会引发 EcgView 的 controller 被异常删除 child: _buildResetButton(), ), ], ), ), if (!isPureSoftwareMode) ...[ if (!isConnectFail) DeviceStatusPosition( deviceStatus: DeviceStatus(connectStatus: connectStatus), ) else _buildErrorButton(), ], ], ); } // Widget _buildDebugButton() { // return Container( // child: Row( // children: [ // const Text( // '测试============?', // style: TextStyle(color: Colors.grey, fontSize: 24), // ), // TextButton( // onPressed: () { // debugTest(); // }, // child: const Text( // '重试', // style: TextStyle(color: Colors.blue, fontSize: 24), // ), // ), // ], // ), // ); // } Widget _buildResetButton() { if (!showResetButton) { return const SizedBox( height: 48, ); } return Container( padding: const EdgeInsets.only(left: 30), child: Row( children: const [ Text( '时长不足30S', style: TextStyle(color: Colors.grey, fontSize: 24), ), // TextButton( // onPressed: () { // ecgPoint = []; // resetEcgView(); // setEcgData(); // setState(() { // showResetButton = false; // }); // }, // child: const Text( // '重试', // style: TextStyle(color: Colors.blue, fontSize: 24), // ), // ), ], ), ); } // 设备初始化 Future<void> initDevice() async { DeviceModel? device = await medicalController.getDevice(DeviceTypes.HEART); if (device == null) { connectStatus = WorkerStatus.unboundDevice; setState(() {}); return; } heart = DeviceControllerManager(DeviceTypes.HEART, device.model, device.mac); worker = heart!.worker as HeartDeviceWorker; connectStatus = heart!.connectStatus; loadListeners(); connect(); } /// 尝试重连 Future<void> tryReconnect() async { await disconnect(); await connect(); } Future<void> disconnect() async { await worker.disconnect(); } Future<void> connect() async { await worker.connect(); } @override void dispose() { heart?.dispose(); heart = null; releaseListeners(); disconnect(); worker.dispose(); super.dispose(); } /// 重置数据以及心电图 void resetEcgView() { try { EcgViewController ecgViewController = Get.find<EcgViewController>(); ecgPoint = []; ecgViewController.reset(); } catch (e) { logger.i('心率图测量失败:$e'); } } /// 数据初始化 Future<void> initData() async { // medicalController.readCachedRecord(); if (medicalController.diagnosisDataValue['Heart'] == null) { medicalController.diagnosisDataValue['Heart'] = {}; } if (medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] != null) { String pointInfo = medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] ?? ''; if ((pointInfo.toString().startsWith('https://') || pointInfo.toString().startsWith('http://')) && kIsOnline) { final response = await http.get(Uri.parse(pointInfo)); pointInfo = response.body; } initEcgData = jsonDecode(pointInfo).cast<int>(); if (initEcgData != null && initEcgData!.isNotEmpty && initEcgData!.length < 125 * 30) { // print("数据不完整 ⭐⭐⭐⭐⭐⭐⭐"); showResetButton = true; } } else { initEcgData = []; } setState(() {}); } Future<void> initHeart() async { await initDevice(); await initData(); } @override void initState() { initHeart(); super.initState(); } void loadListeners() { worker.connectErrorEvent.addListener(_onConnectFail); worker.connectedEvent.addListener(_onConnectSuccess); worker.disconnectedEvent.addListener(_onDisconnected); worker.hrValueUpdateEvent.addListener(_onHrValueUpdate); worker.ecgValueUpdateEvent.addListener(_onEcgValueUpdate); worker.resultReceivedEvent.addListener(_onRCesultReceived); worker.errorEvent.addListener(_onError); } void releaseListeners() { worker.connectErrorEvent.removeListener(_onConnectFail); worker.connectedEvent.removeListener(_onConnectSuccess); worker.disconnectedEvent.removeListener(_onDisconnected); worker.hrValueUpdateEvent.removeListener(_onHrValueUpdate); worker.ecgValueUpdateEvent.removeListener(_onEcgValueUpdate); worker.resultReceivedEvent.removeListener(_onRCesultReceived); worker.errorEvent.removeListener(_onError); } /// 需要封装一下 Widget _buildErrorButton() { return DeviceStatusPosition( deviceStatus: Row( children: [ const Text( '请确认设备是否启动', style: TextStyle(fontSize: 24, color: Colors.red), ), IconButton( onPressed: () { tryReconnect(); setState(() { connectStatus = WorkerStatus.connecting; isConnectFail = false; }); }, icon: const Icon(Icons.refresh), iconSize: 32, ), ], ), ); } void _onConnectFail(sender, e) async { logger.i("设备连接失败:${worker.mac}"); if (errorCount < 3) { errorCount++; await tryReconnect(); } else { isConnectFail = true; } connectStatus = WorkerStatus.connectionFailed; setState(() {}); } void _onConnectSuccess(sender, e) { logger.e('_HeartRateState ${worker.mac}, 设备连接成功'); isConnectFail = false; errorCount = 0; connectStatus = WorkerStatus.connected; setState(() {}); } /// 设备连接中断 void _onDisconnected(sender, e) async { print('设备连接中断'); logger.i("设备连接中断:${worker.mac}"); await tryReconnect(); errorCount = 0; connectStatus = WorkerStatus.disconnected; setState(() {}); } // 心电图数据更新 void _onEcgValueUpdate(_, List<int> e) { try { EcgViewController ecgViewController = Get.find<EcgViewController>(); ecgPoint.addAll(e); if (ecgPoint.length > 125 * 3) { // 3s 后开始塞数据 ecgViewController.addData(e); } } catch (e) { print(e); } } /// 更新心率 void _onHrValueUpdate(_, int e) { if (_deviceError.isNotEmpty || _assess.isNotEmpty) { // 如果上次因错误而停止了,这里需要先重置 _deviceError = ''; _assess = ''; resetEcgView(); } setState(() { _heart = e.toString(); showResetButton = false; }); } void _onError(_, String e) { logger.i('心率更新失败:$e'); _deviceError = e; resetEcgView(); setState(() {}); } /// 接收到最终结果 void _onRCesultReceived(_, HeartExamResult e) async { logger.i('心率更新接收结果:${e.heartRate}'); // print(ecgPoint.toString()); // _heart = e.toString(); _assess = e.analysis.first; _heart = e.heartRate.toString(); medicalController.diagnosisDataValue['Heart']?['HEART'] = e.heartRate.toString(); medicalController.diagnosisDataValue['Heart']?['ASSESS'] = dataConvertManager.heartRateConversion(int.parse(heartRate)); await setEcgData(); medicalController.saveCachedRecord(); /// 判断数据是否完整 if (ecgPoint.isNotEmpty && ecgPoint.length < 125 * 30) { // print("数据不完整 ⭐⭐⭐⭐⭐⭐⭐"); showResetButton = true; } /// FIXME 解除下方注释 // try { // medicalController.saveCachedRecord(); // } catch (e) { // print(e); // } // print(medicalController.diagnosisDataValue); setState(() {}); } /// 设置最终数据 Future<void> setEcgData() async { EcgViewController ecgViewController = Get.find<EcgViewController>(); medicalController.diagnosisDataValue['Heart']?['ECG'] = await ecgViewController.getFullDataImageBase64(); medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] = jsonEncode(ecgViewController.allPoints); } }