@garmin/fitsdk 21.168.0 → 21.169.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/src/decoder.js CHANGED
@@ -1,840 +1,840 @@
1
- /////////////////////////////////////////////////////////////////////////////////////////////
2
- // Copyright 2025 Garmin International, Inc.
3
- // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
- // may not use this file except in compliance with the Flexible and Interoperable Data
5
- // Transfer (FIT) Protocol License.
6
- /////////////////////////////////////////////////////////////////////////////////////////////
7
- // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
- // Profile Version = 21.168.0Release
9
- // Tag = production/release/21.168.0-0-gb831b31
10
- /////////////////////////////////////////////////////////////////////////////////////////////
11
-
12
-
13
- import Accumulator from "../src/accumulator.js";
14
- import BitStream from "../src/bit-stream.js";
15
- import CrcCalculator from "./crc-calculator.js";
16
- import FIT from "./fit.js";
17
- import HrMesgUtils from "./utils-hr-mesg.js";
18
- import MemoGlobUtils from "./utils-memo-glob.js";
19
- import Profile from "./profile.js";
20
- import Stream from "./stream.js";
21
- import Utils from "./utils.js";
22
- import UtilsInternal from "./utils-internal.js";
23
-
24
- const COMPRESSED_HEADER_MASK = 0x80;
25
- const MESG_DEFINITION_MASK = 0x40;
26
- const DEV_DATA_MASK = 0x20;
27
- const MESG_HEADER_MASK = 0x00;
28
- const LOCAL_MESG_NUM_MASK = 0x0F;
29
-
30
- const HEADER_WITH_CRC_SIZE = 14;
31
- const HEADER_WITHOUT_CRC_SIZE = 12;
32
- const CRC_SIZE = 2;
33
-
34
- const DecodeMode = Object.freeze({
35
- NORMAL: "normal",
36
- SKIP_HEADER: "skipHeader",
37
- DATA_ONLY: "dataOnly"
38
- });
39
-
40
- class Decoder {
41
- #localMessageDefinitions = [];
42
- #developerDataDefinitions = {};
43
- #stream = null;
44
- #accumulator = new Accumulator();
45
- #messages = {};
46
- #fieldsWithSubFields = [];
47
- #fieldsToExpand = [];
48
-
49
- #decodeMode = DecodeMode.NORMAL;
50
-
51
- #mesgListener = null;
52
- #mesgDefinitionListener = null;
53
- #fieldDescriptionListener = null;
54
- #optExpandSubFields = true;
55
- #optExpandComponents = true;
56
- #optApplyScaleAndOffset = true;
57
- #optConvertTypesToStrings = true;
58
- #optConvertDateTimesToDates = true;
59
- #optIncludeUnknownData = false;
60
- #optMergeHeartRates = true;
61
- #optDecodeMemoGlobs = false;
62
-
63
- /**
64
- * Creates a FIT File Decoder
65
- * @constructor
66
- * @param {Stream} stream - representing the FIT file to decode
67
- */
68
- constructor(stream) {
69
- if (stream == null) {
70
- throw Error("FIT Runtime Error stream parameter is null or undefined");
71
- }
72
-
73
- this.#stream = stream;
74
- }
75
-
76
- /**
77
- * Inspects the file header to determine if the input stream is a FIT file
78
- * @param {Stream} stream
79
- * @returns {Boolean} True if the stream is a FIT file
80
- * @static
81
- */
82
- static isFIT(stream) {
83
- try {
84
- const fileHeaderSize = stream.peekByte();
85
- if ([HEADER_WITH_CRC_SIZE, HEADER_WITHOUT_CRC_SIZE].includes(fileHeaderSize) != true) {
86
- return false;
87
- }
88
-
89
- if (stream.length < fileHeaderSize + CRC_SIZE) {
90
- return false;
91
- }
92
-
93
- const fileHeader = Decoder.#readFileHeader(stream, { resetPosition: true, });
94
- if (fileHeader.dataType !== ".FIT") {
95
- return false;
96
- }
97
- }
98
- catch (error) {
99
- return false;
100
- }
101
-
102
- return true;
103
- }
104
-
105
- /**
106
- * Inspects the file header to determine if the input stream is a FIT file
107
- * @returns {Boolean} True if the stream is a FIT file
108
- */
109
- isFIT() {
110
- return Decoder.isFIT(this.#stream);
111
- }
112
-
113
- /**
114
- * Checks that the input stream is a FIT file and verifies both the header and file CRC values
115
- * @returns {Boolean} True if the stream passes the isFit() and CRC checks
116
- */
117
- checkIntegrity() {
118
- try {
119
- if (!this.isFIT()) {
120
- return false;
121
- }
122
-
123
- const fileHeader = Decoder.#readFileHeader(this.#stream, { resetPosition: true, });
124
-
125
- if (this.#stream.length < fileHeader.headerSize + fileHeader.dataSize + CRC_SIZE) {
126
- return false;
127
- }
128
-
129
- const buf = new Uint8Array(this.#stream.slice(0, this.#stream.length))
130
-
131
- if (fileHeader.headerSize === HEADER_WITH_CRC_SIZE && fileHeader.headerCRC !== 0x0000
132
- && fileHeader.headerCRC != CrcCalculator.calculateCRC(buf, 0, 12)) {
133
- return false;
134
- }
135
-
136
- const fileCRC = (buf[fileHeader.headerSize + fileHeader.dataSize + 1] << 8) + buf[fileHeader.headerSize + fileHeader.dataSize]
137
- if (fileCRC != CrcCalculator.calculateCRC(buf, 0, fileHeader.headerSize + fileHeader.dataSize)) {
138
- return false;
139
- }
140
- }
141
- catch (error) {
142
- return false;
143
- }
144
-
145
- return true;
146
- }
147
-
148
- /**
149
- * Message Listener Callback
150
- *
151
- * @callback Decoder~mesgListener
152
- * @param {Number} mesgNum - Profile.MesgNum
153
- * @param {Object} message - The message
154
- */
155
-
156
- /**
157
- * Message Definition Listener Callback
158
- *
159
- * @callback Decoder~mesgDefinitionListener
160
- * @param {Object} messageDefinition - The message Definition
161
- */
162
-
163
- /**
164
- * Developer Field Description Listener Callback
165
- *
166
- * @callback Decoder~fieldDescriptionListener
167
- * @param {Number} key - The key associated with this pairing of of Developer Data Id and Field Description Mesgs
168
- * @param {Object} developerDataIdMesg - The Developer Data Id Mesg associated with this pairing
169
- * @param {Object} fieldDescriptionMesg - The Field Description Mesg associated with this pairing
170
- */
171
-
172
- /**
173
- * Read the messages from the stream.
174
- * @param {Object=} [options] - Read options (optional)
175
- * @param {Decoder~mesgListener} [options.mesgListener=null] - (optional, default null) mesgListener(mesgNum, message)
176
- * @param {Decoder~mesgDefinitionListener} [options.mesgDefinitionListener=null] - (optional, default null) mesgDefinitionListener(mesgDefinition)
177
- * @param {Decoder~fieldDescriptionListener} [options.fieldDescriptionListener=null] - (optional, default null) fieldDescriptionListener(key, developerDataIdMesg, fieldDescriptionMesg)
178
- * @param {Boolean} [options.expandSubFields=true] - (optional, default true)
179
- * @param {Boolean} [options.expandComponents=true] - (optional, default true)
180
- * @param {Boolean} [options.applyScaleAndOffset=true] - (optional, default true)
181
- * @param {Boolean} [options.convertTypesToStrings=true] - (optional, default true)
182
- * @param {boolean} [options.convertDateTimesToDates=true] - (optional, default true)
183
- * @param {Boolean} [options.includeUnknownData=false] - (optional, default false)
184
- * @param {boolean} [options.mergeHeartRates=true] - (optional, default true)
185
- * @param {boolean} [options.decodeMemoGlobs=true] - (optional, default false)
186
- * @param {boolean} [options.skipHeader=false] - (optional, default false)
187
- * @param {boolean} [options.dataOnly=false] - (optional, default false)
188
- * @return {Object} result - {messages:Array, errors:Array}
189
- */
190
- read({
191
- mesgListener = null,
192
- mesgDefinitionListener = null,
193
- fieldDescriptionListener = null,
194
- expandSubFields = true,
195
- expandComponents = true,
196
- applyScaleAndOffset = true,
197
- convertTypesToStrings = true,
198
- convertDateTimesToDates = true,
199
- includeUnknownData = false,
200
- mergeHeartRates = true,
201
- decodeMemoGlobs = false,
202
- skipHeader = false,
203
- dataOnly = false,} = {}) {
204
-
205
- this.#mesgListener = mesgListener;
206
- this.#mesgDefinitionListener = mesgDefinitionListener;
207
- this.#fieldDescriptionListener = fieldDescriptionListener;
208
- this.#optExpandSubFields = expandSubFields
209
- this.#optExpandComponents = expandComponents;
210
- this.#optApplyScaleAndOffset = applyScaleAndOffset;
211
- this.#optConvertTypesToStrings = convertTypesToStrings;
212
- this.#optConvertDateTimesToDates = convertDateTimesToDates;
213
- this.#optIncludeUnknownData = includeUnknownData;
214
- this.#optMergeHeartRates = mergeHeartRates;
215
- this.#optDecodeMemoGlobs = decodeMemoGlobs;
216
-
217
- this.#localMessageDefinitions = [];
218
- this.#developerDataDefinitions = {};
219
- this.#messages = {};
220
-
221
- const errors = [];
222
-
223
- try {
224
- if (this.#optMergeHeartRates && (!this.#optApplyScaleAndOffset || !this.#optExpandComponents)) {
225
- this.#throwError("mergeHeartRates requires applyScaleAndOffset and expandComponents to be enabled");
226
- }
227
-
228
- if (dataOnly && skipHeader) {
229
- this.#throwError("dataOnly and skipHeader cannot both be enabled")
230
- }
231
-
232
- this.#decodeMode = skipHeader ? DecodeMode.SKIP_HEADER : dataOnly ? DecodeMode.DATA_ONLY : DecodeMode.NORMAL;
233
-
234
- while (this.#stream.position < this.#stream.length) {
235
- this.#decodeNextFile();
236
- }
237
-
238
- if (this.#optMergeHeartRates) {
239
- HrMesgUtils.mergeHeartRates(this.#messages.hrMesgs, this.#messages.recordMesgs);
240
- }
241
-
242
- if (this.#optDecodeMemoGlobs) {
243
- MemoGlobUtils.decodeMemoGlobs(this.#messages);
244
- }
245
- }
246
- catch (error) {
247
- errors.push(error);
248
- }
249
- finally {
250
- return { messages: this.#messages, errors: errors };
251
- }
252
- }
253
-
254
- #decodeNextFile() {
255
- const position = this.#stream.position;
256
-
257
- if (this.#decodeMode === DecodeMode.NORMAL && !this.isFIT()) {
258
- this.#throwError("input is not a FIT file");
259
- }
260
-
261
- this.#stream.crcCalculator = new CrcCalculator();
262
-
263
- const { headerSize, dataSize } = Decoder.#readFileHeader(this.#stream, { decodeMode: this.#decodeMode });
264
-
265
- // Read data messages and definitions
266
- while (this.#stream.position < (position + headerSize + dataSize)) {
267
- this.#decodeNextRecord();
268
- }
269
-
270
- // Check the CRC
271
- const calculatedCrc = this.#stream.crcCalculator.crc;
272
- const crc = this.#stream.readUInt16();
273
- if (this.#decodeMode === DecodeMode.NORMAL && crc !== calculatedCrc) {
274
- this.#throwError("CRC error");
275
- }
276
- }
277
-
278
- #decodeNextRecord() {
279
- const recordHeader = this.#stream.peekByte();
280
-
281
- if ((recordHeader & COMPRESSED_HEADER_MASK) === COMPRESSED_HEADER_MASK) {
282
- return this.#decodeCompressedTimestampDataMessage();
283
- }
284
-
285
- if ((recordHeader & MESG_DEFINITION_MASK) === MESG_HEADER_MASK) {
286
- return this.#decodeMessage();
287
- }
288
-
289
- if ((recordHeader & MESG_DEFINITION_MASK) === MESG_DEFINITION_MASK) {
290
- return this.#decodeMessageDefinition();
291
- }
292
- }
293
-
294
- #decodeMessageDefinition() {
295
- const recordHeader = this.#stream.readByte();
296
-
297
- const messageDefinition = {};
298
- messageDefinition["recordHeader"] = recordHeader;
299
- messageDefinition["localMesgNum"] = recordHeader & LOCAL_MESG_NUM_MASK;
300
- messageDefinition["reserved"] = this.#stream.readByte();
301
-
302
- messageDefinition["architecture"] = this.#stream.readByte();
303
- messageDefinition["endianness"] = messageDefinition.architecture === 0 ? Stream.LITTLE_ENDIAN : Stream.BIG_ENDIAN;
304
-
305
- messageDefinition["globalMessageNumber"] = this.#stream.readUInt16({ endianness: messageDefinition["endianness"] });
306
- messageDefinition["numFields"] = this.#stream.readByte();
307
- messageDefinition["fieldDefinitions"] = [];
308
- messageDefinition["developerFieldDefinitions"] = [];
309
- messageDefinition["messageSize"] = 0;
310
- messageDefinition["developerDataSize"] = 0;
311
-
312
- for (let i = 0; i < messageDefinition.numFields; i++) {
313
- const fieldDefinition = {
314
- fieldDefinitionNumber: this.#stream.readByte(),
315
- size: this.#stream.readByte(),
316
- baseType: this.#stream.readByte()
317
- };
318
-
319
- if (!(fieldDefinition.baseType in FIT.BaseTypeDefinitions)) {
320
- this.#throwError();
321
- }
322
-
323
- fieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].invalid;
324
- fieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].size;
325
-
326
- messageDefinition.fieldDefinitions.push(fieldDefinition);
327
- messageDefinition.messageSize += fieldDefinition.size;
328
- }
329
-
330
- if ((recordHeader & DEV_DATA_MASK) === DEV_DATA_MASK) {
331
- const numDevFields = this.#stream.readByte();
332
-
333
- for (let i = 0; i < numDevFields; i++) {
334
- const developerFieldDefinition = {
335
- fieldDefinitionNumber: this.#stream.readByte(),
336
- size: this.#stream.readByte(),
337
- developerDataIndex: this.#stream.readByte()
338
- };
339
-
340
- messageDefinition.developerFieldDefinitions.push(developerFieldDefinition);
341
- messageDefinition.developerDataSize += developerFieldDefinition.size;
342
- }
343
- }
344
-
345
- this.#mesgDefinitionListener?.({...messageDefinition});
346
-
347
- let messageProfile = Profile.messages[messageDefinition.globalMessageNumber];
348
-
349
- if (messageProfile == null && this.#optIncludeUnknownData) {
350
- messageProfile = {
351
- name: messageDefinition["globalMessageNumber"].toString(),
352
- messagesKey: messageDefinition["globalMessageNumber"].toString(),
353
- num: messageDefinition["globalMessageNumber"],
354
- fields: {}
355
- };
356
- }
357
-
358
- this.#localMessageDefinitions[messageDefinition.localMesgNum] = { ...messageDefinition, ...messageProfile };
359
-
360
- if (messageProfile && !this.#messages.hasOwnProperty(messageProfile.messagesKey)) {
361
- this.#messages[messageProfile.messagesKey] = [];
362
- }
363
- }
364
-
365
- #decodeMessage() {
366
- const recordHeader = this.#stream.readByte();
367
-
368
- const localMesgNum = recordHeader & LOCAL_MESG_NUM_MASK;
369
- const messageDefinition = this.#localMessageDefinitions[localMesgNum];
370
-
371
- if (messageDefinition == null) {
372
- this.#throwError();
373
- }
374
-
375
- const fields = messageDefinition.fields ?? {};
376
- const mesgNum = messageDefinition.num;
377
- const message = {};
378
- this.#fieldsWithSubFields = [];
379
- this.#fieldsToExpand = [];
380
-
381
- messageDefinition.fieldDefinitions.forEach(fieldDefinition => {
382
- const field = fields[fieldDefinition.fieldDefinitionNumber];
383
- const { fieldName, rawFieldValue } = this.#readFieldValue(messageDefinition, fieldDefinition, field);
384
-
385
- if (fieldName != null && (field != null || this.#optIncludeUnknownData)) {
386
- message[fieldName] = { rawFieldValue, fieldDefinitionNumber: fieldDefinition.fieldDefinitionNumber };
387
-
388
- if (field?.subFields?.length > 0) {
389
- this.#fieldsWithSubFields.push(fieldName);
390
- }
391
-
392
- if (field?.hasComponents) {
393
- this.#fieldsToExpand.push(fieldName);
394
- }
395
-
396
- if (field?.isAccumulated) {
397
- this.#setAccumulatedField(messageDefinition, message, field, rawFieldValue);
398
- }
399
- }
400
- });
401
-
402
- const developerFields = {};
403
-
404
- messageDefinition.developerFieldDefinitions.forEach(developerFieldDefinition => {
405
- const field = this.#lookupDeveloperDataField(developerFieldDefinition)
406
- if (field == null) {
407
- // If there is not a field definition, then read past the field data.
408
- this.#stream.readBytes(developerFieldDefinition.size);
409
- return;
410
- }
411
-
412
- developerFieldDefinition["baseType"] = field.fitBaseTypeId;
413
- developerFieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].invalid;
414
- developerFieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].size;
415
-
416
- const { rawFieldValue: fieldValue } = this.#readFieldValue(messageDefinition, developerFieldDefinition, field);
417
-
418
- if (fieldValue != null) {
419
- developerFields[field.key] = fieldValue;
420
- }
421
- });
422
-
423
- if (mesgNum === Profile.MesgNum.DEVELOPER_DATA_ID) {
424
- this.#addDeveloperDataIdToProfile(message);
425
- }
426
- else if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION) {
427
- const key = Object.keys(this.#developerDataDefinitions)
428
- .reduce((count, key) => count + this.#developerDataDefinitions[key].fields.length, 0);
429
- message["key"] = { fieldValue: key, rawFieldValue: key };
430
-
431
- this.#addFieldDescriptionToProfile(message);
432
- }
433
- else {
434
- this.#expandSubFields(mesgNum, message);
435
- this.#expandComponents(mesgNum, message, fields);
436
- }
437
-
438
- this.#transformValues(message, messageDefinition);
439
-
440
- if (messageDefinition.name != null) {
441
- Object.keys(message).forEach((key) => {
442
- message[key] = message[key].fieldValue;
443
- });
444
-
445
- if (Object.keys(developerFields).length > 0) {
446
- message.developerFields = developerFields;
447
- }
448
-
449
- this.#messages[messageDefinition.messagesKey].push(message);
450
- this.#mesgListener?.(messageDefinition.globalMessageNumber, message);
451
-
452
- if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION && this.#fieldDescriptionListener != null) {
453
- const developerDataIdMesg = this.#messages.developerDataIdMesgs?.find((developerDataIdMesg) => {
454
- return developerDataIdMesg.developerDataIndex === message.developerDataIndex;
455
- }) ?? {};
456
-
457
- this.#fieldDescriptionListener(message.key, {...developerDataIdMesg}, {...message});
458
- }
459
- }
460
- }
461
-
462
- #decodeCompressedTimestampDataMessage() {
463
- this.#throwError("compressed timestamp messages are not currently supported");
464
- }
465
-
466
- #readFieldValue(messageDefinition, fieldDefinition, field) {
467
- const rawFieldValue = this.#readRawFieldValue(messageDefinition, fieldDefinition, field);
468
-
469
- if (rawFieldValue == null) {
470
- return {};
471
- }
472
-
473
- return {
474
- fieldName: (field?.name ?? ~~fieldDefinition.fieldDefinitionNumber),
475
- rawFieldValue
476
- };
477
- }
478
-
479
- #readRawFieldValue(messageDefinition, fieldDefinition, field) {
480
- const rawFieldValue = this.#stream.readValue(
481
- fieldDefinition.baseType,
482
- fieldDefinition.size,
483
- {
484
- endianness: messageDefinition["endianness"],
485
- convertInvalidToNull: !field?.hasComponents ?? false
486
- }
487
- );
488
- return rawFieldValue;
489
- }
490
-
491
- #addDeveloperDataIdToProfile(message) {
492
- if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
493
- return;
494
- }
495
-
496
- this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] = {
497
- developerDataIndex: message.developerDataIndex?.rawFieldValue,
498
- developerId: message.developerId?.rawFieldValue ?? null,
499
- applicationId: message.applicationId?.rawFieldValue ?? null,
500
- manufacturerId: message.manufacturerId?.rawFieldValue ?? null,
501
- applicationVersion: message.applicationVersion?.rawFieldValue ?? null,
502
- fields: []
503
- };
504
- }
505
-
506
- #addFieldDescriptionToProfile(message) {
507
- if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
508
- return;
509
- }
510
-
511
- if (this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] == null) {
512
- return;
513
- }
514
-
515
- this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue].fields.push({
516
- developerDataIndex: message.developerDataIndex?.rawFieldValue,
517
- fieldDefinitionNumber: message.fieldDefinitionNumber?.rawFieldValue,
518
- fitBaseTypeId: message.fitBaseTypeId?.rawFieldValue ?? null,
519
- fieldName: message.fieldName?.rawFieldValue ?? null,
520
- array: message.array?.rawFieldValue ?? null,
521
- components: message.components?.rawFieldValue ?? null,
522
- scale: message.scale?.rawFieldValue ?? null,
523
- offset: message.offset?.rawFieldValue ?? null,
524
- units: message.units?.rawFieldValue ?? null,
525
- bits: message.bits?.rawFieldValue ?? null,
526
- accumulate: message.accumulate?.rawFieldValue ?? null,
527
- refFieldName: message.refFieldName?.rawFieldValue ?? null,
528
- refFieldValue: message.refFieldValue?.rawFieldValue ?? null,
529
- fitBaseUnitId: message.fitBaseUnitId?.rawFieldValue ?? null,
530
- nativeMesgNum: message.nativeMesgNum?.rawFieldValue ?? null,
531
- nativeFieldNum: message.nativeFieldNum?.rawFieldValue ?? null,
532
- key: message.key.rawFieldValue
533
- });
534
- }
535
-
536
- #lookupDeveloperDataField(developerFieldDefinition) {
537
- try {
538
- return this.#developerDataDefinitions[developerFieldDefinition.developerDataIndex]
539
- ?.fields
540
- ?.find(def => def.fieldDefinitionNumber == developerFieldDefinition.fieldDefinitionNumber)
541
- ?? null;
542
- }
543
- catch {
544
- return null;
545
- }
546
- }
547
-
548
- #expandSubFields(mesgNum, message) {
549
- if (!this.#optExpandSubFields || this.#fieldsWithSubFields.length == 0) {
550
- return;
551
- }
552
-
553
- this.#fieldsWithSubFields.forEach((name) => {
554
- const field = Profile.messages[mesgNum].fields[message[name].fieldDefinitionNumber];
555
- this.#expandSubField(message, field);
556
- });
557
- }
558
-
559
- #expandSubField(message, field) {
560
- for (let i = 0; i < field.subFields.length; i++) {
561
- const subField = field.subFields[i];
562
- for (let j = 0; j < subField.map.length; j++) {
563
- const map = subField.map[j];
564
- const referenceField = message[map.name];
565
- if (referenceField == null) {
566
- continue;
567
- }
568
- if (referenceField.rawFieldValue === map.value) {
569
- message[subField.name] = JSON.parse(JSON.stringify(message[field.name]));
570
- message[subField.name].isSubField = true;
571
-
572
- if (subField.hasComponents) {
573
- this.#fieldsToExpand.push(subField.name);
574
- }
575
- break;
576
- }
577
- }
578
- }
579
- }
580
-
581
- #expandComponents(mesgNum, message, fields) {
582
- // TODO - What do do when the target field is not in the Profile?
583
- // TODO - This can happen in theory, but can it happen in practice?
584
-
585
- if (!this.#optExpandComponents || this.#fieldsToExpand.length == 0) {
586
- return;
587
- }
588
-
589
- const mesg = {};
590
-
591
- while (this.#fieldsToExpand.length > 0) {
592
- const name = this.#fieldsToExpand.shift();
593
-
594
- const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name] ?? mesg[name];
595
- let field = Profile.messages[mesgNum].fields[fieldDefinitionNumber];
596
- field = isSubField ? this.#lookupSubfield(field, name) : field;
597
- const baseType = FIT.FieldTypeToBaseType[field.type];
598
-
599
- if (field.hasComponents === false || baseType == null) {
600
- continue;
601
- }
602
-
603
- if (UtilsInternal.onlyInvalidValues(rawFieldValue, FIT.BaseTypeDefinitions[baseType].invalid)) {
604
- continue;
605
- }
606
-
607
- const bitStream = new BitStream(rawFieldValue, baseType);
608
-
609
- for (let j = 0; j < field.components.length; j++) {
610
- if (bitStream.bitsAvailable < field.bits[j]) {
611
- break;
612
- }
613
-
614
- const targetField = fields[field.components[j]];
615
- if (mesg[targetField.name] == null) {
616
- const baseType = FIT.FieldTypeToBaseType[targetField.type];
617
- const invalidValue = baseType != null ? FIT.BaseTypeDefinitions[baseType].invalid : 0xFF;
618
-
619
- mesg[targetField.name] = {
620
- fieldValue: [],
621
- rawFieldValue: [],
622
- fieldDefinitionNumber: targetField.num,
623
- isExpandedField: true,
624
- invalidValue,
625
- };
626
- }
627
-
628
- let value = bitStream.readBits(field.bits[j]);
629
-
630
- if (targetField.isAccumulated) {
631
- value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]);
632
- }
633
-
634
- // Undo component scale and offset before applying the destination field's scale and offset
635
- value = (value / field.scale[j] - field.offset[j]);
636
-
637
- const rawValue = (value + targetField.offset) * targetField.scale;
638
- mesg[targetField.name].rawFieldValue.push(rawValue);
639
-
640
- if (rawValue === mesg[targetField.name].invalidValue) {
641
- mesg[targetField.name].fieldValue.push(null);
642
- }
643
- else {
644
- if (this.#optConvertTypesToStrings) {
645
- value = this.#convertTypeToString(mesg, targetField, value);
646
- }
647
-
648
- mesg[targetField.name].fieldValue.push(value);
649
- }
650
-
651
- if (targetField.hasComponents) {
652
- this.#fieldsToExpand.push(targetField.name);
653
- }
654
-
655
- if (!bitStream.hasBitsAvailable) {
656
- break;
657
- }
658
- }
659
- }
660
-
661
- Object.keys(mesg).forEach((key) => {
662
- mesg[key].fieldValue = UtilsInternal.sanitizeValues(mesg[key].fieldValue);
663
- mesg[key].rawFieldValue = UtilsInternal.sanitizeValues(mesg[key].rawFieldValue);
664
-
665
- message[key] = mesg[key];
666
- });
667
- }
668
-
669
- #transformValues(message, messageDefinition) {
670
- const fields = messageDefinition?.fields ?? {};
671
-
672
- for (const name in message) {
673
-
674
- const { rawFieldValue, fieldDefinitionNumber, isExpandedField, isSubField } = message[name];
675
-
676
- let field = fields[fieldDefinitionNumber];
677
- field = isSubField ? this.#lookupSubfield(field, name) : field;
678
-
679
- if (!isExpandedField) {
680
- const fieldValue = this.#transformValue(messageDefinition, field, rawFieldValue);
681
- message[name].fieldValue = fieldValue;
682
- }
683
- }
684
- }
685
-
686
- #transformValue(messageDefinition, field, rawFieldValue) {
687
- let fieldValue = rawFieldValue;
688
-
689
- if (field == null) {
690
- fieldValue = rawFieldValue;
691
- }
692
- else if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
693
- fieldValue = this.#applyScaleAndOffset(messageDefinition, field, rawFieldValue);
694
- }
695
- else if (field.type === "string") {
696
- fieldValue = rawFieldValue;
697
- }
698
- else if (field.type === "dateTime" && this.#optConvertDateTimesToDates) {
699
- fieldValue = Utils.convertDateTimeToDate(rawFieldValue);
700
- }
701
- else if (this.#optConvertTypesToStrings) {
702
- fieldValue = this.#convertTypeToString(messageDefinition, field, rawFieldValue);
703
- }
704
- else {
705
- fieldValue = rawFieldValue;
706
- }
707
-
708
- return fieldValue;
709
- }
710
-
711
- #applyScaleAndOffset(messageDefinition, field, rawFieldValue) {
712
- if (!this.#optApplyScaleAndOffset) {
713
- return rawFieldValue;
714
- }
715
-
716
- if (FIT.NumericFieldTypes.includes(field?.type ?? -1) === false) {
717
- return rawFieldValue;
718
- }
719
-
720
- if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
721
- return rawFieldValue;
722
- }
723
-
724
- if (rawFieldValue == null) {
725
- return rawFieldValue;
726
- }
727
-
728
- if (Array.isArray(field?.scale ?? 1) && field.scale.length > 1) {
729
- return rawFieldValue;
730
- }
731
-
732
- const scale = Array.isArray(field?.scale ?? 1) ? field?.scale[0] : field?.scale ?? 1;
733
- const offset = Array.isArray(field?.offset ?? 1) ? field?.offset[0] : field?.offset ?? 0;
734
-
735
- try {
736
- if (Array.isArray(rawFieldValue)) {
737
- return rawFieldValue.map((value) => {
738
- return value == null ? value : (value / scale) - offset;
739
- });
740
- }
741
-
742
- return (rawFieldValue / scale) - offset;
743
- }
744
- catch {
745
- return rawFieldValue;
746
- }
747
- }
748
-
749
- #setAccumulatedField(messageDefinition, message, field, rawFieldValue) {
750
- const rawFieldValues = Array.isArray(rawFieldValue) ? rawFieldValue : [rawFieldValue];
751
-
752
- rawFieldValues.forEach((value) => {
753
- Object.values(message).forEach((containingField) => {
754
- let components = messageDefinition.fields[containingField.fieldDefinitionNumber].components ?? []
755
-
756
- components.forEach((componentFieldNum, i) => {
757
- const targetField = messageDefinition.fields[componentFieldNum];
758
-
759
- if(targetField?.num == field.num && targetField?.isAccumulated) {
760
- value = (((value / field.scale) - field.offset) + containingField.offset[i]) * containingField.scale[i];
761
- }
762
- });
763
- });
764
-
765
- this.#accumulator.createAccumulatedField(messageDefinition.num, field.num, value);
766
- });
767
- }
768
-
769
- #convertTypeToString(messageDefinition, field, rawFieldValue) {
770
- if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
771
- return rawFieldValue;
772
- }
773
-
774
- if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
775
- return rawFieldValue;
776
- }
777
-
778
- try {
779
- const type = Profile.types[field?.type ?? -1];
780
-
781
- if (Array.isArray(rawFieldValue)) {
782
- return rawFieldValue.map(value => {
783
- return value == null ? value : type?.[value] ?? value
784
- });
785
- }
786
-
787
- return type?.[rawFieldValue] ?? rawFieldValue;
788
- }
789
- catch {
790
- return rawFieldValue;
791
- }
792
- }
793
-
794
- #lookupSubfield(field, name) {
795
- const subField = field.subFields.find(subField => subField.name === name);
796
- return subField != null ? subField : {};
797
- }
798
-
799
- static #readFileHeader(stream, { resetPosition = false, decodeMode = DecodeMode.NORMAL }) {
800
- const position = stream.position;
801
-
802
- if(decodeMode !== DecodeMode.NORMAL) {
803
- if(decodeMode === DecodeMode.SKIP_HEADER) {
804
- stream.seek(HEADER_WITH_CRC_SIZE);
805
- }
806
-
807
- const headerSize = decodeMode === DecodeMode.SKIP_HEADER ? HEADER_WITH_CRC_SIZE : 0;
808
-
809
- return {
810
- headerSize,
811
- dataSize: stream.length - headerSize - CRC_SIZE,
812
- };
813
- }
814
-
815
- const fileHeader = {
816
- headerSize: stream.readByte(),
817
- protocolVersion: stream.readByte(),
818
- profileVersion: stream.readUInt16(),
819
- dataSize: stream.readUInt32(),
820
- dataType: stream.readString(4),
821
- headerCRC: 0
822
- };
823
-
824
- if (fileHeader.headerSize === 14) {
825
- fileHeader.headerCRC = stream.readUInt16()
826
- }
827
-
828
- if (resetPosition) {
829
- stream.seek(position);
830
- }
831
-
832
- return fileHeader;
833
- }
834
-
835
- #throwError(error = "") {
836
- throw Error(`FIT Runtime Error at byte ${this.#stream.position} ${error}`.trimEnd());
837
- }
838
- }
839
-
1
+ /////////////////////////////////////////////////////////////////////////////////////////////
2
+ // Copyright 2025 Garmin International, Inc.
3
+ // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
4
+ // may not use this file except in compliance with the Flexible and Interoperable Data
5
+ // Transfer (FIT) Protocol License.
6
+ /////////////////////////////////////////////////////////////////////////////////////////////
7
+ // ****WARNING**** This file is auto-generated! Do NOT edit this file.
8
+ // Profile Version = 21.169.0Release
9
+ // Tag = production/release/21.169.0-0-g7105132
10
+ /////////////////////////////////////////////////////////////////////////////////////////////
11
+
12
+
13
+ import Accumulator from "../src/accumulator.js";
14
+ import BitStream from "../src/bit-stream.js";
15
+ import CrcCalculator from "./crc-calculator.js";
16
+ import FIT from "./fit.js";
17
+ import HrMesgUtils from "./utils-hr-mesg.js";
18
+ import MemoGlobUtils from "./utils-memo-glob.js";
19
+ import Profile from "./profile.js";
20
+ import Stream from "./stream.js";
21
+ import Utils from "./utils.js";
22
+ import UtilsInternal from "./utils-internal.js";
23
+
24
+ const COMPRESSED_HEADER_MASK = 0x80;
25
+ const MESG_DEFINITION_MASK = 0x40;
26
+ const DEV_DATA_MASK = 0x20;
27
+ const MESG_HEADER_MASK = 0x00;
28
+ const LOCAL_MESG_NUM_MASK = 0x0F;
29
+
30
+ const HEADER_WITH_CRC_SIZE = 14;
31
+ const HEADER_WITHOUT_CRC_SIZE = 12;
32
+ const CRC_SIZE = 2;
33
+
34
+ const DecodeMode = Object.freeze({
35
+ NORMAL: "normal",
36
+ SKIP_HEADER: "skipHeader",
37
+ DATA_ONLY: "dataOnly"
38
+ });
39
+
40
+ class Decoder {
41
+ #localMessageDefinitions = [];
42
+ #developerDataDefinitions = {};
43
+ #stream = null;
44
+ #accumulator = new Accumulator();
45
+ #messages = {};
46
+ #fieldsWithSubFields = [];
47
+ #fieldsToExpand = [];
48
+
49
+ #decodeMode = DecodeMode.NORMAL;
50
+
51
+ #mesgListener = null;
52
+ #mesgDefinitionListener = null;
53
+ #fieldDescriptionListener = null;
54
+ #optExpandSubFields = true;
55
+ #optExpandComponents = true;
56
+ #optApplyScaleAndOffset = true;
57
+ #optConvertTypesToStrings = true;
58
+ #optConvertDateTimesToDates = true;
59
+ #optIncludeUnknownData = false;
60
+ #optMergeHeartRates = true;
61
+ #optDecodeMemoGlobs = false;
62
+
63
+ /**
64
+ * Creates a FIT File Decoder
65
+ * @constructor
66
+ * @param {Stream} stream - representing the FIT file to decode
67
+ */
68
+ constructor(stream) {
69
+ if (stream == null) {
70
+ throw Error("FIT Runtime Error stream parameter is null or undefined");
71
+ }
72
+
73
+ this.#stream = stream;
74
+ }
75
+
76
+ /**
77
+ * Inspects the file header to determine if the input stream is a FIT file
78
+ * @param {Stream} stream
79
+ * @returns {Boolean} True if the stream is a FIT file
80
+ * @static
81
+ */
82
+ static isFIT(stream) {
83
+ try {
84
+ const fileHeaderSize = stream.peekByte();
85
+ if ([HEADER_WITH_CRC_SIZE, HEADER_WITHOUT_CRC_SIZE].includes(fileHeaderSize) != true) {
86
+ return false;
87
+ }
88
+
89
+ if (stream.length < fileHeaderSize + CRC_SIZE) {
90
+ return false;
91
+ }
92
+
93
+ const fileHeader = Decoder.#readFileHeader(stream, { resetPosition: true, });
94
+ if (fileHeader.dataType !== ".FIT") {
95
+ return false;
96
+ }
97
+ }
98
+ catch (error) {
99
+ return false;
100
+ }
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Inspects the file header to determine if the input stream is a FIT file
107
+ * @returns {Boolean} True if the stream is a FIT file
108
+ */
109
+ isFIT() {
110
+ return Decoder.isFIT(this.#stream);
111
+ }
112
+
113
+ /**
114
+ * Checks that the input stream is a FIT file and verifies both the header and file CRC values
115
+ * @returns {Boolean} True if the stream passes the isFit() and CRC checks
116
+ */
117
+ checkIntegrity() {
118
+ try {
119
+ if (!this.isFIT()) {
120
+ return false;
121
+ }
122
+
123
+ const fileHeader = Decoder.#readFileHeader(this.#stream, { resetPosition: true, });
124
+
125
+ if (this.#stream.length < fileHeader.headerSize + fileHeader.dataSize + CRC_SIZE) {
126
+ return false;
127
+ }
128
+
129
+ const buf = new Uint8Array(this.#stream.slice(0, this.#stream.length))
130
+
131
+ if (fileHeader.headerSize === HEADER_WITH_CRC_SIZE && fileHeader.headerCRC !== 0x0000
132
+ && fileHeader.headerCRC != CrcCalculator.calculateCRC(buf, 0, 12)) {
133
+ return false;
134
+ }
135
+
136
+ const fileCRC = (buf[fileHeader.headerSize + fileHeader.dataSize + 1] << 8) + buf[fileHeader.headerSize + fileHeader.dataSize]
137
+ if (fileCRC != CrcCalculator.calculateCRC(buf, 0, fileHeader.headerSize + fileHeader.dataSize)) {
138
+ return false;
139
+ }
140
+ }
141
+ catch (error) {
142
+ return false;
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Message Listener Callback
150
+ *
151
+ * @callback Decoder~mesgListener
152
+ * @param {Number} mesgNum - Profile.MesgNum
153
+ * @param {Object} message - The message
154
+ */
155
+
156
+ /**
157
+ * Message Definition Listener Callback
158
+ *
159
+ * @callback Decoder~mesgDefinitionListener
160
+ * @param {Object} messageDefinition - The message Definition
161
+ */
162
+
163
+ /**
164
+ * Developer Field Description Listener Callback
165
+ *
166
+ * @callback Decoder~fieldDescriptionListener
167
+ * @param {Number} key - The key associated with this pairing of of Developer Data Id and Field Description Mesgs
168
+ * @param {Object} developerDataIdMesg - The Developer Data Id Mesg associated with this pairing
169
+ * @param {Object} fieldDescriptionMesg - The Field Description Mesg associated with this pairing
170
+ */
171
+
172
+ /**
173
+ * Read the messages from the stream.
174
+ * @param {Object=} [options] - Read options (optional)
175
+ * @param {Decoder~mesgListener} [options.mesgListener=null] - (optional, default null) mesgListener(mesgNum, message)
176
+ * @param {Decoder~mesgDefinitionListener} [options.mesgDefinitionListener=null] - (optional, default null) mesgDefinitionListener(mesgDefinition)
177
+ * @param {Decoder~fieldDescriptionListener} [options.fieldDescriptionListener=null] - (optional, default null) fieldDescriptionListener(key, developerDataIdMesg, fieldDescriptionMesg)
178
+ * @param {Boolean} [options.expandSubFields=true] - (optional, default true)
179
+ * @param {Boolean} [options.expandComponents=true] - (optional, default true)
180
+ * @param {Boolean} [options.applyScaleAndOffset=true] - (optional, default true)
181
+ * @param {Boolean} [options.convertTypesToStrings=true] - (optional, default true)
182
+ * @param {boolean} [options.convertDateTimesToDates=true] - (optional, default true)
183
+ * @param {Boolean} [options.includeUnknownData=false] - (optional, default false)
184
+ * @param {boolean} [options.mergeHeartRates=true] - (optional, default true)
185
+ * @param {boolean} [options.decodeMemoGlobs=true] - (optional, default false)
186
+ * @param {boolean} [options.skipHeader=false] - (optional, default false)
187
+ * @param {boolean} [options.dataOnly=false] - (optional, default false)
188
+ * @return {Object} result - {messages:Array, errors:Array}
189
+ */
190
+ read({
191
+ mesgListener = null,
192
+ mesgDefinitionListener = null,
193
+ fieldDescriptionListener = null,
194
+ expandSubFields = true,
195
+ expandComponents = true,
196
+ applyScaleAndOffset = true,
197
+ convertTypesToStrings = true,
198
+ convertDateTimesToDates = true,
199
+ includeUnknownData = false,
200
+ mergeHeartRates = true,
201
+ decodeMemoGlobs = false,
202
+ skipHeader = false,
203
+ dataOnly = false,} = {}) {
204
+
205
+ this.#mesgListener = mesgListener;
206
+ this.#mesgDefinitionListener = mesgDefinitionListener;
207
+ this.#fieldDescriptionListener = fieldDescriptionListener;
208
+ this.#optExpandSubFields = expandSubFields
209
+ this.#optExpandComponents = expandComponents;
210
+ this.#optApplyScaleAndOffset = applyScaleAndOffset;
211
+ this.#optConvertTypesToStrings = convertTypesToStrings;
212
+ this.#optConvertDateTimesToDates = convertDateTimesToDates;
213
+ this.#optIncludeUnknownData = includeUnknownData;
214
+ this.#optMergeHeartRates = mergeHeartRates;
215
+ this.#optDecodeMemoGlobs = decodeMemoGlobs;
216
+
217
+ this.#localMessageDefinitions = [];
218
+ this.#developerDataDefinitions = {};
219
+ this.#messages = {};
220
+
221
+ const errors = [];
222
+
223
+ try {
224
+ if (this.#optMergeHeartRates && (!this.#optApplyScaleAndOffset || !this.#optExpandComponents)) {
225
+ this.#throwError("mergeHeartRates requires applyScaleAndOffset and expandComponents to be enabled");
226
+ }
227
+
228
+ if (dataOnly && skipHeader) {
229
+ this.#throwError("dataOnly and skipHeader cannot both be enabled")
230
+ }
231
+
232
+ this.#decodeMode = skipHeader ? DecodeMode.SKIP_HEADER : dataOnly ? DecodeMode.DATA_ONLY : DecodeMode.NORMAL;
233
+
234
+ while (this.#stream.position < this.#stream.length) {
235
+ this.#decodeNextFile();
236
+ }
237
+
238
+ if (this.#optMergeHeartRates) {
239
+ HrMesgUtils.mergeHeartRates(this.#messages.hrMesgs, this.#messages.recordMesgs);
240
+ }
241
+
242
+ if (this.#optDecodeMemoGlobs) {
243
+ MemoGlobUtils.decodeMemoGlobs(this.#messages);
244
+ }
245
+ }
246
+ catch (error) {
247
+ errors.push(error);
248
+ }
249
+ finally {
250
+ return { messages: this.#messages, errors: errors };
251
+ }
252
+ }
253
+
254
+ #decodeNextFile() {
255
+ const position = this.#stream.position;
256
+
257
+ if (this.#decodeMode === DecodeMode.NORMAL && !this.isFIT()) {
258
+ this.#throwError("input is not a FIT file");
259
+ }
260
+
261
+ this.#stream.crcCalculator = new CrcCalculator();
262
+
263
+ const { headerSize, dataSize } = Decoder.#readFileHeader(this.#stream, { decodeMode: this.#decodeMode });
264
+
265
+ // Read data messages and definitions
266
+ while (this.#stream.position < (position + headerSize + dataSize)) {
267
+ this.#decodeNextRecord();
268
+ }
269
+
270
+ // Check the CRC
271
+ const calculatedCrc = this.#stream.crcCalculator.crc;
272
+ const crc = this.#stream.readUInt16();
273
+ if (this.#decodeMode === DecodeMode.NORMAL && crc !== calculatedCrc) {
274
+ this.#throwError("CRC error");
275
+ }
276
+ }
277
+
278
+ #decodeNextRecord() {
279
+ const recordHeader = this.#stream.peekByte();
280
+
281
+ if ((recordHeader & COMPRESSED_HEADER_MASK) === COMPRESSED_HEADER_MASK) {
282
+ return this.#decodeCompressedTimestampDataMessage();
283
+ }
284
+
285
+ if ((recordHeader & MESG_DEFINITION_MASK) === MESG_HEADER_MASK) {
286
+ return this.#decodeMessage();
287
+ }
288
+
289
+ if ((recordHeader & MESG_DEFINITION_MASK) === MESG_DEFINITION_MASK) {
290
+ return this.#decodeMessageDefinition();
291
+ }
292
+ }
293
+
294
+ #decodeMessageDefinition() {
295
+ const recordHeader = this.#stream.readByte();
296
+
297
+ const messageDefinition = {};
298
+ messageDefinition["recordHeader"] = recordHeader;
299
+ messageDefinition["localMesgNum"] = recordHeader & LOCAL_MESG_NUM_MASK;
300
+ messageDefinition["reserved"] = this.#stream.readByte();
301
+
302
+ messageDefinition["architecture"] = this.#stream.readByte();
303
+ messageDefinition["endianness"] = messageDefinition.architecture === 0 ? Stream.LITTLE_ENDIAN : Stream.BIG_ENDIAN;
304
+
305
+ messageDefinition["globalMessageNumber"] = this.#stream.readUInt16({ endianness: messageDefinition["endianness"] });
306
+ messageDefinition["numFields"] = this.#stream.readByte();
307
+ messageDefinition["fieldDefinitions"] = [];
308
+ messageDefinition["developerFieldDefinitions"] = [];
309
+ messageDefinition["messageSize"] = 0;
310
+ messageDefinition["developerDataSize"] = 0;
311
+
312
+ for (let i = 0; i < messageDefinition.numFields; i++) {
313
+ const fieldDefinition = {
314
+ fieldDefinitionNumber: this.#stream.readByte(),
315
+ size: this.#stream.readByte(),
316
+ baseType: this.#stream.readByte()
317
+ };
318
+
319
+ if (!(fieldDefinition.baseType in FIT.BaseTypeDefinitions)) {
320
+ this.#throwError();
321
+ }
322
+
323
+ fieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].invalid;
324
+ fieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].size;
325
+
326
+ messageDefinition.fieldDefinitions.push(fieldDefinition);
327
+ messageDefinition.messageSize += fieldDefinition.size;
328
+ }
329
+
330
+ if ((recordHeader & DEV_DATA_MASK) === DEV_DATA_MASK) {
331
+ const numDevFields = this.#stream.readByte();
332
+
333
+ for (let i = 0; i < numDevFields; i++) {
334
+ const developerFieldDefinition = {
335
+ fieldDefinitionNumber: this.#stream.readByte(),
336
+ size: this.#stream.readByte(),
337
+ developerDataIndex: this.#stream.readByte()
338
+ };
339
+
340
+ messageDefinition.developerFieldDefinitions.push(developerFieldDefinition);
341
+ messageDefinition.developerDataSize += developerFieldDefinition.size;
342
+ }
343
+ }
344
+
345
+ this.#mesgDefinitionListener?.({...messageDefinition});
346
+
347
+ let messageProfile = Profile.messages[messageDefinition.globalMessageNumber];
348
+
349
+ if (messageProfile == null && this.#optIncludeUnknownData) {
350
+ messageProfile = {
351
+ name: messageDefinition["globalMessageNumber"].toString(),
352
+ messagesKey: messageDefinition["globalMessageNumber"].toString(),
353
+ num: messageDefinition["globalMessageNumber"],
354
+ fields: {}
355
+ };
356
+ }
357
+
358
+ this.#localMessageDefinitions[messageDefinition.localMesgNum] = { ...messageDefinition, ...messageProfile };
359
+
360
+ if (messageProfile && !this.#messages.hasOwnProperty(messageProfile.messagesKey)) {
361
+ this.#messages[messageProfile.messagesKey] = [];
362
+ }
363
+ }
364
+
365
+ #decodeMessage() {
366
+ const recordHeader = this.#stream.readByte();
367
+
368
+ const localMesgNum = recordHeader & LOCAL_MESG_NUM_MASK;
369
+ const messageDefinition = this.#localMessageDefinitions[localMesgNum];
370
+
371
+ if (messageDefinition == null) {
372
+ this.#throwError();
373
+ }
374
+
375
+ const fields = messageDefinition.fields ?? {};
376
+ const mesgNum = messageDefinition.num;
377
+ const message = {};
378
+ this.#fieldsWithSubFields = [];
379
+ this.#fieldsToExpand = [];
380
+
381
+ messageDefinition.fieldDefinitions.forEach(fieldDefinition => {
382
+ const field = fields[fieldDefinition.fieldDefinitionNumber];
383
+ const { fieldName, rawFieldValue } = this.#readFieldValue(messageDefinition, fieldDefinition, field);
384
+
385
+ if (fieldName != null && (field != null || this.#optIncludeUnknownData)) {
386
+ message[fieldName] = { rawFieldValue, fieldDefinitionNumber: fieldDefinition.fieldDefinitionNumber };
387
+
388
+ if (field?.subFields?.length > 0) {
389
+ this.#fieldsWithSubFields.push(fieldName);
390
+ }
391
+
392
+ if (field?.hasComponents) {
393
+ this.#fieldsToExpand.push(fieldName);
394
+ }
395
+
396
+ if (field?.isAccumulated) {
397
+ this.#setAccumulatedField(messageDefinition, message, field, rawFieldValue);
398
+ }
399
+ }
400
+ });
401
+
402
+ const developerFields = {};
403
+
404
+ messageDefinition.developerFieldDefinitions.forEach(developerFieldDefinition => {
405
+ const field = this.#lookupDeveloperDataField(developerFieldDefinition)
406
+ if (field == null) {
407
+ // If there is not a field definition, then read past the field data.
408
+ this.#stream.readBytes(developerFieldDefinition.size);
409
+ return;
410
+ }
411
+
412
+ developerFieldDefinition["baseType"] = field.fitBaseTypeId;
413
+ developerFieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].invalid;
414
+ developerFieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].size;
415
+
416
+ const { rawFieldValue: fieldValue } = this.#readFieldValue(messageDefinition, developerFieldDefinition, field);
417
+
418
+ if (fieldValue != null) {
419
+ developerFields[field.key] = fieldValue;
420
+ }
421
+ });
422
+
423
+ if (mesgNum === Profile.MesgNum.DEVELOPER_DATA_ID) {
424
+ this.#addDeveloperDataIdToProfile(message);
425
+ }
426
+ else if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION) {
427
+ const key = Object.keys(this.#developerDataDefinitions)
428
+ .reduce((count, key) => count + this.#developerDataDefinitions[key].fields.length, 0);
429
+ message["key"] = { fieldValue: key, rawFieldValue: key };
430
+
431
+ this.#addFieldDescriptionToProfile(message);
432
+ }
433
+ else {
434
+ this.#expandSubFields(mesgNum, message);
435
+ this.#expandComponents(mesgNum, message, fields);
436
+ }
437
+
438
+ this.#transformValues(message, messageDefinition);
439
+
440
+ if (messageDefinition.name != null) {
441
+ Object.keys(message).forEach((key) => {
442
+ message[key] = message[key].fieldValue;
443
+ });
444
+
445
+ if (Object.keys(developerFields).length > 0) {
446
+ message.developerFields = developerFields;
447
+ }
448
+
449
+ this.#messages[messageDefinition.messagesKey].push(message);
450
+ this.#mesgListener?.(messageDefinition.globalMessageNumber, message);
451
+
452
+ if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION && this.#fieldDescriptionListener != null) {
453
+ const developerDataIdMesg = this.#messages.developerDataIdMesgs?.find((developerDataIdMesg) => {
454
+ return developerDataIdMesg.developerDataIndex === message.developerDataIndex;
455
+ }) ?? {};
456
+
457
+ this.#fieldDescriptionListener(message.key, {...developerDataIdMesg}, {...message});
458
+ }
459
+ }
460
+ }
461
+
462
+ #decodeCompressedTimestampDataMessage() {
463
+ this.#throwError("compressed timestamp messages are not currently supported");
464
+ }
465
+
466
+ #readFieldValue(messageDefinition, fieldDefinition, field) {
467
+ const rawFieldValue = this.#readRawFieldValue(messageDefinition, fieldDefinition, field);
468
+
469
+ if (rawFieldValue == null) {
470
+ return {};
471
+ }
472
+
473
+ return {
474
+ fieldName: (field?.name ?? ~~fieldDefinition.fieldDefinitionNumber),
475
+ rawFieldValue
476
+ };
477
+ }
478
+
479
+ #readRawFieldValue(messageDefinition, fieldDefinition, field) {
480
+ const rawFieldValue = this.#stream.readValue(
481
+ fieldDefinition.baseType,
482
+ fieldDefinition.size,
483
+ {
484
+ endianness: messageDefinition["endianness"],
485
+ convertInvalidToNull: !field?.hasComponents ?? false
486
+ }
487
+ );
488
+ return rawFieldValue;
489
+ }
490
+
491
+ #addDeveloperDataIdToProfile(message) {
492
+ if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
493
+ return;
494
+ }
495
+
496
+ this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] = {
497
+ developerDataIndex: message.developerDataIndex?.rawFieldValue,
498
+ developerId: message.developerId?.rawFieldValue ?? null,
499
+ applicationId: message.applicationId?.rawFieldValue ?? null,
500
+ manufacturerId: message.manufacturerId?.rawFieldValue ?? null,
501
+ applicationVersion: message.applicationVersion?.rawFieldValue ?? null,
502
+ fields: []
503
+ };
504
+ }
505
+
506
+ #addFieldDescriptionToProfile(message) {
507
+ if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
508
+ return;
509
+ }
510
+
511
+ if (this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] == null) {
512
+ return;
513
+ }
514
+
515
+ this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue].fields.push({
516
+ developerDataIndex: message.developerDataIndex?.rawFieldValue,
517
+ fieldDefinitionNumber: message.fieldDefinitionNumber?.rawFieldValue,
518
+ fitBaseTypeId: message.fitBaseTypeId?.rawFieldValue ?? null,
519
+ fieldName: message.fieldName?.rawFieldValue ?? null,
520
+ array: message.array?.rawFieldValue ?? null,
521
+ components: message.components?.rawFieldValue ?? null,
522
+ scale: message.scale?.rawFieldValue ?? null,
523
+ offset: message.offset?.rawFieldValue ?? null,
524
+ units: message.units?.rawFieldValue ?? null,
525
+ bits: message.bits?.rawFieldValue ?? null,
526
+ accumulate: message.accumulate?.rawFieldValue ?? null,
527
+ refFieldName: message.refFieldName?.rawFieldValue ?? null,
528
+ refFieldValue: message.refFieldValue?.rawFieldValue ?? null,
529
+ fitBaseUnitId: message.fitBaseUnitId?.rawFieldValue ?? null,
530
+ nativeMesgNum: message.nativeMesgNum?.rawFieldValue ?? null,
531
+ nativeFieldNum: message.nativeFieldNum?.rawFieldValue ?? null,
532
+ key: message.key.rawFieldValue
533
+ });
534
+ }
535
+
536
+ #lookupDeveloperDataField(developerFieldDefinition) {
537
+ try {
538
+ return this.#developerDataDefinitions[developerFieldDefinition.developerDataIndex]
539
+ ?.fields
540
+ ?.find(def => def.fieldDefinitionNumber == developerFieldDefinition.fieldDefinitionNumber)
541
+ ?? null;
542
+ }
543
+ catch {
544
+ return null;
545
+ }
546
+ }
547
+
548
+ #expandSubFields(mesgNum, message) {
549
+ if (!this.#optExpandSubFields || this.#fieldsWithSubFields.length == 0) {
550
+ return;
551
+ }
552
+
553
+ this.#fieldsWithSubFields.forEach((name) => {
554
+ const field = Profile.messages[mesgNum].fields[message[name].fieldDefinitionNumber];
555
+ this.#expandSubField(message, field);
556
+ });
557
+ }
558
+
559
+ #expandSubField(message, field) {
560
+ for (let i = 0; i < field.subFields.length; i++) {
561
+ const subField = field.subFields[i];
562
+ for (let j = 0; j < subField.map.length; j++) {
563
+ const map = subField.map[j];
564
+ const referenceField = message[map.name];
565
+ if (referenceField == null) {
566
+ continue;
567
+ }
568
+ if (referenceField.rawFieldValue === map.value) {
569
+ message[subField.name] = JSON.parse(JSON.stringify(message[field.name]));
570
+ message[subField.name].isSubField = true;
571
+
572
+ if (subField.hasComponents) {
573
+ this.#fieldsToExpand.push(subField.name);
574
+ }
575
+ break;
576
+ }
577
+ }
578
+ }
579
+ }
580
+
581
+ #expandComponents(mesgNum, message, fields) {
582
+ // TODO - What do do when the target field is not in the Profile?
583
+ // TODO - This can happen in theory, but can it happen in practice?
584
+
585
+ if (!this.#optExpandComponents || this.#fieldsToExpand.length == 0) {
586
+ return;
587
+ }
588
+
589
+ const mesg = {};
590
+
591
+ while (this.#fieldsToExpand.length > 0) {
592
+ const name = this.#fieldsToExpand.shift();
593
+
594
+ const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name] ?? mesg[name];
595
+ let field = Profile.messages[mesgNum].fields[fieldDefinitionNumber];
596
+ field = isSubField ? this.#lookupSubfield(field, name) : field;
597
+ const baseType = FIT.FieldTypeToBaseType[field.type];
598
+
599
+ if (field.hasComponents === false || baseType == null) {
600
+ continue;
601
+ }
602
+
603
+ if (UtilsInternal.onlyInvalidValues(rawFieldValue, FIT.BaseTypeDefinitions[baseType].invalid)) {
604
+ continue;
605
+ }
606
+
607
+ const bitStream = new BitStream(rawFieldValue, baseType);
608
+
609
+ for (let j = 0; j < field.components.length; j++) {
610
+ if (bitStream.bitsAvailable < field.bits[j]) {
611
+ break;
612
+ }
613
+
614
+ const targetField = fields[field.components[j]];
615
+ if (mesg[targetField.name] == null) {
616
+ const baseType = FIT.FieldTypeToBaseType[targetField.type];
617
+ const invalidValue = baseType != null ? FIT.BaseTypeDefinitions[baseType].invalid : 0xFF;
618
+
619
+ mesg[targetField.name] = {
620
+ fieldValue: [],
621
+ rawFieldValue: [],
622
+ fieldDefinitionNumber: targetField.num,
623
+ isExpandedField: true,
624
+ invalidValue,
625
+ };
626
+ }
627
+
628
+ let value = bitStream.readBits(field.bits[j]);
629
+
630
+ if (targetField.isAccumulated) {
631
+ value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]);
632
+ }
633
+
634
+ // Undo component scale and offset before applying the destination field's scale and offset
635
+ value = (value / field.scale[j] - field.offset[j]);
636
+
637
+ const rawValue = (value + targetField.offset) * targetField.scale;
638
+ mesg[targetField.name].rawFieldValue.push(rawValue);
639
+
640
+ if (rawValue === mesg[targetField.name].invalidValue) {
641
+ mesg[targetField.name].fieldValue.push(null);
642
+ }
643
+ else {
644
+ if (this.#optConvertTypesToStrings) {
645
+ value = this.#convertTypeToString(mesg, targetField, value);
646
+ }
647
+
648
+ mesg[targetField.name].fieldValue.push(value);
649
+ }
650
+
651
+ if (targetField.hasComponents) {
652
+ this.#fieldsToExpand.push(targetField.name);
653
+ }
654
+
655
+ if (!bitStream.hasBitsAvailable) {
656
+ break;
657
+ }
658
+ }
659
+ }
660
+
661
+ Object.keys(mesg).forEach((key) => {
662
+ mesg[key].fieldValue = UtilsInternal.sanitizeValues(mesg[key].fieldValue);
663
+ mesg[key].rawFieldValue = UtilsInternal.sanitizeValues(mesg[key].rawFieldValue);
664
+
665
+ message[key] = mesg[key];
666
+ });
667
+ }
668
+
669
+ #transformValues(message, messageDefinition) {
670
+ const fields = messageDefinition?.fields ?? {};
671
+
672
+ for (const name in message) {
673
+
674
+ const { rawFieldValue, fieldDefinitionNumber, isExpandedField, isSubField } = message[name];
675
+
676
+ let field = fields[fieldDefinitionNumber];
677
+ field = isSubField ? this.#lookupSubfield(field, name) : field;
678
+
679
+ if (!isExpandedField) {
680
+ const fieldValue = this.#transformValue(messageDefinition, field, rawFieldValue);
681
+ message[name].fieldValue = fieldValue;
682
+ }
683
+ }
684
+ }
685
+
686
+ #transformValue(messageDefinition, field, rawFieldValue) {
687
+ let fieldValue = rawFieldValue;
688
+
689
+ if (field == null) {
690
+ fieldValue = rawFieldValue;
691
+ }
692
+ else if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
693
+ fieldValue = this.#applyScaleAndOffset(messageDefinition, field, rawFieldValue);
694
+ }
695
+ else if (field.type === "string") {
696
+ fieldValue = rawFieldValue;
697
+ }
698
+ else if (field.type === "dateTime" && this.#optConvertDateTimesToDates) {
699
+ fieldValue = Utils.convertDateTimeToDate(rawFieldValue);
700
+ }
701
+ else if (this.#optConvertTypesToStrings) {
702
+ fieldValue = this.#convertTypeToString(messageDefinition, field, rawFieldValue);
703
+ }
704
+ else {
705
+ fieldValue = rawFieldValue;
706
+ }
707
+
708
+ return fieldValue;
709
+ }
710
+
711
+ #applyScaleAndOffset(messageDefinition, field, rawFieldValue) {
712
+ if (!this.#optApplyScaleAndOffset) {
713
+ return rawFieldValue;
714
+ }
715
+
716
+ if (FIT.NumericFieldTypes.includes(field?.type ?? -1) === false) {
717
+ return rawFieldValue;
718
+ }
719
+
720
+ if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
721
+ return rawFieldValue;
722
+ }
723
+
724
+ if (rawFieldValue == null) {
725
+ return rawFieldValue;
726
+ }
727
+
728
+ if (Array.isArray(field?.scale ?? 1) && field.scale.length > 1) {
729
+ return rawFieldValue;
730
+ }
731
+
732
+ const scale = Array.isArray(field?.scale ?? 1) ? field?.scale[0] : field?.scale ?? 1;
733
+ const offset = Array.isArray(field?.offset ?? 1) ? field?.offset[0] : field?.offset ?? 0;
734
+
735
+ try {
736
+ if (Array.isArray(rawFieldValue)) {
737
+ return rawFieldValue.map((value) => {
738
+ return value == null ? value : (value / scale) - offset;
739
+ });
740
+ }
741
+
742
+ return (rawFieldValue / scale) - offset;
743
+ }
744
+ catch {
745
+ return rawFieldValue;
746
+ }
747
+ }
748
+
749
+ #setAccumulatedField(messageDefinition, message, field, rawFieldValue) {
750
+ const rawFieldValues = Array.isArray(rawFieldValue) ? rawFieldValue : [rawFieldValue];
751
+
752
+ rawFieldValues.forEach((value) => {
753
+ Object.values(message).forEach((containingField) => {
754
+ let components = messageDefinition.fields[containingField.fieldDefinitionNumber].components ?? []
755
+
756
+ components.forEach((componentFieldNum, i) => {
757
+ const targetField = messageDefinition.fields[componentFieldNum];
758
+
759
+ if(targetField?.num == field.num && targetField?.isAccumulated) {
760
+ value = (((value / field.scale) - field.offset) + containingField.offset[i]) * containingField.scale[i];
761
+ }
762
+ });
763
+ });
764
+
765
+ this.#accumulator.createAccumulatedField(messageDefinition.num, field.num, value);
766
+ });
767
+ }
768
+
769
+ #convertTypeToString(messageDefinition, field, rawFieldValue) {
770
+ if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
771
+ return rawFieldValue;
772
+ }
773
+
774
+ if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
775
+ return rawFieldValue;
776
+ }
777
+
778
+ try {
779
+ const type = Profile.types[field?.type ?? -1];
780
+
781
+ if (Array.isArray(rawFieldValue)) {
782
+ return rawFieldValue.map(value => {
783
+ return value == null ? value : type?.[value] ?? value
784
+ });
785
+ }
786
+
787
+ return type?.[rawFieldValue] ?? rawFieldValue;
788
+ }
789
+ catch {
790
+ return rawFieldValue;
791
+ }
792
+ }
793
+
794
+ #lookupSubfield(field, name) {
795
+ const subField = field.subFields.find(subField => subField.name === name);
796
+ return subField != null ? subField : {};
797
+ }
798
+
799
+ static #readFileHeader(stream, { resetPosition = false, decodeMode = DecodeMode.NORMAL }) {
800
+ const position = stream.position;
801
+
802
+ if(decodeMode !== DecodeMode.NORMAL) {
803
+ if(decodeMode === DecodeMode.SKIP_HEADER) {
804
+ stream.seek(HEADER_WITH_CRC_SIZE);
805
+ }
806
+
807
+ const headerSize = decodeMode === DecodeMode.SKIP_HEADER ? HEADER_WITH_CRC_SIZE : 0;
808
+
809
+ return {
810
+ headerSize,
811
+ dataSize: stream.length - headerSize - CRC_SIZE,
812
+ };
813
+ }
814
+
815
+ const fileHeader = {
816
+ headerSize: stream.readByte(),
817
+ protocolVersion: stream.readByte(),
818
+ profileVersion: stream.readUInt16(),
819
+ dataSize: stream.readUInt32(),
820
+ dataType: stream.readString(4),
821
+ headerCRC: 0
822
+ };
823
+
824
+ if (fileHeader.headerSize === 14) {
825
+ fileHeader.headerCRC = stream.readUInt16()
826
+ }
827
+
828
+ if (resetPosition) {
829
+ stream.seek(position);
830
+ }
831
+
832
+ return fileHeader;
833
+ }
834
+
835
+ #throwError(error = "") {
836
+ throw Error(`FIT Runtime Error at byte ${this.#stream.position} ${error}`.trimEnd());
837
+ }
838
+ }
839
+
840
840
  export default Decoder;