fit_js 0.94.5 → 0.94.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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;