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: case 3: 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]; 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:距离单位:0x01:MILE 0x00:KM // BB:12小时24小时显示:0x01: 12小时制,0x00:24小时制度。 // CC:抬手检查使能标志, 0x01:使能, 0x00:关闭 // DD:温度单位切换: 0x01:华摄氏度 ,0x00:摄氏度 默认摄氏度 // EE:夜间模式: 0x01开启 , 0x00:关闭(夜间模式开启后晚上六点到凌晨7点,自动调节显示亮度到最暗) // FF:ANCS使能开关: 0x01:使能,0x00:关闭 // II:基础心率设置 // JJ:保留 // KK:屏幕亮度调节,调节范围80-8f(0为最亮,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;