@garmin/fitsdk 21.141.0 → 21.161.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,8 @@ The Read method accepts an optional options object that can be used to customize
68
68
  ````js
69
69
  const { messages, errors } = decoder.read({
70
70
  mesgListener: (messageNumber, message) => {},
71
+ mesgDefinitionListener: (mesgDefinition) => {},
72
+ fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {},
71
73
  applyScaleAndOffset: true,
72
74
  expandSubFields: true,
73
75
  expandComponents: true,
@@ -97,6 +99,10 @@ const { messages, errors } = decoder.read({
97
99
 
98
100
  console.log(recordFields);
99
101
  ````
102
+ #### mesgDefinitionListener: (mesgDefinition) => {}
103
+ Optional callback function that can be used to inspect message defintions as they are decoded from the file.
104
+ #### fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {}
105
+ Optional callback function that can be used to inspect developer field descriptions as they are decoded from the file.
100
106
  #### applyScaleAndOffset: true | false
101
107
  When true the scale and offset values as defined in the FIT Profile are applied to the raw field values.
102
108
  ````js
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@garmin/fitsdk",
3
- "version": "21.141.0",
3
+ "version": "21.161.0",
4
4
  "description": "FIT JavaScript SDK",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "node .",
9
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
9
+ "test": "vitest run"
10
10
  },
11
11
  "author": "Garmin International, Inc.",
12
12
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -18,9 +18,6 @@
18
18
  "src/"
19
19
  ],
20
20
  "devDependencies": {
21
- "jest": "^28.1.2"
22
- },
23
- "jest": {
24
- "transform": {}
21
+ "vitest": "^2.1.8"
25
22
  }
26
- }
23
+ }
@@ -1,12 +1,12 @@
1
1
  /////////////////////////////////////////////////////////////////////////////////////////////
2
- // Copyright 2024 Garmin International, Inc.
2
+ // Copyright 2025 Garmin International, Inc.
3
3
  // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
4
  // may not use this file except in compliance with the Flexible and Interoperable Data
5
5
  // Transfer (FIT) Protocol License.
6
6
  /////////////////////////////////////////////////////////////////////////////////////////////
7
7
  // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
- // Profile Version = 21.141.0Release
9
- // Tag = production/release/21.141.0-0-g2aa27e1
8
+ // Profile Version = 21.161.0Release
9
+ // Tag = production/release/21.161.0-0-g58854c0
10
10
  /////////////////////////////////////////////////////////////////////////////////////////////
11
11
 
12
12
 
@@ -32,16 +32,26 @@ class AccumulatedField {
32
32
  class Accumulator {
33
33
  #messages = {};
34
34
 
35
- add(mesgNum, fieldNum, value) {
35
+ createAccumulatedField(mesgNum, fieldNum, value) {
36
+ const accumualtedField = new AccumulatedField(value);
37
+
36
38
  if (this.#messages[mesgNum] == null) {
37
39
  this.#messages[mesgNum] = {};
38
40
  }
39
41
 
40
- this.#messages[mesgNum][fieldNum] = new AccumulatedField(value);
42
+ this.#messages[mesgNum][fieldNum] = accumualtedField;
43
+
44
+ return accumualtedField;
41
45
  }
42
46
 
43
47
  accumulate(mesgNum, fieldNum, value, bits) {
44
- return this.#messages[mesgNum]?.[fieldNum]?.accumulate(value, bits) ?? value;
48
+ let accumualtedField = this.#messages[mesgNum]?.[fieldNum];
49
+
50
+ if(accumualtedField == null) {
51
+ accumualtedField = this.createAccumulatedField(mesgNum, fieldNum, value);
52
+ }
53
+
54
+ return accumualtedField.accumulate(value, bits);
45
55
  }
46
56
  }
47
57
 
package/src/bit-stream.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /////////////////////////////////////////////////////////////////////////////////////////////
2
- // Copyright 2024 Garmin International, Inc.
2
+ // Copyright 2025 Garmin International, Inc.
3
3
  // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
4
  // may not use this file except in compliance with the Flexible and Interoperable Data
5
5
  // Transfer (FIT) Protocol License.
6
6
  /////////////////////////////////////////////////////////////////////////////////////////////
7
7
  // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
- // Profile Version = 21.141.0Release
9
- // Tag = production/release/21.141.0-0-g2aa27e1
8
+ // Profile Version = 21.161.0Release
9
+ // Tag = production/release/21.161.0-0-g58854c0
10
10
  /////////////////////////////////////////////////////////////////////////////////////////////
11
11
 
12
12
 
@@ -1,12 +1,12 @@
1
1
  /////////////////////////////////////////////////////////////////////////////////////////////
2
- // Copyright 2024 Garmin International, Inc.
2
+ // Copyright 2025 Garmin International, Inc.
3
3
  // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
4
  // may not use this file except in compliance with the Flexible and Interoperable Data
5
5
  // Transfer (FIT) Protocol License.
6
6
  /////////////////////////////////////////////////////////////////////////////////////////////
7
7
  // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
- // Profile Version = 21.141.0Release
9
- // Tag = production/release/21.141.0-0-g2aa27e1
8
+ // Profile Version = 21.161.0Release
9
+ // Tag = production/release/21.161.0-0-g58854c0
10
10
  /////////////////////////////////////////////////////////////////////////////////////////////
11
11
 
12
12
 
package/src/decoder.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /////////////////////////////////////////////////////////////////////////////////////////////
2
- // Copyright 2024 Garmin International, Inc.
2
+ // Copyright 2025 Garmin International, Inc.
3
3
  // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
4
  // may not use this file except in compliance with the Flexible and Interoperable Data
5
5
  // Transfer (FIT) Protocol License.
6
6
  /////////////////////////////////////////////////////////////////////////////////////////////
7
7
  // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
- // Profile Version = 21.141.0Release
9
- // Tag = production/release/21.141.0-0-g2aa27e1
8
+ // Profile Version = 21.161.0Release
9
+ // Tag = production/release/21.161.0-0-g58854c0
10
10
  /////////////////////////////////////////////////////////////////////////////////////////////
11
11
 
12
12
 
@@ -25,8 +25,17 @@ const MESG_DEFINITION_MASK = 0x40;
25
25
  const DEV_DATA_MASK = 0x20;
26
26
  const MESG_HEADER_MASK = 0x00;
27
27
  const LOCAL_MESG_NUM_MASK = 0x0F;
28
+
29
+ const HEADER_WITH_CRC_SIZE = 14;
30
+ const HEADER_WITHOUT_CRC_SIZE = 12;
28
31
  const CRC_SIZE = 2;
29
32
 
33
+ const DecodeMode = Object.freeze({
34
+ NORMAL: "normal",
35
+ SKIP_HEADER: "skipHeader",
36
+ DATA_ONLY: "dataOnly"
37
+ });
38
+
30
39
  class Decoder {
31
40
  #localMessageDefinitions = [];
32
41
  #developerDataDefinitions = {};
@@ -36,7 +45,11 @@ class Decoder {
36
45
  #fieldsWithSubFields = [];
37
46
  #fieldsToExpand = [];
38
47
 
48
+ #decodeMode = DecodeMode.NORMAL;
49
+
39
50
  #mesgListener = null;
51
+ #mesgDefinitionListener = null;
52
+ #fieldDescriptionListener = null;
40
53
  #optExpandSubFields = true;
41
54
  #optExpandComponents = true;
42
55
  #optApplyScaleAndOffset = true;
@@ -67,7 +80,7 @@ class Decoder {
67
80
  static isFIT(stream) {
68
81
  try {
69
82
  const fileHeaderSize = stream.peekByte();
70
- if ([14, 12].includes(fileHeaderSize) != true) {
83
+ if ([HEADER_WITH_CRC_SIZE, HEADER_WITHOUT_CRC_SIZE].includes(fileHeaderSize) != true) {
71
84
  return false;
72
85
  }
73
86
 
@@ -75,7 +88,7 @@ class Decoder {
75
88
  return false;
76
89
  }
77
90
 
78
- const fileHeader = Decoder.#readFileHeader(stream, true);
91
+ const fileHeader = Decoder.#readFileHeader(stream, { resetPosition: true, });
79
92
  if (fileHeader.dataType !== ".FIT") {
80
93
  return false;
81
94
  }
@@ -105,7 +118,7 @@ class Decoder {
105
118
  return false;
106
119
  }
107
120
 
108
- const fileHeader = Decoder.#readFileHeader(this.#stream, true);
121
+ const fileHeader = Decoder.#readFileHeader(this.#stream, { resetPosition: true, });
109
122
 
110
123
  if (this.#stream.length < fileHeader.headerSize + fileHeader.dataSize + CRC_SIZE) {
111
124
  return false;
@@ -113,7 +126,7 @@ class Decoder {
113
126
 
114
127
  const buf = new Uint8Array(this.#stream.slice(0, this.#stream.length))
115
128
 
116
- if (fileHeader.headerSize === 14 && fileHeader.headerCRC !== 0x0000
129
+ if (fileHeader.headerSize === HEADER_WITH_CRC_SIZE && fileHeader.headerCRC !== 0x0000
117
130
  && fileHeader.headerCRC != CrcCalculator.calculateCRC(buf, 0, 12)) {
118
131
  return false;
119
132
  }
@@ -138,11 +151,28 @@ class Decoder {
138
151
  * @param {Object} message - The message
139
152
  */
140
153
 
154
+ /**
155
+ * Message Definition Listener Callback
156
+ *
157
+ * @callback Decoder~mesgDefinitionListener
158
+ * @param {Object} messageDefinition - The message Definition
159
+ */
160
+
161
+ /**
162
+ * Developer Field Description Listener Callback
163
+ *
164
+ * @callback Decoder~fieldDescriptionListener
165
+ * @param {Number} key - The key associated with this pairing of of Developer Data Id and Field Description Mesgs
166
+ * @param {Object} developerDataIdMesg - The Developer Data Id Mesg associated with this pairing
167
+ * @param {Object} fieldDescriptionMesg - The Field Description Mesg associated with this pairing
168
+ */
141
169
 
142
170
  /**
143
171
  * Read the messages from the stream.
144
172
  * @param {Object=} [options] - Read options (optional)
145
173
  * @param {Decoder~mesgListener} [options.mesgListener=null] - (optional, default null) mesgListener(mesgNum, message)
174
+ * @param {Decoder~mesgDefinitionListener} [options.mesgDefinitionListener=null] - (optional, default null) mesgDefinitionListener(mesgDefinition)
175
+ * @param {Decoder~fieldDescriptionListener} [options.fieldDescriptionListener=null] - (optional, default null) fieldDescriptionListener(key, developerDataIdMesg, fieldDescriptionMesg)
146
176
  * @param {Boolean} [options.expandSubFields=true] - (optional, default true)
147
177
  * @param {Boolean} [options.expandComponents=true] - (optional, default true)
148
178
  * @param {Boolean} [options.applyScaleAndOffset=true] - (optional, default true)
@@ -150,19 +180,27 @@ class Decoder {
150
180
  * @param {boolean} [options.convertDateTimesToDates=true] - (optional, default true)
151
181
  * @param {Boolean} [options.includeUnknownData=false] - (optional, default false)
152
182
  * @param {boolean} [options.mergeHeartRates=true] - (optional, default false)
183
+ * @param {boolean} [options.skipHeader=false] - (optional, default false)
184
+ * @param {boolean} [options.dataOnly=false] - (optional, default false)
153
185
  * @return {Object} result - {messages:Array, errors:Array}
154
186
  */
155
187
  read({
156
188
  mesgListener = null,
189
+ mesgDefinitionListener = null,
190
+ fieldDescriptionListener = null,
157
191
  expandSubFields = true,
158
192
  expandComponents = true,
159
193
  applyScaleAndOffset = true,
160
194
  convertTypesToStrings = true,
161
195
  convertDateTimesToDates = true,
162
196
  includeUnknownData = false,
163
- mergeHeartRates = true } = {}) {
197
+ mergeHeartRates = true,
198
+ skipHeader = false,
199
+ dataOnly = false,} = {}) {
164
200
 
165
201
  this.#mesgListener = mesgListener;
202
+ this.#mesgDefinitionListener = mesgDefinitionListener;
203
+ this.#fieldDescriptionListener = fieldDescriptionListener;
166
204
  this.#optExpandSubFields = expandSubFields
167
205
  this.#optExpandComponents = expandComponents;
168
206
  this.#optApplyScaleAndOffset = applyScaleAndOffset;
@@ -182,7 +220,11 @@ class Decoder {
182
220
  this.#throwError("mergeHeartRates requires applyScaleAndOffset and expandComponents to be enabled");
183
221
  }
184
222
 
185
- this.#stream.reset();
223
+ if (dataOnly && skipHeader) {
224
+ this.#throwError("dataOnly and skipHeader cannot both be enabled")
225
+ }
226
+
227
+ this.#decodeMode = skipHeader ? DecodeMode.SKIP_HEADER : dataOnly ? DecodeMode.DATA_ONLY : DecodeMode.NORMAL;
186
228
 
187
229
  while (this.#stream.position < this.#stream.length) {
188
230
  this.#decodeNextFile();
@@ -203,23 +245,23 @@ class Decoder {
203
245
  #decodeNextFile() {
204
246
  const position = this.#stream.position;
205
247
 
206
- if (!this.isFIT()) {
248
+ if (this.#decodeMode === DecodeMode.NORMAL && !this.isFIT()) {
207
249
  this.#throwError("input is not a FIT file");
208
250
  }
209
251
 
210
252
  this.#stream.crcCalculator = new CrcCalculator();
211
253
 
212
- const fileHeader = Decoder.#readFileHeader(this.#stream);
254
+ const { headerSize, dataSize } = Decoder.#readFileHeader(this.#stream, { decodeMode: this.#decodeMode });
213
255
 
214
256
  // Read data messages and definitions
215
- while (this.#stream.position < (position + fileHeader.headerSize + fileHeader.dataSize)) {
257
+ while (this.#stream.position < (position + headerSize + dataSize)) {
216
258
  this.#decodeNextRecord();
217
259
  }
218
260
 
219
261
  // Check the CRC
220
262
  const calculatedCrc = this.#stream.crcCalculator.crc;
221
263
  const crc = this.#stream.readUInt16();
222
- if (crc !== calculatedCrc) {
264
+ if (this.#decodeMode === DecodeMode.NORMAL && crc !== calculatedCrc) {
223
265
  this.#throwError("CRC error");
224
266
  }
225
267
  }
@@ -291,6 +333,8 @@ class Decoder {
291
333
  }
292
334
  }
293
335
 
336
+ this.#mesgDefinitionListener?.({...messageDefinition});
337
+
294
338
  let messageProfile = Profile.messages[messageDefinition.globalMessageNumber];
295
339
 
296
340
  if (messageProfile == null && this.#optIncludeUnknownData) {
@@ -341,7 +385,7 @@ class Decoder {
341
385
  }
342
386
 
343
387
  if (field?.isAccumulated) {
344
- this.#accumulator.add(mesgNum, fieldDefinition.fieldDefinitionNumber, rawFieldValue);
388
+ this.#setAccumulatedField(messageDefinition, message, field, rawFieldValue);
345
389
  }
346
390
  }
347
391
  });
@@ -395,6 +439,14 @@ class Decoder {
395
439
 
396
440
  this.#messages[messageDefinition.messagesKey].push(message);
397
441
  this.#mesgListener?.(messageDefinition.globalMessageNumber, message);
442
+
443
+ if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION && this.#fieldDescriptionListener != null) {
444
+ const developerDataIdMesg = this.#messages.developerDataIdMesgs?.find((developerDataIdMesg) => {
445
+ return developerDataIdMesg.developerDataIndex === message.developerDataIndex;
446
+ }) ?? {};
447
+
448
+ this.#fieldDescriptionListener(message.key, {...developerDataIdMesg}, {...message});
449
+ }
398
450
  }
399
451
  }
400
452
 
@@ -530,7 +582,7 @@ class Decoder {
530
582
  while (this.#fieldsToExpand.length > 0) {
531
583
  const name = this.#fieldsToExpand.shift();
532
584
 
533
- const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name];
585
+ const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name] ?? mesg[name];
534
586
  let field = Profile.messages[mesgNum].fields[fieldDefinitionNumber];
535
587
  field = isSubField ? this.#lookupSubfield(field, name) : field;
536
588
  const baseType = FIT.FieldTypeToBaseType[field.type];
@@ -546,6 +598,10 @@ class Decoder {
546
598
  const bitStream = new BitStream(rawFieldValue, baseType);
547
599
 
548
600
  for (let j = 0; j < field.components.length; j++) {
601
+ if (bitStream.bitsAvailable < field.bits[j]) {
602
+ break;
603
+ }
604
+
549
605
  const targetField = fields[field.components[j]];
550
606
  if (mesg[targetField.name] == null) {
551
607
  const baseType = FIT.FieldTypeToBaseType[targetField.type];
@@ -560,22 +616,22 @@ class Decoder {
560
616
  };
561
617
  }
562
618
 
563
- if (bitStream.bitsAvailable < field.bits[j]) {
564
- break;
565
- }
566
-
567
619
  let value = bitStream.readBits(field.bits[j]);
568
620
 
569
- value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]) ?? value;
621
+ if (targetField.isAccumulated) {
622
+ value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]);
623
+ }
624
+
625
+ // Undo component scale and offset before applying the destination field's scale and offset
626
+ value = (value / field.scale[j] - field.offset[j]);
570
627
 
571
- mesg[targetField.name].rawFieldValue.push(value);
628
+ const rawValue = (value + targetField.offset) * targetField.scale;
629
+ mesg[targetField.name].rawFieldValue.push(rawValue);
572
630
 
573
- if (value === mesg[targetField.name].invalidValue) {
631
+ if (rawValue === mesg[targetField.name].invalidValue) {
574
632
  mesg[targetField.name].fieldValue.push(null);
575
633
  }
576
634
  else {
577
- value = value / field.scale[j] - field.offset[j];
578
-
579
635
  if (this.#optConvertTypesToStrings) {
580
636
  value = this.#convertTypeToString(mesg, targetField, value);
581
637
  }
@@ -681,6 +737,26 @@ class Decoder {
681
737
  }
682
738
  }
683
739
 
740
+ #setAccumulatedField(messageDefinition, message, field, rawFieldValue) {
741
+ const rawFieldValues = Array.isArray(rawFieldValue) ? rawFieldValue : [rawFieldValue];
742
+
743
+ rawFieldValues.forEach((value) => {
744
+ Object.values(message).forEach((containingField) => {
745
+ let components = messageDefinition.fields[containingField.fieldDefinitionNumber].components ?? []
746
+
747
+ components.forEach((componentFieldNum, i) => {
748
+ const targetField = messageDefinition.fields[componentFieldNum];
749
+
750
+ if(targetField?.num == field.num && targetField?.isAccumulated) {
751
+ value = (((value / field.scale) - field.offset) + containingField.offset[i]) * containingField.scale[i];
752
+ }
753
+ });
754
+ });
755
+
756
+ this.#accumulator.createAccumulatedField(messageDefinition.num, field.num, value);
757
+ });
758
+ }
759
+
684
760
  #convertTypeToString(messageDefinition, field, rawFieldValue) {
685
761
  if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
686
762
  return rawFieldValue;
@@ -711,9 +787,22 @@ class Decoder {
711
787
  return subField != null ? subField : {};
712
788
  }
713
789
 
714
- static #readFileHeader(stream, resetPosition = false) {
790
+ static #readFileHeader(stream, { resetPosition = false, decodeMode = DecodeMode.NORMAL }) {
715
791
  const position = stream.position;
716
792
 
793
+ if(decodeMode !== DecodeMode.NORMAL) {
794
+ if(decodeMode === DecodeMode.SKIP_HEADER) {
795
+ stream.seek(HEADER_WITH_CRC_SIZE);
796
+ }
797
+
798
+ const headerSize = decodeMode === DecodeMode.SKIP_HEADER ? HEADER_WITH_CRC_SIZE : 0;
799
+
800
+ return {
801
+ headerSize,
802
+ dataSize: stream.length - headerSize - CRC_SIZE,
803
+ };
804
+ }
805
+
717
806
  const fileHeader = {
718
807
  headerSize: stream.readByte(),
719
808
  protocolVersion: stream.readByte(),