heart_rate.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:fis_common/logger/logger.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:get/get.dart';
  6. import 'package:path_provider/path_provider.dart';
  7. import 'package:vitalapp/global.dart';
  8. import 'package:vitalapp/managers/interfaces/data_convert.dart';
  9. import 'package:vitalapp/managers/interfaces/models/device.dart';
  10. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_card.dart';
  11. import 'package:vitalapp/pages/medical/controller.dart';
  12. import 'package:vitalapp/pages/medical/controllers/heart.dart';
  13. import 'package:vitalapp/pages/medical/models/worker.dart';
  14. import 'package:vitalapp/pages/medical/widgets/device_status.dart';
  15. import 'package:vitalapp/pages/medical/widgets/device_status_position.dart';
  16. import 'package:vitalapp/pages/medical/widgets/ecg_view/index.dart';
  17. import 'package:vitalapp/pages/medical/widgets/side_bar.dart';
  18. import 'package:vnote_device_plugin/consts/types.dart';
  19. import 'package:vnote_device_plugin/devices/heart.dart';
  20. import 'package:vnote_device_plugin/models/exams/heart.dart';
  21. import 'package:http/http.dart' as http;
  22. class HeartRate extends StatefulWidget {
  23. const HeartRate({
  24. super.key,
  25. });
  26. @override
  27. State<HeartRate> createState() => _HeartRateState();
  28. }
  29. class _HeartRateState extends State<HeartRate> {
  30. final MedicalController medicalController = Get.find<MedicalController>();
  31. final IDataConvertManager dataConvertManager =
  32. Get.find<IDataConvertManager>();
  33. late HeartDeviceController heartDeviceController;
  34. late HeartDeviceWorker worker;
  35. WorkerStatus connectStatus = WorkerStatus.connecting;
  36. List<int> ecgPoint = [];
  37. int errorCount = 0;
  38. bool isConnectFail = false;
  39. late String _heart =
  40. medicalController.diagnosisDataValue['Heart']?['HEART']?.toString() ?? '';
  41. late String _assess =
  42. medicalController.diagnosisDataValue['Heart']?['ASSESS'] ?? '';
  43. late final heartRate =
  44. medicalController.diagnosisDataValue['Heart']?['HEART'] ?? 0;
  45. /// 是否显示重试按钮
  46. bool showResetButton = false;
  47. /// 初始时的心电初始数据
  48. List<int>? initEcgData;
  49. String _deviceError = '';
  50. @override
  51. Widget build(BuildContext context) {
  52. return Stack(
  53. children: [
  54. ExamCard(
  55. titleText: const SizedBox(),
  56. bottomPadding: 10,
  57. content: Column(
  58. mainAxisAlignment: MainAxisAlignment.start,
  59. children: [
  60. Row(
  61. mainAxisAlignment: MainAxisAlignment.end,
  62. children: [
  63. const SideBar(
  64. title: '心率',
  65. value: '',
  66. unit: '',
  67. ),
  68. const Expanded(child: SizedBox()),
  69. Text(
  70. _heart.isEmpty ? '--' : _heart,
  71. style: const TextStyle(
  72. fontSize: 60,
  73. color: Colors.black,
  74. ),
  75. ),
  76. const Text(
  77. " bpm",
  78. style: TextStyle(fontSize: 25),
  79. ),
  80. const SizedBox(
  81. width: 15,
  82. )
  83. ],
  84. ),
  85. if (_deviceError.isNotEmpty)
  86. Row(
  87. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  88. crossAxisAlignment: CrossAxisAlignment.start,
  89. children: [
  90. Container(
  91. padding: const EdgeInsets.symmetric(
  92. horizontal: 30,
  93. ),
  94. child: const Text(
  95. '心率测量出错',
  96. style: TextStyle(
  97. fontSize: 25,
  98. ),
  99. ),
  100. ),
  101. Container(
  102. padding: const EdgeInsets.symmetric(
  103. horizontal: 50,
  104. ),
  105. child: Text(
  106. _deviceError,
  107. style: const TextStyle(
  108. fontSize: 24,
  109. ),
  110. ),
  111. ),
  112. ],
  113. ),
  114. if (_assess.isNotEmpty)
  115. Row(
  116. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  117. crossAxisAlignment: CrossAxisAlignment.start,
  118. children: [
  119. Container(
  120. padding: const EdgeInsets.symmetric(
  121. horizontal: 30,
  122. ),
  123. child: const Text(
  124. '心率评估',
  125. style: TextStyle(
  126. fontSize: 25,
  127. ),
  128. ),
  129. ),
  130. Container(
  131. padding: const EdgeInsets.symmetric(
  132. horizontal: 50,
  133. ),
  134. child: Text(
  135. dataConvertManager.heartRateConversion(
  136. int.parse(heartRate.toString())),
  137. style: const TextStyle(
  138. fontSize: 24,
  139. ),
  140. ),
  141. ),
  142. ],
  143. ),
  144. const SizedBox(
  145. height: 10,
  146. ),
  147. SizedBox(
  148. height: 240,
  149. child: LayoutBuilder(builder: (context, constraints) {
  150. if (initEcgData == null) {
  151. return Container();
  152. } else {
  153. return EcgView(
  154. width: constraints.maxWidth,
  155. height: constraints.maxHeight,
  156. initData: initEcgData!,
  157. );
  158. }
  159. }),
  160. ),
  161. Container(
  162. // !! 这一层 Container 不能删 ,否则会引发 EcgView 的 controller 被异常删除
  163. child: _buildResetButton(),
  164. ),
  165. ],
  166. ),
  167. ),
  168. if (!isConnectFail)
  169. DeviceStatusPosition(
  170. deviceStatus: DeviceStatus(connectStatus: connectStatus),
  171. )
  172. else
  173. _buildErrorButton(),
  174. ],
  175. );
  176. }
  177. // Widget _buildDebugButton() {
  178. // return Container(
  179. // child: Row(
  180. // children: [
  181. // const Text(
  182. // '测试============?',
  183. // style: TextStyle(color: Colors.grey, fontSize: 24),
  184. // ),
  185. // TextButton(
  186. // onPressed: () {
  187. // debugTest();
  188. // },
  189. // child: const Text(
  190. // '重试',
  191. // style: TextStyle(color: Colors.blue, fontSize: 24),
  192. // ),
  193. // ),
  194. // ],
  195. // ),
  196. // );
  197. // }
  198. Widget _buildResetButton() {
  199. if (!showResetButton) {
  200. return const SizedBox(
  201. height: 48,
  202. );
  203. }
  204. return Container(
  205. padding: const EdgeInsets.only(left: 30),
  206. child: Row(
  207. children: [
  208. const Text(
  209. '测量数据不完整,是否需要重试?',
  210. style: TextStyle(color: Colors.grey, fontSize: 24),
  211. ),
  212. TextButton(
  213. onPressed: () {
  214. ecgPoint = [];
  215. resetEcgView();
  216. setEcgData();
  217. setState(() {
  218. showResetButton = false;
  219. });
  220. },
  221. child: const Text(
  222. '重试',
  223. style: TextStyle(color: Colors.blue, fontSize: 24),
  224. ),
  225. ),
  226. ],
  227. ),
  228. );
  229. }
  230. // 设备初始化
  231. Future<void> initDevice() async {
  232. DeviceModel? device = await medicalController.getDevice(DeviceTypes.HEART);
  233. if (device == null) {
  234. connectStatus = WorkerStatus.unboundDevice;
  235. setState(() {});
  236. return;
  237. }
  238. heartDeviceController = HeartDeviceController(device.model, device.mac);
  239. worker = heartDeviceController.worker;
  240. connectStatus = heartDeviceController.connectStatus;
  241. loadListeners();
  242. worker.connect();
  243. }
  244. /// 尝试重连
  245. Future<void> tryReconnect() async {
  246. await worker.disconnect();
  247. await worker.connect();
  248. }
  249. @override
  250. void dispose() async {
  251. super.dispose();
  252. releaseListeners();
  253. await worker.disconnect();
  254. }
  255. /// 重置数据以及心电图
  256. void resetEcgView() {
  257. try {
  258. EcgViewController ecgViewController = Get.find<EcgViewController>();
  259. ecgPoint = [];
  260. ecgViewController.reset();
  261. } catch (e) {
  262. logger.i('心率图测量失败:$e');
  263. }
  264. }
  265. /// 数据初始化
  266. Future<void> initData() async {
  267. await medicalController.readCachedRecord();
  268. if (medicalController.diagnosisDataValue['Heart'] == null) {
  269. medicalController.diagnosisDataValue['Heart'] = {};
  270. }
  271. if (medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] != null) {
  272. String pointInfo =
  273. medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] ?? '';
  274. if (pointInfo.toString().startsWith('https://') && kIsOnline) {
  275. final response = await http.get(Uri.parse(pointInfo));
  276. pointInfo = response.body;
  277. }
  278. initEcgData = jsonDecode(pointInfo).cast<int>();
  279. if (initEcgData != null &&
  280. initEcgData!.isNotEmpty &&
  281. initEcgData!.length < 125 * 30) {
  282. // print("数据不完整 ⭐⭐⭐⭐⭐⭐⭐");
  283. showResetButton = true;
  284. }
  285. } else {
  286. initEcgData = [];
  287. }
  288. setState(() {});
  289. }
  290. Future<void> initHeart() async {
  291. await initDevice();
  292. await initData();
  293. }
  294. @override
  295. void initState() {
  296. initHeart();
  297. super.initState();
  298. }
  299. void loadListeners() {
  300. worker.connectErrorEvent.addListener(_onConnectFail);
  301. worker.connectedEvent.addListener(_onConnectSuccess);
  302. worker.disconnectedEvent.addListener(_onDisconnected);
  303. worker.hrValueUpdateEvent.addListener(_onHrValueUpdate);
  304. worker.ecgValueUpdateEvent.addListener(_onEcgValueUpdate);
  305. worker.resultReceivedEvent.addListener(_onRCesultReceived);
  306. worker.errorEvent.addListener(_onError);
  307. }
  308. void releaseListeners() {
  309. worker.connectErrorEvent.removeListener(_onConnectFail);
  310. worker.connectedEvent.removeListener(_onConnectSuccess);
  311. worker.disconnectedEvent.removeListener(_onDisconnected);
  312. worker.hrValueUpdateEvent.removeListener(_onHrValueUpdate);
  313. worker.ecgValueUpdateEvent.removeListener(_onEcgValueUpdate);
  314. worker.resultReceivedEvent.removeListener(_onRCesultReceived);
  315. worker.errorEvent.removeListener(_onError);
  316. }
  317. /// 需要封装一下
  318. Widget _buildErrorButton() {
  319. return DeviceStatusPosition(
  320. deviceStatus: Row(
  321. children: [
  322. const Text(
  323. '请确认设备是否启动',
  324. style: TextStyle(fontSize: 24, color: Colors.red),
  325. ),
  326. IconButton(
  327. onPressed: () {
  328. tryReconnect();
  329. setState(() {
  330. connectStatus = WorkerStatus.connecting;
  331. isConnectFail = false;
  332. });
  333. },
  334. icon: const Icon(Icons.refresh),
  335. iconSize: 32,
  336. ),
  337. ],
  338. ),
  339. );
  340. }
  341. void _onConnectFail(sender, e) async {
  342. logger.i("设备连接失败:${worker.mac}");
  343. if (errorCount < 3) {
  344. errorCount++;
  345. await tryReconnect();
  346. } else {
  347. isConnectFail = true;
  348. }
  349. connectStatus = WorkerStatus.connectionFailed;
  350. setState(() {});
  351. }
  352. void _onConnectSuccess(sender, e) {
  353. logger.e('_HeartRateState ${worker.mac}, 设备连接成功');
  354. isConnectFail = false;
  355. errorCount = 0;
  356. connectStatus = WorkerStatus.connected;
  357. setState(() {});
  358. }
  359. /// 设备连接中断
  360. void _onDisconnected(sender, e) async {
  361. print('设备连接中断');
  362. logger.i("设备连接中断:${worker.mac}");
  363. if (errorCount < 3) {
  364. errorCount++;
  365. await tryReconnect();
  366. } else {
  367. isConnectFail = true;
  368. }
  369. connectStatus = WorkerStatus.disconnected;
  370. setState(() {});
  371. }
  372. // 心电图数据更新
  373. void _onEcgValueUpdate(_, List<int> e) {
  374. try {
  375. EcgViewController ecgViewController = Get.find<EcgViewController>();
  376. ecgPoint.addAll(e);
  377. if (ecgPoint.length > 125 * 3) {
  378. // 3s 后开始塞数据
  379. ecgViewController.addData(e);
  380. }
  381. } catch (e) {
  382. print(e);
  383. }
  384. }
  385. /// 更新心率
  386. void _onHrValueUpdate(_, int e) {
  387. logger.i('心率更新:$e');
  388. if (_deviceError.isNotEmpty || _assess.isNotEmpty) {
  389. // 如果上次因错误而停止了,这里需要先重置
  390. _deviceError = '';
  391. _assess = '';
  392. resetEcgView();
  393. }
  394. setState(() {
  395. _heart = e.toString();
  396. showResetButton = false;
  397. });
  398. }
  399. void _onError(_, String e) {
  400. logger.i('心率更新失败:$e');
  401. _deviceError = e;
  402. resetEcgView();
  403. setState(() {});
  404. }
  405. /// 接收到最终结果
  406. void _onRCesultReceived(_, HeartExamResult e) async {
  407. logger.i('心率更新接收结果:${e.heartRate}');
  408. // print(ecgPoint.toString());
  409. // _heart = e.toString();
  410. _assess = e.analysis.first;
  411. _heart = e.heartRate.toString();
  412. medicalController.diagnosisDataValue['Heart']?['HEART'] =
  413. e.heartRate.toString();
  414. medicalController.diagnosisDataValue['Heart']?['ASSESS'] =
  415. dataConvertManager.heartRateConversion(int.parse(heartRate));
  416. await setEcgData();
  417. medicalController.saveCachedRecord();
  418. /// 判断数据是否完整
  419. if (ecgPoint.isNotEmpty && ecgPoint.length < 125 * 30) {
  420. // print("数据不完整 ⭐⭐⭐⭐⭐⭐⭐");
  421. showResetButton = true;
  422. }
  423. /// FIXME 解除下方注释
  424. // try {
  425. // medicalController.saveCachedRecord();
  426. // } catch (e) {
  427. // print(e);
  428. // }
  429. // print(medicalController.diagnosisDataValue);
  430. setState(() {});
  431. }
  432. void debugTest() async {
  433. logger.i('心率更新接收结果 测试');
  434. // print(ecgPoint.toString());
  435. // _heart = e.toString();
  436. _assess = "ss";
  437. _heart = "90";
  438. medicalController.diagnosisDataValue['Heart']?['HEART'] = "90";
  439. medicalController.diagnosisDataValue['Heart']?['ASSESS'] =
  440. dataConvertManager.heartRateConversion(int.parse(heartRate));
  441. await setEcgData();
  442. /// 判断数据是否完整
  443. // if (ecgPoint.isNotEmpty && ecgPoint.length < 125 * 30) {
  444. // }
  445. print("数据不完整 ⭐⭐⭐⭐⭐⭐⭐");
  446. showResetButton = true;
  447. medicalController.saveCachedRecord();
  448. print(medicalController.diagnosisDataValue);
  449. setState(() {});
  450. }
  451. /// 设置最终数据
  452. Future<void> setEcgData() async {
  453. EcgViewController ecgViewController = Get.find<EcgViewController>();
  454. medicalController.diagnosisDataValue['Heart']?['ECG'] =
  455. await ecgViewController.getFullDataImageBase64();
  456. medicalController.diagnosisDataValue['Heart']?['ECG_POINT'] =
  457. jsonEncode(ecgViewController.allPoints);
  458. }
  459. }