heart_rate.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import 'package:get/get.dart';
  5. import 'package:vitalapp/managers/device_controller_manager.dart';
  6. import 'package:vitalapp/managers/interfaces/data_convert.dart';
  7. import 'package:vitalapp/managers/interfaces/models/device.dart';
  8. import 'package:vitalapp/pages/medical/controllers/heart.dart';
  9. import 'package:vitalapp/pages/medical/widgets/device_status.dart';
  10. import 'package:vitalapp/pages/medical/widgets/device_status_position.dart';
  11. import 'package:vitalapp/pages/medical/widgets/ecg_view/index.dart';
  12. import 'package:vnote_device_plugin/consts/types.dart';
  13. import 'package:vnote_device_plugin/devices/heart.dart';
  14. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_card.dart';
  15. import 'package:vitalapp/pages/medical/controller.dart';
  16. import 'package:vitalapp/pages/medical/models/worker.dart';
  17. import 'package:vitalapp/pages/medical/widgets/side_bar.dart';
  18. import 'dart:ui' as ui;
  19. import 'dart:typed_data';
  20. import 'package:fis_common/logger/logger.dart';
  21. import 'package:vnote_device_plugin/models/exams/heart.dart';
  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. var controller = Get.find<MedicalController>();
  31. late DeviceControllerManager heart;
  32. late HeartDeviceWorker worker;
  33. final dataConvertManager = Get.find<IDataConvertManager>();
  34. WorkerStatus _connectStatus = WorkerStatus.connecting;
  35. List<int> ecgPoint = [];
  36. int errorCount = 0;
  37. bool isConnectFail = false;
  38. late String _heart =
  39. controller.diagnosisDataValue['Heart']?['HEART']?.toString() ?? '';
  40. late String _assess = controller.diagnosisDataValue['Heart']?['ASSESS'] ?? '';
  41. late final String _ecg = controller.diagnosisDataValue['Heart']?['ECG'] ?? '';
  42. late final String _ecgPoint =
  43. controller.diagnosisDataValue['Heart']?['ECG_POINT'] ?? '';
  44. late final heartRate = controller.diagnosisDataValue['Heart']?['HEART'] ?? 0;
  45. String _deviceError = '';
  46. @override
  47. void initState() {
  48. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  49. initHeart();
  50. });
  51. super.initState();
  52. }
  53. Future<void> connect() async {
  54. await worker.connect();
  55. }
  56. Future<void> disconnect() async {
  57. if (!(worker == null)) {
  58. await worker.disconnect();
  59. releaseListeners();
  60. }
  61. }
  62. void releaseListeners() {
  63. worker.connectErrorEvent.removeListener(_onConnectFail);
  64. worker.connectedEvent.removeListener(_onConnectSuccess);
  65. // worker.successEvent.removeListener(_onSuccess);
  66. worker.disconnectedEvent.removeListener(_onDisconnected);
  67. worker.hrValueUpdateEvent.removeListener(_onHrValueUpdate);
  68. worker.ecgValueUpdateEvent.removeListener(_onEcgValueUpdate);
  69. worker.resultReceivedEvent.removeListener(_onRCesultReceived);
  70. worker.errorEvent.removeListener(_onError);
  71. }
  72. @override
  73. void dispose() {
  74. disconnect();
  75. super.dispose();
  76. }
  77. void loadListeners() {
  78. // worker.successEvent.addListener(_onSuccess);
  79. worker.connectErrorEvent.addListener(_onConnectFail);
  80. worker.connectedEvent.addListener(_onConnectSuccess);
  81. worker.disconnectedEvent.addListener(_onDisconnected);
  82. worker.hrValueUpdateEvent.addListener(_onHrValueUpdate);
  83. worker.ecgValueUpdateEvent.addListener(_onEcgValueUpdate);
  84. worker.resultReceivedEvent.addListener(_onRCesultReceived);
  85. worker.errorEvent.addListener(_onError);
  86. worker.connect();
  87. }
  88. Future<void> currentDevice() async {
  89. DeviceModel? device = await controller.getDevice(DeviceTypes.HEART);
  90. if (device == null) {
  91. _connectStatus = WorkerStatus.unboundDevice;
  92. setState(() {});
  93. return;
  94. }
  95. heart =
  96. DeviceControllerManager(DeviceTypes.HEART, device.model, device.mac);
  97. worker = heart.worker as HeartDeviceWorker;
  98. _connectStatus = heart.connectStatus;
  99. loadListeners();
  100. }
  101. Future<void> initHeart() async {
  102. currentDevice();
  103. await initData();
  104. }
  105. Future<void> initData() async {
  106. // await Future.delayed(const Duration(milliseconds: 800));
  107. await controller.readCachedRecord();
  108. if (controller.diagnosisDataValue['Heart'] == null) {
  109. controller.diagnosisDataValue['Heart'] = {};
  110. }
  111. setState(() {});
  112. }
  113. void _onHrValueUpdate(_, int e) {
  114. logger.i('心率更新:$e');
  115. if (_deviceError.isNotEmpty || _assess.isNotEmpty) {
  116. _deviceError = '';
  117. _assess = '';
  118. ecgReset();
  119. print('object');
  120. }
  121. setState(() {
  122. _heart = e.toString();
  123. });
  124. }
  125. void _onError(_, String e) {
  126. logger.i('心率更新失败:$e');
  127. _deviceError = e;
  128. ecgReset();
  129. setState(() {});
  130. }
  131. void ecgReset() {
  132. try {
  133. EcgViewController ecgViewController = Get.find<EcgViewController>();
  134. ecgPoint = [];
  135. ecgViewController.reset();
  136. } catch (e) {
  137. logger.i('心率图测量失败:$e');
  138. }
  139. }
  140. Future<void> setEcgData() async {
  141. EcgViewController ecgViewController = Get.find<EcgViewController>();
  142. controller.diagnosisDataValue['Heart']?['ECG'] =
  143. await ecgViewController.getFullDataImageBase64();
  144. controller.diagnosisDataValue['Heart']?['ECG_POINT'] =
  145. jsonEncode(ecgViewController.allPoints);
  146. }
  147. void _onRCesultReceived(_, HeartExamResult e) async {
  148. logger.i('心率更新接收结果:${e.heartRate}');
  149. print(ecgPoint.toString());
  150. // _heart = e.toString();
  151. _assess = e.analysis.first;
  152. _heart = e.heartRate.toString();
  153. controller.diagnosisDataValue['Heart']?['HEART'] = e.heartRate.toString();
  154. controller.diagnosisDataValue['Heart']?['ASSESS'] =
  155. dataConvertManager.heartRateConversion(int.parse(heartRate));
  156. await setEcgData();
  157. controller.saveCachedRecord();
  158. print(controller.diagnosisDataValue);
  159. setState(() {});
  160. }
  161. void _onEcgValueUpdate(_, List<int> e) {
  162. try {
  163. EcgViewController ecgViewController = Get.find<EcgViewController>();
  164. ecgPoint.addAll(e);
  165. if (ecgPoint.length > 125 * 3) {
  166. ecgViewController.addData(e);
  167. }
  168. } catch (e) {
  169. print(e);
  170. }
  171. }
  172. void _onConnectFail(sender, e) {
  173. logger.i("设备连接失败:${worker.mac}");
  174. if (errorCount < 3) {
  175. errorCount++;
  176. disconnect();
  177. loadListeners();
  178. } else {
  179. isConnectFail = true;
  180. }
  181. _connectStatus = WorkerStatus.connectionFailed;
  182. setState(() {});
  183. }
  184. void _onDisconnected(sender, e) {
  185. print('设备连接中断');
  186. logger.i("设备连接中断:${worker.mac}");
  187. if (errorCount < 3) {
  188. errorCount++;
  189. disconnect();
  190. loadListeners();
  191. } else {
  192. isConnectFail = true;
  193. }
  194. _connectStatus = WorkerStatus.disconnected;
  195. setState(() {});
  196. }
  197. void _onConnectSuccess(sender, e) {
  198. logger.e('_HeartRateState ${worker.mac}, 设备连接成功');
  199. isConnectFail = false;
  200. errorCount = 0;
  201. _connectStatus = WorkerStatus.connected;
  202. setState(() {});
  203. }
  204. Future<String> createEcgImageBase64(List<int> points) async {
  205. final int width = points.length ~/ 2;
  206. const int height = 240;
  207. final ui.PictureRecorder recorder = ui.PictureRecorder();
  208. final Canvas canvas = Canvas(recorder);
  209. final Path path = Path();
  210. final Paint paint = Paint();
  211. const double strokeWidth = 1.5;
  212. paint.color = Colors.green;
  213. paint.style = PaintingStyle.stroke;
  214. paint.isAntiAlias = true;
  215. paint.strokeWidth = strokeWidth;
  216. path.reset();
  217. int pointCount = points.length;
  218. if (pointCount > 0) {
  219. path.moveTo(0, points[0].toDouble());
  220. int i = 0;
  221. int j = pointCount ~/ 2;
  222. while (i < j) {
  223. path.lineTo(i.toDouble(), points[i].toDouble());
  224. path.lineTo((i + 1).toDouble(), points[i + 1].toDouble());
  225. i += 2;
  226. }
  227. canvas.drawPath(path, paint);
  228. }
  229. final ui.Picture picture = recorder.endRecording();
  230. final ui.Image image = await picture.toImage(width, height);
  231. final ByteData? byteData =
  232. await image.toByteData(format: ui.ImageByteFormat.png);
  233. final Uint8List pngBytes = byteData!.buffer.asUint8List();
  234. final String base64Image = base64Encode(pngBytes);
  235. return base64Image;
  236. }
  237. /// 需要封装一下
  238. Widget _buildErrorButton() {
  239. return DeviceStatusPosition(
  240. deviceStatus: Row(
  241. children: [
  242. const Text(
  243. '请确认设备是否启动',
  244. style: TextStyle(fontSize: 24, color: Colors.red),
  245. ),
  246. IconButton(
  247. onPressed: () {
  248. worker.connect();
  249. setState(() {
  250. _connectStatus = WorkerStatus.connecting;
  251. isConnectFail = false;
  252. });
  253. },
  254. icon: const Icon(Icons.refresh),
  255. iconSize: 32,
  256. ),
  257. ],
  258. ),
  259. );
  260. }
  261. @override
  262. Widget build(BuildContext context) {
  263. return Stack(
  264. children: [
  265. ExamCard(
  266. titleText: const SizedBox(),
  267. // clickCard: () {},
  268. content: Column(
  269. mainAxisAlignment: MainAxisAlignment.start,
  270. children: [
  271. Row(
  272. mainAxisAlignment: MainAxisAlignment.end,
  273. children: [
  274. const SideBar(
  275. title: '心率',
  276. value: '',
  277. unit: '',
  278. ),
  279. const Expanded(child: SizedBox()),
  280. Text(
  281. _heart.isEmpty ? '--' : _heart,
  282. style: const TextStyle(
  283. fontSize: 60,
  284. color: Colors.black,
  285. ),
  286. ),
  287. const Text(
  288. " bpm",
  289. style: TextStyle(fontSize: 25),
  290. ),
  291. const SizedBox(
  292. width: 15,
  293. )
  294. ],
  295. ),
  296. if (_deviceError.isNotEmpty)
  297. Row(
  298. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  299. crossAxisAlignment: CrossAxisAlignment.start,
  300. children: [
  301. Container(
  302. padding: const EdgeInsets.symmetric(
  303. horizontal: 30,
  304. ),
  305. child: const Text(
  306. '心率测量出错',
  307. style: TextStyle(
  308. fontSize: 25,
  309. ),
  310. ),
  311. ),
  312. Container(
  313. padding: const EdgeInsets.symmetric(
  314. horizontal: 50,
  315. ),
  316. child: Text(
  317. _deviceError,
  318. style: const TextStyle(
  319. fontSize: 24,
  320. ),
  321. ),
  322. ),
  323. ],
  324. ),
  325. if (_assess.isNotEmpty)
  326. Row(
  327. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  328. crossAxisAlignment: CrossAxisAlignment.start,
  329. children: [
  330. Container(
  331. padding: const EdgeInsets.symmetric(
  332. horizontal: 30,
  333. ),
  334. child: const Text(
  335. '心率评估',
  336. style: TextStyle(
  337. fontSize: 25,
  338. ),
  339. ),
  340. ),
  341. Container(
  342. padding: const EdgeInsets.symmetric(
  343. horizontal: 50,
  344. ),
  345. child: Text(
  346. dataConvertManager.heartRateConversion(
  347. int.parse(heartRate.toString())),
  348. style: const TextStyle(
  349. fontSize: 24,
  350. ),
  351. ),
  352. ),
  353. ],
  354. ),
  355. const SizedBox(
  356. height: 10,
  357. ),
  358. SizedBox(
  359. height: 240,
  360. child: LayoutBuilder(builder: (context, constraints) {
  361. List<int> initData = [];
  362. if (controller.diagnosisDataValue['Heart']?['ECG_POINT'] !=
  363. null) {
  364. initData = jsonDecode(controller
  365. .diagnosisDataValue['Heart']?['ECG_POINT'])
  366. .cast<int>();
  367. }
  368. return EcgView(
  369. width: constraints.maxWidth,
  370. height: constraints.maxHeight,
  371. initData: initData, // TODO: 传入初始数据
  372. );
  373. }),
  374. )
  375. ],
  376. )),
  377. if (!isConnectFail)
  378. DeviceStatusPosition(
  379. deviceStatus: DeviceStatus(connectStatus: _connectStatus),
  380. )
  381. else
  382. _buildErrorButton(),
  383. ],
  384. );
  385. }
  386. }