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

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