fit_js 0.94.5 → 0.94.7

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