first commit

main
sjchen 7 months ago
commit 21eca7fdcf

@ -0,0 +1 @@
{"containers":[],"config":{}}

@ -0,0 +1,6 @@
{
"files.associations": {
"*.wxml": "html",
"*.wxss": "css"
}
}

@ -0,0 +1,5 @@
App({
onLaunch: function () {
}
})

@ -0,0 +1,15 @@
{
"pages": [
"index/index",
"ecg/index",
"ecg2/index"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
},
"permission": {},
"sitemapLocation": "sitemap.json"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,651 @@
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);
}
});
});
},
});

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

@ -0,0 +1,24 @@
<view>
<view class="container">
<canvas canvas-id="ecgCanvas" style="width: 90%; height: 200px; border: 1px solid black;"></canvas>
</view>
<button bind:tap="startECG">继续</button>
<button bind:tap="stopECG">停止</button>
<button bind:tap="drawFullECG">绘制完整的心电图</button>
<button bind:tap="saveCanvasAsImage">保存心电图</button>
总结点:{{totalReceivedNodes}}
消费节点:{{totalConsumedNodes}}
<scroll-view scroll-x="true" style="width: 90%; height: 200px;display: flex;flex-wrap: nowrap;">
<!-- <canvas canvas-id="wanzheng" style="width: 50000px; height: 200px; border: 1px solid black"></canvas> -->
<view style="width:{{canvasList.length * 1000}}px;height:auto;display:flex;flex-wrap: nowrap;">
<block wx:for="{{canvasList}}" wx:key="index">
<canvas
canvas-id="{{item.canvasId}}"
style="width: 800px; height: 200px;margin:0px;padding:0px"
data-index="{{index}}"
></canvas>
</block>
</view>
</scroll-view>
<canvas canvas-id="targetCanvas" style="width:{{xuanranCss.width}}px;height:{{xuanranCss.height}}px"></canvas>
</view>

@ -0,0 +1,7 @@
.container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}

@ -0,0 +1,157 @@
Page({
data: {
gridWidth: 5,
width: 800,
height: 300,
},
onReady() {
// 使用 wx.createCanvasContext 创建绘图上下文
const backgroundCtx = wx.createCanvasContext('background');
console.log(backgroundCtx)
this.drawGrid(backgroundCtx);
backgroundCtx.draw(); // 必须调用 draw 方法将绘制提交到 canvas 上
// 启动波形绘制
const lineCtx = wx.createCanvasContext('line');
this.initializeWaves(lineCtx);
},
drawGrid(ctx) {
const { width, height, gridWidth } = this.data;
const hLineNum = parseInt(height / gridWidth);
const vLineNum = parseInt(width / gridWidth);
ctx.setLineWidth(1);
ctx.setStrokeStyle("#ffbebe");
// 横线
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("#FF7F50");
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();
}
},
initializeWaves(ctx) {
// 配置波形参数
const waveViews = [
{
frameSize: 250,
yMax: 250,
y_offset: 0,
step: 10,
speedRatio: 1,
strokeStyle: '#00ff00',
},
{
frameSize: 125,
yMax: 250,
y_offset: 100,
step: 10,
speedRatio: 0.5,
strokeStyle: '#00ff00',
},
{
frameSize: 60,
yMax: 250,
y_offset: 200,
step: 4,
speedRatio: 1,
strokeStyle: '#00F5FF',
},
];
// 初始化波形实例并启动动画
waveViews.forEach((config) => {
const wave = new this.WaveView(ctx, config);
wave.loop();
});
},
WaveView(ctx, { frameSize, yMax, y_offset, step, speedRatio, strokeStyle }) {
this.ctx = ctx;
this.currentX = 0;
this.currentY = 0;
this.lastX = 0;
this.lastY = 0;
this.step = step;
this.yMax = yMax;
this.y_offset = y_offset;
this.queue = [];
this.strokeStyle = strokeStyle;
this.speedRatio = speedRatio;
this.itemHeight = 100;
this.clearGap = 20;
this.drawInterval = Math.floor((1 / frameSize) * 1000 * step);
this.draw = () => {
ctx.beginPath();
ctx.setStrokeStyle(this.strokeStyle);
// 清除部分画布,避免图像重叠
if (this.lastX === 0) {
ctx.clearRect(this.currentX - 2, this.y_offset, this.clearGap, this.itemHeight);
} else {
ctx.clearRect(this.currentX + this.lastX, this.y_offset, this.clearGap, this.itemHeight);
}
// 绘制波形
for (let i = 0; i < this.step; i++) {
if (this.queue.length === 0) {
this.currentY = this.itemHeight / 2;
} else {
this.currentY = (-1.0 * this.queue.shift()) / this.yMax * this.itemHeight + this.itemHeight;
}
ctx.moveTo(this.currentX + this.lastX, this.y_offset + this.lastY);
ctx.lineTo(this.currentX, this.y_offset + this.currentY);
this.lastX = this.currentX;
this.lastY = this.currentY;
this.currentX += (this.step * 25 * this.speedRatio) / frameSize;
if (this.currentX >= 800 - 25) {
this.currentX = 0;
this.lastX = 0;
}
}
ctx.stroke();
ctx.draw(); // 必须提交绘制
};
this.loop = () => {
this.draw();
setTimeout(this.loop, this.drawInterval); // 循环调用
};
this.addData = (arr) => {
this.queue.push(...arr); // 添加数据到队列
};
},
});

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

@ -0,0 +1,5 @@
<view>
<view class="container">
<canvas canvas-id="ecgCanvas" style="width: 90%; height: 200px; border: 1px solid black;"></canvas>
</view>
</view>

@ -0,0 +1,8 @@
.boack {
position: absolute;
left: 0;
top: 10rpx;
width: 800rpx;
height: 300rpx;
}

@ -0,0 +1,558 @@
const SendCommandUtil = require('../utils/SendCommandUtil.js'); // 引用 发送指令工具类
const DataPacket = require('../utils/DataPacket.js');
const ParseDataPacket = require('../utils/ParseDataPacket.js');
const DynamicArrayBuffer = require('../utils/DynamicArrayBuffer.js');
const CommonUtil = require('../utils/CommonUtil.js');
const HealthDataCache = require('../utils/HealthDataCache.js');
Page({
// 数据定义区域,存储蓝牙设备 ID、服务 ID 和特征 ID
data: {
deviceId: '', // 存储蓝牙设备的ID(IOS是UUID 安卓是MAC)
serviceId: '0000FFF0-0000-1000-8000-00805F9B34FB',// 存储蓝牙设备的服务 UUID
notifyCharacteristicId:'0000FFF7-0000-1000-8000-00805F9B34FB',// 通知特征
initIndex: 0,
initTime: Math.floor(new Date().getTime() / 1000),
// 定义蓝牙数据缓存
sleepDataBuffer: new DynamicArrayBuffer(), // 创建缓存对象
isDataComplete: false, // 判断数据是否上传完毕
sleepList:[],
},
// 页面加载时触发的函数,用于初始化蓝牙适配器
onLoad() {
wx.getLocation({
type: 'gcj02',
success(res) {
console.log("开启位置授权:", res)
},
fail(err) {
console.error('获取位置失败', err);
}
});
wx.openBluetoothAdapter({ // 开启蓝牙适配器
success: (res) => {
console.log('蓝牙适配器初始化成功'); // 蓝牙适配器初始化成功的回调
this.startBluetoothDevicesDiscovery(); // 开始扫描附近的蓝牙设备
},
fail: (err) => {
console.error('蓝牙适配器初始化失败', err); // 蓝牙适配器初始化失败的回调
}
});
},
getLocation(){
wx.getLocation({
type: 'gcj02',
success(res) {
console.log("开启位置授权:", res)
},
fail(err) {
console.error('获取位置失败', err);
}
});
},
// 扫描蓝牙设备
startBluetoothDevicesDiscovery() {
wx.startBluetoothDevicesDiscovery({ // 启动设备扫描
allowDuplicatesKey: false, // 是否允许重复设备
success: (res) => {
console.log('开始扫描附近的蓝牙设备'); // 扫描成功回调
this.getBluetoothDevices(); // 调用函数获取蓝牙设备列表
},
fail: (res) => {
console.log('开始扫描附近的蓝牙设备', res);
},
});
},
// 获取附近的蓝牙设备列表,并尝试连接到目标设备
getBluetoothDevices() {
wx.onBluetoothDeviceFound((res) => {
// 过滤出名称以 "GJ-SH-01" 开头的设备
const targetDevices = res.devices.filter(device => device.name && device.name.startsWith("J2203"));
// 遍历并连接符合条件的设备
targetDevices.forEach(device => {
console.log("找到符合规则的蓝牙设备:", device);
const hexStr = this.uint8ArrayToMac(new Uint8Array(device.advertisData))
console.log("advertisData: ",hexStr)
// 连接指定设备
if(device.deviceId=="03:02:02:07:65:98"){
this.connectToDevice(device.deviceId);
}
});
});
},
// 连接指定的蓝牙设备
connectToDevice(deviceId) {
wx.createBLEConnection({ // 建立与蓝牙设备的连接
deviceId, // 设备的 ID
success: () => {
this.setData({ deviceId }); // 将设备 ID 保存到 data 中
console.log('蓝牙设备连接成功'); // 连接成功的回调
// 获取服务IDs和特征值
this.getCharacteristicId()
},
fail: (err) => {
console.error('蓝牙设备连接失败', err); // 连接失败的回调
}
});
},
// 获取服务下的特征 UUID
getCharacteristicId() {
// 获取设备服务列表
wx.getBLEDeviceServices({
deviceId: this.data.deviceId,
success: (res) => {
res.services.forEach(service => {
// 获取每个服务的特征值
wx.getBLEDeviceCharacteristics({
deviceId: this.data.deviceId,
serviceId: service.uuid,
success: (res) => {
res.characteristics.forEach(characteristic => {
if(characteristic.uuid.startsWith("0000FFF7")){
// 开始订阅特征
this.subscribeAndReceiveData()
}
});
}
});
});
}
});
},
// 连接指定的蓝牙设备
connectToDevice(deviceId) {
wx.createBLEConnection({ // 建立与蓝牙设备的连接
deviceId, // 设备的 ID
success: () => {
this.setData({
deviceId
}); // 将设备 ID 保存到 data 中
console.log('蓝牙设备连接成功'); // 连接成功的回调
// 获取服务IDs和特征值
this.getCharacteristicId()
},
fail: (err) => {
console.error('蓝牙设备连接失败', err); // 连接失败的回调
}
});
},
// 获取数据(健康数据、运动数据)
getData() {
SendCommandUtil.getHealthData(this.data.deviceId);
},
// 订阅 并 接收数据
subscribeAndReceiveData() {
wx.notifyBLECharacteristicValueChange ({
deviceId: this.data.deviceId, // 蓝牙设备 ID
serviceId: this.data.serviceId, // 蓝牙服务 UUID
characteristicId: this.data.notifyCharacteristicId, // 健康数据
state: true,
success: (res) => {
console.log('订阅手表向手机端发送数据事件', res);
},
fail: (err) => {
console.error('手表向手机端发送数据订阅失败', err);
},
});
wx.onBLECharacteristicValueChange((res) => {
const hexStr = this.uint8ArrayToHex(new Uint8Array(res.value))
// console.log("收到手表端发送的数据>>>>>>:",hexStr);
const dataView = new DataView(res.value);
// 获取第一个字节
let cmdType = dataView.getUint8(0);
console.log("cmdType",cmdType);
switch (cmdType) {
case 0x01:
console.log("同步时间成功");
break;
case 0x02:
console.log("同步用户信息成功");
break;
case 0x04:
let basicInfo = ParseDataPacket.parseWatchBasic(dataView,0);
console.log("手环基本信息:",basicInfo);
break;
case 0x13:
let power = dataView.getUint8(1);
console.log("获得电量成功:",power);
break;
case 0x23:
wx.showToast({
title: '设置成功', // 提示的内容
icon: 'success', // 图标,可选值为 'success', 'loading', 'none'
duration: 2000 // 提示的持续时间,单位为毫秒
});
break;
case 0x25:
wx.showToast({
title: '设置成功',
icon: 'success',
duration: 2000
});
break;
case 0x26:
let sedentaryInfo = ParseDataPacket.parseSedentary(dataView,0);
console.log("久坐信息:",sedentaryInfo);
break;
case 0x4B:
let targetInfo = ParseDataPacket.parseStepTarget(dataView,0);
console.log("运动目标信息:",targetInfo);
break;
case 0x51:
let stepList = ParseDataPacket.parseStepList(dataView,0);
console.log("步数总数据:",stepList);
break;
case 0x52:
let stepDetailList = ParseDataPacket.parseStepDetailList(dataView,0);
console.log("步数详情数据:",stepDetailList);
// 调用后台接口 todo ....
// 更新缓存
HealthDataCache.updateStepData(stepDetailList[0].startTime);
break;
case 0x55:
let heartRateList = ParseDataPacket.parseDataList(dataView,0);
let respList = this.getRespiratoryList(heartRateList);
console.log("心率数据:",heartRateList);
// console.log("呼吸率数据:",respList);
// 调用后台接口 todo ....
// 更新缓存
HealthDataCache.updateHeartRateData(heartRateList[0].time);
break;
case 0x56:
let parseBloodList = ParseDataPacket.parseBloodPressureList(dataView,0);
console.log("血压数据:",parseBloodList);
// 调用后台接口 todo ....
// 更新缓存
HealthDataCache.updateBloodPressureData(parseBloodList[0].time);
break;
case 0x57:
let alarmClockList = ParseDataPacket.parseAlarmClockList(dataView,0);
console.log("获取闹钟数据成功:",alarmClockList);
break;
case 0x66:
let bloodOxygenList = ParseDataPacket.parseDataList(dataView,0);
console.log("血氧数据:",bloodOxygenList);
// 调用后台接口 todo ....
// 更新缓存
HealthDataCache.updateBloodOxygenData(bloodOxygenList[0].time);
break;
case 0x65:
let tempList = ParseDataPacket.parseTempList(dataView,0);
// 目前返回的包括环境温度,后续固件改成只采集佩戴情况下的体温,暂时过滤掉 oneValue 小于 35.0 的数据
const filteredData = tempList.filter(item => item.oneValue >= 35.0);
console.log("体温数据:",tempList);
if(filteredData.length > 0){
// todo 调用后端接口
// 更新缓存
HealthDataCache.updateTemperatureData(filteredData[0].time);
}
break;
case 0x53:
let id = dataView.getUint8(1);
// 睡眠数据结束符
if(dataView.byteLength == 2 && id == 0xff){
let list = this.data.sleepList;
let arr = CommonUtil.assembleSleepData(list);
console.log("接口需要的睡眠数据:",arr);
// // 更新缓存
// HealthDataCache.updateSleepData(list[0].time);
// 调用接口传睡眠后,清空数据
this.setData({
sleepList: []
});
} else {
let sleepList = ParseDataPacket.parseSleepList(dataView,0);
// 排序minute 越大排前面
sleepList.sort((a, b) => {
return Number(b.minute) - Number(a.minute); // b.minute > a.minute 排前面
});
console.log("排序后数据:",sleepList)
// 获取当前已存在的 sleepList如果有
let currentSleepList = this.data.sleepList || [];
// 将新的 sleepList 追加到现有的 sleepList 中
let updatedSleepList = currentSleepList.concat(sleepList);
// 更新 data 中的 sleepList
this.setData({
sleepList: updatedSleepList
});
}
break;
default:
console.warn('未知数据类型', cmdType);
break;
}
});
},
init(){
// 同步时间
const setTimePacket = DataPacket.generateSetTimePacket();
SendCommandUtil.sendPackage(this.data.deviceId,setTimePacket);
// 同步用户性别、身高、体重、年龄
const userInfoPacket = DataPacket.generateSetUserInfoPacket(1,30,168,55);
SendCommandUtil.sendPackage(this.data.deviceId,userInfoPacket);
},
getPower(){
// 获取电量
const batteryPacket = DataPacket.generateBatteryCheckPacket();
SendCommandUtil.sendPackage(this.data.deviceId,batteryPacket);
},
getStep(){
// 获取步数(按天的总步数)
const stepPacket = DataPacket.generateReadStepTotalDataPacket();
SendCommandUtil.sendPackage(this.data.deviceId,stepPacket);
},
sendHealthCommand(){
// 指令依次是:心率,血氧、血压、体温、睡眠、步数
// 0x55,0x66,0x56,0x65,0x53,0x52
const commands = [0x55,0x66,0x56,0x65,0x53,0x52];
// 递归发送命令并加上 5 秒延迟
function sendNextCommand(index = 0) {
if (index >= commands.length) return; // 所有命令发送完毕,结束
const cmd = commands[index];
const packet = DataPacket.generateReadDataPacket(cmd);
console.log("hexStr", this.uint8ArrayToHex(packet));
// 发送命令包
SendCommandUtil.sendPackage(this.data.deviceId, packet);
// 每发送完一个命令后,等待 3 秒继续发送下一个
setTimeout(() => {
sendNextCommand.call(this, index + 1);
}, 3500); // 延迟 3.5 秒
}
// 开始发送第一个命令,并保持 `this` 上下文
sendNextCommand.call(this,0);
},
// 获取健康数据
getHealthData(){
// 睡眠数据
// const sleepPacket = DataPacket.generateReadSleepDataPacket();
// SendCommandUtil.sendPackage(this.data.deviceId,sleepPacket);
// 获取当前时间
let currentTime = Math.floor(new Date().getTime() / 1000);
// 如果是第一次调用则不判断时间如果是第二次调用要判断本次调用与上次是否间隔18秒因手表机制问题不能频繁发送指令给手表端
console.log(this.data.initTime,this.data.initIndex)
if(this.data.initIndex === 0){
this.sendHealthCommand();
this.setData({
initIndex: this.data.initIndex + 1,
initTime: currentTime
})
} else {
// 初始时间
let initTime = this.data.initTime
let timeDifference = currentTime - initTime;
if(this.data.initIndex > 0 && timeDifference > 18){
console.log(timeDifference,this.data.initIndex)
this.setData({
initIndex: this.data.initIndex + 1,
initTime: currentTime
})
this.sendHealthCommand();
}
}
},
/**
* 佩戴管理设置
*/
wearManageSetting() {
// 测试用例 > 手表基础信息设置(抬腕亮屏,屏幕亮度)------------------------------------------------------------
// let basicPacket = DataPacket.generateWatchBasicPacket(0,0);
// let hex = this.uint8ArrayToHex(basicPacket);
// console.log('发送抬腕亮屏,屏幕亮度指令:', hex);
// SendCommandUtil.sendPackage(this.data.deviceId,basicPacket);
// 测试用例 > 闹钟、吃药、喝水------------------------------------------------------------------------------
// const week = [0,1,1,1,1,1,0];// 表示周天、周一、周二、周三、周四、周五、周六
// let binaryWeek = CommonUtil.getWeekBinary(week);
// const alarms = [
// {
// id: 0, // 闹钟编号
// enabled: true, // 是否启用
// type: 1, // 闹钟类型1闹钟2吃药提示3喝水提示
// hour: 14, // 小时
// minute: 11, // 分钟
// weekEnable: binaryWeek, // 星期使能:每天都启用
// text: "闹钟", // 文本内容
// },
// {
// id: 1,
// enabled: true,
// type: 3,
// hour: 14,
// minute: 13,
// weekEnable: 0b01111111, // 启用周一、周三、周五
// text: "喝水",
// },
// {
// id: 2,
// enabled: true,
// type: 2,
// hour: 10,
// minute: 28,
// weekEnable: 0b01111111, // 启用周一、周三、周五
// text: "吃药",
// },
// ];
// // 这里的公式写死,不可修改
// let maxLength = 39 * alarms.length + 2;
// let alarmClockPackets = DataPacket.generateAlarmClockPackets(maxLength,alarms);
// let hex = this.uint8ArrayToHex(alarmClockPackets);
// console.log('发送闹钟指令:', hex);
// SendCommandUtil.sendPackage(this.data.deviceId,alarmClockPackets);
// 测试用例 > 久坐-------------------------------------------------------------------------------------
let sportTimePackets = DataPacket.generateSedentaryPacket(9, 0, 20, 0, [0,1,1,1,1,1,0], 60, 100, 1);
let hex1 = this.uint8ArrayToHex(sportTimePackets);
console.log('发送久坐指令:', hex1);
SendCommandUtil.sendPackage(this.data.deviceId,sportTimePackets);
// 测试用例 > 运动目标-------------------------------------------------------------------------------------
let targetPacket = DataPacket.generateStepTargetPacket(5000,0,4,160,480);
let hex2 = this.uint8ArrayToHex(sportTimePackets);
console.log('发送运动目标指令:', hex2);
SendCommandUtil.sendPackage(this.data.deviceId,targetPacket);
},
// 获取佩戴管理各项设置值
wearManageGetting() {
// 获取手环基本信息
// let basic = DataPacket.getWatchBasic();
// SendCommandUtil.sendPackage(this.data.deviceId,basic);
// 获取闹钟数据(闹钟、吃药、喝水)
// let alarmClockPacket = DataPacket.getAlarmClock();
// SendCommandUtil.sendPackage(this.data.deviceId,alarmClockPacket);
// 获取久坐数据
// let sedentaryPacket = DataPacket.getSedentary();
// SendCommandUtil.sendPackage(this.data.deviceId,sedentaryPacket);
// 获得运动目标
// let stepTargetPacket = DataPacket.getStepTarget();
// SendCommandUtil.sendPackage(this.data.deviceId,stepTargetPacket);
},
/**
* 下发天气指令
*/
downWeather(){
let packet = DataPacket.generateWeatherPacket("晴","20","30","10","66","长沙市")
let hex = this.uint8ArrayToHex(packet);
console.log('发送天气指令:', hex);
SendCommandUtil.sendPackage(this.data.deviceId,packet);
},
removeCache(){
wx.removeStorageSync('healthDataCache');
this.setData({
index1:0,
index2:0,
index3:0,
index4:0,
index5:0
})
},
// 字节转16进制
uint8ArrayToHex(array) {
return Array.from(array)
.map(byte => byte.toString(16).padStart(2, '0')) // 转换每个字节为 2 位的 16 进制,并补零
.join(' '); // 用空格连接所有字节
},
// IOS MAC地址转换
uint8ArrayToMac(array) {
return Array.from(array.slice(2, 8))
.map(byte => byte.toString(16).padStart(2, '0').toUpperCase()) // 转换为两位 16 进制
.join(':'); // 用冒号连接
},
/**
* 根据心率批量计算呼吸率
* @param {解析后的心率列表} heartList
*/
getRespiratoryList(heartList){
let respList = [];
heartList.forEach(item => {
let respValue = this.calculateRespiratory(item.oneValue);
let respItem = {
type:5,
oneValue:respValue,
time:item.time
}
respList.push(respItem);
})
return respList;
},
/**
* 根据心率计算呼吸率
* @param {心率} heartValue
*/
calculateRespiratory(heartValue){
//心率值
let X = heartValue;
//呼吸率
let Y = 0;
//正常心率计算法
if(X >= 50 && X <= 100){
Y= ( ( X - 2 ) / 4 );
//心率大于100的计算法
} else if ( X >100 ){
Y= ( 25 + ( X - 100 ) / 4 );
//心率小于50的计算法
} else if ( X < 50 ){
//计算结果向下取整
Y = Math.floor(12 - (50 - X) / 4.0);
} else {
Y = 0;
}
return parseInt(Y);
},
});

@ -0,0 +1,12 @@
<!--program/pages/bluetoothWatch/bluetoothWatch.wxml-->
<view>
<button bindtap="init">初始化信息</button>
<button bindtap="getPower">获取电量</button>
<button bindtap="getStep">获取步数</button>
<button bindtap="getHealthData">获取健康数据</button>
<button bindtap="wearManageSetting">佩戴管理设置</button>
<button bindtap="wearManageGetting">佩戴管理获取</button>
<button bindtap="downWeather">下发天气</button>
<button bindtap="removeCache">清除时间戳缓存</button>
</view>

@ -0,0 +1,41 @@
page {
color: #333;
}
.devices_summary {
margin-top: 30px;
padding: 10px;
font-size: 16px;
}
.device_list {
height: 300px;
margin: 50px 5px;
margin-top: 0;
border: 1px solid #EEE;
border-radius: 5px;
width: auto;
}
.device_item {
border-bottom: 1px solid #EEE;
padding: 10px;
color: #666;
}
.device_item_hover {
background-color: rgba(0, 0, 0, .1);
}
.connected_info {
position: fixed;
bottom: 0;
width: 100%;
background-color: #F0F0F0;
padding: 10px;
padding-bottom: 20px;
margin-bottom: env(safe-area-inset-bottom);
font-size: 14px;
min-height: 100px;
box-shadow: 0px 0px 3px 0px;
}
.connected_info .operation {
position: absolute;
display: inline-block;
right: 30px;
}

@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs"
}
}

@ -0,0 +1,47 @@
{
"description": "项目配置文件详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": true,
"minified": true,
"newFeature": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"coverView": true,
"lazyloadPlaceholderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"useIsolateContext": true,
"enhance": true,
"useMultiFrameRuntime": true,
"showShadowRootInWxmlPanel": true,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"ignoreUploadUnusedFiles": true,
"minifyWXML": true,
"checkInvalidKey": true,
"useStaticServer": true,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"compileType": "miniprogram",
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"libVersion": "3.6.4",
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxb761a6f5c6200ed8"
}

@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "2203_bluetooth",
"setting": {
"compileHotReLoad": true
}
}

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

2705
typings/wx.d.ts vendored

File diff suppressed because it is too large Load Diff

@ -0,0 +1,293 @@
const CommonUtil = {
/**
* 获取当前时间
*/
getCurrenTime(){
// 创建一个 Date 对象,表示当前时间
let now = new Date();
// 获取年、月、日、时、分、秒
// 获取年月日时分秒
let time = {
year: now.getFullYear().toString().slice(-2), // 获取年份后两位
month: (now.getMonth() + 1), // 月份从0开始需加1
day: now.getDate(),
hours: now.getHours(),
minutes: now.getMinutes(),
seconds: now.getSeconds()
};
return time;
},
/**
* 通过时间戳 获取年月日 时分秒
* @param {10 时间戳} timestamp
*/
getDateByTimestamp(timestamp){
if(timestamp === 0){
return {
year:0,
month:0,
day:0,
hours:0,
minutes:0,
seconds:0
}
}
// 将时间戳转为毫秒JavaScript的Date对象使用毫秒为单位
const date = new Date(timestamp * 1000);
// 格式化为年月日时分秒
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
console.log(formattedDate); // 输出2024-12-20 00:00:00
return {
year:date.getFullYear() - 2000,
month:date.getMonth() + 1,
day:date.getDate(),
hours:date.getHours(),
minutes:date.getMinutes(),
seconds:date.getSeconds()
}
},
/**
* 发送屏幕亮度0~5级别转换
* @param {*} level
*/
sendSBLevelConv(level){
switch(level){
case 0:
return 0x8F;
case 1:
return 0x88;
case 2:
return 0x86;
case 3:
return 0x84;
case 4:
return 0x82;
case 5:
return 0x80;
default:
return 0x84;//如果不是0~5则默认均衡亮度
}
},
/**
* 获得屏幕亮度0~5级别转换
* @param {*} level
*/
respSBLevelConv(level){
switch(level){
case 0x0F:
return 0;
case 0x0D:
return 1;
case 0x0B:
return 2;
case 0x09:
return 3;
case 0x07:
return 4;
case 0x05:
return 5;
default:
return 2;//如果不是0~5则默认均衡亮度
}
},
getWeekBinary(week) {
let binaryValue = 0;
for (let i = 0; i < week.length; i++) {
// 逐位设置week[0] 代表周天offset 0
binaryValue |= week[i] << i;
}
return binaryValue;
},
stringToBytes(str) {
let encoded = unescape(encodeURIComponent(str));
let bytes = [];
for (let i = 0; i < encoded.length; i++) {
bytes.push(encoded.charCodeAt(i));
}
console.log("bytes:",bytes)
return bytes;
},
bytesToString(bytes) {
let str = '';
for (let i = 0; i < bytes.length; i++) {
str += String.fromCharCode(bytes[i]);
}
return decodeURIComponent(escape(str)); // 确保正确解码
},
/**
* 字段转指令
* @param {字段} field
*/
fieldToCommand(field){
switch(field){
case "sleepLastTime":
return 0x53;
case "heartRateLastTime":
return 0x55;
case "temperatureLastTime":
return 0x65;
case "bloodOxygenLastTime":
return 0x66;
case "bloodPressureLastTime":
return 0x56;
case "stepLastTime":
return 0x52;
default:
return 0;
}
},
/**
* 指令转字段
* @param {指令} command
*/
commandToField(command){
switch(command){
case 0x53:
return "sleepLastTime";
case 0x55:
return "heartRateLastTime";
case 0x65:
return "temperatureLastTime";
case 0x66:
return "bloodOxygenLastTime";
case 0x56:
return "bloodPressureLastTime";
case 0x52:
return "stepLastTime";
default:
return 0;
}
},
/**
* 组装睡眠数据
*/
assembleSleepData(sleepArray) {
let dateObj = sleepArray[0]; // 获取第一个数据作为比对的时间
let res = []; // 存放最终结果
let resItem = { // 每天的睡眠数据对象
date: '',
sleepList: []
};
// 取出第一个数据并初始化
let firstSleepData = sleepArray[0];
resItem.date = firstSleepData.yyyyMMdd;
resItem.sleepList.push(firstSleepData); // 将第一个数据放入到 `sleepList`
sleepArray.forEach(item => {
// 判断是否是同一天的数据
let isSameDay = this.timeComparison(dateObj.yyyyMMdd, item.yyyyMMdd, item.minute);
// 如果状态发生变化,且日期不同,就表示开始新的睡眠数据
if (item.status !== firstSleepData.status) {
if (isSameDay) {
resItem.sleepList.push(item); // 如果是同一天,继续添加到当天的睡眠数据
} else {
res.push(resItem); // 否则,将当天的数据推入结果数组
resItem = { // 新的一天开始
date: item.yyyyMMdd,
sleepList: [item] // 将当前项作为新的睡眠数据开始
};
dateObj = item; // 更新 `dateObj`,用于比对后续数据
}
}
firstSleepData = item; // 更新当前比对的睡眠数据
});
// 最后一次的睡眠数据也需要被加入到 `res`
if (resItem.sleepList.length > 0) {
res.push(resItem);
}
return res; // 返回按天拆分后的睡眠数据
},
/**
* 计算出一天的时间范围
* @param {日期} dateStr
*/
getPreviousDayTimeRange(dateStr) {
// 将 "YYYYMMDD" 格式的日期转换为 "YYYY-MM-DD" 格式,方便解析
const formattedDateStr = dateStr.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
// 创建目标日期对象
const date = new Date(formattedDateStr); // 例如2024-12-12
// 1. 获取前一天的 20:00:00
const prevDayStart = new Date(date); // 克隆原日期
prevDayStart.setDate(date.getDate() - 1); // 设置为前一天
prevDayStart.setHours(20, 0, 0, 0); // 设置为20:00:00
// 2. 获取当天的 10:00:00
const nextDayEnd = new Date(date); // 克隆原日期
nextDayEnd.setDate(date.getDate()); // 保持为当天
nextDayEnd.setHours(10, 0, 0, 0); // 设置为10:00:00
// 返回时间戳
return {
startTimestamp: prevDayStart.getTime(), // 转换为10位时间戳
endTimestamp: nextDayEnd.getTime() // 转换为10位时间戳
};
},
/**
*
* @param {日期} dateStr
* @param {一天的第N分钟} minutes
*/
getTimestampFromDateAndMinutes(dateStr, minutes) {
// 将 "YYYYMMDD" 格式的日期转换为 "YYYY-MM-DD" 格式,方便解析
const formattedDateStr = dateStr.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
// 创建目标日期对象
const date = new Date(formattedDateStr); // 例如2024-12-12
// 计算总分钟数
const totalMinutes = minutes;
// 将分钟数转化为小时和分钟
const hours = Math.floor(totalMinutes / 60); // 计算小时数
const mins = totalMinutes % 60; // 计算剩余的分钟数
// 设置时间为指定的小时和分钟
date.setHours(hours, mins, 0, 0); // 设置为相应的时分秒
// 返回时间戳转换为10位时间戳
return date.getTime();
},
/**
*
* @param {日期范围} date
* @param {分钟} minute
* @param {比较值} comparisonValue
*/
timeComparison(date,comparisonValue,minute){
let dateRange = this.getPreviousDayTimeRange(date);
let time = this.getTimestampFromDateAndMinutes(comparisonValue,minute);
if(time >= dateRange.startTimestamp && time <= dateRange.endTimestamp){
// 在范围内,是同一天睡眠
return true;
}
return false;
}
}
// 导出模块
module.exports = CommonUtil;

@ -0,0 +1,455 @@
// 数据包组装模块
const { calculateCRC } = require('./crc');
const CommonUtil = require('./CommonUtil');
const WeatherUtils = require('./WeatherUtils');
const HealthDataCache = require('./HealthDataCache.js');
const DataPacket = {
// 转换为 BCD 格式
toBCD(value) {
return ((value / 10) << 4) | (value % 10);
},
// 组装时间设置命令包
generateSetTimePacket() {
let arr = CommonUtil.getCurrenTime();
// 将时间参数转换为BCD格式
const bcdYear = this.toBCD(arr.year);
const bcdMonth = this.toBCD(arr.month);
const bcdDay = this.toBCD(arr.day);
const bcdHour = this.toBCD(arr.hours);
const bcdMinute = this.toBCD(arr.minutes);
const bcdSecond = this.toBCD(arr.seconds);
// 创建Uint8Array
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x01; // 命令字节
packet[1] = bcdYear;
packet[2] = bcdMonth;
packet[3] = bcdDay;
packet[4] = bcdHour;
packet[5] = bcdMinute;
packet[6] = bcdSecond;
// 填充其他字节为 0
packet.fill(0x00, 7, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
generateSetUserInfoPacket(gender, age, height, weight) {
// 输入参数验证
if (![0, 1].includes(gender)) {
throw new Error('性别只能为 0 或 1');
}
if (age < 0 || age > 150) {
throw new Error('年龄无效');
}
if (height < 0 || height > 300) {
throw new Error('身高无效');
}
if (weight < 0 || weight > 500) {
throw new Error('体重无效');
}
// 创建 Uint8Array 数据包
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x02; // 命令字节
packet[1] = gender; // 性别
packet[2] = age; // 年龄
packet[3] = height; // 身高
packet[4] = weight; // 体重
packet[5] = 70; // 步长
// 填充其他字节为 0
packet.fill(0x00, 6, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
/**
* 设置手环基本参数
* @param {抬腕亮屏 1-开启0-关闭} WRSFlag
* @param {屏幕亮度级别 分6个等级参数传值0-50:最暗5:最亮} SBLevel
*/
generateWatchBasicPacket(WRSFlag,SBLevel){
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x03; // 命令字节
packet[1] = 0x80; // 固定值距离单位0x81MILE0x80:KM
packet[2] = 0x80; // 固定值12小时24小时显示0x81: 12小时制 0x8024小时制度
packet[3] = WRSFlag==1 ? 0x81:0x80; // 固定值,抬腕亮屏 0x81开启 0x80:关闭
packet[4] = 0x80; // 固定值,温度单位切换 0x81华摄氏度 0x80摄氏度默认摄氏度
packet[5] = 0x80; // 固定值,夜间模式 0x81开启 0x80关闭(夜间模式开启后晚上六点到凌晨7点自动调节显示亮度到最暗)
packet[6] = 0x80; // 固定值ANCS使能开关 0x81开启 0x80关闭
packet.fill(0x00, 7, 10); // 填充其他字节为0
packet[11] = CommonUtil.sendSBLevelConv(SBLevel);// 屏幕亮度
packet[12] = 0x80;// 表盘界面更换80-8A
packet[13] = 0x80;// 社交距离开关 0x81 开 0x80 关
packet[14] = 0x81;//中英文切换80代表英文(默认)81代表中文
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
console.log("Generated Packet:", packet); // 调试最终数据包
return packet;
},
/**
* 设置手环基本参数
* @param {抬腕亮屏 1-开启0-关闭} WRSFlag
*/
getWatchBasic(){
const packet = new Uint8Array(16);
packet[0] = 0x04;
packet[1] = 0x00;
packet.fill(0x00, 2, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
// 获取电量
generateBatteryCheckPacket() {
// 创建 Uint8Array 数据包
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x13; // 命令字节
packet[1] = 0x00; // 固定值,表示电量检测
packet.fill(0x00, 2, 15); // 填充其他字节为0
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
/**
*
* @param {指令} command
*/
generateReadDataPacket(command){
// 创建 Uint8Array 数据包
const packet = new Uint8Array(16);
packet[0] = command; // 命令字节
packet[1] = 0x00; //0x99: 删除数据0x00读取最近数据0x01读取指定位置数据0x02继续读取上一次位置
let field = CommonUtil.commandToField(command);// 根据指令获取字段
// console.log("field>>>:",field);
let lastTime = HealthDataCache.getLastTimeField(field);// 根据字段获取最近一次同步数据的时间戳
// console.log("lastTime1>>>:",lastTime);
let date = CommonUtil.getDateByTimestamp(lastTime);
packet[2] = 0x00; // 占位
packet[3] = 0x00; // 占位
packet[4] = "0x"+ date.year; //年
packet[5] = "0x"+ date.month; //月
packet[6] = "0x"+ date.day; //日
packet[7] = "0x"+ date.hours; //时
packet[8] = "0x"+ date.minutes; //分
packet[9] = "0x"+ date.seconds; //秒
packet.fill(0x00, 10, 15); // 填充其他字节为0
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
// 获取步数总数
generateReadStepTotalDataPacket() {
// 创建 Uint8Array 数据包
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x51; // 命令字节
packet[1] = 0x00; // 读取记步总数据
packet.fill(0x00, 2, 15); // 填充其他字节为0
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
// 获取睡眠数据
generateReadSleepDataPacket() {
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x53;
packet[1] = 0x00;
packet.fill(0x00, 2, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
// 生成获取时间命令包
generateGetTimePacket() {
const packet = new Uint8Array(16);
// 填充数据
packet[0] = 0x41; // 命令字节
packet.fill(0x00, 1, 15); // 填充其他字节为 0
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
// 生成闹钟、吃药、喝水提示
generateAlarmClockPackets(maxLength, alarms) {
const commands = new Uint8Array(maxLength); // 创建固定长度的 Uint8Array
let offset = 0; // 当前写入位置
const packetLength = 39; // 单个指令长度
const maxAlarmsPerPacket = Math.floor(maxLength / packetLength); // 每条指令最大包含闹钟个数
const totalPackets = Math.ceil(alarms.length / maxAlarmsPerPacket); // 总共需要多少条指令
for (let i = 0; i < totalPackets; i++) {
const startIdx = i * maxAlarmsPerPacket;
const endIdx = Math.min(startIdx + maxAlarmsPerPacket, alarms.length);
for (let j = startIdx; j < endIdx; j++) {
const alarm = alarms[j];
if (offset + packetLength > maxLength) {
throw new Error("Maximum length exceeded, cannot fit all alarms.");
}
// 写入闹钟数据
commands[offset++] = 0x23; // 固定头部
commands[offset++] = alarms.length; // XX: 总闹钟个数
commands[offset++] = alarm.id; // AA: 闹钟编号
commands[offset++] = alarm.enabled ? 0x01 : 0x00; // BB: Enable/Disable
commands[offset++] = alarm.type; // CC: 闹钟类型 1:闹钟2:吃药提示3:喝水提示。
commands[offset++] = this.toBCD(alarm.hour); // DD: 小时 (BCD)
commands[offset++] = this.toBCD(alarm.minute); // EE: 分钟 (BCD)
commands[offset++] = alarm.weekEnable; // FF: 星期使能
commands[offset++] = alarm.text.length; // GG: 文本长度
// 写入文本内容或填充 0x00
for (let t = 0; t < 15; t++) {
// commands[offset++] = t < alarm.text.length ? alarm.text.charCodeAt(t) : 0x00;
if (t < alarm.text.length) {
const charCode = alarm.text.charCodeAt(t);
commands[offset++] = (charCode >> 8) & 0xFF; // 高字节
commands[offset++] = charCode & 0xFF; // 低字节
} else {
commands[offset++] = 0x00; // 填充 0x00
commands[offset++] = 0x00;
}
}
}
}
// 尾巴
commands[offset++] = 0x23;
commands[offset++] = 0xFF;
return commands;
},
/**
* 获取闹钟数据
*/
getAlarmClock(){
const packet = new Uint8Array(16);
packet[0] = 0x57;
packet[1] = 0x00;
packet.fill(0x00, 2, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
/**
*
* @param {开始 } startHour
* @param {开始 } startMinute
* @param {结束 } endHour
* @param {结束 } endMinute
* @param {星期天 星期六周} weekDays
* @param {运动提醒周期单位为分钟} reminderInterval
* @param {最少运动步数} minSteps
* @param {运动总开关} sportSwitch
*/
generateSedentaryPacket(startHour, startMinute, endHour, endMinute, weekDays, reminderInterval, minSteps, sportSwitch){
const packet = new Uint8Array(16);
// 填充命令数据包
packet[0] = 0x25; // 命令字节
// 开始运动时间
packet[1] = startHour; // AA: 开始运动时间的小时部分
packet[2] = startMinute; // BB: 开始运动时间的分钟部分
// 结束运动时间
packet[3] = endHour; // CC: 结束运动时间的小时部分
packet[4] = endMinute; // DD: 结束运动时间的分钟部分
let binaryWeek = CommonUtil.getWeekBinary(weekDays);
packet[5] = binaryWeek; // EE: 星期日到星期六
// 运动提醒周期FF
packet[6] = reminderInterval; // FF: 运动提醒周期,单位为分钟
// 最少运动步数GG
packet[7] = minSteps; // GG: 最少运动步数
// 运动总开关HH
packet[8] = sportSwitch === 1 ? 0x01 : 0x00; // HH: 运动总开关 1-开启 0-关闭
// 填充后续字节为0x00
packet.fill(0x00, 9, 15);
// 计算CRC并设置CRC字节CRC算法需根据协议提供
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc; // CRC校验字节
return packet;
},
/**
* 获取久坐数据
*/
getSedentary(){
const packet = new Uint8Array(16);
packet[0] = 0x26;
packet[1] = 0x00;
packet.fill(0x00, 2, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
/**
* 设置运动目标
* @param {步数目标} targetSteps
* @param {目标时间} targetTime
* @param {距离目标} targetDistance
* @param {卡路里目标} targetCalories
* @param {睡眠目标} targetSleep
*/
generateStepTargetPacket(targetSteps, targetTime, targetDistance, targetCalories, targetSleep){
// 1. 组装目标步数4字节低字节在前
const stepBytes = [
targetSteps & 0xFF, // AA: 低字节
(targetSteps >> 8) & 0xFF, // BB: 次低字节
(targetSteps >> 16) & 0xFF, // CC: 次高字节
(targetSteps >> 24) & 0xFF // DD: 高字节
];
// 2. 组装目标步数时间2字节分钟低字节在前
const timeBytes = [
targetTime & 0xFF, // EE: 低字节
(targetTime >> 8) & 0xFF // FF: 高字节
];
// 3. 组装目标距离2字节低字节在前
const distanceBytes = [
targetDistance & 0xFF, // GG: 低字节
(targetDistance >> 8) & 0xFF // HH: 高字节
];
// 4. 组装目标卡路里2字节低字节在前
const caloriesBytes = [
targetCalories & 0xFF, // II: 低字节
(targetCalories >> 8) & 0xFF // JJ: 高字节
];
// 5. 组装目标睡眠时间2字节分钟低字节在前
const sleepBytes = [
targetSleep & 0xFF, // KK: 低字节
(targetSleep >> 8) & 0xFF // LL: 高字节
];
// 6. 组装命令字节0x0B
const commandByte = 0x0B;
// 7. 保留字节
const reservedBytes = [0x00, 0x00];
// 8. 拼接所有字节
const packet = [
commandByte, ...stepBytes, ...timeBytes, ...distanceBytes,
...caloriesBytes, ...sleepBytes, ...reservedBytes
];
// 9. 计算CRC校验字节
const crc = calculateCRC(packet); // 需要实现CRC校验算法
// 10. 将CRC字节添加到数据包末尾
packet.push(crc);
return new Uint8Array(packet);
},
/**
* 获取运动目标
*/
getStepTarget(){
const packet = new Uint8Array(16);
packet[0] = 0x4B;
packet[1] = 0x00;
packet.fill(0x00, 2, 15);
// 计算并设置 CRC 校验字节
const crc = calculateCRC(packet.subarray(0, 15));
packet[15] = crc;
return packet;
},
/**
*
* @param {当前天气} weather
* @param {当前气温} temperature
* @param {最大气温} maxTemperature
* @param {最小气温} minTemperature
* @param {空气质量} aqi
* @param {地址长度} length
* @param {地址最大32位} address
*/
generateWeatherPacket(weather,temperature,maxTemperature,minTemperature,aqi,address){
let aqiInt = parseInt(aqi);
const packet = new Uint8Array(8);
packet[0] = 0x15; // 命令字节
const weatherCode = WeatherUtils.getWeatherCode(weather);
packet[1] = weatherCode;
packet[2] = parseInt(temperature);
packet[4] = parseInt(maxTemperature);
packet[3] = parseInt(minTemperature);
packet[5] = aqiInt & 0xFF;// 低字节
packet[6] = (aqiInt >> 8) & 0xFF; //高字节
// 将地址转换为ASCII字节数组
const addressBytes = CommonUtil.stringToBytes(address);
packet[7] = parseInt(addressBytes.length);
const text = new Uint8Array(addressBytes.length);
// 填充地址到数据包中
for (let i = 0; i < addressBytes.length; i++) {
text[i] = addressBytes[i];
}
// 拼接 header 和 payload
const fullPackage = new Uint8Array(packet.length + addressBytes.length);
fullPackage.set(packet, 0);
fullPackage.set(text, 8);
return fullPackage;
},
}
// 导出模块
module.exports = DataPacket;

@ -0,0 +1,43 @@
// utils/dynamicArrayBuffer.js
class DynamicArrayBuffer {
constructor(initialSize = 1024) {
this.buffer = new ArrayBuffer(initialSize); // 初始的ArrayBuffer
this.view = new Uint8Array(this.buffer); // Uint8Array视图用于操作ArrayBuffer
this.offset = 0; // 记录当前已写入的字节数
}
// 扩展ArrayBuffer的大小
expandBuffer(newSize) {
const newBuffer = new ArrayBuffer(newSize); // 创建新的ArrayBuffer
const newView = new Uint8Array(newBuffer); // 创建新的Uint8Array视图
// 复制旧数据到新buffer
newView.set(this.view);
// 更新当前buffer和视图
this.buffer = newBuffer;
this.view = newView;
}
// 向ArrayBuffer添加数据
appendData(data) {
const dataLength = data.length;
// 如果当前的数据空间不足扩展buffer
if (this.offset + dataLength > this.buffer.byteLength) {
let newSize = Math.max(this.buffer.byteLength * 2, this.offset + dataLength);
this.expandBuffer(newSize); // 扩展buffer
}
// 将数据写入buffer
this.view.set(data, this.offset);
this.offset += dataLength; // 更新偏移量
}
// 获取当前缓冲区的内容
getBuffer() {
return this.view.slice(0, this.offset); // 返回有效数据部分
}
}
module.exports = DynamicArrayBuffer;

@ -0,0 +1,116 @@
const HealthDataCache = {
CACHE_KEY: 'healthDataCache',
// 获取缓存数据,若没有缓存则返回初始数据
getCache() {
let cache = wx.getStorageSync(this.CACHE_KEY);
if (!cache) {
// 初始化缓存数据
cache = {
sleepLastTime: 0,
heartRateLastTime: 0,
temperatureLastTime: 0,
bloodOxygenLastTime: 0,
bloodPressureLastTime: 0,
stepLastTime:0
};
wx.setStorageSync(this.CACHE_KEY, cache); // 如果没有缓存,设置默认值
}
return cache;
},
// 更新缓存数据
updateCache(field, value) {
const cache = this.getCache(); // 获取缓存数据
if (cache[field] !== undefined) {
cache[field] = value; // 更新对应字段
wx.setStorageSync(this.CACHE_KEY, cache); // 保存更新后的缓存数据
console.log(`缓存更新成功,${field} 设置为 ${value}`);
} else {
console.log(`字段 ${field} 不存在,无法更新`);
}
},
// 更新血压数据
updateBloodPressureData(newDate) {
let lastTime = this.getLastTimeField('bloodPressureLastTime');
if(newDate > lastTime){
this.updateCache('bloodPressureLastTime', newDate);
}
},
// 更新心率数据
updateHeartRateData(newDate) {
// 判断最新传入的时间戳是否 大于 上次更新数据的时间,只有大于才更新缓存时间
let lastTime = this.getLastTimeField('heartRateLastTime');
if(newDate > lastTime){
console.log("本次更新时间大于上次更新时间");
console.log("上次更新时间>>>:",lastTime);
console.log("本次更新时间>>>:",newDate);
this.updateCache('heartRateLastTime', newDate);
}
},
// 更新体温数据
updateTemperatureData(newDate) {
let lastTime = this.getLastTimeField('temperatureLastTime');
if(newDate > lastTime){
this.updateCache('temperatureLastTime', newDate);
}
},
// 更新血氧数据
updateBloodOxygenData(newDate) {
let lastTime = this.getLastTimeField('bloodOxygenLastTime');
if(newDate > lastTime){
this.updateCache('bloodOxygenLastTime', newDate);
}
},
// 更新睡眠数据
updateSleepData(newDate) {
let lastTime = this.getLastTimeField('sleepLastTime');
if(newDate > lastTime){
this.updateCache('sleepLastTime', newDate);
}
},
// 更新步数数据
updateStepData(newDate) {
let lastTime = this.getLastTimeField('stepLastTime');
if(newDate > lastTime){
this.updateCache('stepLastTime', newDate);
}
},
// 更新所有时间字段的时间
updateAllFieldsTime(newDate) {
this.updateCache('sleepLastTime', newDate);
this.updateCache('heartRateLastTime', newDate);
this.updateCache('temperatureLastTime', newDate);
this.updateCache('bloodOxygenLastTime', newDate);
this.updateCache('bloodPressureLastTime', newDate);
this.updateCache('stepLastTime', newDate);
console.log(`所有字段时间更新为: ${newDate}`);
},
// 获取缓存中指定字段的值
getField(field) {
const cache = this.getCache();
return cache[field];
},
// 根据字段获取时间戳若返回空则默认取过去7天作为起始时间
getLastTimeField(field){
let lastTime = this.getField(field);
if(lastTime === null || lastTime === ''|| lastTime === undefined){
// 获取九宫格的数据时间作为 开始同步时间,这里暂时获取当前时间戳
return new Date().getTime() / 1000;
}
return lastTime;
},
};
// 导出模块
module.exports = HealthDataCache;

@ -0,0 +1,114 @@
// Constants
const AN_DEVICE_DFU_DATA = "00006387-3C17-D293-8E48-14FE2E4DA212"; // 在接受响应上写
const AN_DEVICE_FIRMWARE_UPDATE_CHAR = "00006487-3C17-D293-8E48-14FE2E4DA212"; // 接受控制指令的通道 写/通知
// DFU Opcode
const OPCODE_DFU_START_DFU = 0x01;
const OPCODE_DFU_RECEIVE_FW_IMAGE = 0x02;
const OPCODE_DFU_VALIDATE_FW_IMAGE = 0x03;
const OPCODE_DFU_ACTIVE_IMAGE_RESET = 0x04;
const OPCODE_DFU_RESET_SYSTEM = 0x05;
const OPCODE_DFU_REPORT_RECEIVED_IMAGE_INFO = 0x06;
const OPCODE_DFU_PACKET_RECEIPT_NOTIFICATION_REQUEST = 0x07;
const OPCODE_DFU_RESPONSE_CODE = 0x10;
const OPCODE_DFU_PACKET_RECEIPT_NOTIFICATION = 0x11;
// Error Codes
const ERROR_STATE_SUCCESS = 0x01;
const ERROR_STATE_OPCODE_NOT_SUPPORTED = 0x02;
const ERROR_STATE_INVALID_PARA = 0x03;
const ERROR_STATE_OPERATION_FAILED = 0x04;
const ERROR_STATE_DATA_SIZE_EXCEEDS = 0x05;
const ERROR_STATE_CRC_ERROR = 0x06;
// Structures and data definitions
function DEF_RESPONSE_HEADER(opcode, reqOpcode, rspValue) {
this.Opcode = opcode;
this.ReqOpcode = reqOpcode;
this.RspValue = rspValue;
}
function DFU_START_DFU(opcode, offset, signature, version, checksum, length, otaFlag, reserved8, reservedForAes) {
this.Opcode = opcode;
this.Offset = offset;
this.Signature = signature;
this.Version = version;
this.Checksum = checksum;
this.Length = length;
this.OtaFlag = otaFlag;
this.Reserved8 = reserved8;
this.ReservedForAes = reservedForAes;
}
function DFU_START_DFU_RESPONSE(opcode, reqOpcode, rspValue) {
this.Opcode = opcode;
this.ReqOpcode = reqOpcode;
this.RspValue = rspValue;
}
function RECEIVE_FW_IMAGE(opcode, nSignature, nImageUpdateOffset) {
this.Opcode = opcode;
this.NSignature = nSignature;
this.NImageUpdateOffset = nImageUpdateOffset;
}
function RECEIVE_FW_IMAGE_RESPONSE(opcode, reqOpcode, rspValue) {
this.Opcode = opcode;
this.ReqOpcode = reqOpcode;
this.RspValue = rspValue;
}
function VALIDATE_FW_IMAGE(opcode, nSignature) {
this.Opcode = opcode;
this.NSignature = nSignature;
}
function VALIDATE_FW_IMAGE_RESPONSE(opcode, reqOpcode, rspValue) {
this.Opcode = opcode;
this.ReqOpcode = reqOpcode;
this.RspValue = rspValue;
}
function ACTIVE_IMAGE_RESET(opcode) {
this.Opcode = opcode;
}
function RESET_SYSTEM(opcode) {
this.Opcode = opcode;
}
function REPORT_RECEIVED_IMAGE_INFO(opcode, nSignature) {
this.Opcode = opcode;
this.NSignature = nSignature;
}
function REPORT_RECEIVED_IMAGE_INFO_RESPONSE(opcode, reqOpcode, rspValue, originalFWVersion, nImageUpdateOffset) {
this.Opcode = opcode;
this.ReqOpcode = reqOpcode;
this.RspValue = rspValue;
this.OriginalFWVersion = originalFWVersion;
this.NImageUpdateOffset = nImageUpdateOffset;
}
function DFU_FW_IMAGE_INFO(FWImageSize, FWVersion, OriginalVersion, ImageUpdateOffset) {
this.FWImageSize = FWImageSize;
this.FWVersion = FWVersion;
this.OriginalVersion = OriginalVersion;
this.ImageUpdateOffset = ImageUpdateOffset;
}
function DFU_DATA_INFO(flag, curInfo, imgInfo) {
this.Flag = flag;
this.CurInfo = curInfo;
this.ImgInfo = imgInfo;
}
function IMAGE_HEADER(offset, signature, version, checksum, length, otaFlag, reserved8) {
this.Offset = offset;
this.Signature = signature;
this.Version = version;
this.Checksum = checksum;
this.Length = length;
this.OtaFlag = otaFlag;
this.Reserved8 = reserved8;
}

@ -0,0 +1,543 @@
const { validateCRC } = require('./crc');
const CommonUtil = require('./CommonUtil');
const parseDataPacket = {
/**
* 时间转时间戳
* @param {*} year
* @param {*} month
* @param {*} day
* @param {*} hours
* @param {*} minutes
* @param {*} seconds
*/
dateFormat(year,month,day,hours,minutes,seconds){
year = parseInt(year);
month = parseInt(month);
day = parseInt(day);
hours = parseInt(hours);
minutes = parseInt(minutes);
seconds = parseInt(seconds);
// 创建 Date 对象
const date = new Date(2000 + year,month-1, day, hours, minutes, seconds);
// 获取时间戳(单位:秒)
const timestamp = Math.floor(date.getTime() / 1000); // 转换为秒
return timestamp;
},
/**
* level<=1:深睡
* level==2:浅睡
* level==3:REM
* level>=4:清醒
* @param {*} status
*/
mapSleepStatus(status) {
switch (status) {
case 1:
case 0:
return 2; // 深睡
case 2:
return 1; // 浅睡
case 3:
return 4; // REM
case 4:
default:
return 3; // 清醒
}
},
/**
* 将字节数组按照小端模式Little Endian解析为整数
* @param {DataView} dataView - DataView 对象
* @param {number} offset - 解析起始位置
* @returns {number} 解析后的整数值
*/
convertToDecimal(dataView, offset) {
// 读取 4 个字节,并按照小端模式进行转换
return dataView.getUint32(offset, true); // true 代表小端模式
},
/**
* 解析步数总数包27字节包括头部和数据部分
*/
parseStepItem(dataView, offset) {
// 解析头部信息
const id = dataView.getUint8(offset + 1); // ID: 倒数第X天的总数据0 表示当天
const year = dataView.getUint8(offset + 2).toString(16); // 年份BCD格式
const month = dataView.getUint8(offset + 3).toString(16); // 月份BCD格式
const day = dataView.getUint8(offset + 4).toString(16); // 日期BCD格式
// 解析数据部分
const steps = this.convertToDecimal(dataView, offset + 5); // 步数(高位在后),单位:步数
const exerciseTime = this.convertToDecimal(dataView, offset + 9); // 运动时间(高位在后),单位:秒
const distance = this.convertToDecimal(dataView, offset + 13) * 0.01; // 距离高位在后单位KM
const calories = this.convertToDecimal(dataView, offset + 17) * 0.01; // 卡路里高位在后单位KCAL
const target = dataView.getUint16(offset + 21, true); // 目标(高位在后),单位:目标数,使用小端模式
const fastExerciseTime = this.convertToDecimal(dataView, offset + 23); // 快速运动时间(高位在后),单位:分钟
// 输出解析结果
console.log(`ID: ${id}, Date: ${year}-${month}-${day}, Steps: ${steps}, Exercise Time: ${exerciseTime} secs, Distance: ${distance} KM, Calories: ${calories} KCAL, Target: ${target}, Fast Exercise Time: ${fastExerciseTime} mins`);
return {
id,
date: {
year,
month,
day
},
steps,
exerciseTime,
distance,
calories,
target,
fastExerciseTime
};
},
/**
* 解析整个数据包逐个处理每个 27 字节的记录
*/
parseStepList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
// 每个数据包 27 字节,因此以 27 字节为单位拆分
for (let offset = startOffset; offset + 26 <= totalLength; offset += 27) {
const dataPacket = this.parseStepItem(dataView, offset); // 解析每个 27 字节的数据包
results.push(dataPacket);
}
return results;
},
/**
* 解析步数详情包25字节
*/
parseStepDetailItem(dataView, offset) {
// 解析头部信息
const id = dataView.getUint16(offset + 1,true); // ID: 倒数第X天的总数据0 表示当天
// 解析日期时间YY MM DD HH mm SS并转换为16进制字符串
const year = dataView.getUint8(offset + 3).toString(16).padStart(2, '0'); // 年份BCD格式
const month = dataView.getUint8(offset + 4).toString(16).padStart(2, '0'); // 月份BCD格式
const day = dataView.getUint8(offset + 5).toString(16).padStart(2, '0'); // 日期BCD格式
const hour = dataView.getUint8(offset + 6).toString(16).padStart(2, '0'); // 小时BCD格式
const minute = dataView.getUint8(offset + 7).toString(16).padStart(2, '0'); // 分钟BCD格式
const second = dataView.getUint8(offset + 8).toString(16).padStart(2, '0'); // 秒BCD格式
let time = this.dateFormat(year,month,day,hour,minute,second);
// 解析数据部分
const steps = dataView.getUint16(offset + 9,true);; // 步数(高位在后)单位:步数
const calories = dataView.getUint16(offset + 11,true); // 卡路里高位在后单位KCAL
const distance = dataView.getUint16(offset + 13,true); // 距离高位在后单位KM
return {
calories:calories,
steps:steps,
distance:distance,
startTime:time,
endTime:time + (10 * 60),
stringTime:`${year}-${month}-${day} ${hour}:${minute}:${second}`
};
},
/**
* 解析整个数据包逐个处理每个 25 字节的记录
*/
parseStepDetailList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
for (let offset = startOffset; offset + 24 <= totalLength; offset += 25) {
const dataPacket = this.parseStepDetailItem(dataView, offset);
results.push(dataPacket);
}
return results;
},
/**
* 解析心率血氧数据包每个是10字节
* @param {*} dataView
* @param {*} offset
*/
parseDataItem(dataView, offset) {
let type = 1;
const cmdType = dataView.getUint8(offset); // 指令
switch (cmdType) {
case 0x55:
type = 1;
break;
case 0x66:
type = 3;
break;
default:
console.warn('未知数据类型', cmdType);
break;
}
// 解析 ID1 和 ID2数据编号高位在后
const id1 = dataView.getUint8(offset + 1); // ID1
const id2 = dataView.getUint8(offset + 2); // ID2
const id = (id2 << 8) | id1; // 组合成一个16位的ID小端模式
// 解析日期时间YY MM DD HH mm SS并转换为16进制字符串
const year = dataView.getUint8(offset + 3).toString(16).padStart(2, '0'); // 年份BCD格式
const month = dataView.getUint8(offset + 4).toString(16).padStart(2, '0'); // 月份BCD格式
const day = dataView.getUint8(offset + 5).toString(16).padStart(2, '0'); // 日期BCD格式
const hour = dataView.getUint8(offset + 6).toString(16).padStart(2, '0'); // 小时BCD格式
const minute = dataView.getUint8(offset + 7).toString(16).padStart(2, '0'); // 分钟BCD格式
const second = dataView.getUint8(offset + 8).toString(16).padStart(2, '0'); // 秒BCD格式
let time = this.dateFormat(year,month,day,hour,minute,second);
// 解析心率值HH
const value = dataView.getUint8(offset + 9); // 心率值单位BPM,血氧,体温
return {
type:type,//1.心率2血压3,血氧4.体温5.呼吸率6步数
oneValue:value,
time:time,
stringTime:`${year}-${month}-${day} ${hour}:${minute}:${second}`
};
},
/**
* 解析整个数据包逐个处理每个数据项
*/
parseDataList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
// 每个数据包 10 字节,因此以 10 字节为单位拆分
for (let offset = startOffset; offset + 9 <= totalLength; offset += 10) {
const dataPacket = this.parseDataItem(dataView, offset); // 解析每个 10 字节的数据包
results.push(dataPacket);
}
return results;
},
/**
* 解析体温数据包每个是11字节
* @param {*} dataView
* @param {*} offset
*/
parseTempItem(dataView, offset) {
// 解析 ID1 和 ID2数据编号高位在后
const id = dataView.getUint16(offset + 1, true); // id
// 解析日期时间YY MM DD HH mm SS并转换为16进制字符串
const year = dataView.getUint8(offset + 3).toString(16).padStart(2, '0'); // 年份BCD格式
const month = dataView.getUint8(offset + 4).toString(16).padStart(2, '0'); // 月份BCD格式
const day = dataView.getUint8(offset + 5).toString(16).padStart(2, '0'); // 日期BCD格式
const hour = dataView.getUint8(offset + 6).toString(16).padStart(2, '0'); // 小时BCD格式
const minute = dataView.getUint8(offset + 7).toString(16).padStart(2, '0'); // 分钟BCD格式
const second = dataView.getUint8(offset + 8).toString(16).padStart(2, '0'); // 秒BCD格式
let time = this.dateFormat(year,month,day,hour,minute,second);
// 解析体温值T1 和 T2
const t1 = dataView.getUint16(offset + 9, true); // 小端数据,保留一位小数
// 计算温度值T1T2组合成一个16位数字并转为浮动的值
const temperature = t1 / 10.0; // 保留1位小数
return {
type: 4,//4.体温
oneValue:temperature,
time:time,
stringTime:`${year}-${month}-${day} ${hour}:${minute}:${second}`
};
},
/**
* 解析体温整个数据包逐个处理每个数据项
*/
parseTempList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
// 每个数据包 11 字节,因此以 11 字节为单位拆分
for (let offset = startOffset; offset + 10 <= totalLength; offset += 11) {
const dataPacket = this.parseTempItem(dataView, offset); // 解析每个 10 字节的数据包
results.push(dataPacket);
}
return results;
},
/**
* 解析血压数据包每个是15字节
* @param {*} dataView
* @param {*} offset
*/
parseBloodPressureItem(dataView, offset) {
// 解析 ID1 和 ID2数据编号高位在后
const id = dataView.getUint16(offset + 1,true);
// 解析日期时间YY MM DD HH mm SS并转换为16进制字符串
const year = dataView.getUint8(offset + 3).toString(16).padStart(2, '0'); // 年份BCD格式
const month = dataView.getUint8(offset + 4).toString(16).padStart(2, '0'); // 月份BCD格式
const day = dataView.getUint8(offset + 5).toString(16).padStart(2, '0'); // 日期BCD格式
const hour = dataView.getUint8(offset + 6).toString(16).padStart(2, '0'); // 小时BCD格式
const minute = dataView.getUint8(offset + 7).toString(16).padStart(2, '0'); // 分钟BCD格式
const second = dataView.getUint8(offset + 8).toString(16).padStart(2, '0'); // 秒BCD格式
let time = this.dateFormat(year,month,day,hour,minute,second);
// 解析血压->高压
const high = dataView.getUint8(offset + 13);
// 解析血压->高压
const low = dataView.getUint8(offset + 14);
return {
type: 2,//2.血压
oneValue:high,
twoValue:low,
time:time,
stringTime:`${year}-${month}-${day} ${hour}:${minute}:${second}`
};
},
/**
* 解析体温整个数据包逐个处理每个数据项
*/
parseBloodPressureList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
// 每个数据包 15 字节,因此以 15 字节为单位拆分
for (let offset = startOffset; offset + 14 <= totalLength; offset += 15) {
const dataPacket = this.parseBloodPressureItem(dataView, offset); // 解析每个 15 字节的数据包
results.push(dataPacket);
}
return results;
},
/**
* 解析睡眠数据包每个是11字节
* @param {*} dataView
* @param {*} offset
*/
parseSleepItem(dataView, offset) {
// 解析 ID1 和 ID2数据编号高位在后
const id1 = dataView.getUint8(offset + 1); // ID1
// 解析日期时间YY MM DD HH mm SS并转换为16进制字符串
const year = dataView.getUint8(offset + 3).toString(16).padStart(2, '0'); // 年份BCD格式
const month = dataView.getUint8(offset + 4).toString(16).padStart(2, '0'); // 月份BCD格式
const day = dataView.getUint8(offset + 5).toString(16).padStart(2, '0'); // 日期BCD格式
const hour = dataView.getUint8(offset + 6).toString(16).padStart(2, '0'); // 小时BCD格式
const minute = dataView.getUint8(offset + 7).toString(16).padStart(2, '0'); // 分钟BCD格式
const second = dataView.getUint8(offset + 8).toString(16).padStart(2, '0'); // 秒BCD格式
// 解析睡眠长度
const length = dataView.getUint8(offset + 9, true);
// 解析每一分钟的睡眠质量数据SD1 到 SD120最多 120 个数据
const sleepData = [];
for (let i = 0; i < length; i++) {
sleepData.push(dataView.getUint8(offset + 10 + i)); // 每分钟的睡眠质量
}
let time = this.dateFormat(year,month,day,hour,minute,second);
// 获取当天00:00:00的时间戳
const midnight = this.dateFormat(year,month,day,0,0,0);
let startMinutesOfDay = Math.floor((time - midnight) / 60); // 计算从当天00:00:00开始到当前时间的分钟数
// 用于存储转换后的结果
let result = [];
let currentMinute = startMinutesOfDay; // 起始分钟418分钟
// 开始循环遍历 sleepData 数据
for (let i = 0; i < sleepData.length; i++) {
const status = sleepData[i];
// 处理每分钟的睡眠数据
result.push({
minute: currentMinute + 1, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
yyyyMMdd: `${parseInt(year) + 2000}${month}${day}`, // 格式化为yyyyMMdd
dateTime: `${parseInt(year)}-${month}-${day} ${hour}:${minute}:${second}`, // 格式化为yyyyMMdd
time: time
});
// 递增分钟数
currentMinute++;
}
return result;
},
/**
* 解析睡眠整个数据包逐个处理每个数据项
*/
parseSleepList(dataView, startOffset) {
const totalLength = dataView.byteLength;
// 每个数据包 130 字节,因此以 130 字节为单位拆分
for (let offset = startOffset; offset + 129 <= totalLength; offset += 130) {
const dataPacket = this.parseSleepItem(dataView, offset); // 解析每个 130 字节的数据包
return dataPacket;
}
},
/**
* 解析手环基本参数
*/
parseWatchBasic(dataView, offset){
// AA:距离单位0x01MILE 0x00:KM
// BB:12小时24小时显示0x01: 12小时制0x0024小时制度。
// CC:抬手检查使能标志, 0x01使能 0x00:关闭
// DD:温度单位切换: 0x01华摄氏度 0x00摄氏度 默认摄氏度
// EE:夜间模式: 0x01开启 0x00关闭(夜间模式开启后晚上六点到凌晨7点自动调节显示亮度到最暗)
// FF:ANCS使能开关 0x01使能0x00关闭
// II:基础心率设置
// JJ:保留
// KK:屏幕亮度调节调节范围80-8f0为最亮f为最暗(0~15值越大则越暗)
// LL表盘界面
// MM: 社交距离开关 0x01 开 0x00 关
// NN:中英文切换, 0代表英文(默认) 1代表中文
const distanceFlag = dataView.getUint8(offset + 1);
const timeFlag = dataView.getUint8(offset + 2);
const WRSFlag = dataView.getUint8(offset + 3);
const tempFlag = dataView.getUint8(offset + 4);
const nightModeFlag = dataView.getUint8(offset + 5);
const ANCSFlag = dataView.getUint8(offset + 6);
const heartRateWarningFlag = dataView.getUint8(offset + 7);
const JJ = dataView.getUint8(offset + 10);
const KK = dataView.getUint8(offset + 11);
const LL = dataView.getUint8(offset + 12);
const MM = dataView.getUint8(offset + 13);
const NN = dataView.getUint8(offset + 14);
return {
distanceFlag:distanceFlag,timeFlag:timeFlag,WRSFlag:WRSFlag,tempFlag:tempFlag,
nightModeFlag:nightModeFlag,ANCSFlag:ANCSFlag,heartRateWarningFlag:heartRateWarningFlag,
JJ:JJ,SBLevel:CommonUtil.respSBLevelConv(KK),LL:LL,MM:MM,NN:NN
}
},
/**
* 解析闹钟数据包每个是41字节
* @param {*} dataView
* @param {*} offset
*/
parseAlarmClockItem(dataView, offset) {
const id = dataView.getUint16(offset + 1, true); // id
if (id === 0x57FF) {
return;
}
const count = dataView.getUint8(offset + 3); // 闹钟个数
const number = dataView.getUint8(offset + 4); // 闹钟编号
const enable = dataView.getUint8(offset + 5); // 闹钟状态 0关闭1开启
const type = dataView.getUint8(offset + 6); // 闹钟类型 1闹钟2吃药提示3喝水提示 4:吃饭闹钟 5洗手闹钟
const hour = dataView.getUint8(offset + 7).toString(16); // 闹钟:时
const minute = dataView.getUint8(offset + 8).toString(16);// 闹钟:分
const week = dataView.getUint8(offset + 9);// 周8bits表示周一到周天
const length = 30; // 长度,30个字节是闹钟内容目前发送了手表端不会显示暂不处理
// 解析星期使能位
const weekDays = [
(week & 0x01) !== 0 ? 1 : 0, // Sunday
(week & 0x02) !== 0 ? 1 : 0, // Monday
(week & 0x04) !== 0 ? 1 : 0, // Tuesday
(week & 0x08) !== 0 ? 1 : 0, // Wednesday
(week & 0x10) !== 0 ? 1 : 0, // Thursday
(week & 0x20) !== 0 ? 1 : 0, // Friday
(week & 0x40) !== 0 ? 1 : 0 // Saturday
];
return {
count: count,
number:number,
enable:enable,
type:type,
hour:hour,
minute:minute,
weekDays:weekDays,
// textContent:textContent
};
},
/**
* 解析闹钟整个数据包逐个处理每个数据项
*/
parseAlarmClockList(dataView, startOffset) {
let results = [];
const totalLength = dataView.byteLength;
// 每个数据包 41 字节,因此以 41 字节为单位拆分
for (let offset = startOffset; offset + 40 <= totalLength; offset += 41) {
const dataPacket = this.parseAlarmClockItem(dataView, offset); // 解析每个 41 字节的数据包
results.push(dataPacket);
}
return results;
},
/**
* 解析久坐数据
* @param {*} dataView
* @param {*} offset
*/
parseSedentary(dataView, offset){
const startHour = dataView.getUint8(offset + 1); // 开始运动时间 小时
const startMinute = dataView.getUint8(offset + 2);
const endHour =dataView.getUint8(offset + 3);
const endMinute = dataView.getUint8(offset + 4);
const week = dataView.getUint8(offset + 5);
const reminderInterval = dataView.getUint8(offset + 6);
const minSteps = dataView.getUint8(offset + 7);
const sportSwitch = dataView.getUint8(offset + 8);
// 解析星期使能位
const weekDays = [
(week & 0x01) !== 0 ? 1 : 0, // Sunday
(week & 0x02) !== 0 ? 1 : 0, // Monday
(week & 0x04) !== 0 ? 1 : 0, // Tuesday
(week & 0x08) !== 0 ? 1 : 0, // Wednesday
(week & 0x10) !== 0 ? 1 : 0, // Thursday
(week & 0x20) !== 0 ? 1 : 0, // Friday
(week & 0x40) !== 0 ? 1 : 0 // Saturday
];
return {
startHour: startHour,
startMinute:startMinute,
endHour:endHour,
endMinute:endMinute,
weekDays:weekDays,
reminderInterval:reminderInterval,
minSteps:minSteps,
sportSwitch:sportSwitch
};
},
/**
* 运动目标解析
* @param {*} dataView
* @param {*} offset
*/
parseStepTarget(dataView, offset){
const targetSteps = dataView.getUint32(offset + 1,true);
const targetTime = dataView.getUint16(offset + 5,true);
const targetDistance = dataView.getUint16(offset + 7,true);
const targetCalories = dataView.getUint16(offset + 9,true);
const targetSleep = dataView.getUint16(offset + 11,true);
return {
targetSteps:targetSteps,
targetTime:targetTime,
targetDistance:targetDistance,
targetCalories:targetCalories,
targetSleep:targetSleep
}
}
}
// 导出模块
module.exports = parseDataPacket;

@ -0,0 +1,27 @@
// 发送指令工具类
const serviceId = "0000FFF0-0000-1000-8000-00805F9B34FB"; //写服务ID
const writeCharacteristicId = "0000FFF6-0000-1000-8000-00805F9B34FB"; //写特征ID
const SendCommandUtil = {
sendPackage(deviceId, packageData){
// 通过蓝牙向设备写入命令
wx.writeBLECharacteristicValue({
deviceId: deviceId, // 蓝牙设备 ID
serviceId: serviceId, // 蓝牙服务 UUID
characteristicId: writeCharacteristicId, // 蓝牙特征 UUID
value: packageData.buffer, // 发送的命令缓冲区
success: (res) => {
// console.log('命令发送成功', res);
},
fail: (err) => {
console.error('命令发送失败', err);
},
complete: (res) => {
// console.log('接口调用结束', res);
},
});
},
};
// 导出模块
module.exports = SendCommandUtil;

@ -0,0 +1,61 @@
// WeatherUtils.js
const WeatherEnum = {
0: '晴',
4: '多云',
5: '晴间多云',
6: '晴间多云',
7: '大部多云',
8: '大部多云',
9: '阴',
10: '阵雨',
11: '雷阵雨',
12: '雷阵雨伴有冰雹',
13: '小雨',
14: '中雨',
15: '大雨',
16: '暴雨',
17: '大暴雨',
18: '特大暴雨',
19: '冻雨',
20: '雨夹雪',
21: '阵雪',
22: '小雪',
23: '中雪',
24: '大雪',
25: '暴雪',
26: '浮尘',
27: '扬沙',
28: '沙尘暴',
29: '强沙尘暴',
30: '雾',
31: '霾',
32: '风',
33: '大风',
34: '飓风',
35: '热带风暴',
36: '龙卷风',
37: '冷',
38: '热',
// 0xfe: '查询更新时间',
0x99: '未知'
};
class WeatherUtils {
// 根据描述获取对应的天气编码
static getWeatherCode(description) {
for (const [code, desc] of Object.entries(WeatherEnum)) {
if (desc === description) {
return parseInt(code); // 返回对应的编码
}
}
return null; // 如果没有找到,返回 null
}
// 根据编码获取对应的天气描述
static getWeatherDescription(code) {
return WeatherEnum[code] || '未知'; // 返回对应的描述,如果没有找到返回 '未知'
}
}
// 导出工具类
module.exports = WeatherUtils;

@ -0,0 +1,22 @@
// CRC 校验模块
// 计算数据包的 CRC 校验值
function calculateCRC(dataPacket) {
let sum = 0;
for (let i = 0; i < 15; i++) {
sum += dataPacket[i];
}
return sum & 0xFF; // 取最低8位
}
// 校验CRC值
function validateCRC(dataPacket) {
const receivedCRC = dataPacket[15];
const calculatedCRC = calculateCRC(dataPacket);
return receivedCRC === calculatedCRC;
}
module.exports = {
calculateCRC,
validateCRC
};
Loading…
Cancel
Save