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

586 lines
22 KiB
JavaScript

This file contains ambiguous Unicode characters!

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

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 1; // 浅睡
case 2:
case 3:
return 2; // 深睡
// 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开始到当前时间的分钟数
console.log("midnight:",midnight)
console.log("startMinutesOfDay:",startMinutesOfDay)
// 1天最后1分钟是1440
let maxLastminute = 1440;
let deff = maxLastminute - startMinutesOfDay;
// 用于存储转换后的结果
let result = [];
let currentMinute = startMinutesOfDay; // 起始分钟418分钟
if(deff < sleepData.length){
// 将sleepData拆分为两个数组
let firstPart = sleepData.slice(0, deff); // 截取前deff个数据
let secondPart = sleepData.slice(deff); // 从deff开始到数组结尾的数据
// 开始循环遍历 sleepData 数据
for (let i = 0; i < firstPart.length; i++) {
const status = firstPart[i];
// 处理每分钟的睡眠数据
result.push({
minute: currentMinute + 1, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
yyyyMMdd: `${parseInt(year) + 2000}${month}${day}`, // 格式化为yyyyMMdd
});
// 递增分钟数
currentMinute++;
}
currentMinute = 0;
// 开始循环遍历 sleepData 数据
for (let i = 0; i < secondPart.length; i++) {
const status = secondPart[i];
// 处理每分钟的睡眠数据
result.push({
minute: currentMinute + 1, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
yyyyMMdd: `${parseInt(year) + 2000}${month}${(parseInt(day) + 1).toString().padStart(2, '0')}`, // 格式化为yyyyMMdd
});
// 递增分钟数
currentMinute++;
}
return result;
} else {
// 开始循环遍历 sleepData 数据
for (let i = 0; i < sleepData.length; i++) {
const status = sleepData[i];
let m = currentMinute + 1
if(m > 660 && m < 1200){
break;
}
// 处理每分钟的睡眠数据
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;