healthMeter.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. <template>
  2. <view class="page">
  3. <!-- <view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
  4. <view class="nav-title">血压仪连接</view>
  5. <view class="nav-back" @tap="goBack">
  6. <text class="back-text">返回</text>
  7. </view>
  8. </view> -->
  9. <view class="content">
  10. <!-- 连接状态 -->
  11. <view class="status-card" :class="{ connected: connected }">
  12. <view class="status-icon">{{ connected ? '✓' : '○' }}</view>
  13. <view class="status-text">{{ statusText }}</view>
  14. <view v-if="connected && bpWaitUser != null" class="bp-wait-hint">设备就绪 (用户{{ bpWaitUser }}) · LCD 显示「bt」闪烁,可开始测量</view>
  15. <view v-if="connected && bpMeasureStarted" class="bp-wait-hint bp-measure-ok">血压计已确认,开始量测</view>
  16. <view v-if="connected && bpCurrentPressure != null" class="bp-pressure-live">测量中 压力: {{ bpCurrentPressure }} mmHg</view>
  17. <view v-if="bpErrorMsg" class="bp-error-msg">{{ bpErrorMsg }}</view>
  18. <view v-if="lastBloodPressure.systolic != null" class="measure-value">
  19. <text class="value-label">血压</text>
  20. <text class="value-num">{{ lastBloodPressure.systolic }}/{{ lastBloodPressure.diastolic }}</text>
  21. <text class="value-unit">mmHg</text>
  22. <text v-if="lastBloodPressure.pulse != null" class="value-extra">脉率 {{ lastBloodPressure.pulse }} 次/分</text>
  23. <text v-if="lastBloodPressure.ihb === 1" class="value-extra value-ihb">心律不齐</text>
  24. </view>
  25. <view v-if="lastBloodPressure.time" class="measure-time">{{ lastBloodPressure.time }}</view>
  26. <view v-if="lastGlucose.value !== null" class="measure-value">
  27. <text class="value-label">血糖</text>
  28. <text class="value-num">{{ lastGlucose.value }}</text>
  29. <text class="value-unit">{{ lastGlucose.unit }}</text>
  30. </view>
  31. <view v-if="lastGlucose.time" class="measure-time">{{ lastGlucose.time }}</view>
  32. </view>
  33. <!-- 操作区 -->
  34. <view class="actions">
  35. <button class="btn btn-primary" :disabled="scanning" :loading="scanning" @tap="startScan">
  36. {{ scanning ? '扫描中...' : '扫描设备' }}
  37. </button>
  38. <view v-if="deviceList.length > 0" class="device-list">
  39. <view class="list-title">可选设备(请确保血压仪/血糖仪已开机)</view>
  40. <view
  41. v-for="d in deviceList"
  42. :key="d.deviceId"
  43. class="device-item"
  44. :class="{ active: selectedDeviceId === d.deviceId }"
  45. @tap="selectDevice(d)"
  46. >
  47. <radio :checked="selectedDeviceId === d.deviceId" color="#FF5030" />
  48. <view class="device-info">
  49. <text class="device-name">{{ d.name || d.localName || '未知设备' }}</text>
  50. <text class="device-id">{{ d.deviceId }}</text>
  51. </view>
  52. </view>
  53. </view>
  54. <button
  55. class="btn btn-connect"
  56. :disabled="!selectedDeviceId || connecting"
  57. :loading="connecting"
  58. @tap="connectDevice"
  59. >
  60. {{ connected ? '已连接' : connecting ? '连接中...' : '连接设备' }}
  61. </button>
  62. <button v-if="connected" class="btn btn-warn" :disabled="disconnecting" @tap="disconnect">
  63. 断开连接
  64. </button>
  65. </view>
  66. <!-- 日志(调试用,可折叠) -->
  67. <view class="log-section">
  68. <view class="log-title" @tap="showLog = !showLog">
  69. <text>运行日志</text>
  70. <text class="log-toggle">{{ showLog ? '收起' : '展开' }}</text>
  71. </view>
  72. <scroll-view v-if="showLog" class="log-list" scroll-y>
  73. <text v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</text>
  74. </scroll-view>
  75. </view>
  76. </view>
  77. </view>
  78. </template>
  79. <script>
  80. // uni 蓝牙 API Promise 化
  81. const $p = (api, opts = {}) =>
  82. new Promise((res, rej) => {
  83. opts.success = res;
  84. opts.fail = rej;
  85. uni[api](opts);
  86. });
  87. // 解析 BLE 血压测量特征值(Bluetooth GATT 0x2A18 格式)
  88. // 固定 UUID:接收用 0xFFF1,发送用 0xFFF2,同属 Service 0xFFF0
  89. const BLE_SERVICE_UUID = 'fff0';
  90. const BLE_CHAR_UUID_NOTIFY = 'fff1'; // 接收
  91. const BLE_CHAR_UUID_WRITE = 'fff2'; // 发送
  92. // 血压协议:命令头 0xFD,0xFD;命令尾 0x0D,0x0A
  93. const BP_HEADER = [0xfd, 0xfd];
  94. const BP_TAIL = [0x0d, 0x0a];
  95. // 血压计开机后每 0.5s 发送的“等待上位机连接”包:FD FD A4 01 01 用户 01 0D 0A,用户字节 01=用户1、02=用户2,LCD 显示 "bt" 闪烁
  96. const BP_WAIT_CMD = 0xa4;
  97. const BP_WAIT_PACKET_LEN = 9; // FD FD A4 01 01 [user] 01 0D 0A
  98. // 连接成功后 APP 发送,告知血压计可量测:FD FD FA 05 0D 0A
  99. const BP_CMD_CONNECT_OK = new Uint8Array([0xfd, 0xfd, 0xfa, 0x05, 0x0d, 0x0a]);
  100. // 血压计回复确认并开始量测:FD FD 06 0D 0A
  101. const BP_REPLY_START_MEASURE_LEN = 5;
  102. // 量测过程中压力信号,每 0.5s 一次:FD FD FB PressureH PressureL 0D 0A,压力 = PressureH*256+PressureL
  103. const BP_PRESSURE_CMD = 0xfb;
  104. const BP_PRESSURE_PACKET_LEN = 7;
  105. // 量测完成测试结果:FD FD FC SYS DIA PUL IHB 0D 0A,SYS=收缩压,DIA=舒张压,PUL=心率,IHB=心律不齐(0x00正常,0x01不齐),未收到回复会连传3次
  106. const BP_RESULT_CMD = 0xfc;
  107. const BP_RESULT_PACKET_LEN = 9;
  108. // 量测错误码:FD FD FD err 0D 0A
  109. const BP_ERROR_CMD = 0xfd;
  110. const BP_ERROR_PACKET_LEN = 6;
  111. const BP_ERROR_MESSAGES = {
  112. 0x01: 'E-1 人体心跳信号太小或压力突降。量测错误,请根据说明书重新戴好袖带,保持安静,重新量测。',
  113. 0x02: 'E-2 杂讯干扰。量测错误,请根据说明书重新戴好袖带,保持安静,重新量测。',
  114. 0x03: 'E-3 充气时间过长。量测错误,请根据说明书重新戴好袖带,保持安静,重新量测。',
  115. 0x05: 'E-5 测得的结果异常。量测错误,请根据说明书重新戴好袖带,保持安静,重新量测。',
  116. 0x0e: 'E-E 量测错误,请根据说明书重新戴好袖带,保持安静,重新量测。',
  117. 0x0b: 'E-B 电源低电压,电池电量低,请更换电池。'
  118. };
  119. // 血糖协议:命令头 0xA5,0xA5;命令尾 0x5A,0x5A
  120. const GLUCOSE_HEADER = [0xa5, 0xa5];
  121. const GLUCOSE_TAIL = [0x5a, 0x5a];
  122. function matchHeader(arr, header) {
  123. if (arr.length < header.length) return false;
  124. return header.every((b, i) => arr[i] === b);
  125. }
  126. function matchTail(arr, tail) {
  127. if (arr.length < tail.length) return false;
  128. return tail.every((b, i) => arr[arr.length - tail.length + i] === b);
  129. }
  130. // 判断是否为血压计“等待连接”包,若是则返回 { user: 1|2 },否则返回 null
  131. function parseBpWaitForConnection(arr) {
  132. if (arr.length !== BP_WAIT_PACKET_LEN) return null;
  133. if (!matchHeader(arr, BP_HEADER) || !matchTail(arr, BP_TAIL)) return null;
  134. if (arr[2] !== BP_WAIT_CMD || arr[3] !== 0x01 || arr[4] !== 0x01 || arr[6] !== 0x01) return null;
  135. const userByte = arr[5];
  136. if (userByte === 0x01) return { user: 1 };
  137. if (userByte === 0x02) return { user: 2 };
  138. return null;
  139. }
  140. // 解析量测结果:FD FD FC SYS DIA PUL IHB 0D 0A
  141. function parseBpResultPacket(arr) {
  142. if (arr.length !== BP_RESULT_PACKET_LEN || arr[2] !== BP_RESULT_CMD) return null;
  143. if (!matchHeader(arr, BP_HEADER) || !matchTail(arr, BP_TAIL)) return null;
  144. const systolic = arr[3];
  145. const diastolic = arr[4];
  146. const pulse = arr[5];
  147. const ihb = arr[6]; // 0x00 正常,0x01 心律不齐
  148. if (systolic > 300 || diastolic > 300) return null;
  149. return {
  150. systolic,
  151. diastolic,
  152. pulse,
  153. ihb: ihb === 0x01 ? 1 : 0,
  154. time: new Date().toLocaleString()
  155. };
  156. }
  157. // 解析量测错误码:FD FD FD err 0D 0A,返回错误文案或 null
  158. function parseBpErrorPacket(arr) {
  159. if (arr.length !== BP_ERROR_PACKET_LEN || arr[2] !== BP_ERROR_CMD) return null;
  160. if (!matchHeader(arr, BP_HEADER) || !matchTail(arr, BP_TAIL)) return null;
  161. const code = arr[3];
  162. return BP_ERROR_MESSAGES[code] || ('错误码 E-' + (code < 10 ? '0' + code.toString(16) : code.toString(16).toUpperCase()));
  163. }
  164. // 解析血压数据:命令头 0xFD,0xFD;命令尾 0x0D,0x0A;排除“等待连接”包(A4 01 01 用户 01)
  165. function parseBloodPressure(buffer) {
  166. if (!buffer || buffer.byteLength < 8) return null;
  167. const arr = new Uint8Array(buffer);
  168. if (!matchHeader(arr, BP_HEADER) || !matchTail(arr, BP_TAIL)) return null;
  169. if (parseBpWaitForConnection(arr)) return null; // 等待连接包不当作血压数据
  170. const len = arr.length - BP_HEADER.length - BP_TAIL.length;
  171. if (len < 4) return null;
  172. const payload = arr.slice(BP_HEADER.length, BP_HEADER.length + len);
  173. // 常见格式:收缩压(2字节)、舒张压(2字节)、脉率(1字节,可选),小端
  174. const systolic = payload[0] | (payload[1] << 8);
  175. const diastolic = payload[2] | (payload[3] << 8);
  176. const pulse = payload.length >= 5 ? payload[4] : null;
  177. if (systolic > 300 || diastolic > 300) return null; // 合理范围校验
  178. return {
  179. type: 'bloodPressure',
  180. systolic,
  181. diastolic,
  182. pulse,
  183. time: new Date().toLocaleString()
  184. };
  185. }
  186. // 解析血糖数据:命令头 0xA5,0xA5;命令尾 0x5A,0x5A;中间为有效载荷
  187. function parseGlucoseMeasurement(buffer) {
  188. if (!buffer || buffer.byteLength < 6) return null;
  189. const arr = new Uint8Array(buffer);
  190. if (!matchHeader(arr, GLUCOSE_HEADER) || !matchTail(arr, GLUCOSE_TAIL)) return null;
  191. const len = arr.length - GLUCOSE_HEADER.length - GLUCOSE_TAIL.length;
  192. if (len < 2) return null;
  193. const payload = arr.slice(GLUCOSE_HEADER.length, GLUCOSE_HEADER.length + len);
  194. // 血糖值常见为 2 字节,单位 mmol/L(或需 *0.1)
  195. const raw = payload[0] | (payload[1] << 8);
  196. let value = raw;
  197. if (value > 1000) value = raw * 0.1;
  198. value = Math.round(value * 10) / 10;
  199. if (value <= 0 || value > 100) return null;
  200. return {
  201. type: 'glucose',
  202. value,
  203. unit: 'mmol/L',
  204. time: new Date().toLocaleString()
  205. };
  206. }
  207. export default {
  208. data() {
  209. return {
  210. scanning: false,
  211. connecting: false,
  212. disconnecting: false,
  213. connected: false,
  214. deviceList: [],
  215. selectedDeviceId: '',
  216. selectedDeviceName: '',
  217. deviceId: '',
  218. serviceId: '',
  219. characteristicId: '',
  220. notifyServiceId: '',
  221. notifyCharId: '',
  222. writeServiceId: '',
  223. writeCharId: '',
  224. lastGlucose: {
  225. value: null,
  226. unit: 'mmol/L',
  227. time: ''
  228. },
  229. lastBloodPressure: {
  230. systolic: null,
  231. diastolic: null,
  232. pulse: null,
  233. ihb: null, // 0 正常,1 心律不齐
  234. time: ''
  235. },
  236. // 血压计“等待连接”状态:收到 FD FD A4 01 01 用户 01 0D 0A 时置为 1 或 2,LCD 显示 bt 闪烁
  237. bpWaitUser: null,
  238. // 血压计已回复 FD FD 06 0D 0A,已开始量测
  239. bpMeasureStarted: false,
  240. // 量测过程中实时压力值(FD FD FB PressureH PressureL 0D 0A),单位 mmHg,收到最终结果后清空
  241. bpCurrentPressure: null,
  242. // 量测错误提示(E-1/E-2/E-3/E-5/E-E/E-B),收到成功结果或断开时清空
  243. bpErrorMsg: '',
  244. logs: [],
  245. showLog: true,
  246. statusText: '未连接',
  247. statusBarHeight: 0
  248. };
  249. },
  250. onLoad() {
  251. const sys = uni.getSystemInfoSync();
  252. this.statusBarHeight = sys.statusBarHeight || 0;
  253. },
  254. onUnload() {
  255. this.closeBLE();
  256. },
  257. methods: {
  258. goBack() {
  259. uni.navigateBack({ delta: 1 });
  260. },
  261. log(msg) {
  262. const line = `${new Date().toLocaleTimeString()} ${msg}`;
  263. this.logs.unshift(line);
  264. if (this.logs.length > 100) this.logs.pop();
  265. },
  266. async startScan() {
  267. this.deviceList = [];
  268. this.selectedDeviceId = '';
  269. this.log('打开蓝牙适配器...');
  270. try {
  271. await $p('openBluetoothAdapter');
  272. this.log('开始扫描低功耗蓝牙设备...');
  273. await $p('startBluetoothDevicesDiscovery', {
  274. allowDuplicatesKey: false
  275. });
  276. this.scanning = true;
  277. this.statusText = '正在扫描...';
  278. uni.onBluetoothDeviceFound((res) => {
  279. const nameContains = (d) => {
  280. const name = (d.name || d.localName || '').toLowerCase();
  281. return name.indexOf('bluetooth') !== -1;
  282. };
  283. const newDevices = (res.devices || []).filter(
  284. (d) => nameContains(d) && !this.deviceList.some((x) => x.deviceId === d.deviceId)
  285. );
  286. if (newDevices.length) {
  287. this.deviceList = [...this.deviceList, ...newDevices];
  288. }
  289. });
  290. setTimeout(() => this.stopScan(), 10000);
  291. } catch (e) {
  292. this.scanning = false;
  293. this.statusText = '未连接';
  294. const msg = (e.errMsg || e.message || String(e)).toLowerCase();
  295. if (msg.indexOf('bluetooth') !== -1 || msg.indexOf('adapter') !== -1) {
  296. uni.showToast({ title: '请开启手机蓝牙', icon: 'none' });
  297. }
  298. this.log('扫描失败:' + (e.errMsg || e));
  299. }
  300. },
  301. async stopScan() {
  302. if (!this.scanning) return;
  303. try {
  304. await $p('stopBluetoothDevicesDiscovery');
  305. } catch (e) {}
  306. this.scanning = false;
  307. this.statusText = this.connected ? '已连接' : '未连接';
  308. this.log('扫描已停止,共发现 ' + this.deviceList.length + ' 个设备');
  309. },
  310. selectDevice(d) {
  311. this.selectedDeviceId = d.deviceId;
  312. this.selectedDeviceName = d.name || d.localName || '未知设备';
  313. },
  314. async connectDevice() {
  315. if (!this.selectedDeviceId) {
  316. uni.showToast({ title: '请先选择设备', icon: 'none' });
  317. return;
  318. }
  319. this.connecting = true;
  320. this.log('正在连接 ' + this.selectedDeviceName + '...');
  321. this.statusText = '连接中...';
  322. try {
  323. await $p('createBLEConnection', {
  324. deviceId: this.selectedDeviceId,
  325. timeout: 10000
  326. });
  327. this.deviceId = this.selectedDeviceId;
  328. this.connected = true;
  329. this.statusText = '已连接';
  330. this.log('连接成功,正在发现服务...');
  331. await new Promise((r) => setTimeout(r, 600));
  332. const { services } = await $p('getBLEDeviceServices', {
  333. deviceId: this.deviceId
  334. });
  335. if (!services || services.length === 0) {
  336. this.log('未发现服务');
  337. this.connecting = false;
  338. return;
  339. }
  340. // 固定使用 Service 0xFFF0,接收 0xFFF1,发送 0xFFF2
  341. const svc = services.find((s) => s.uuid.toLowerCase().indexOf(BLE_SERVICE_UUID) !== -1);
  342. if (!svc) {
  343. this.log('未发现服务 0xFFF0');
  344. this.connecting = false;
  345. return;
  346. }
  347. this.serviceId = svc.uuid;
  348. this.log('使用服务: ' + this.serviceId);
  349. const { characteristics } = await $p('getBLEDeviceCharacteristics', {
  350. deviceId: this.deviceId,
  351. serviceId: this.serviceId
  352. });
  353. if (!characteristics || characteristics.length === 0) {
  354. this.log('未发现特征值');
  355. this.connecting = false;
  356. return;
  357. }
  358. console.log('特征值: ' + characteristics);
  359. const notifyChar = characteristics.find(
  360. (c) => c.uuid.toLowerCase().indexOf(BLE_CHAR_UUID_NOTIFY) !== -1
  361. );
  362. const writeChar = characteristics.find(
  363. (c) => c.uuid.toLowerCase().indexOf(BLE_CHAR_UUID_WRITE) !== -1
  364. );
  365. if (!notifyChar) {
  366. this.log('未找到接收特征 0xFFF1');
  367. this.connecting = false;
  368. return;
  369. }
  370. this.notifyServiceId = svc.uuid;
  371. this.notifyCharId = notifyChar.uuid;
  372. this.characteristicId = notifyChar.uuid;
  373. if (writeChar) {
  374. this.writeServiceId = svc.uuid;
  375. this.writeCharId = writeChar.uuid;
  376. this.log('发送特征 0xFFF2 已就绪');
  377. }
  378. this.log('Service: 0xFFF0, 接收(FFF1), 发送(FFF2)');
  379. await $p('notifyBLECharacteristicValueChange', {
  380. deviceId: this.deviceId,
  381. serviceId: this.notifyServiceId,
  382. characteristicId: this.notifyCharId,
  383. state: true
  384. });
  385. this.log('已开启数据通知');
  386. // 对码:连接成功后发送 FD FD FA 05 0D 0A,告知血压计可量测
  387. if (this.writeCharId) {
  388. try {
  389. const ab = new ArrayBuffer(BP_CMD_CONNECT_OK.length);
  390. console.log('发送',this.writeServiceId,this.writeCharId,ab)
  391. new Uint8Array(ab).set(BP_CMD_CONNECT_OK);
  392. await $p('writeBLECharacteristicValue', {
  393. deviceId: this.deviceId,
  394. serviceId: this.writeServiceId,
  395. characteristicId: this.writeCharId,
  396. value: ab
  397. });
  398. this.log('已发送对码:FD FD FA 05 0D 0A,等待血压计确认');
  399. } catch (err) {
  400. console.log('发送对码失败: ' + (err.errMsg || err.message || err));
  401. this.log('发送对码失败: ' + (err.errMsg || err.message || err));
  402. }
  403. }
  404. uni.onBLECharacteristicValueChange((res) => {
  405. if (res.deviceId !== this.deviceId) return;
  406. const buf = res.value;
  407. const arr = new Uint8Array(buf);
  408. const hex = Array.from(arr).map((b) => ('0' + b.toString(16)).slice(-2)).join(' ');
  409. this.log('收到数据: ' + hex);
  410. if (matchHeader(arr, BP_HEADER) && matchTail(arr, BP_TAIL)) {
  411. // 血压计回复:连接成功并开始量测 FD FD 06 0D 0A
  412. if (arr.length === BP_REPLY_START_MEASURE_LEN && arr[2] === 0x06) {
  413. this.bpMeasureStarted = true;
  414. this.log('血压计已确认,开始量测');
  415. return;
  416. }
  417. // 量测过程中压力信号:FD FD FB PressureH PressureL 0D 0A,压力=PressureH*256+PressureL
  418. if (arr.length === BP_PRESSURE_PACKET_LEN && arr[2] === BP_PRESSURE_CMD) {
  419. const pressure = arr[3] * 256 + arr[4];
  420. this.bpCurrentPressure = pressure;
  421. this.log('测量中 压力: ' + pressure + ' mmHg');
  422. return;
  423. }
  424. const waitInfo = parseBpWaitForConnection(arr);
  425. if (waitInfo) {
  426. this.bpWaitUser = waitInfo.user;
  427. this.log('血压计就绪 (用户' + waitInfo.user + '),LCD bt 闪烁,可开始测量');
  428. return;
  429. }
  430. // 量测完成测试结果:FD FD FC SYS DIA PUL IHB 0D 0A
  431. const result = parseBpResultPacket(arr);
  432. if (result) {
  433. this.bpCurrentPressure = null;
  434. this.bpErrorMsg = '';
  435. this.lastBloodPressure = {
  436. systolic: result.systolic,
  437. diastolic: result.diastolic,
  438. pulse: result.pulse,
  439. ihb: result.ihb,
  440. time: result.time
  441. };
  442. this.log('血压: ' + result.systolic + '/' + result.diastolic + ' mmHg 脉率' + result.pulse + (result.ihb ? ' 心律不齐' : ''));
  443. return;
  444. }
  445. // 量测错误码:FD FD FD err 0D 0A
  446. const errMsg = parseBpErrorPacket(arr);
  447. if (errMsg) {
  448. this.bpCurrentPressure = null;
  449. this.bpErrorMsg = errMsg;
  450. this.log('血压计错误: ' + errMsg);
  451. uni.showToast({ title: errMsg.slice(0, 20) + '…', icon: 'none', duration: 3000 });
  452. return;
  453. }
  454. const parsed = parseBloodPressure(buf);
  455. if (parsed) {
  456. this.bpCurrentPressure = null;
  457. this.bpErrorMsg = '';
  458. this.lastBloodPressure = {
  459. systolic: parsed.systolic,
  460. diastolic: parsed.diastolic,
  461. pulse: parsed.pulse,
  462. ihb: parsed.ihb != null ? parsed.ihb : null,
  463. time: parsed.time
  464. };
  465. this.log('血压: ' + parsed.systolic + '/' + parsed.diastolic + ' mmHg' + (parsed.pulse != null ? ' 脉率' + parsed.pulse : ''));
  466. }
  467. return;
  468. }
  469. if (matchHeader(arr, GLUCOSE_HEADER) && matchTail(arr, GLUCOSE_TAIL)) {
  470. const parsed = parseGlucoseMeasurement(buf);
  471. if (parsed && parsed.value != null) {
  472. this.lastGlucose = {
  473. value: parsed.value,
  474. unit: parsed.unit || 'mmol/L',
  475. time: parsed.time || new Date().toLocaleString()
  476. };
  477. this.log('血糖: ' + this.lastGlucose.value + ' ' + this.lastGlucose.unit);
  478. }
  479. }
  480. });
  481. uni.showToast({ title: '连接成功', icon: 'success' });
  482. } catch (e) {
  483. this.connected = false;
  484. this.deviceId = '';
  485. this.statusText = '未连接';
  486. const errMsg = e.errMsg || e.message || String(e);
  487. this.log('连接失败: ' + errMsg);
  488. uni.showToast({ title: '连接失败', icon: 'none' });
  489. }
  490. this.connecting = false;
  491. },
  492. async disconnect() {
  493. this.disconnecting = true;
  494. this.log('断开连接...');
  495. await this.closeBLE();
  496. this.connected = false;
  497. this.deviceId = '';
  498. this.serviceId = '';
  499. this.characteristicId = '';
  500. this.notifyServiceId = '';
  501. this.notifyCharId = '';
  502. this.writeServiceId = '';
  503. this.writeCharId = '';
  504. this.lastGlucose = { value: null, unit: 'mmol/L', time: '' };
  505. this.lastBloodPressure = { systolic: null, diastolic: null, pulse: null, ihb: null, time: '' };
  506. this.bpWaitUser = null;
  507. this.bpMeasureStarted = false;
  508. this.bpCurrentPressure = null;
  509. this.bpErrorMsg = '';
  510. this.statusText = '未连接';
  511. this.disconnecting = false;
  512. this.log('已断开');
  513. },
  514. async closeBLE() {
  515. try {
  516. if (this.deviceId) {
  517. await $p('closeBLEConnection', { deviceId: this.deviceId });
  518. }
  519. } catch (e) {}
  520. try {
  521. await $p('closeBluetoothAdapter');
  522. } catch (e) {}
  523. }
  524. }
  525. };
  526. </script>
  527. <style lang="scss" scoped>
  528. .page {
  529. min-height: 100vh;
  530. background: #f7f7f7;
  531. }
  532. .nav-bar {
  533. position: sticky;
  534. top: 0;
  535. z-index: 10;
  536. display: flex;
  537. align-items: center;
  538. justify-content: center;
  539. height: 88rpx;
  540. padding: 0 24rpx;
  541. background: #fff;
  542. border-bottom: 1rpx solid #eee;
  543. }
  544. .nav-title {
  545. font-size: 36rpx;
  546. font-weight: 600;
  547. color: #333;
  548. }
  549. .nav-back {
  550. position: absolute;
  551. left: 24rpx;
  552. padding: 10rpx 0;
  553. }
  554. .back-text {
  555. font-size: 30rpx;
  556. color: #FF5030;
  557. }
  558. .content {
  559. padding: 24rpx;
  560. padding-bottom: 60rpx;
  561. }
  562. .status-card {
  563. background: #fff;
  564. border-radius: 24rpx;
  565. padding: 48rpx;
  566. margin-bottom: 32rpx;
  567. text-align: center;
  568. border: 2rpx solid #eee;
  569. }
  570. .status-card.connected {
  571. border-color: #FF5030;
  572. background: #fffaf7;
  573. }
  574. .status-icon {
  575. font-size: 56rpx;
  576. color: #999;
  577. margin-bottom: 16rpx;
  578. }
  579. .status-card.connected .status-icon {
  580. color: #FF5030;
  581. }
  582. .status-text {
  583. font-size: 28rpx;
  584. color: #666;
  585. margin-bottom: 24rpx;
  586. }
  587. .bp-wait-hint {
  588. font-size: 24rpx;
  589. color: #999;
  590. margin-bottom: 16rpx;
  591. }
  592. .bp-measure-ok {
  593. color: #07c160;
  594. }
  595. .bp-pressure-live {
  596. font-size: 28rpx;
  597. color: #FF5030;
  598. font-weight: 600;
  599. margin-bottom: 16rpx;
  600. }
  601. .bp-error-msg {
  602. font-size: 24rpx;
  603. color: #e64340;
  604. background: #fff5f5;
  605. padding: 16rpx;
  606. border-radius: 12rpx;
  607. margin-bottom: 16rpx;
  608. line-height: 1.5;
  609. }
  610. .value-ihb {
  611. display: block;
  612. color: #e64340;
  613. margin-top: 4rpx;
  614. }
  615. .measure-value {
  616. margin: 24rpx 0 8rpx;
  617. }
  618. .value-label {
  619. display: block;
  620. font-size: 24rpx;
  621. color: #999;
  622. margin-bottom: 4rpx;
  623. }
  624. .value-num {
  625. font-size: 72rpx;
  626. font-weight: 700;
  627. color: #FF5030;
  628. }
  629. .value-unit {
  630. font-size: 28rpx;
  631. color: #999;
  632. margin-left: 8rpx;
  633. }
  634. .value-extra {
  635. display: block;
  636. font-size: 26rpx;
  637. color: #666;
  638. margin-top: 8rpx;
  639. }
  640. .measure-time {
  641. font-size: 24rpx;
  642. color: #999;
  643. }
  644. .actions {
  645. background: #fff;
  646. border-radius: 24rpx;
  647. padding: 32rpx;
  648. margin-bottom: 24rpx;
  649. }
  650. .btn {
  651. width: 100%;
  652. height: 88rpx;
  653. line-height: 88rpx;
  654. border-radius: 44rpx;
  655. font-size: 32rpx;
  656. margin-bottom: 24rpx;
  657. }
  658. .btn:last-child {
  659. margin-bottom: 0;
  660. }
  661. .btn-primary {
  662. background: #FF5030;
  663. color: #fff;
  664. border: none;
  665. }
  666. .btn-connect {
  667. background: #07c160;
  668. color: #fff;
  669. border: none;
  670. }
  671. .btn-warn {
  672. background: #fff;
  673. color: #e64340;
  674. border: 2rpx solid #e64340;
  675. }
  676. .device-list {
  677. margin: 24rpx 0;
  678. padding: 0 0 16rpx;
  679. border-bottom: 1rpx solid #eee;
  680. }
  681. .list-title {
  682. font-size: 26rpx;
  683. color: #999;
  684. margin-bottom: 16rpx;
  685. }
  686. .device-item {
  687. display: flex;
  688. align-items: center;
  689. padding: 20rpx 0;
  690. border-radius: 12rpx;
  691. }
  692. .device-item.active {
  693. background: #fff5f0;
  694. }
  695. .device-info {
  696. margin-left: 20rpx;
  697. display: flex;
  698. flex-direction: column;
  699. }
  700. .device-name {
  701. font-size: 30rpx;
  702. color: #333;
  703. }
  704. .device-id {
  705. font-size: 22rpx;
  706. color: #999;
  707. margin-top: 4rpx;
  708. }
  709. .log-section {
  710. background: #fff;
  711. border-radius: 24rpx;
  712. padding: 24rpx;
  713. }
  714. .log-title {
  715. display: flex;
  716. justify-content: space-between;
  717. font-size: 28rpx;
  718. color: #666;
  719. padding: 8rpx 0;
  720. }
  721. .log-toggle {
  722. color: #FF5030;
  723. font-size: 26rpx;
  724. }
  725. .log-list {
  726. max-height: 360rpx;
  727. margin-top: 16rpx;
  728. padding: 16rpx;
  729. background: #f5f5f5;
  730. border-radius: 12rpx;
  731. }
  732. .log-line {
  733. display: block;
  734. font-size: 22rpx;
  735. color: #666;
  736. line-height: 1.6;
  737. word-break: break-all;
  738. }
  739. </style>