import { hrDataArray } from './hrdata.js' const SendCommandUtil = require('../utils/SendCommandUtil.js'); // 引用 发送指令工具类 Page({ data: { // 渲染物品的动态CSS, xuanranCss:{ width:'0', height:'0' }, arrList: [], totalReceivedNodes: 0, // 记录设备推送的总节点数 totalConsumedNodes: 0, // 记录消费的总节点数 setInter: null, getDataIndex: 0, heartRateData: [], // 当前绘制的数据 windowSize: 1800, // 可视窗口大小 canvasWidth: 800, canvasHeight: 200, maxLevel: 2000, // 最大电平值 minLevel: -2000, // 最小电平值 isNotifyBLEC: false, //是否订阅 dataQueue: [], // 缓存接收到的长数组数据 isDrawing: false, // 绘制状态标志,避免重复触发 // deviceId: "66:3A:12:88:01:20", deviceId: "66:3A:12:88:01:20", serviceId: "000001FF-3C17-D293-8E48-14FE2E4DA212", ecgCharacteristicId: "0000FF0A-0000-1000-8000-00805F9B34FB", canvasList: [], // Canvas 列表,用于动态生成 Canvas pointsPerCanvas: 1800, // 每个 Canvas 绘制的点数 }, onReady() { // 初始化画布 this.ctx = wx.createCanvasContext('ecgCanvas'); // this.startDrawing(); this.notifyBLEC(); }, // 滑动窗口更新数据 pushHeartRate(newData) { let heartRateData = this.data.heartRateData; // 添加新数据 heartRateData.push(...newData); // 如果数据超过窗口大小,移除最早的数据 if (heartRateData.length > this.data.windowSize) { heartRateData.splice(0, heartRateData.length - this.data.windowSize); } // 动态计算最大值和最小值 // const maxLevel = Math.max(...heartRateData); // const minLevel = Math.min(...heartRateData); this.setData({ heartRateData, // maxLevel, // minLevel }); }, // 绘制 ECG 曲线 drawECG() { const ctx = this.ctx; const { heartRateData, canvasHeight, maxLevel, minLevel, canvasWidth, windowSize } = this.data; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); // 绘制网格 this.drawGrid(ctx); // 计算缩放比例 const scaleY = canvasHeight / (maxLevel - minLevel); // 数据缩放到画布高度范围 const step = canvasWidth / (windowSize - 1); // 每点的水平间隔 // 绘制心电图 ctx.beginPath(); heartRateData.forEach((value, index) => { const x = index * step; const y = canvasHeight - (value - minLevel) * scaleY; if (index === 0) { ctx.moveTo(x, y); } else { const prevX = (index - 1) * step; const prevY = canvasHeight - (heartRateData[index - 1] - minLevel) * scaleY; // 使用二次贝塞尔曲线平滑点之间的连接 const midX = (prevX + x) / 2; const midY = (prevY + y) / 2; ctx.quadraticCurveTo(prevX, prevY, midX, midY); } }); ctx.setStrokeStyle('red'); ctx.setLineWidth(0.5); ctx.stroke(); ctx.draw(); }, // 绘制网格 drawGrid(ctx) { const { canvasWidth, canvasHeight, } = this.data; var width = canvasWidth var height = canvasHeight var gridWidth = 5 const hLineNum = parseInt(height / gridWidth); const vLineNum = parseInt(width / gridWidth); console.log(hLineNum, vLineNum) ctx.setLineWidth(1); ctx.setStrokeStyle("#ccc"); // 横线 for (let i = 0; i < hLineNum; i++) { if (i === 0 || i % 5 !== 0) { ctx.beginPath(); ctx.moveTo(0, i * gridWidth); ctx.lineTo(width, i * gridWidth); ctx.stroke(); } } // 竖线 for (let i = 0; i < vLineNum; i++) { if (i === 0 || i % 5 !== 0) { ctx.beginPath(); ctx.moveTo(i * gridWidth, 0); ctx.lineTo(i * gridWidth, height); ctx.stroke(); } } // 粗线 ctx.setStrokeStyle("#9B9B9B"); for (let i = 5; i <= vLineNum; i += 5) { ctx.beginPath(); ctx.moveTo(i * gridWidth, 0); ctx.lineTo(i * gridWidth, height); ctx.stroke(); } for (let i = 5; i <= hLineNum; i += 5) { ctx.beginPath(); ctx.moveTo(0, i * gridWidth); ctx.lineTo(width, i * gridWidth); ctx.stroke(); } }, // 启动绘制逻辑 startDrawing() { if (this.data.isDrawing) return; // 避免重复启动 this.setData({ isDrawing: true }); const pointsPerRender = 50; // 每次绘制的点数,控制绘制速度 const interval = 100; // 绘制间隔(ms) const drawInterval = setInterval(() => { const { dataQueue, heartRateData, windowSize } = this.data; // 如果队列为空,停止绘制 if (dataQueue.length === 0) { clearInterval(drawInterval); this.setData({ isDrawing: false }); return; } // 从队列中取出一批数据 const newData = dataQueue.splice(0, pointsPerRender); // 更新消费的总节点数 this.setData({ totalConsumedNodes: this.data.totalConsumedNodes + newData.length }); // 更新心率数据(保持滑动窗口) const updatedData = heartRateData.concat(newData); if (updatedData.length > windowSize) { updatedData.splice(0, updatedData.length - windowSize); } this.setData({ heartRateData: updatedData, dataQueue }); this.drawECG(); // 绘制心电图 }, interval); }, stopECG() { clearInterval(this.data.setInter) }, startECG() { // this.startDrawing() SendCommandUtil.getECGData(this.data.deviceId) }, notifyBLEC() { wx.notifyBLECharacteristicValueChange({ deviceId: this.data.deviceId, // 蓝牙设备 ID serviceId: this.data.serviceId, // 蓝牙服务 UUID characteristicId: this.data.ecgCharacteristicId, // 心电 state: true, success: (res) => { console.log('心电特征订阅成功', res); this.setData({ isNotifyBLEC: true }) this.subscribeAndReceiveData(); }, fail: (err) => { console.error('特征订阅失败', err); }, }); }, // QRS滤波器处理函数 applyQRSFilter(rawData) { const filteredData = []; // 高通滤波器移除基线漂移 const highPassData = rawData.map((value, index, arr) => { if (index === 0) return value; return value - arr[index - 1] + 0.95 * (arr[index - 1] || 0); }); // 微分滤波器增强信号 const diffData = highPassData.map((value, index, arr) => { if (index < 4) return 0; // 前几个点无法计算微分 return ( (2 * arr[index] + arr[index - 1] - arr[index - 3] - 2 * arr[index - 4]) / 8 ); }); // 平滑滤波器减少噪声 const smoothData = diffData.map((value, index, arr) => { if (index < 5) return 0; // 前几个点无法平滑 return ( (arr[index] + arr[index - 1] + arr[index - 2] + arr[index - 3] + arr[index - 4]) / 5 ); }); // 阈值处理检测QRS波 const threshold = 0.6; // 自定义阈值(需要根据实际情况调整) smoothData.forEach((value, index) => { if (Math.abs(value) > threshold) { filteredData.push(value); // 保留QRS特征 } else { filteredData.push(0); // 非特征部分置零 } }); return filteredData; }, subscribeAndReceiveData() { if (this.data.isNotifyBLEC) { // 监听数据特征值变化 wx.onBLECharacteristicValueChange((res) => { // console.log('------------------------', res) if (res.characteristicId === this.data.ecgCharacteristicId) { const dataView = new DataView(res.value); const hexStr = this.uint8ArrayToHex(new Uint8Array(res.value)) // console.log("心电报文:", hexStr); // console.log("心电报文View:", dataView); // 获取下标 9-10 的字节 (dataType) let cmdType = dataView.getUint16(8); if (cmdType == 0x01) { // console.log("心电指令,ECG数据返回:", cmdType); } let dataType = dataView.getUint8(10); // console.log("dataType:", dataType); switch (dataType) { case 0x00: // console.log(this.data.arrList.join(',')) // console.log("ECG 测试结束:"); break; case 0x01: // console.log("ECG 测试开始:"); break; case 0x02: var bodyData = this.uint8ArrayToHex(new Uint8Array(res.value)) var bodyFor = [] for (let i = 0; i <= bodyData.length; i++) { if (bodyData[i] == ' ') { bodyFor.push(',') } else { bodyFor.push(bodyData[i]) } } bodyData = bodyFor.join('').split(',').splice(-8) var BPM = parseInt(bodyData[0], 16); var EcgProgress = parseInt(bodyData[7], 16) // console.log("ECG 参数同步:", BPM, EcgProgress) break; case 0x03: const rawData = new Uint8Array(res.value); console.log("原始数据:",rawData) let filterSigns = []; if (rawData.length >= 50) { for (let i = 0; i < 25; i++) { let filter = ((rawData[i * 2 + 1] & 0xff) << 8) | (rawData[i * 2 + 2] & 0xff); if (filter > 32767) { filter = filter - 65536; } filterSigns.push(filter); } } console.log('处理后的数据:',filterSigns) filterSigns = filterSigns.splice(5) this.setData({ arrList: [...this.data.arrList, ...filterSigns], dataQueue: this.data.dataQueue.concat(filterSigns), totalReceivedNodes: this.data.totalReceivedNodes + filterSigns.length, }); this.startDrawing(); break; case 0x04: // console.log("ECG 测试状态返回:"); break; default: // console.warn('未知数据类型', dataType); break; } } }); } else { this.notifyBLEC(); wx.showLoading({ title: '等待订阅中', }) setTimeout(() => { wx.hideLoading() this.subscribeAndReceiveData() }, 5000); } }, uint8ArrayToHex(array) { return Array.from(array) .map(byte => byte.toString(16).padStart(2, '0')) // 转换每个字节为 2 位的 16 进制,并补零 .join(' '); // 用空格连接所有字节 }, /** * 将十六进制字符串转换为十进制数组 * @param {string} hexString - 输入的十六进制字符串,每两个字符代表一个字节 * @returns {number[]} - 转换后的十进制数组 */ hexStringToDecimalArray(hexString) { if (!hexString || typeof hexString !== "string") { throw new Error("输入必须是有效的十六进制字符串!"); } // 确保字符串长度是偶数 if (hexString.length % 2 !== 0) { throw new Error("十六进制字符串长度必须为偶数!"); } const decimalArray = []; for (let i = 0; i < hexString.length; i += 2) { // 截取每两个字符并转换为十进制 const hexByte = hexString.substr(i, 2); const decimalValue = parseInt(hexByte, 16); // 检查转换结果是否有效 if (isNaN(decimalValue)) { throw new Error(`无效的十六进制字节:${hexByte}`); } decimalArray.push(decimalValue); } return decimalArray; }, // 将两个字节合并成一个有符号的 16 位整数 // 将每两个字节解析为一个有符号的 16 位整数 hexStringToSignedDecimalArray(hexString) { if (!hexString || typeof hexString !== "string") { throw new Error("输入必须是有效的十六进制字符串!"); } // 确保字符串长度是偶数 if (hexString.length % 2 !== 0) { throw new Error("十六进制字符串的长度必须是偶数!"); } const signedDecimalArray = []; for (let i = 0; i < hexString.length; i += 4) { // 每次处理两个字节 // 取出两个字节,拼接为一个 16 位整数(16 进制) const hexByte1 = hexString.substr(i, 2); // 第一个字节 const hexByte2 = hexString.substr(i + 2, 2); // 第二个字节 // 将这两个字节转为十进制数 const int16 = (parseInt(hexByte2, 16) << 8) | parseInt(hexByte1, 16); // 检查是否需要转换为负数 if (int16 >= 0x8000) { // 转换为负数(补码转换) signedDecimalArray.push(int16 - 0x10000); } else { signedDecimalArray.push(int16); } } return signedDecimalArray; }, // ============================= drawGridNews(ctx) { const { canvasWidth, canvasHeight, } = this.data; var width = 1000 var height = canvasHeight var gridWidth = 5 const hLineNum = parseInt(height / gridWidth); const vLineNum = parseInt(width / gridWidth); // console.log(hLineNum, vLineNum) ctx.setLineWidth(1); ctx.setStrokeStyle("#ccc"); // 横线 for (let i = 0; i < hLineNum; i++) { if (i === 0 || i % 5 !== 0) { ctx.beginPath(); ctx.moveTo(0, i * gridWidth); ctx.lineTo(width, i * gridWidth); ctx.stroke(); } } // 竖线 for (let i = 0; i < vLineNum; i++) { if (i === 0 || i % 5 !== 0) { ctx.beginPath(); ctx.moveTo(i * gridWidth, 0); ctx.lineTo(i * gridWidth, height); ctx.stroke(); } } // 粗线 ctx.setStrokeStyle("#9B9B9B"); for (let i = 5; i <= vLineNum; i += 5) { ctx.beginPath(); ctx.moveTo(i * gridWidth, 0); ctx.lineTo(i * gridWidth, height); ctx.stroke(); } for (let i = 5; i <= hLineNum; i += 5) { ctx.beginPath(); ctx.moveTo(0, i * gridWidth); ctx.lineTo(width, i * gridWidth); ctx.stroke(); } }, drawEcgSegment(canvasId, index) { const { arrList, // 原始数据 pointsPerCanvas, // 每个 Canvas 显示的点数 canvasHeight, // 画布高度 maxLevel, // 最大电平值 minLevel // 最小电平值 } = this.data; // 确定当前分片的起始和结束索引 const startIndex = index * pointsPerCanvas; const endIndex = Math.min(startIndex + pointsPerCanvas, arrList.length); const segmentData = arrList.slice(startIndex, endIndex); // 取当前分片的数据 // 水平缩放比例 (每点的水平间隔) const step = this.data.canvasWidth / pointsPerCanvas; // 垂直缩放比例 const scaleY = canvasHeight / (maxLevel - minLevel); // 获取 Canvas Context const ctx = wx.createCanvasContext(canvasId, this); // 清空画布 ctx.clearRect(0, 0, this.data.canvasWidth, canvasHeight); // 绘制网格 this.drawGridNews(ctx); // 绘制心电图 ctx.setStrokeStyle('red'); ctx.setLineWidth(1); ctx.beginPath(); segmentData.forEach((value, i) => { const x = i * step; const y = canvasHeight - (value - minLevel) * scaleY; if (i === 0) { ctx.moveTo(x, y); } else { const prevX = (i - 1) * step; const prevY = canvasHeight - (segmentData[i - 1] - minLevel) * scaleY; // 使用二次贝塞尔曲线平滑连接点 const midX = (prevX + x) / 2; const midY = (prevY + y) / 2; ctx.quadraticCurveTo(prevX, prevY, midX, midY); } }); ctx.stroke(); ctx.draw(); }, drawFullECG() { const { arrList, pointsPerCanvas } = this.data; const totalLength = arrList.length; const canvasCount = Math.ceil(totalLength / pointsPerCanvas); // 计算需要的 Canvas 数量 // 生成 Canvas 列表 const canvasList = Array.from({ length: canvasCount }, (_, index) => ({ canvasId: `ecgCanvas-${index}`, })); this.setData({ canvasList }, () => { // // 循环绘制每个 Canvas canvasList.forEach((item, index) => { this.drawEcgSegment(item.canvasId, index); }); }); }, // =============================================== // 绘制并保存 canvas 为图片 saveCanvasAsImage() { const that = this; const { canvasList, canvasWidth, canvasHeight } = that.data; // 创建一个目标 canvas const targetCanvas = wx.createCanvasContext('targetCanvas', this); // 设置目标 canvas 宽度和高度 const targetWidth = canvasList.length * 1000; const targetHeight = 200; // 假设每个 canvas 高度一致 // 将目标 canvas 的宽高设置好 console.log(targetCanvas,'-------------------') // targetCanvas.canvas.width = targetWidth; // targetCanvas.canvas.height = targetHeight; this.setData({ xuanranCss:{ width:targetWidth, height:targetHeight } }) // 循环通过每个 canvas 将图像拼接到目标 canvas canvasList.forEach((item, index) => { // 获取每个 canvas 的上下文并将其转为图片 wx.canvasToTempFilePath({ canvasId: item.canvasId, success(res) { // 每次拿到一个 canvas 的图片路径后,画到目标 canvas 上 targetCanvas.drawImage(res.tempFilePath, index * canvasWidth, 0, canvasWidth, canvasHeight); // 如果是最后一个 canvas,就绘制完毕并导出图片 if (index === canvasList.length - 1) { targetCanvas.draw(false, () => { // 合成完成后导出最终拼接的图像 wx.canvasToTempFilePath({ canvasId: 'targetCanvas', success(res) { // 这里可以获取到拼接后的图片路径 console.log('拼接后的图片路径:', res.tempFilePath); // 可以在页面中展示拼接后的图片 that.setData({ combinedImagePath: res.tempFilePath }); const tempFilePath = res.tempFilePath; // 选择保存到相册 wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success: (saveRes) => { wx.showToast({ title: '保存成功', icon: 'success', }); }, fail: (saveErr) => { wx.showToast({ title: '保存失败', icon: 'none', }); console.error('保存失败', saveErr); }, }); }, fail(error) { console.log('导出拼接图失败', error); } }); }); } }, fail(error) { console.log('转换 canvas 为临时文件失败', error); } }); }); }, });