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.

585 lines
22 KiB
JavaScript

5 months ago
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) {
5 months ago
case 0:
case 1:
return 2; // 深睡
case 2:
return 1; // 浅睡
5 months ago
case 3:
5 months ago
return 4; // 快速眼动
5 months ago
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);
// 解析血压->高压
5 months ago
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开始到当前时间的分钟数
5 months ago
console.log("当天的0点0分0秒:",midnight)
console.log("几点几分的睡眠数据:",startMinutesOfDay)
5 months ago
// 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({
5 months ago
minute: currentMinute, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
yyyyMMdd: `${parseInt(year) + 2000}${month}${day}`, // 格式化为yyyyMMdd
});
// 递增分钟数
currentMinute++;
5 months ago
}
currentMinute = 0;
// 开始循环遍历 sleepData 数据
for (let i = 0; i < secondPart.length; i++) {
const status = secondPart[i];
let date = CommonUtil.getNextDay(`${parseInt(year) + 2000}-${month}-${day}`);
// 处理每分钟的睡眠数据
result.push({
5 months ago
minute: currentMinute, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
// yyyyMMdd: `${parseInt(year) + 2000}${month}${(parseInt(day) + 1).toString().padStart(2, '0')}`, // 格式化为yyyyMMdd
yyyyMMdd: date
});
// 递增分钟数
currentMinute++;
}
return result;
} else {
// 开始循环遍历 sleepData 数据
for (let i = 0; i < sleepData.length; i++) {
const status = sleepData[i];
5 months ago
let m = currentMinute;
if(m > 660 && m < 1200){
break;
}
// 处理每分钟的睡眠数据
result.push({
5 months ago
minute: currentMinute, // 分钟递增
status: this.mapSleepStatus(status), // 映射睡眠状态
yyyyMMdd: `${parseInt(year) + 2000}${month}${day}`, // 格式化为yyyyMMdd
});
// 递增分钟数
currentMinute++;
}
return result;
}
5 months ago
},
/**
* 解析睡眠整个数据包逐个处理每个数据项
*/
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;