From 65e44ecd3c016db0029d008ff639de2245f8e96e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 16 Dec 2024 11:40:22 -0600 Subject: [PATCH] Garmin FIT SDK 21.158.0 Change-Id: I0b08b878f194ca5253c66d90d41bb0239f233937 --- package.json | 2 +- src/accumulator.js | 20 ++++-- src/bit-stream.js | 4 +- src/crc-calculator.js | 4 +- src/decoder.js | 104 +++++++++++++++++++++------- src/fit.js | 4 +- src/index.js | 4 +- src/profile.js | 73 ++++++++++++++++---- src/stream.js | 14 ++-- src/utils-hr-mesg.js | 4 +- src/utils-internal.js | 4 +- src/utils.js | 4 +- test/accumulator.test.js | 34 ++++++--- test/data/test-data.js | 44 +++++++++++- test/decoder.test.js | 144 ++++++++++++++++++++++++++++++++++++++- 15 files changed, 387 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 10bb58d..f4c84e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@garmin/fitsdk", - "version": "21.141.0", + "version": "21.158.0", "description": "FIT JavaScript SDK", "main": "src/index.js", "type": "module", diff --git a/src/accumulator.js b/src/accumulator.js index eeba879..d88bd34 100644 --- a/src/accumulator.js +++ b/src/accumulator.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// @@ -32,16 +32,26 @@ class AccumulatedField { class Accumulator { #messages = {}; - add(mesgNum, fieldNum, value) { + createAccumulatedField(mesgNum, fieldNum, value) { + const accumualtedField = new AccumulatedField(value); + if (this.#messages[mesgNum] == null) { this.#messages[mesgNum] = {}; } - this.#messages[mesgNum][fieldNum] = new AccumulatedField(value); + this.#messages[mesgNum][fieldNum] = accumualtedField; + + return accumualtedField; } accumulate(mesgNum, fieldNum, value, bits) { - return this.#messages[mesgNum]?.[fieldNum]?.accumulate(value, bits) ?? value; + let accumualtedField = this.#messages[mesgNum]?.[fieldNum]; + + if(accumualtedField == null) { + accumualtedField = this.createAccumulatedField(mesgNum, fieldNum, value); + } + + return accumualtedField.accumulate(value, bits); } } diff --git a/src/bit-stream.js b/src/bit-stream.js index 3070dd6..90c99a9 100644 --- a/src/bit-stream.js +++ b/src/bit-stream.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/crc-calculator.js b/src/crc-calculator.js index 5604fbb..1e0d1c7 100644 --- a/src/crc-calculator.js +++ b/src/crc-calculator.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/decoder.js b/src/decoder.js index 882143a..2cc461a 100644 --- a/src/decoder.js +++ b/src/decoder.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// @@ -25,8 +25,17 @@ const MESG_DEFINITION_MASK = 0x40; const DEV_DATA_MASK = 0x20; const MESG_HEADER_MASK = 0x00; const LOCAL_MESG_NUM_MASK = 0x0F; + +const HEADER_WITH_CRC_SIZE = 14; +const HEADER_WITHOUT_CRC_SIZE = 12; const CRC_SIZE = 2; +const DecodeMode = Object.freeze({ + NORMAL: "normal", + SKIP_HEADER: "skipHeader", + DATA_ONLY: "dataOnly" +}); + class Decoder { #localMessageDefinitions = []; #developerDataDefinitions = {}; @@ -36,6 +45,8 @@ class Decoder { #fieldsWithSubFields = []; #fieldsToExpand = []; + #decodeMode = DecodeMode.NORMAL; + #mesgListener = null; #optExpandSubFields = true; #optExpandComponents = true; @@ -67,7 +78,7 @@ class Decoder { static isFIT(stream) { try { const fileHeaderSize = stream.peekByte(); - if ([14, 12].includes(fileHeaderSize) != true) { + if ([HEADER_WITH_CRC_SIZE, HEADER_WITHOUT_CRC_SIZE].includes(fileHeaderSize) != true) { return false; } @@ -75,7 +86,7 @@ class Decoder { return false; } - const fileHeader = Decoder.#readFileHeader(stream, true); + const fileHeader = Decoder.#readFileHeader(stream, { resetPosition: true, }); if (fileHeader.dataType !== ".FIT") { return false; } @@ -105,7 +116,7 @@ class Decoder { return false; } - const fileHeader = Decoder.#readFileHeader(this.#stream, true); + const fileHeader = Decoder.#readFileHeader(this.#stream, { resetPosition: true, }); if (this.#stream.length < fileHeader.headerSize + fileHeader.dataSize + CRC_SIZE) { return false; @@ -113,7 +124,7 @@ class Decoder { const buf = new Uint8Array(this.#stream.slice(0, this.#stream.length)) - if (fileHeader.headerSize === 14 && fileHeader.headerCRC !== 0x0000 + if (fileHeader.headerSize === HEADER_WITH_CRC_SIZE && fileHeader.headerCRC !== 0x0000 && fileHeader.headerCRC != CrcCalculator.calculateCRC(buf, 0, 12)) { return false; } @@ -150,6 +161,8 @@ class Decoder { * @param {boolean} [options.convertDateTimesToDates=true] - (optional, default true) * @param {Boolean} [options.includeUnknownData=false] - (optional, default false) * @param {boolean} [options.mergeHeartRates=true] - (optional, default false) + * @param {boolean} [options.skipHeader=false] - (optional, default false) + * @param {boolean} [options.dataOnly=false] - (optional, default false) * @return {Object} result - {messages:Array, errors:Array} */ read({ @@ -160,7 +173,9 @@ class Decoder { convertTypesToStrings = true, convertDateTimesToDates = true, includeUnknownData = false, - mergeHeartRates = true } = {}) { + mergeHeartRates = true, + skipHeader = false, + dataOnly = false,} = {}) { this.#mesgListener = mesgListener; this.#optExpandSubFields = expandSubFields @@ -182,7 +197,11 @@ class Decoder { this.#throwError("mergeHeartRates requires applyScaleAndOffset and expandComponents to be enabled"); } - this.#stream.reset(); + if (dataOnly && skipHeader) { + this.#throwError("dataOnly and skipHeader cannot both be enabled") + } + + this.#decodeMode = skipHeader ? DecodeMode.SKIP_HEADER : dataOnly ? DecodeMode.DATA_ONLY : DecodeMode.NORMAL; while (this.#stream.position < this.#stream.length) { this.#decodeNextFile(); @@ -203,23 +222,23 @@ class Decoder { #decodeNextFile() { const position = this.#stream.position; - if (!this.isFIT()) { + if (this.#decodeMode === DecodeMode.NORMAL && !this.isFIT()) { this.#throwError("input is not a FIT file"); } this.#stream.crcCalculator = new CrcCalculator(); - const fileHeader = Decoder.#readFileHeader(this.#stream); + const { headerSize, dataSize } = Decoder.#readFileHeader(this.#stream, { decodeMode: this.#decodeMode }); // Read data messages and definitions - while (this.#stream.position < (position + fileHeader.headerSize + fileHeader.dataSize)) { + while (this.#stream.position < (position + headerSize + dataSize)) { this.#decodeNextRecord(); } // Check the CRC const calculatedCrc = this.#stream.crcCalculator.crc; const crc = this.#stream.readUInt16(); - if (crc !== calculatedCrc) { + if (this.#decodeMode === DecodeMode.NORMAL && crc !== calculatedCrc) { this.#throwError("CRC error"); } } @@ -341,7 +360,7 @@ class Decoder { } if (field?.isAccumulated) { - this.#accumulator.add(mesgNum, fieldDefinition.fieldDefinitionNumber, rawFieldValue); + this.#setAccumulatedField(messageDefinition, message, field, rawFieldValue); } } }); @@ -530,7 +549,7 @@ class Decoder { while (this.#fieldsToExpand.length > 0) { const name = this.#fieldsToExpand.shift(); - const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name]; + const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name] ?? mesg[name]; let field = Profile.messages[mesgNum].fields[fieldDefinitionNumber]; field = isSubField ? this.#lookupSubfield(field, name) : field; const baseType = FIT.FieldTypeToBaseType[field.type]; @@ -546,6 +565,10 @@ class Decoder { const bitStream = new BitStream(rawFieldValue, baseType); for (let j = 0; j < field.components.length; j++) { + if (bitStream.bitsAvailable < field.bits[j]) { + break; + } + const targetField = fields[field.components[j]]; if (mesg[targetField.name] == null) { const baseType = FIT.FieldTypeToBaseType[targetField.type]; @@ -560,22 +583,22 @@ class Decoder { }; } - if (bitStream.bitsAvailable < field.bits[j]) { - break; - } - let value = bitStream.readBits(field.bits[j]); - value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]) ?? value; + if (targetField.isAccumulated) { + value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]); + } + + // Undo component scale and offset before applying the destination field's scale and offset + value = (value / field.scale[j] - field.offset[j]); - mesg[targetField.name].rawFieldValue.push(value); + const rawValue = (value + targetField.offset) * targetField.scale; + mesg[targetField.name].rawFieldValue.push(rawValue); - if (value === mesg[targetField.name].invalidValue) { + if (rawValue === mesg[targetField.name].invalidValue) { mesg[targetField.name].fieldValue.push(null); } else { - value = value / field.scale[j] - field.offset[j]; - if (this.#optConvertTypesToStrings) { value = this.#convertTypeToString(mesg, targetField, value); } @@ -681,6 +704,26 @@ class Decoder { } } + #setAccumulatedField(messageDefinition, message, field, rawFieldValue) { + const rawFieldValues = Array.isArray(rawFieldValue) ? rawFieldValue : [rawFieldValue]; + + rawFieldValues.forEach((value) => { + Object.values(message).forEach((containingField) => { + let components = messageDefinition.fields[containingField.fieldDefinitionNumber].components ?? [] + + components.forEach((componentFieldNum, i) => { + const targetField = messageDefinition.fields[componentFieldNum]; + + if(targetField?.num == field.num && targetField?.isAccumulated) { + value = (((value / field.scale) - field.offset) + containingField.offset[i]) * containingField.scale[i]; + } + }); + }); + + this.#accumulator.createAccumulatedField(messageDefinition.num, field.num, value); + }); + } + #convertTypeToString(messageDefinition, field, rawFieldValue) { if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) { return rawFieldValue; @@ -711,9 +754,22 @@ class Decoder { return subField != null ? subField : {}; } - static #readFileHeader(stream, resetPosition = false) { + static #readFileHeader(stream, { resetPosition = false, decodeMode = DecodeMode.NORMAL }) { const position = stream.position; + if(decodeMode !== DecodeMode.NORMAL) { + if(decodeMode === DecodeMode.SKIP_HEADER) { + stream.seek(HEADER_WITH_CRC_SIZE); + } + + const headerSize = decodeMode === DecodeMode.SKIP_HEADER ? HEADER_WITH_CRC_SIZE : 0; + + return { + headerSize, + dataSize: stream.length - headerSize - CRC_SIZE, + }; + } + const fileHeader = { headerSize: stream.readByte(), protocolVersion: stream.readByte(), diff --git a/src/fit.js b/src/fit.js index ad3aed6..cc9af92 100644 --- a/src/fit.js +++ b/src/fit.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/index.js b/src/index.js index 05f2029..1850c0d 100644 --- a/src/index.js +++ b/src/index.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/profile.js b/src/profile.js index 6143e90..f3f714b 100644 --- a/src/profile.js +++ b/src/profile.js @@ -5,15 +5,15 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// const Profile = { version: { major: 21, - minor: 141, + minor: 158, patch: 0, type: "Release" }, @@ -16324,6 +16324,20 @@ const Profile = { hasComponents: false, subFields: [] }, + 17: { + num: 17, // Description of the workout + name: "wktDescription", + type: "string", + array: "true", + scale: 1, + offset: 0, + units: "", + bits: [], + components: [], + isAccumulated: false, + hasComponents: false, + subFields: [] + }, }, }, 158: { @@ -18897,7 +18911,7 @@ const Profile = { subFields: [] }, 1: { - num: 1, // Body battery level + num: 1, // Body battery level: [0,100] Blank: -16 name: "level", type: "sint8", array: "true", @@ -18960,7 +18974,7 @@ const Profile = { subFields: [] }, 0: { - num: 0, // Event ID + num: 0, // Event ID. Health SDK use only name: "eventId", type: "uint8", array: "false", @@ -19205,7 +19219,7 @@ const Profile = { subFields: [] }, 0: { - num: 0, // Processing interval length in seconds + num: 0, // Processing interval length in seconds. File start: 0xFFFFFFEF File stop: 0xFFFFFFEE name: "processingInterval", type: "uint16", array: "false", @@ -19268,7 +19282,7 @@ const Profile = { subFields: [] }, 1: { - num: 1, // SpO2 Reading + num: 1, // SpO2 Reading: [70,100] Blank: 240 name: "readingSpo2", type: "uint8", array: "true", @@ -19282,7 +19296,7 @@ const Profile = { subFields: [] }, 2: { - num: 2, // SpO2 Confidence + num: 2, // SpO2 Confidence: [0,254] name: "confidence", type: "uint8", array: "true", @@ -19331,7 +19345,7 @@ const Profile = { subFields: [] }, 1: { - num: 1, // Stress Level ( 0 - 100 ) -300 indicates invalid -200 indicates large motion -100 indicates off wrist + num: 1, // Stress Level: [0,100] Off wrist: -1 Excess motion: -2 Not enough data: -3 Recovering from exercise: -4 Unidentified: -5 Blank: -16 name: "stressLevel", type: "sint8", array: "true", @@ -19380,7 +19394,7 @@ const Profile = { subFields: [] }, 1: { - num: 1, // Breaths * 100 /min -300 indicates invalid -200 indicates large motion -100 indicates off wrist + num: 1, // Breaths / min: [1,100] Invalid: 255 Excess motion: 254 Off wrist: 253 Not available: 252 Blank: 2.4 name: "respirationRate", type: "sint16", array: "true", @@ -19443,7 +19457,7 @@ const Profile = { subFields: [] }, 2: { - num: 2, // Beats / min + num: 2, // Beats / min. Blank: 0 name: "heartRate", type: "uint8", array: "true", @@ -19478,7 +19492,7 @@ const Profile = { subFields: [] }, 0: { - num: 0, + num: 0, // Encoded configuration data. Health SDK use only name: "data", type: "byte", array: "true", @@ -20920,7 +20934,7 @@ const Profile = { subFields: [] }, 0: { - num: 0, // ms since last overnight_raw_bbi message + num: 0, // Millisecond resolution of the timestamp name: "timestampMs", type: "uint16", array: "false", @@ -20962,7 +20976,7 @@ const Profile = { subFields: [] }, 3: { - num: 3, + num: 3, // 1 = high confidence. 0 = low confidence. N/A when gap = 1 name: "quality", type: "uint8", array: "true", @@ -20976,7 +20990,7 @@ const Profile = { subFields: [] }, 4: { - num: 4, + num: 4, // 1 = gap (time represents ms gap length). 0 = BBI data name: "gap", type: "uint8", array: "true", @@ -21129,6 +21143,20 @@ const Profile = { hasComponents: false, subFields: [] }, + 6: { + num: 6, + name: "standardDeviation", + type: "uint32", + array: "false", + scale: 1000, + offset: 0, + units: "m/s", + bits: [], + components: [], + isAccumulated: false, + hasComponents: false, + subFields: [] + }, }, }, 388: { @@ -22296,6 +22324,7 @@ types: { 4: "positionWaypoint", 5: "positionMarked", 6: "off", + 13: "autoSelect", }, lapTrigger: { 0: "manual", @@ -22709,6 +22738,8 @@ types: { 148: "ezon", 149: "laisi", 150: "myzone", + 151: "abawo", + 152: "bafang", 255: "development", 257: "healthandlife", 258: "lezyne", @@ -22783,6 +22814,9 @@ types: { 327: "magicshine", 328: "ictrainer", 329: "absoluteCycling", + 330: "eoSwimbetter", + 331: "mywhoosh", + 332: "ravemen", 5759: "actigraphcorp", }, garminProduct: { @@ -23082,6 +23116,7 @@ types: { 3449: "marqCommanderAsia", 3450: "marqExpeditionAsia", 3451: "marqAthleteAsia", + 3461: "indexSmartScale2", 3466: "instinctSolar", 3469: "fr45Asia", 3473: "vivoactive3Daimler", @@ -23204,10 +23239,18 @@ types: { 4426: "vivoactive5", 4432: "fr165", 4433: "fr165Music", + 4440: "edge1050", 4442: "descentT2", 4446: "hrmFit", 4472: "marqGen2Commander", + 4477: "lilyAthlete", // aka the Lily 2 Active + 4532: "fenix8Solar", + 4533: "fenix8SolarLarge", + 4534: "fenix8Small", + 4536: "fenix8", 4556: "d2Mach1Pro", + 4575: "enduro3", + 4666: "fenixE", 10007: "sdm4", // SDM4 footpod 10014: "edgeRemote", 20533: "tacxTrainingAppWin", diff --git a/src/stream.js b/src/stream.js index 89ad594..6e04634 100644 --- a/src/stream.js +++ b/src/stream.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// @@ -112,10 +112,10 @@ class Stream { throw Error(`FIT Runtime Error end of stream at byte ${this.#position}`); } - const bytes = this.#arrayBuffer.slice(this.#position, this.#position + size); + const bytes = new Uint8Array(this.#arrayBuffer, this.#position, size); this.#position += size; - this.#crcCalculator?.addBytes(new Uint8Array(bytes), 0, size); + this.#crcCalculator?.addBytes(bytes, 0, size); return bytes; } @@ -168,14 +168,14 @@ class Stream { const baseTypeSize = FIT.BaseTypeDefinitions[baseType].size; const baseTypeInvalid = FIT.BaseTypeDefinitions[baseType].invalid; - const arrayBuffer = this.readBytes(size); + const bytes = this.readBytes(size); if (size % baseTypeSize !== 0) { return convertInvalidToNull ? null : baseTypeInvalid; } if (baseType === FIT.BaseType.STRING) { - const string = this.#textDecoder.decode(arrayBuffer).replace(/\uFFFD/g, ""); + const string = this.#textDecoder.decode(bytes).replace(/\uFFFD/g, ""); const strings = string.split('\0'); while (strings[strings.length - 1] === "") { @@ -189,7 +189,7 @@ class Stream { return strings.length === 1 ? strings[0] : strings; } - const dataView = new DataView(arrayBuffer); + const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let values = []; const count = size / baseTypeSize; diff --git a/src/utils-hr-mesg.js b/src/utils-hr-mesg.js index 557efa5..6bde5d8 100644 --- a/src/utils-hr-mesg.js +++ b/src/utils-hr-mesg.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils-internal.js b/src/utils-internal.js index 004b6ee..19becd0 100644 --- a/src/utils-internal.js +++ b/src/utils-internal.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils.js b/src/utils.js index b10c6b5..17ef3d7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,8 +5,8 @@ // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.141.0Release -// Tag = production/release/21.141.0-0-g2aa27e1 +// Profile Version = 21.158.0Release +// Tag = production/release/21.158.0-0-gc9428aa ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/test/accumulator.test.js b/test/accumulator.test.js index 296f74e..820d5ed 100644 --- a/test/accumulator.test.js +++ b/test/accumulator.test.js @@ -9,19 +9,37 @@ import Accumulator from "../src/accumulator.js"; describe("Accumulator Tests", () => { - test("Happy Path", () => { + test("Accumulates field", () => { const accumulator = new Accumulator(); - accumulator.add(0, 0, 0); - expect(accumulator.accumulate(0, 0, 1, 8)).toBe(1); + accumulator.createAccumulatedField(0, 0, 0); - accumulator.add(0, 0, 0); + expect(accumulator.accumulate(0, 0, 1, 8)).toBe(1); expect(accumulator.accumulate(0, 0, 2, 8)).toBe(2); - - accumulator.add(0, 0, 0); expect(accumulator.accumulate(0, 0, 3, 8)).toBe(3); - - accumulator.add(0, 0, 0); expect(accumulator.accumulate(0, 0, 4, 8)).toBe(4); }); + + test("Accumulates multiple fields independently", () => { + const accumulator = new Accumulator(); + + accumulator.createAccumulatedField(0, 0, 0); + expect(accumulator.accumulate(0, 0, 254, 8)).toBe(254); + + accumulator.createAccumulatedField(1, 1, 0); + expect(accumulator.accumulate(1, 1, 2, 8)).toBe(2); + + expect(accumulator.accumulate(0, 0, 0, 8)).toBe(256); + }); + + test("Accumulates when field rolls over", () => { + const accumulator = new Accumulator(); + + accumulator.createAccumulatedField(0, 0, 250); + + expect(accumulator.accumulate(0, 0, 254, 8)).toBe(254); + expect(accumulator.accumulate(0, 0, 255, 8)).toBe(255); + expect(accumulator.accumulate(0, 0, 0, 8)).toBe(256); + expect(accumulator.accumulate(0, 0, 3, 8)).toBe(259); + }); }); diff --git a/test/data/test-data.js b/test/data/test-data.js index ed40a04..2f3f806 100644 --- a/test/data/test-data.js +++ b/test/data/test-data.js @@ -12,6 +12,23 @@ const fitFileShort = [ 0x00, 0x04, 0x01, 0x00, 0x00, 0xCA, 0x9A, 0x3B, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x00, // Message 0x5D, 0xF2]; // CRC +const fitFileShortInvalidHeader = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // File Header + 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x02, 0x84, 0x04, 0x04, 0x86, 0x08, 0x0A, 0x07, // Message Definition + 0x00, 0x04, 0x01, 0x00, 0x00, 0xCA, 0x9A, 0x3B, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x00, // Message + 0x5D, 0xF2]; // CRC + +const fitFileShortDataOnly = [ + 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x02, 0x84, 0x04, 0x04, 0x86, 0x08, 0x0A, 0x07, // Message Definition + 0x00, 0x04, 0x01, 0x00, 0x00, 0xCA, 0x9A, 0x3B, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x00, // Message + 0x5D, 0xF2]; // CRC + +const fitFileShortInvalidCRC = [ + 0x0E, 0x20, 0x8B, 0x08, 0x24, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x8E, 0xA3, // File Header + 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x02, 0x84, 0x04, 0x04, 0x86, 0x08, 0x0A, 0x07, // Message Definition + 0x00, 0x04, 0x01, 0x00, 0x00, 0xCA, 0x9A, 0x3B, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x00, // Message + 0x00, 0x00]; // CRC + const fitFileShortWithWrongFieldDefSize = [ 0x0E, 0x20, 0x8B, 0x08, 0x21, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x8E, 0xA3, // File Header 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x02, 0x84, 0x04, 0x01, 0x86, 0x08, 0x0A, 0x07, // Message Definition @@ -21,7 +38,7 @@ const fitFileShortWithWrongFieldDefSize = [ const fitFileShortCompressedTimestamp = [0x0E, 0x20, 0x8B, 0x08, 0x24, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x8E, 0xA3, // File Header 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x02, 0x84, 0x04, 0x04, 0x86, 0x08, 0x0A, 0x07, // Message Definition 0x80, 0x04, 0x01, 0x00, 0x00, 0xCA, 0x9A, 0x3B, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x00, // Message - 0x5D, 0xF2]; // CRC + 0x5D, 0xF2]; // CRC const fitFileChained = [ 0x0E, 0x20, 0x9F, 0x03, 0x64, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, @@ -93,6 +110,25 @@ const fitFileMonitoring = [ 0x3F, 0x2A, 0xE2, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x1E, 0xED, 0xF9 ]; +const fitFileAccumulatedComponents = [ + 0x0E, 0x20, 0xE8, 0x03, 0x0F, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, + 0x4D, 0x89, 0x40, 0x00, 0x00, 0x14, 0x00, 0x01, 0x12, 0x01, 0x02, 0x00, + 0xFE, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x7D +]; + +const fitFileCompressedSpeedAndDistance = [ + 0x0E, 0x20, 0xE8, 0x03, 0x11, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, + 0xCD, 0x09, 0x40, 0x00, 0x00, 0x14, 0x00, 0x01, 0x08, 0x03, 0x0D, 0x00, + 0x8B, 0x00, 0x08, 0x00, 0xF9, 0x00, 0x14, 0x50, 0x0B +]; + +const fitFileCompressedSpeedAndDistanceWithInitialDistance = [ + 0x0E, 0x20, 0xE8, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, + 0x4C, 0x85, 0x40, 0x00, 0x00, 0x14, 0x00, 0x01, 0x05, 0x04, 0x86, 0x00, + 0xC8, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x14, 0x00, 0x01, 0x08, 0x03, + 0x0D, 0x00, 0x8B, 0x00, 0x08, 0x00, 0xF9, 0x00, 0x14, 0x65, 0xB1 +]; + const gearChangeData = [ { "timestamp": 1024873717, @@ -479,10 +515,16 @@ export default { fitFileShort, fitFileShortWithWrongFieldDefSize, fitFileShortCompressedTimestamp, + fitFileShortInvalidHeader, + fitFileShortInvalidCRC, + fitFileShortDataOnly, fitFileChained, fitFileChainedWeirdVivoki, fitFileDevDataWithoutFieldDescription, fitFileMonitoring, + fitFileAccumulatedComponents, + fitFileCompressedSpeedAndDistanceWithInitialDistance, + fitFileCompressedSpeedAndDistance, gearChangeData, workout800mRepeatsLittleEndian, workout800mRepeatsBigEndian diff --git a/test/decoder.test.js b/test/decoder.test.js index dcbfea7..797a27a 100644 --- a/test/decoder.test.js +++ b/test/decoder.test.js @@ -112,6 +112,19 @@ describe("Decoder Tests", () => { expect(errors.length).toBeGreaterThanOrEqual(1); }); + + test("Decoding should not reset the stream", () => { + const stream = Stream.fromByteArray(Data.fitFileShort); + const decode = new Decoder(stream); + + decode.read(); + expect(stream.position).toBe(stream.length); + + const { messages, errors } = decode.read(); + expect(errors.length).toBe(0); + expect(messages).toEqual({}); + }); + test("There should be 1 file_id messsage", () => { const stream = Stream.fromByteArray(Data.fitFileShort); const decode = new Decoder(stream); @@ -120,7 +133,16 @@ describe("Decoder Tests", () => { expect(errors.length).toBe(0); expect(messages["fileIdMesgs"].length).toBe(1); }); + + test("File with invalid CRC should fail", () => { + const stream = Stream.fromByteArray(Data.fitFileShortInvalidCRC); + const decode = new Decoder(stream); + const { messages, errors } = decode.read(); + expect(errors.length).toBe(1); + expect(messages["fileIdMesgs"].length).toBe(1); + }); }); + describe("Chained FIT File Tests", () => { test("There should be 2 file_id messsages", () => { const stream = Stream.fromByteArray(Data.fitFileChained); @@ -139,6 +161,79 @@ describe("Decoder Tests", () => { }); }); + describe("Skip Header Flag Tests", () => { + test("File with invalid header should not fail when skipHeader: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShortInvalidHeader); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ skipHeader: true}); + + expect(errors.length).toBe(0); + expect(messages["fileIdMesgs"].length).toBe(1); + }); + + test("File with invalid header should fail when skipHeader: false", () => { + const stream = Stream.fromByteArray(Data.fitFileShortInvalidHeader); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ skipHeader: false}); + + expect(errors.length).toBe(1); + }); + + test("File with valid header should not fail when skipHeader: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShort); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ skipHeader: true}); + + expect(errors.length).toBe(0); + expect(messages["fileIdMesgs"].length).toBe(1); + }); + + test("File with invalid CRC should not fail when skipHeader: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShortInvalidCRC); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ skipHeader: true}); + + expect(errors.length).toBe(0); + expect(messages["fileIdMesgs"].length).toBe(1); + }) + }); + + describe("Data Only Flag Tests", () => { + test("File with no header should not fail when dataOnly: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShortDataOnly); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ dataOnly: true}); + + expect(errors.length).toBe(0); + expect(messages["fileIdMesgs"].length).toBe(1); + }); + + test("File with no header should faile when dataOnly: false", () => { + const stream = Stream.fromByteArray(Data.fitFileShortDataOnly); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ dataOnly: false}); + + expect(errors.length).toBe(1); + }); + + test("File with valid header should not fail when dataOnly: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShort); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ dataOnly: true}); + + expect(errors.length).toBe(1); + }); + + test("File with invalid CRC should not fail when dataOnly: true", () => { + const stream = Stream.fromByteArray(Data.fitFileShortInvalidCRC.slice(14)); + const decode = new Decoder(stream); + const { messages, errors } = decode.read({ dataOnly: true}); + + expect(errors.length).toBe(0); + expect(messages["fileIdMesgs"].length).toBe(1); + }); + }); + describe("Convert Date Options Tests", () => { test("Date Time should be Date by default", () => { const stream = Stream.fromByteArray(Data.fitFileShort); @@ -367,6 +462,23 @@ describe("Decoder Tests", () => { expect(messages.monitoringMesgs[3].cycles).toBe(15); }); + test("Component Expansion should expand components which have their own components", () => { + const stream = Stream.fromByteArray(Data.fitFileCompressedSpeedAndDistance); + const decode = new Decoder(stream); + const { messages, errors } = decode.read(); + expect(errors.length).toBe(0); + + expect(messages.recordMesgs[0].distance).toBe(8) + expect(messages.recordMesgs[0].speed).toBe(1.39) + + // Speed should also expand into enhanced speed + expect(messages.recordMesgs[0].enhancedSpeed).toBe(1.39) + + expect(messages.recordMesgs[1].distance).toBe(20) + expect(messages.recordMesgs[1].speed).toBe(2.49) + expect(messages.recordMesgs[1].enhancedSpeed).toBe(2.49) + }); + }); describe("Sub-Field Expansion Tests", () => { @@ -439,6 +551,36 @@ describe("Decoder Tests", () => { }); }); + describe("Accumulated Field Tests", () => { + test("Expanded Components which accumulate should accumulate", () => { + const stream = Stream.fromByteArray(Data.fitFileAccumulatedComponents); + const decode = new Decoder(stream); + const { messages, errors } = decode.read(); + expect(errors.length).toBe(0); + + expect(messages.recordMesgs[0].cycles).toBe(254) + expect(messages.recordMesgs[0].totalCycles).toBe(254) + + expect(messages.recordMesgs[1].cycles).toBe(0) + expect(messages.recordMesgs[1].totalCycles).toBe(256) + + expect(messages.recordMesgs[2].cycles).toBe(1) + expect(messages.recordMesgs[2].totalCycles).toBe(257) + }); + + test("Expanded Components which accumulate and have an initial non-expanded value should accumulate", () => { + const stream = Stream.fromByteArray(Data.fitFileCompressedSpeedAndDistanceWithInitialDistance); + const decode = new Decoder(stream); + const { messages, errors } = decode.read(); + expect(errors.length).toBe(0); + + // The first distance field is not expanded from a compressedSpeedDistance field + expect(messages.recordMesgs[0].distance).toBe(2) + expect(messages.recordMesgs[1].distance).toBe(264) + expect(messages.recordMesgs[2].distance).toBe(276) + }); + }); + describe("Sub Field and Component Expansion Tests", () => { test("Sub Field Plus Component Expansion should create gear change data fields", () => { const buf = fs.readFileSync("test/data/WithGearChangeData.fit"); @@ -540,7 +682,7 @@ describe("Decoder Tests", () => { }); }); - + describe("Decode Include Unknown Data", () => { test("When decoding a file with the includeUnknownData flag set to true", () => { const buf = fs.readFileSync("test/data/WithGearChangeData.fit");