增加ET620

main
sjchen 4 weeks ago
parent cebaad20b8
commit 5c09e1af28

@ -1,12 +1,13 @@
{ {
"pages": [ "pages": [
"pages/hrv/hrv",
"pages/sjchen/sjchen", "pages/sjchen/sjchen",
"index/index", "index/index",
"ecg/index", "ecg/index",
"ecg2/index" "ecg2/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff", "navigationBarBackgroundColor": "#fff",

@ -0,0 +1,66 @@
// pages/hrv/hrv.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

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

@ -0,0 +1,208 @@
<view class="hrv">
<view class="group_220">
<view class="frame_1120">
<text class="text_1">06-17 15:45</text>
<image class="vector_3" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234532/e0ef/7589/9668/7a78cda87b9baaa5d1f13aadc90d852e.png" />
</view>
<view class="frame_1068">
<view class="flexcontainer_2">
<view class="frame_289">
<text class="text_2">200</text>
<text class="text_3">160</text>
<text class="text_4">120</text>
<text class="text_5">80</text>
<text class="text_6">40</text>
<text class="text_7">0</text>
</view>
<view class="flexcontainer_3">
<view class="group_288">
<view class="line_3" />
<view class="line_4" />
<view class="line_5" />
<view class="line_6" />
<view class="line_7" />
</view>
<view class="frame_292">
<view class="flexcontainer_4">
<image class="ellipse_30" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
<view class="vector_2_1">
<view class="flexcontainer_5">
<image class="ellipse_32" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
<image class="ellipse_26" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007238035/2c13/d0c0/67a2/5fc50f666cfdfe507fef442833c6c85f.png" />
<image class="ellipse_28" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
</view>
<view class="flexcontainer_6">
<image class="ellipse_27" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
<image class="ellipse_29" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
</view>
</view>
<image class="ellipse_33" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
<image class="ellipse_31" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237422/4075/ad27/5628/f5e3fd90625bf59382e2ca86ba592be2.png" />
</view>
<image class="vector_1_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007237362/24d5/8441/2ba7/f6eff25455ea01b926f80c402eecff60.png" />
</view>
</view>
</view>
<view class="flexcontainer_7">
<text class="text_8">04:57</text>
<text class="text_9">04:58</text>
<text class="text_10">04:59</text>
<text class="text_11">05:00</text>
<text class="text_12">05:01</text>
<text class="text_13">05:02</text>
<text class="text_14">05:03</text>
<text class="text_15">05:04</text>
</view>
</view>
<text class="text_16">平均值/10分钟</text>
</view>
<view class="group_44">
<view class="group_14">
<text class="text_17">82</text>
<text class="div_2">
<text class="text_18">心脏健康指数:</text>
<text class="text_19">正常</text>
</text>
</view>
<view class="flexcontainer_8">
<view class="flexcontainer_9">
<image class="subtract" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007235654/89b9/45fc/95f6/d5a24d1dc445d04ac1700456e87100a8.png" />
<view class="rectangle_7" />
</view>
<view class="rectangle_5" />
<view class="rectangle_6" />
</view>
<view class="flexcontainer_10">
<text class="text_20">0</text>
<text class="text_21">40</text>
<text class="text_22">异常</text>
<text class="text_23">轻度</text>
<text class="text_24">正常</text>
<text class="text_25">60</text>
<text class="text_26">100</text>
</view>
</view>
<view class="group_221">
<text class="text_27">HRV概况</text>
<view class="frame_1087">
<view class="frame_1084">
<view class="frame_573">
<text class="text_28">最大值</text>
<text class="text_29">18.1</text>
<text class="text_30">ms</text>
</view>
</view>
<view class="frame_1085">
<view class="frame_573_1">
<text class="text_31">最小值</text>
<text class="text_32">75.24</text>
<text class="text_33">ms</text>
</view>
</view>
<view class="frame_1086">
<view class="frame_573_2">
<text class="text_34">平均值</text>
<text class="text_35">17.2</text>
<text class="text_36">ms</text>
</view>
</view>
</view>
</view>
<view class="group_219">
<view class="frame_897">
<view class="flexcontainer_11">
<text class="text_37">散点图</text>
<text class="text_38">热度图</text>
</view>
<view class="frame_1075" />
</view>
<view class="flexcontainer_12">
<view class="frame_1068_1">
<view class="frame_289_1">
<text class="text_39">2000</text>
<text class="text_40">RRN+1(ms)</text>
<text class="text_41">0</text>
</view>
<view class="flexcontainer_13">
<view class="group_288_1">
<view class="line_3_1" />
<view class="line_4_1" />
<view class="line_5_1" />
<view class="line_6_1" />
<view class="line_7_1" />
</view>
<view class="flexcontainer_14">
<text class="text_42">0</text>
<text class="text_43">2000</text>
<text class="text_44">RRN(ms)</text>
</view>
</view>
</view>
<text class="text_45">无数据</text>
</view>
<image class="group_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007236723/fca1/a6aa/d933/7c66c26f7b4d21c7a5678e5273902414.png" />
<view class="group_217">
<view class="rectangle_56" />
<view class="rectangle_57" />
<view class="rectangle_58" />
<view class="rectangle_59" />
<view class="rectangle_60" />
</view>
</view>
<view class="group_218">
<text class="text_46">最新血压</text>
<view class="frame_9">
<view class="frame_8">
<view class="frame_7">
<text class="text_47">心率变化</text>
<view class="frame_571">
<image class="frame_2" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_3" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_4" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_5" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_6" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
</view>
</view>
<text class="text_48">测量期间心率的最大变化正常,心率变化的范围偏低,一般身体素质较好,有良好睡眠习惯的人群,夜间心率的变化范围会较低,但有较小概率为心动过缓,如无不适,无需特别治疗,以观察为主。</text>
</view>
<view class="rectangle_62" />
<view class="frame_9_1">
<view class="frame_7_1">
<text class="text_49">心率突变</text>
<view class="frame_571_1">
<image class="frame_2_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_3_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_4_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_5_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_6_1" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
</view>
</view>
<text class="text_50">心率最大突变发生于正常范围,突变程度正常。</text>
</view>
<view class="rectangle_63" />
<view class="frame_10">
<view class="frame_7_2">
<text class="text_51">神经状态</text>
<view class="frame_571_2">
<image class="frame_2_2" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_3_2" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
</view>
</view>
<text class="text_52">有轻微的早搏风险,可发生于正常人(情绪激动,神经紧张,疲劳,消化不良,过度吸烟、饮酒或喝浓茶等),如无不适,建议长期观察</text>
</view>
<view class="rectangle_64" />
<view class="frame_11">
<view class="frame_7_3">
<text class="text_53">心律变化</text>
<view class="frame_571_3">
<image class="frame_2_3" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
<image class="frame_3_3" src="https://seal-img.nos-jd.163yun.com/obj/w5rCgMKVw6DCmGzCmsK-/62007234654/7aa2/459a/aebf/2125b0d7b34218891184db2b76b99d08.png" />
</view>
</view>
<text class="text_54">夜间心律平稳,心脏状况良好。</text>
</view>
</view>
</view>
<text class="text_55">温馨提示:健康手环数据仅作为参考,不作为诊断治疗依据</text>
<!-- <home-indicator class="home_indicator" />-->
</view>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,264 @@
import { veepooBle,veepooFeature} from '../../veeoopSDK/sdk/index';
import timePointUtils from '../../utils/TimePointUtils';
import dataPointCacheUtil from '../../utils/DataPointCacheUtil';
const ET620ParseDataUtil = require('../../utils/ET620ParseDataUtil.js');
import DayConverter from '../../utils/DayConverter'
Page({
/**
* 页面的初始数据
*/
data: {
bleReady: false,
devices: [],
sleepFlagBitType: 0,// 精准睡眠标识
ET620Param: {},//请求参数
},
// 开始连接蓝牙并且监听数据
connectDevice(event){
const device = event.currentTarget.dataset.item;
console.log("连接设备信息:",device)
veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager(device, res => {
if(res.connection){
console.log('veepooBle已连接手表')
// 监听返回数据
veepooBle.veepooWeiXinSDKNotifyMonitorValueChange(res => {
// console.log("收到设备端上报的数据", res)
if(res!== undefined && res.name == '读取日常数据' ){
// res.type:5 等于5表示日常数据回调
// res.progress 表示进度 0~100
// res.content 表示健康数据
if(res.content!== undefined ){
console.log("获取日常数据:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>",res.content);
let heartArray = ET620ParseDataUtil.parseHeartRateOrBloodOxygenData(res.content,1);
let bloodOxygenArray = ET620ParseDataUtil.parseHeartRateOrBloodOxygenData(res.content,3);
let bpArray = ET620ParseDataUtil.parseBloodPressureData(res.content);
let bodyTempArray = ET620ParseDataUtil.parseBodyTemperatureData(res.content);
let stepArray = ET620ParseDataUtil.parseStepData(res.content,'TOM',571);
const totalSteps = stepArray.reduce((sum, item) => sum + item.step, 0);
console.log("总步数",totalSteps)
// 血压接口 todo...
console.log("心率数据长度:",heartArray.length)
console.log("血氧数据长度:",bloodOxygenArray.length)
console.log("血压数据长度:",bpArray.length)
console.log("体温数据长度:",bodyTempArray.length)
console.log("步数数据长度:",stepArray.length)
console.log("心率数据返回:",heartArray)
console.log("血氧数据返回:",bloodOxygenArray)
console.log("血压数据返回:",bpArray)
console.log("体温数据返回:",bodyTempArray)
console.log("步数数据返回:",stepArray)
// res.content.forEach((item, index) => {
// heartArray.forEach((item, index) => {
// console.log(`心率[${index}]`, item);
// });
// bloodOxygenArray.forEach((item, index) => {
// console.log(`血氧[${index}]`, item);
// });
// bpArray.forEach((item, index) => {
// console.log(`血压[${index}]`, item);
// });
// bodyTempArray.forEach((item, index) => {
// console.log(`体温[${index}]`, item);
// });
// stepArray.forEach((item, index) => {
// console.log(`步数[${index}]`, item);
// });
if(res.content != undefined && res.content.length > 0){
// 缓存point
this.setCachePoint(res.content[res.content.length-1].date);
}
// 重新构建请求参数
// this.buildRequestParam();
}
}
/**
* 在手环功能汇总 package=2
* sleepFlagBitType [1-精准睡眠 2-无睡眠 4-精准睡眠数据结构与1一致曲线修改为2bit表示其他-普通睡眠]
*/
if(res!== undefined && res.name == '手环功能汇总' && res.package === 2){
this.setData({
sleepFlagBitType : res.sleepFlagBitType
})
}
if(res!== undefined && res.name == '精准睡眠数据'){
if(res.content!== undefined && res.content.length > 0){
/**
* 解析睡眠ET620只返回精准睡眠
*/
let sleepArray = ET620ParseDataUtil.parseSleepData(res,1);
console.log("睡眠数据返回:",sleepArray)
}
}
})
// 密钥确认
veepooFeature.veepooBlePasswordCheckManager()
// 1、读取健康数据
this.readHealthData();
// 3、读取实时计步、卡路里、距离
veepooFeature.veepooReadStepCalorieDistanceManager({
})
}
})
},
startScan(){
// 开始扫描蓝牙
veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice((res) => {
console.log('res=>',res)
res.forEach(device => {
if (device?.localName === 'ET620') {
veepooBle.veepooWeiXinSDKStopSearchBleManager((res) => {
console.log("veepooBle停止扫描", res)
})
}
this.setData({
devices:res,
bleReady:true
})
})
})
},
// 等待工具函数
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// 读取健康数据
async readHealthData(){
let params = this.data.ET620Param;
for (const item of params) {
// 读取日常数据
veepooFeature.veepooSendReadDailyDataManager(item);
// 读取精准睡眠数据
veepooFeature.veepooSendReadPreciseSleepManager(item);
if(params.length > 0){
await this.sleep(12000); // 预计耗时12秒才读完1天的数据
}
}
},
/**
* 读取数据的point缓存到本地便于下一次读取
* @param {*} datetime
*/
setCachePoint(datetime){
dataPointCacheUtil.setDateTime(dataPointCacheUtil.CACHE_KEY,datetime);
},
/**
* 构建日常数据请求参数
* @returns 返回读取数据的point => package day
*/
buildRequestParam(){
let point = 0;
let cacheDateTime = dataPointCacheUtil.getDateTime(dataPointCacheUtil.CACHE_KEY);
let params = [];
if(cacheDateTime != null){
const HHmm = timePointUtils.getHHmmFromDateTime(cacheDateTime);
point = timePointUtils.getPackageByHHmm(HHmm);
params = DayConverter.getDayParam(cacheDateTime,point);
} else {
// 默认返回3天的数据请求参数
params = [{"day":0,"package":0},{"day":1,"package":0},{"day":2,"package":point}];
// params = [{"day":0,"package":0}];
}
this.setData({
ET620Param:params
})
console.log(`---------------------请求参数: ${params}, 参数长度: ${params.length}----------------------------`);
},
/**
* 同步历史数据
*/
synchronizeHistoricalData(){
// 清除缓存
dataPointCacheUtil.remove(dataPointCacheUtil.CACHE_KEY);
// 构建请求参数
this.buildRequestParam();
// 开始读取健康数据
this.readHealthData();
},
test(){
dataPointCacheUtil.remove(dataPointCacheUtil.CACHE_KEY);
console.log("清除缓存的TOKEN");
this.buildRequestParam();
},
onLoad(){
// 开始扫描
this.startScan()
// 先构建请求参数
this.buildRequestParam();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

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

@ -0,0 +1,25 @@
<!--pages/sjchen/sjchen.wxml-->
<view class="container">
<!-- 状态显示 -->
<view wx:if="{{!bleReady}}" class="tip">蓝牙功能初始化中...</view>
<!-- 设备扫描区域 -->
<view wx:if="{{bleReady}}">
<button bindtap="startScan" type="primary">扫描设备</button>
<!-- 设备列表 -->
<view wx:if="{{devices.length > 0}}" class="device-list">
<view wx:for="{{devices}}" wx:key="deviceId"
bindtap="connectDevice" data-item="{{item}}"
class="device-item">
<text>MAC:{{item.mac}}</text>
<text>信号强度: {{item.RSSI}}dBm</text>
</view>
</view>
<view wx:else class="tip">未发现设备,请点击扫描</view>
</view>
<button bindtap="readHealthData">获取健康数据</button>
<button bindtap="test">test</button>
<button bindtap="synchronizeHistoricalData">同步历史数据</button>
</view>

@ -0,0 +1,17 @@
/* pages/sjchen/sjchen.wxss */
.tip {
color: #000;
text-align: center;
margin: 20px;
}
.device-list {
margin-top: 15px;
}
.device-item {
padding: 12px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}

@ -0,0 +1,99 @@
// utils/DataPointCacheUtil.js
class DataPointCacheUtil {
// 定义常量 key
static CACHE_KEY = 'ET620_HEALTH_DATA_POINT';
/**
* 比较两个时间字符串
* @param {string} time1 - 当前时间
* @param {string} time2 - 缓存的时间
* @returns {number} true 大于 false 小于
*/
static compareDateTime(time1, time2) {
let currentTime = this.convertToTimestamp(time1);
let cacheTime = this.convertToTimestamp(time2);
// console.log("currentTime:",currentTime)
// console.log("cacheTime:",cacheTime)
console.log("对比结果:",currentTime > cacheTime)
return currentTime > cacheTime;
}
/**
* yyyy-MM-dd-HH-mm 格式转换为时间戳
* @param {string} datetime - 时间字符串
* @returns {number} 时间戳毫秒
*/
static convertToTimestamp(datetime) {
const [year, month, day, hour, minute] = datetime.split('-').map(Number);
return new Date(year, month - 1, day, hour, minute).getTime();
}
/**
* 永久存储时间数据
* @param {string} key - 存储键名
* @param {string} datetime - 时间字符串格式 yyyy-MM-dd-HH-mm
*/
static setDateTime(key, datetime) {
try {
// 验证时间格式
if (!/^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/.test(datetime)) {
throw new Error('时间格式应为 yyyy-MM-dd-HH-mm');
}
// 获取已缓存的时间
const cacheDateTime = this.getDateTime(key);
// 如果缓存中没有时间,或者新时间大于缓存时间,则存储
if (cacheDateTime != null) {
if(this.compareDateTime(datetime, cacheDateTime)){
console.log("后续设置缓存成功:",datetime);
wx.setStorageSync(key, datetime);
return true;
}
} else {
console.log("首次设置缓存成功:",datetime);
// 使用同步API存储确保立即生效
wx.setStorageSync(key, datetime);
return true;
}
} catch (err) {
console.error('存储失败:', err);
return false;
}
}
/**
* 获取永久存储的时间数据
* @param {string} key - 存储键名
* @returns {string|null} 时间字符串或null
*/
static getDateTime(key) {
try {
const datetime = wx.getStorageSync(key);
console.log("获取缓存成功:",datetime);
// 二次验证数据格式
if (datetime && /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/.test(datetime)) {
return datetime;
}
return null;
} catch (err) {
return null;
}
}
/**
* 移除永久存储的数据
* @param {string} key - 存储键名
*/
static remove(key) {
try {
wx.removeStorageSync(key);
return true;
} catch (err) {
return false;
}
}
}
export default DataPointCacheUtil;

@ -0,0 +1,179 @@
class DayConverter {
constructor(data) {
this.nameToValueMap = {};
this.valueToNameMap = {};
// 初始化映射关系
data.forEach(item => {
this.nameToValueMap[item.dayName] = item.dayValue;
this.valueToNameMap[item.dayValue] = item.dayName;
});
}
// 根据dayName获取dayValue
getValueByName(dayName) {
return this.nameToValueMap[dayName];
}
// 根据dayValue获取dayName
getNameByValue(dayValue) {
return this.valueToNameMap[dayValue];
}
// 根据dayName获取对应日期
getDateByDayName(dayName) {
const today = new Date();
const dayValue = this.getValueByName(dayName);
if (dayValue === undefined) {
throw new Error(`Invalid dayName: ${dayName}`);
}
const targetDate = new Date(today);
targetDate.setDate(today.getDate() - dayValue);
// 格式化为YYYY-MM-DD
const year = targetDate.getFullYear();
const month = String(targetDate.getMonth() + 1).padStart(2, '0');
const day = String(targetDate.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
// return targetDate;
}
/**
* -1 获取今天昨天前天的数据
* 0 获取今天
* 1 获取昨天
* 2 获取前天
* @param {时间} dateTime
*/
getDayParam(dateTime,point){
console.log(`获取参数,dateTime: ${dateTime},point:${point}`);
// 正确解析日期(处理时区问题)
const parseDate = (dateStr) => {
const [year, month, day] = dateStr.split('-').slice(0, 3).map(Number);
return new Date(year, month - 1, day); // 月份要减1
};
const cachedDateObj = parseDate(dateTime);
if (isNaN(cachedDateObj.getTime())) {
console.error('无效的日期');
return -1;
}
// 获取今天、昨天、前天的日期(统一时区处理)
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const dayBeforeYesterday = new Date(today);
dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);
// 比较日期
if (this.isSameDay(cachedDateObj, today)) {
return [{"day": 0, "package": point}]; // 今天
} else if (this.isSameDay(cachedDateObj, yesterday)) {
return [{"day": 0, "package": 0}, {"day": 1, "package": point}]; // 昨天
}
// else if (this.isSameDay(cachedDateObj, dayBeforeYesterday)) {
// return [{"day": 0, "package": 0}, {"day": 1, "package": 0}, {"day": 2, "package": point}]; //
// }
else {
return [{"day": 0, "package": 0}, {"day": 1, "package": 0}, {"day": 2, "package": point}]; // 前天或更早的日期
}
}
/**
* 判断两个Date对象是否是同一天
* @param {Date} date1
* @param {Date} date2
* @returns {boolean}
*/
isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
/**
* 13 位时间戳例子1751295900000
* @param {string} fallAsleepTime 例子06-30-23-05
*/
formatFullDateTime(fallAsleepTime){
if (!fallAsleepTime || typeof fallAsleepTime !== 'string') {
throw new Error('Invalid fallAsleepTime format. Expected "MM-dd-HH-mm"');
}
const currentYear = new Date().getFullYear();
// 拼接成 "2025-06-30-23-05"
const fullDateTimeString = `${currentYear}-${fallAsleepTime}`;
// 转换为标准格式 "2025-06-30T23:05"
const isoFormat = fullDateTimeString.replace(/-(\d{2})-(\d{2})$/, 'T$1:$2');
// 创建 Date 对象并获取时间戳
const timestamp = new Date(isoFormat).getTime();
console.log("完整日期时间字符串:", fullDateTimeString);
console.log("ISO 格式:", isoFormat);
console.log("时间戳:", timestamp);
return timestamp;
}
/**
* 计算时间戳对应的当天分钟数0-1439
* @param {number} timestamp - 13位毫秒级时间戳
* @returns {number} 分钟数
*/
getMinuteOfDay(timestamp) {
const date = new Date(timestamp);
return date.getHours() * 60 + date.getMinutes();
}
/**
* 睡眠片段日期格式化
* @param {*} timestamp
*/
formatDate(timestamp) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
}
/**
* 获取1天的第N分钟
* @param {Number} timeString 例子06-30-23-05
*/
getMinutesFromTime(timeString) {
const dateParts = timeString.split('-');
if (dateParts.length !== 4) {
throw new Error("时间格式必须是 MM-DD-HH-mm");
}
const hour = parseInt(dateParts[2], 10); // 明确转换为十进制整数
const minute = parseInt(dateParts[3], 10);
if (isNaN(hour) || isNaN(minute)) {
throw new Error("小时和分钟必须是数字");
}
if (hour < 0 || hour >= 24 || minute < 0 || minute >= 60) {
throw new Error("小时需在 0-23 之间,分钟需在 0-59 之间");
}
return hour * 60 + minute;
}
}
// 使用示例
const data = [
{"dayName":"today","dayValue":0},
{"dayName":"yesterday","dayValue":1},
{"dayName":"dayBeforeYesterday","dayValue":2}
];
const dayConverter = new DayConverter(data);
export default dayConverter;

@ -0,0 +1,280 @@
import DayConverter from './DayConverter';
import CommonUtil from './CommonUtil.js';
const ET620ParseDataUtil = {
/**
* @returns {Array} data 心率或血氧数据
* @param {Array} data 手表SDK数据
* @param {Number} dataType 数据类型 1-心率 3-血氧
*/
parseHeartRateOrBloodOxygenData(data,dataType){
if (!Array.isArray(data)) {
console.error('输入数据必须是数组');
return [];
}
return ET620ParseDataUtil.get10MinuteAverage(data,dataType);
},
/**
*
* @param {Array} data 手表SDK数据
*/
parseBodyTemperatureData(data){
if (!Array.isArray(data)) {
console.error('输入数据必须是数组');
return [];
}
return data
.filter(item => this.isValidBodyTemperature(item.bodyTemperature))
.map(item => this.formatBodyTemperatureItem(item));
},
parseBloodPressureData(data){
if (!Array.isArray(data)) {
console.error('输入数据必须是数组');
return [];
}
return data
.filter(item => this.isValidBloodPressure(item.bloodPressure))
.map(item => this.formatBloodPressureItem(item));
},
/**
* @returns {Object} 返回血压数据
* @param {Number} bp
*/
isValidBloodPressure(bp) {
return bp &&
bp.bloodPressureHigh > 0 &&
bp.bloodPressureLow > 0;
},
/**
* @returns {Object} 返回体温数据
* @param {} bt
*/
isValidBodyTemperature(bt) {
return bt &&
bt.bodyTemperature > 0.0 ;
},
/**
* @returns {Object} 返回体温数据
* @param {Number} bt
*/
isValidStep(step){
return step &&
step.stepCount > 0 ;
},
/**
* 组装体温数据
* @param {Object} item
*/
formatBodyTemperatureItem(item) {
let timestamp = this.getTimestamp(item.date)
// const tempStr = item.bodyTemperature.bodyTemperature.toFixed(1); // "36.0"
const [integerPart, decimalPart] = item.bodyTemperature.bodyTemperature.split('.');
return {
type: 4, // 4.体温
oneValue: integerPart, // 体温整数部分
twoValue: decimalPart, // 体温小数部分
time: timestamp, // 11位时间戳
stringTime: item.date
};
},
/**
* 组装步数数据
* @param {Object} item
*/
formatStepItem(item,name,userId) {
let timestamp = this.getTimestamp(item.date)
return {
calories: item.step.calorie,
distance: item.step.distance,
step: item.step.stepCount,
startTime: timestamp - (5 * 60), // 开始时间
endTime: timestamp, // 结束时间
createBy: name, // 体温整数部分
createTime: timestamp * 1000, // 体温小数部分
updateTime: timestamp * 1000, // 11位时间戳
userId: userId
};
},
/**
* 组装血压数据
* @param {Object} item
*/
formatBloodPressureItem(item) {
let timestamp = this.getTimestamp(item.date)
return {
type: 2, // 2.血压
oneValue: item.bloodPressure.bloodPressureHigh, // 高压
twoValue: item.bloodPressure.bloodPressureLow, // 低压
time: timestamp, // 11位时间戳
stringTime: item.date // 直接使用原始date字段
};
},
/**
* 解析心率血氧数据心率和血氧开启科学睡眠后是1分钟采集1次按每10分钟分组并计算平均值传给后端
* @param {Array} data - 心率数据数组格式为[{date: "YYYY-MM-DD-HH-mm", pulseReat: [number...]}, ...]
* @returns {Array} - 分组后的结果数组
*/
get10MinuteAverage(data,dataType) {
// 1. 创建用于分组的Map
const groupMap = new Map();
// 2. 遍历每个数据点
for (const item of data) {
// 解析日期和时间
const [year, month, day, hour, minute] = item.date.split('-').map(Number);
// 计算分组标识 (每10分钟一组)
const groupMinute = Math.floor(minute / 10) * 10;
const groupKey = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}-${hour.toString().padStart(2, '0')}-${groupMinute.toString().padStart(2, '0')}`;
// 提取有效值(大于0) SDK文档有心率字段实际没有心率字段与厂家确认使用脉率 pulseReat
const validPulseRates = dataType== 1 ? item.pulseReat.filter(rate => rate > 0) : item.bloodOxygen.oxygens.filter(rate => rate > 0);
// 添加到分组
if (!groupMap.has(groupKey)) {
groupMap.set(groupKey, []);
}
groupMap.get(groupKey).push(...validPulseRates);
}
// 3. 计算每个分组的平均值
const result = [];
groupMap.forEach((values, groupKey) => {
let average = 0;
if (values.length > 0) {
// 计算平均值
const sum = values.reduce((total, rate) => total + rate, 0);
average = Math.round(sum / values.length);// 四舍五入
}
let timestamp = this.getTimestamp(groupKey)
if(average > 0){
result.push({
type: dataType, // 1.心率 , 3.血氧
oneValue: average,
time: timestamp, // 11位时间戳
stringTime: groupKey // 直接使用原始date字段
});
}
});
// 4. 按时间排序(可选)
result.sort((a, b) => a.stringTime.localeCompare(b.stringTime));
return result;
},
/**
* 步数详情
* @param {Array} data 手表SDK数据
* @param {string} name 用户姓名
* @param {Number} userId
*/
parseStepData(data,name,userId){
if (!Array.isArray(data)) {
console.error('输入数据必须是数组');
return [];
}
return data
.filter(item => this.isValidStep(item.step))
.map(item => this.formatStepItem(item,name,userId));
},
/**
* 获取11位的时间戳
* @param {Long} date
*/
getTimestamp(date){
const dateParts = date.split('-');
const year = dateParts[0];
const month = dateParts[1];
const day = dateParts[2];
const hour = dateParts[3];
const minute = dateParts[4];
// 创建Date对象并获取时间戳
const dateObj = new Date(
`${year}-${month}-${day}T${hour}:${minute}:00`
);
// 单独 +5 分钟:使数据的时间更接近现在的时刻
const timestamp = (dateObj.getTime() / 1000) + (5 * 60);
return timestamp;
},
/**
* 解析睡眠数据
* @param {*} data 睡眠数据
* @param {*} sleepFlagBitType 1-精准睡眠 2-无睡眠 4-精准睡眠数据结构与1一致曲线修改为2bit表示其他-普通睡眠
*/
parseSleepData(data,sleepFlagBitType){
let sleepList = [];
console.log("data.content ===============================================",data.content)
data.content.forEach(item=>{
// 睡眠片段的入睡时间13位时间戳
let startTimestamp = DayConverter.formatFullDateTime(item.fallAsleepTime);
// 计算睡眠曲线
const timeline = item.sleepCurve.map((state, index) => {
const currentTimestamp = startTimestamp + index * 60000; // 每分钟 +60,000ms
/**
* 睡眠一个字符代表1分钟时长0深睡1浅睡2快速眼动3失眠4苏醒
*/
// console.log(`minute: ${DayConverter.getMinuteOfDay(currentTimestamp)}, status: ${state}, yyyyMMdd: ${DayConverter.formatDate(currentTimestamp)}`);
return {
yyyyMMdd: DayConverter.formatDate(currentTimestamp),
timestamp: currentTimestamp,
minute: DayConverter.getMinuteOfDay(currentTimestamp),
status: this.et620SleepStatus(state)
};
});
sleepList.push(...timeline);
})
console.log("解析后格式:",sleepList.length)
let completeList = CommonUtil.completeMissingData(sleepList);
console.log("完整的数据:",completeList.length)
// 组装数据
let arr = CommonUtil.assembleSleepData(completeList);
arr[0].date = DayConverter.getDateByDayName(data.readDay);
console.log("睡眠日期:",DayConverter.getDateByDayName(data.readDay))
return arr;
},
/**
* 普通睡眠
*/
calculateNormalSleep(){
},
/**
* 精准睡眠
*/
calculatePreciseSleep(){
},
/**
* 0深睡1浅睡2快速眼动3失眠4苏醒
* @param {*} status
*/
et620SleepStatus(status) {
switch (status) {
case 0:
return 2; // 深睡
case 1:
return 1; // 浅睡
case 2:
return 4; // 快速眼动
default:
return 3; // 清醒
}
},
}
// 导出模块
module.exports = ET620ParseDataUtil;

@ -0,0 +1,61 @@
// utils/RequestParamUtil.js
class RequestParamUtil {
/**
* 判断日期是前天昨天还是今天
* @param {string} cachedDate - 缓存的日期字符串格式为 yyyy-MM-dd-HH-mm
* @returns {number} 前天返回2昨天返回1今天返回0其他情况返回-1
*/
static judgeRecentDate(cachedDate) {
// 验证日期格式
if (!/^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/.test(cachedDate)) {
console.error('日期格式不正确,应为 yyyy-MM-dd-HH-mm');
return -1;
}
// 提取日期部分(忽略时间)
const dateStr = cachedDate.split('-').slice(0, 3).join('-');
// 转换为Date对象
const cachedDateObj = new Date(dateStr);
if (isNaN(cachedDateObj.getTime())) {
console.error('无效的日期');
return -1;
}
// 获取今天、昨天、前天的日期
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const dayBeforeYesterday = new Date(today);
dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);
// 比较日期
if (this.isSameDay(cachedDateObj, today)) {
return 0; // 今天
} else if (this.isSameDay(cachedDateObj, yesterday)) {
return 1; // 昨天
} else if (this.isSameDay(cachedDateObj, dayBeforeYesterday)) {
return 2; // 前天
} else {
return -1; // 更早的日期
}
}
/**
* 判断两个Date对象是否是同一天
* @param {Date} date1
* @param {Date} date2
* @returns {boolean}
*/
static isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
}
export default RequestParamUtil;

@ -0,0 +1,70 @@
// timePointUtils.js
class TimePointUtils {
constructor() {
this.timePoints = this.generateTimePoints();
}
// 生成时间点数组,格式如下:[{"HHmm":"0000","package":0},{"HHmm":"0005","package":1},{"HHmm":"0010","package":2},...]
generateTimePoints() {
const points = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 5) {
const hh = hour.toString().padStart(2, '0');
const mm = minute.toString().padStart(2, '0');
const packageNum = hour * 12 + minute / 5;
points.push({
HHmm: hh + mm,
package: packageNum
});
}
}
return points;
}
// 通过HHmm获取package编号
getPackageByHHmm(hhmm) {
if (!/^([0-1][0-9]|2[0-3])([0-5][0-9])$/.test(hhmm)) {
console.error('Invalid HHmm format, should be like "0000", "2355"');
return null;
}
const point = this.timePoints.find(item => item.HHmm === hhmm);
return point ? point.package : null;
}
// 通过package编号获取HHmm
getHHmmByPackage(packageNum) {
if (packageNum < 0 || packageNum > 287 || !Number.isInteger(packageNum)) {
console.error('Invalid package number, should be integer between 0 and 287');
return null;
}
const point = this.timePoints.find(item => item.package === packageNum);
return point ? point.HHmm : null;
}
/**
* 从yyyy-MM-dd-HH-mm格式的时间字符串中提取HHmm部分
* @param {string} datetime - 格式为"yyyy-MM-dd-HH-mm"的时间字符串例如"2025-06-27-10-40"
* @returns {string} 返回HHmm格式的时间字符串例如"1040"
*/
getHHmmFromDateTime(datetime) {
// 验证输入格式
if (!/^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/.test(datetime)) {
console.error('Invalid datetime format, should be "yyyy-MM-dd-HH-mm"');
return null;
}
// 分割字符串获取小时和分钟部分
const parts = datetime.split('-');
const hours = parts[3]; // 小时部分
const minutes = parts[4]; // 分钟部分
// 组合成HHmm格式
return hours + minutes;
}
}
// 导出单例
const timePointUtils = new TimePointUtils();
export default timePointUtils;

@ -0,0 +1,35 @@
// // utils/bleSDKLoader.js
// export const loadVeepooSDK = () => {
// return new Promise((resolve, reject) => {
// // 使用分包的绝对路径(从项目根目录开始)
// require.async('/veepooSDK/sdk/index.js', (sdkModule) => {
// // 检查模块导出结构
// if (sdkModule?.default) { // 兼容不同导出方式
// sdkModule = sdkModule.default;
// }
// const { veepooBle, veepooFeature } = sdkModule;
// if (!veepooBle || !veepooFeature) {
// reject(new Error('SDK模块加载不完整'));
// return;
// }
// resolve({ veepooBle, veepooFeature });
// }, (err) => {
// console.error("子包加载失败",err.message)
// reject(new Error(`子包加载失败: ${err.message}`));
// });
// });
// };
const { veepooBle, veepooFeature } = require('./veepooSDK/sdk/index.js');
console.log(veepooBle,veepooFeature)
export const loadVeepooSDK = () => {
return Promise.resolve({ veepooBle, veepooFeature });
};
// 也可以提供同步加载选项
export const getVeepooBle = () => veepooBle;
export const getVeepooFeature = () => veepooFeature;

@ -0,0 +1,3 @@
declare let veepooBle: any;
declare let veepooFeature: any;
export { veepooBle, veepooFeature };

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save