You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

651 lines
17 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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);
}
});
});
},
});