fit-file-parser 5.1.0 → 5.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Added
6
+
7
+ - Add opt-in, message-filterable `raw_messages` output with exact native and
8
+ developer-field bytes, wire base types, endianness, message occurrence, and
9
+ reconstructed compressed timestamps. Existing decoded output remains
10
+ unchanged when the option is disabled.
11
+ - Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
12
+ activity collections alongside retained messages.
13
+
14
+ ### Fixed
15
+
16
+ - Decode compressed timestamp records without consuming bytes for the omitted
17
+ timestamp field.
18
+
5
19
  ## 5.1.0
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -145,6 +145,8 @@ All options are optional.
145
145
  | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
146
146
  | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
147
147
  | `includeRawDeveloperFields` | `true`, `false`, message-number array | `false` | Adds a lossless, message-associated view of developer-field bytes at `raw_developer_fields`. |
148
+ | `includeRawMessages` | `true`, `false`, message-number array | `false` | Adds selected FIT messages with exact native/developer bytes and wire metadata at `raw_messages`. |
149
+ | `rawMessagesOnly` | `true`, `false` | `false` | Omits decoded activity collections when a consumer needs only the opt-in raw-message representation. |
148
150
 
149
151
  `force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
150
152
  too short, have an invalid header size or signature, or declare data beyond the
@@ -184,6 +186,47 @@ provider-specific semantics. Pass `true` to retain developer fields from every
184
186
  global message, or an array such as `[18]` to bound collection to specific FIT
185
187
  message numbers.
186
188
 
189
+ ### Lossless selected messages
190
+
191
+ Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
192
+ than the parser's intentionally formatted enum names, or needs native and
193
+ developer fields grouped exactly by message occurrence:
194
+
195
+ ```javascript
196
+ const data = await new FitParser({
197
+ force: false,
198
+ includeRawMessages: [18, 26, 72, 206, 207],
199
+ rawMessagesOnly: true,
200
+ }).parseAsync(content)
201
+
202
+ for (const message of data.raw_messages ?? []) {
203
+ console.log({
204
+ globalMessageNumber: message.global_message_number,
205
+ messageIndex: message.message_index,
206
+ littleEndian: message.little_endian,
207
+ fields: message.fields,
208
+ developerFields: message.developer_fields,
209
+ })
210
+ }
211
+ ```
212
+
213
+ Each native field includes its field-definition number, wire base-type byte,
214
+ and a defensive plain-number copy of its exact bytes. Developer fields retain
215
+ their developer-data index, field-definition number, and exact bytes. Empty
216
+ selected messages are retained with empty field arrays. For a compressed
217
+ timestamp record, the timestamp is not present on the wire and therefore is
218
+ not invented in `fields`; its reconstructed native FIT timestamp is exposed as
219
+ `compressed_timestamp` instead.
220
+
221
+ The parser does not apply profile enum formatting, scaling, invalid-sentinel
222
+ filtering, or provider semantics to this opt-in representation. Its normal
223
+ decoded output remains unchanged. Pass `true` to retain every message or a
224
+ global-message-number array to keep memory use bounded. `raw_messages` and
225
+ `raw_developer_fields` are independent opt-ins; enabling `raw_messages` does
226
+ not add the flattened `raw_developer_fields` property. Set `rawMessagesOnly`
227
+ when only this representation is needed; the parser still validates and walks
228
+ the complete FIT data section but does not retain decoded activity collections.
229
+
187
230
  ## Output modes
188
231
 
189
232
  The mode controls where sessions, laps, records, and related activity
package/dist/binary.d.ts CHANGED
@@ -25,12 +25,20 @@ export interface RawDeveloperFieldValue {
25
25
  fieldDefinitionNumber: number;
26
26
  rawValue: number[];
27
27
  }
28
+ export interface RawFieldValue {
29
+ fieldDefinitionNumber: number;
30
+ baseType: number;
31
+ rawValue: number[];
32
+ }
28
33
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
29
34
  export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
30
35
  messageType: MessageName | 'definition' | '';
31
36
  nextIndex: number;
32
37
  message?: any;
33
38
  globalMessageNumber?: number;
39
+ littleEndian?: boolean;
40
+ compressedTimestamp?: number;
41
+ rawFields?: RawFieldValue[];
34
42
  rawDeveloperFields?: RawDeveloperFieldValue[];
35
43
  };
36
44
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
package/dist/binary.js CHANGED
@@ -8,6 +8,37 @@ const GarminTimeOffset = 631065600000;
8
8
  const InvalidFieldData = Symbol('invalid FIT field data');
9
9
  const formatTypeMetadata = new Map();
10
10
  const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
11
+ const fitBaseTypeWidths = new Map([
12
+ [0, 1],
13
+ [1, 1],
14
+ [2, 1],
15
+ [3, 2],
16
+ [4, 2],
17
+ [5, 4],
18
+ [6, 4],
19
+ [7, 1],
20
+ [8, 4],
21
+ [9, 8],
22
+ [10, 1],
23
+ [11, 2],
24
+ [12, 4],
25
+ [13, 1],
26
+ [14, 8],
27
+ [15, 8],
28
+ [16, 8],
29
+ ]);
30
+ function retainsRawMessages(options) {
31
+ return options.includeRawMessages === true
32
+ || Array.isArray(options.includeRawMessages);
33
+ }
34
+ function isValidRawFieldDefinition(size, baseType) {
35
+ if (size <= 0 || (baseType & 0x60) !== 0) {
36
+ return false;
37
+ }
38
+ const typeId = baseType & 0x1F;
39
+ const width = fitBaseTypeWidths.get(typeId);
40
+ return width !== undefined && (typeId === 7 || size % width === 0);
41
+ }
11
42
  function baseTypeSize(type) {
12
43
  switch (type) {
13
44
  case 'enum':
@@ -368,7 +399,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
368
399
  if (!Number.isInteger(baseType) || type === undefined) {
369
400
  developerFieldDef.resolvedFieldDef = undefined;
370
401
  developerFieldDef.resolvedFrom = undefined;
371
- if (options.force) {
402
+ if (options.force || retainsRawMessages(options)) {
372
403
  return undefined;
373
404
  }
374
405
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -401,6 +432,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
401
432
  }
402
433
  export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
403
434
  var _a, _b, _c;
435
+ if (startIndex < 0 || startIndex >= dataEnd) {
436
+ throw new Error('Invalid FIT record bounds');
437
+ }
404
438
  const recordHeader = blob[startIndex];
405
439
  let localMessageType = recordHeader & 15;
406
440
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -411,12 +445,29 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
411
445
  else if ((recordHeader & 64) === 64) {
412
446
  // is definition message
413
447
  // startIndex + 1 is reserved
448
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
449
+ throw new Error('Invalid FIT definition bounds');
450
+ }
414
451
  const hasDeveloperData = (recordHeader & 32) === 32;
415
452
  const lEnd = blob[startIndex + 2] === 0;
416
453
  const numberOfFields = blob[startIndex + 5];
454
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
455
+ if (retainsRawMessages(options)
456
+ && ((recordHeader & 16) !== 0
457
+ || blob[startIndex + 1] !== 0
458
+ || blob[startIndex + 2] > 1
459
+ || nativeDefinitionsEnd > dataEnd
460
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
461
+ throw new Error('Invalid FIT definition');
462
+ }
417
463
  const numberOfDeveloperDataFields = hasDeveloperData
418
464
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
419
465
  : 0;
466
+ const definitionEnd = nativeDefinitionsEnd
467
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
468
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
469
+ throw new Error('Invalid FIT developer definition bounds');
470
+ }
420
471
  const mTypeDef = {
421
472
  littleEndian: lEnd,
422
473
  globalMessageNumber: addEndian(lEnd, [
@@ -429,17 +480,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
429
480
  rawData: [],
430
481
  };
431
482
  const message = getFitMessage(mTypeDef.globalMessageNumber);
483
+ const nativeFieldNumbers = new Set();
432
484
  for (let i = 0; i < numberOfFields; i++) {
433
485
  const fDefIndex = startIndex + 6 + i * 3;
434
486
  const baseType = blob[fDefIndex + 2];
487
+ const fieldNumber = blob[fDefIndex];
488
+ const fieldSize = blob[fDefIndex + 1];
489
+ if (retainsRawMessages(options)
490
+ && (nativeFieldNumbers.has(fieldNumber)
491
+ || !isValidRawFieldDefinition(fieldSize, baseType))) {
492
+ throw new Error('Invalid FIT native field definition');
493
+ }
494
+ nativeFieldNumbers.add(fieldNumber);
435
495
  const wireType = FIT.types.fit_base_type[baseType];
436
496
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
437
497
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
438
498
  const fDef = {
439
499
  type: profileCompatible ? type : wireType,
440
500
  rawType: wireType,
441
- fDefNo: blob[fDefIndex],
442
- size: blob[fDefIndex + 1],
501
+ fDefNo: fieldNumber,
502
+ size: fieldSize,
443
503
  array: profileCompatible
444
504
  ? array === true || String(type).endsWith('_array')
445
505
  : false,
@@ -451,12 +511,19 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
451
511
  scale: profileCompatible ? scale : null,
452
512
  offset: profileCompatible ? offset : 0,
453
513
  units: profileCompatible ? units : '',
454
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
514
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
455
515
  };
456
516
  mTypeDef.fieldDefs.push(fDef);
457
517
  }
518
+ const developerFieldNumbers = new Set();
458
519
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
459
520
  const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
521
+ const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
522
+ if (retainsRawMessages(options)
523
+ && (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
524
+ throw new Error('Invalid FIT developer field definition');
525
+ }
526
+ developerFieldNumbers.add(developerFieldKey);
460
527
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
461
528
  fieldDefinitionNumber: blob[fDefIndex],
462
529
  size: blob[fDefIndex + 1],
@@ -472,33 +539,73 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
472
539
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
473
540
  };
474
541
  }
475
- const messageType = messageTypes[localMessageType] || messageTypes[0];
542
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
543
+ throw new Error('Invalid FIT data record header');
544
+ }
545
+ const messageType = messageTypes[localMessageType];
546
+ if (!messageType) {
547
+ throw new Error('FIT data record has no local definition');
548
+ }
549
+ if (isCompressedTimestamp && retainsRawMessages(options)) {
550
+ const timestampField = messageType.fieldDefs[0];
551
+ if (!timestampField
552
+ || timestampField.fDefNo !== 253
553
+ || timestampField.size !== 4
554
+ || (timestampField.baseTypeNo & 0x1F) !== 6) {
555
+ throw new Error('Invalid FIT compressed timestamp definition');
556
+ }
557
+ }
476
558
  let messageSize = 0;
477
559
  let readDataFromIndex = startIndex + 1;
478
560
  const fields = {};
479
561
  const message = getFitMessage(messageType.globalMessageNumber);
480
562
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
481
563
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
564
+ const includeRawMessage = options.includeRawMessages === true
565
+ || (Array.isArray(options.includeRawMessages)
566
+ && options.includeRawMessages.includes(messageType.globalMessageNumber));
482
567
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
483
568
  || (Array.isArray(options.includeRawDeveloperFields)
484
569
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
485
- const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
570
+ const rawFields = includeRawMessage ? [] : undefined;
571
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
572
+ if (retainsRawMessages(options)) {
573
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
574
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
575
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
576
+ throw new Error('Invalid FIT data record bounds');
577
+ }
578
+ }
486
579
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
487
580
  let validFieldCount = 0;
488
581
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
489
582
  const fDef = messageType.fieldDefs[i];
583
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
584
+ rawData[i] = InvalidFieldData;
585
+ continue;
586
+ }
587
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
588
+ rawFields.push({
589
+ fieldDefinitionNumber: fDef.fDefNo,
590
+ baseType: fDef.baseTypeNo,
591
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
592
+ });
593
+ }
490
594
  const data = readData(blob, dataView, fDef, readDataFromIndex);
491
595
  if (data !== InvalidFieldData
492
596
  && !isInvalidValue(data, fDef.type)
493
597
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
494
598
  rawData[i] = data;
495
599
  validFieldCount++;
496
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
497
- decoderState.lastTimestamp = data;
600
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
601
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
498
602
  }
499
603
  }
500
604
  else {
501
605
  rawData[i] = InvalidFieldData;
606
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
607
+ decoderState.lastTimestamp = undefined;
608
+ }
502
609
  }
503
610
  readDataFromIndex += fDef.size;
504
611
  messageSize += fDef.size;
@@ -575,6 +682,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
575
682
  fields[field] = formatFieldValue(data, fDef, options, fields);
576
683
  }
577
684
  }
685
+ let compressedTimestamp;
578
686
  if (isCompressedTimestamp) {
579
687
  const previousTimestamp = decoderState.lastTimestamp;
580
688
  if (previousTimestamp === undefined) {
@@ -586,8 +694,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
586
694
  const timeOffset = recordHeader & CompressedTimestampMask;
587
695
  const previousOffset = previousTimestamp & CompressedTimestampMask;
588
696
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
589
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
697
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
698
+ if (timestamp >= 0xFFFFFFFF) {
699
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
700
+ }
590
701
  decoderState.lastTimestamp = timestamp;
702
+ compressedTimestamp = timestamp;
591
703
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
592
704
  validFieldCount++;
593
705
  }
@@ -620,9 +732,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
620
732
  }
621
733
  return {
622
734
  globalMessageNumber: messageType.globalMessageNumber,
735
+ littleEndian: messageType.littleEndian,
736
+ compressedTimestamp,
623
737
  messageType: message.name,
624
738
  nextIndex: startIndex + messageSize + 1,
625
739
  message: fields,
740
+ rawFields,
626
741
  rawDeveloperFields,
627
742
  };
628
743
  }
@@ -25,12 +25,20 @@ export interface RawDeveloperFieldValue {
25
25
  fieldDefinitionNumber: number;
26
26
  rawValue: number[];
27
27
  }
28
+ export interface RawFieldValue {
29
+ fieldDefinitionNumber: number;
30
+ baseType: number;
31
+ rawValue: number[];
32
+ }
28
33
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
29
34
  export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
30
35
  messageType: MessageName | 'definition' | '';
31
36
  nextIndex: number;
32
37
  message?: any;
33
38
  globalMessageNumber?: number;
39
+ littleEndian?: boolean;
40
+ compressedTimestamp?: number;
41
+ rawFields?: RawFieldValue[];
34
42
  rawDeveloperFields?: RawDeveloperFieldValue[];
35
43
  };
36
44
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
@@ -14,6 +14,37 @@ const GarminTimeOffset = 631065600000;
14
14
  const InvalidFieldData = Symbol('invalid FIT field data');
15
15
  const formatTypeMetadata = new Map();
16
16
  const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
17
+ const fitBaseTypeWidths = new Map([
18
+ [0, 1],
19
+ [1, 1],
20
+ [2, 1],
21
+ [3, 2],
22
+ [4, 2],
23
+ [5, 4],
24
+ [6, 4],
25
+ [7, 1],
26
+ [8, 4],
27
+ [9, 8],
28
+ [10, 1],
29
+ [11, 2],
30
+ [12, 4],
31
+ [13, 1],
32
+ [14, 8],
33
+ [15, 8],
34
+ [16, 8],
35
+ ]);
36
+ function retainsRawMessages(options) {
37
+ return options.includeRawMessages === true
38
+ || Array.isArray(options.includeRawMessages);
39
+ }
40
+ function isValidRawFieldDefinition(size, baseType) {
41
+ if (size <= 0 || (baseType & 0x60) !== 0) {
42
+ return false;
43
+ }
44
+ const typeId = baseType & 0x1F;
45
+ const width = fitBaseTypeWidths.get(typeId);
46
+ return width !== undefined && (typeId === 7 || size % width === 0);
47
+ }
17
48
  function baseTypeSize(type) {
18
49
  switch (type) {
19
50
  case 'enum':
@@ -374,7 +405,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
374
405
  if (!Number.isInteger(baseType) || type === undefined) {
375
406
  developerFieldDef.resolvedFieldDef = undefined;
376
407
  developerFieldDef.resolvedFrom = undefined;
377
- if (options.force) {
408
+ if (options.force || retainsRawMessages(options)) {
378
409
  return undefined;
379
410
  }
380
411
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -407,6 +438,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
407
438
  }
408
439
  function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
409
440
  var _a, _b, _c;
441
+ if (startIndex < 0 || startIndex >= dataEnd) {
442
+ throw new Error('Invalid FIT record bounds');
443
+ }
410
444
  const recordHeader = blob[startIndex];
411
445
  let localMessageType = recordHeader & 15;
412
446
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -417,12 +451,29 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
417
451
  else if ((recordHeader & 64) === 64) {
418
452
  // is definition message
419
453
  // startIndex + 1 is reserved
454
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
455
+ throw new Error('Invalid FIT definition bounds');
456
+ }
420
457
  const hasDeveloperData = (recordHeader & 32) === 32;
421
458
  const lEnd = blob[startIndex + 2] === 0;
422
459
  const numberOfFields = blob[startIndex + 5];
460
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
461
+ if (retainsRawMessages(options)
462
+ && ((recordHeader & 16) !== 0
463
+ || blob[startIndex + 1] !== 0
464
+ || blob[startIndex + 2] > 1
465
+ || nativeDefinitionsEnd > dataEnd
466
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
467
+ throw new Error('Invalid FIT definition');
468
+ }
423
469
  const numberOfDeveloperDataFields = hasDeveloperData
424
470
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
425
471
  : 0;
472
+ const definitionEnd = nativeDefinitionsEnd
473
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
474
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
475
+ throw new Error('Invalid FIT developer definition bounds');
476
+ }
426
477
  const mTypeDef = {
427
478
  littleEndian: lEnd,
428
479
  globalMessageNumber: addEndian(lEnd, [
@@ -435,17 +486,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
435
486
  rawData: [],
436
487
  };
437
488
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
489
+ const nativeFieldNumbers = new Set();
438
490
  for (let i = 0; i < numberOfFields; i++) {
439
491
  const fDefIndex = startIndex + 6 + i * 3;
440
492
  const baseType = blob[fDefIndex + 2];
493
+ const fieldNumber = blob[fDefIndex];
494
+ const fieldSize = blob[fDefIndex + 1];
495
+ if (retainsRawMessages(options)
496
+ && (nativeFieldNumbers.has(fieldNumber)
497
+ || !isValidRawFieldDefinition(fieldSize, baseType))) {
498
+ throw new Error('Invalid FIT native field definition');
499
+ }
500
+ nativeFieldNumbers.add(fieldNumber);
441
501
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
442
502
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
443
503
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
444
504
  const fDef = {
445
505
  type: profileCompatible ? type : wireType,
446
506
  rawType: wireType,
447
- fDefNo: blob[fDefIndex],
448
- size: blob[fDefIndex + 1],
507
+ fDefNo: fieldNumber,
508
+ size: fieldSize,
449
509
  array: profileCompatible
450
510
  ? array === true || String(type).endsWith('_array')
451
511
  : false,
@@ -457,12 +517,19 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
457
517
  scale: profileCompatible ? scale : null,
458
518
  offset: profileCompatible ? offset : 0,
459
519
  units: profileCompatible ? units : '',
460
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
520
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
461
521
  };
462
522
  mTypeDef.fieldDefs.push(fDef);
463
523
  }
524
+ const developerFieldNumbers = new Set();
464
525
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
465
526
  const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
527
+ const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
528
+ if (retainsRawMessages(options)
529
+ && (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
530
+ throw new Error('Invalid FIT developer field definition');
531
+ }
532
+ developerFieldNumbers.add(developerFieldKey);
466
533
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
467
534
  fieldDefinitionNumber: blob[fDefIndex],
468
535
  size: blob[fDefIndex + 1],
@@ -478,33 +545,73 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
478
545
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
479
546
  };
480
547
  }
481
- const messageType = messageTypes[localMessageType] || messageTypes[0];
548
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
549
+ throw new Error('Invalid FIT data record header');
550
+ }
551
+ const messageType = messageTypes[localMessageType];
552
+ if (!messageType) {
553
+ throw new Error('FIT data record has no local definition');
554
+ }
555
+ if (isCompressedTimestamp && retainsRawMessages(options)) {
556
+ const timestampField = messageType.fieldDefs[0];
557
+ if (!timestampField
558
+ || timestampField.fDefNo !== 253
559
+ || timestampField.size !== 4
560
+ || (timestampField.baseTypeNo & 0x1F) !== 6) {
561
+ throw new Error('Invalid FIT compressed timestamp definition');
562
+ }
563
+ }
482
564
  let messageSize = 0;
483
565
  let readDataFromIndex = startIndex + 1;
484
566
  const fields = {};
485
567
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
486
568
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
487
569
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
570
+ const includeRawMessage = options.includeRawMessages === true
571
+ || (Array.isArray(options.includeRawMessages)
572
+ && options.includeRawMessages.includes(messageType.globalMessageNumber));
488
573
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
489
574
  || (Array.isArray(options.includeRawDeveloperFields)
490
575
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
491
- const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
576
+ const rawFields = includeRawMessage ? [] : undefined;
577
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
578
+ if (retainsRawMessages(options)) {
579
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
580
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
581
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
582
+ throw new Error('Invalid FIT data record bounds');
583
+ }
584
+ }
492
585
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
493
586
  let validFieldCount = 0;
494
587
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
495
588
  const fDef = messageType.fieldDefs[i];
589
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
590
+ rawData[i] = InvalidFieldData;
591
+ continue;
592
+ }
593
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
594
+ rawFields.push({
595
+ fieldDefinitionNumber: fDef.fDefNo,
596
+ baseType: fDef.baseTypeNo,
597
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
598
+ });
599
+ }
496
600
  const data = readData(blob, dataView, fDef, readDataFromIndex);
497
601
  if (data !== InvalidFieldData
498
602
  && !isInvalidValue(data, fDef.type)
499
603
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
500
604
  rawData[i] = data;
501
605
  validFieldCount++;
502
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
503
- decoderState.lastTimestamp = data;
606
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
607
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
504
608
  }
505
609
  }
506
610
  else {
507
611
  rawData[i] = InvalidFieldData;
612
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
613
+ decoderState.lastTimestamp = undefined;
614
+ }
508
615
  }
509
616
  readDataFromIndex += fDef.size;
510
617
  messageSize += fDef.size;
@@ -581,6 +688,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
581
688
  fields[field] = formatFieldValue(data, fDef, options, fields);
582
689
  }
583
690
  }
691
+ let compressedTimestamp;
584
692
  if (isCompressedTimestamp) {
585
693
  const previousTimestamp = decoderState.lastTimestamp;
586
694
  if (previousTimestamp === undefined) {
@@ -592,8 +700,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
592
700
  const timeOffset = recordHeader & CompressedTimestampMask;
593
701
  const previousOffset = previousTimestamp & CompressedTimestampMask;
594
702
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
595
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
703
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
704
+ if (timestamp >= 0xFFFFFFFF) {
705
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
706
+ }
596
707
  decoderState.lastTimestamp = timestamp;
708
+ compressedTimestamp = timestamp;
597
709
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
598
710
  validFieldCount++;
599
711
  }
@@ -626,9 +738,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
626
738
  }
627
739
  return {
628
740
  globalMessageNumber: messageType.globalMessageNumber,
741
+ littleEndian: messageType.littleEndian,
742
+ compressedTimestamp,
629
743
  messageType: message.name,
630
744
  nextIndex: startIndex + messageSize + 1,
631
745
  message: fields,
746
+ rawFields,
632
747
  rawDeveloperFields,
633
748
  };
634
749
  }
@@ -2,7 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
- export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
6
6
  export interface FitParserOptions {
7
7
  force?: boolean;
8
8
  speedUnit?: string;
@@ -13,6 +13,10 @@ export interface FitParserOptions {
13
13
  mode?: 'list' | 'cascade' | 'both';
14
14
  /** Retains exact developer-field bytes for all or selected global message numbers. */
15
15
  includeRawDeveloperFields?: boolean | readonly number[];
16
+ /** Retains exact native and developer fields for all or selected global message numbers. */
17
+ includeRawMessages?: boolean | readonly number[];
18
+ /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
+ rawMessagesOnly?: boolean;
16
20
  }
17
21
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
18
22
  export default class FitParser {
@@ -8,7 +8,7 @@ Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function
8
8
  Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
9
9
  class FitParser {
10
10
  constructor(options = {}) {
11
- var _a;
11
+ var _a, _b, _c;
12
12
  this.options = {
13
13
  force: options.force != null ? options.force : true,
14
14
  speedUnit: options.speedUnit || 'm/s',
@@ -18,6 +18,8 @@ class FitParser {
18
18
  pressureUnit: options.pressureUnit || 'bar',
19
19
  mode: options.mode || 'list',
20
20
  includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
21
+ includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
22
+ rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
21
23
  };
22
24
  }
23
25
  parseAsync(content) {
@@ -124,6 +126,10 @@ class FitParser {
124
126
  || Array.isArray(this.options.includeRawDeveloperFields)
125
127
  ? []
126
128
  : undefined;
129
+ const rawMessages = this.options.includeRawMessages === true
130
+ || Array.isArray(this.options.includeRawMessages)
131
+ ? []
132
+ : undefined;
127
133
  const messageCountsByGlobalNumber = new Map();
128
134
  let loopIndex = headerLength;
129
135
  const messageTypes = [];
@@ -135,11 +141,24 @@ class FitParser {
135
141
  let lastStopTimestamp;
136
142
  let pausedTime = 0;
137
143
  while (loopIndex < crcStart) {
138
- const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
144
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
139
145
  loopIndex = nextIndex;
140
146
  if (globalMessageNumber !== undefined) {
141
147
  const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
142
148
  messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
149
+ if (recordRawFields && littleEndian !== undefined) {
150
+ rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
151
+ ? {}
152
+ : { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
153
+ field_definition_number: field.fieldDefinitionNumber,
154
+ base_type: field.baseType,
155
+ raw_value: field.rawValue,
156
+ })), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
157
+ developer_data_index: field.developerDataIndex,
158
+ field_definition_number: field.fieldDefinitionNumber,
159
+ raw_value: field.rawValue,
160
+ })) }));
161
+ }
143
162
  recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
144
163
  rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
145
164
  global_message_number: globalMessageNumber,
@@ -150,6 +169,9 @@ class FitParser {
150
169
  });
151
170
  });
152
171
  }
172
+ if (this.options.rawMessagesOnly) {
173
+ continue;
174
+ }
153
175
  if (messageType !== ''
154
176
  && messageType !== 'definition'
155
177
  && message !== undefined) {
@@ -269,6 +291,16 @@ class FitParser {
269
291
  break;
270
292
  }
271
293
  }
294
+ if (this.options.rawMessagesOnly) {
295
+ if (rawDeveloperFields) {
296
+ fitObj.raw_developer_fields = rawDeveloperFields;
297
+ }
298
+ if (rawMessages) {
299
+ fitObj.raw_messages = rawMessages;
300
+ }
301
+ callback(undefined, fitObj);
302
+ return;
303
+ }
272
304
  fitObj.hr_zone = hr_zone;
273
305
  fitObj.power_zone = power_zone;
274
306
  fitObj.dive_gases = dive_gases;
@@ -291,6 +323,9 @@ class FitParser {
291
323
  if (rawDeveloperFields) {
292
324
  fitObj.raw_developer_fields = rawDeveloperFields;
293
325
  }
326
+ if (rawMessages) {
327
+ fitObj.raw_messages = rawMessages;
328
+ }
294
329
  if (isCascadeNeeded) {
295
330
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
296
331
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
@@ -16,6 +16,24 @@ export interface ParsedRawDeveloperField {
16
16
  field_definition_number: number;
17
17
  raw_value: number[];
18
18
  }
19
+ export interface ParsedRawFitField {
20
+ field_definition_number: number;
21
+ base_type: number;
22
+ raw_value: number[];
23
+ }
24
+ export interface ParsedRawFitMessageDeveloperField {
25
+ developer_data_index: number;
26
+ field_definition_number: number;
27
+ raw_value: number[];
28
+ }
29
+ export interface ParsedRawFitMessage {
30
+ global_message_number: number;
31
+ message_index: number;
32
+ little_endian: boolean;
33
+ compressed_timestamp?: number;
34
+ fields: ParsedRawFitField[];
35
+ developer_fields: ParsedRawFitMessageDeveloperField[];
36
+ }
19
37
  export type Activity = 'manual' | 'auto_multi_sport' | number;
20
38
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
21
39
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2078,6 +2096,7 @@ export interface ParsedFit {
2078
2096
  profileVersion?: number;
2079
2097
  messages?: ParsedMessages;
2080
2098
  raw_developer_fields?: ParsedRawDeveloperField[];
2099
+ raw_messages?: ParsedRawFitMessage[];
2081
2100
  file_creator: ParsedFileCreator;
2082
2101
  device_settings: ParsedDeviceSettings;
2083
2102
  dive_summary?: ParsedDiveSummary;
@@ -182,6 +182,24 @@ function generateUtilities() {
182
182
  generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
183
183
  generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
184
184
  ]));
185
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
186
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
187
+ generateProperty('base_type', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
188
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
189
+ ]));
190
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
191
+ generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
192
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
193
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
194
+ ]));
195
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
196
+ generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
197
+ generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
198
+ generateProperty('little_endian', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.BooleanKeyword), false),
199
+ generateProperty('compressed_timestamp', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
200
+ generateArrayProperty('fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitField'), false),
201
+ generateArrayProperty('developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
202
+ ]));
185
203
  return nodes;
186
204
  }
187
205
  function generateAdditionalFields(msg) {
@@ -266,6 +284,7 @@ function generateFitType() {
266
284
  generateProperty('profileVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
267
285
  generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
268
286
  generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
287
+ generateArrayProperty('raw_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
269
288
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
270
289
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
271
290
  ? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
@@ -2,7 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
- export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
6
6
  export interface FitParserOptions {
7
7
  force?: boolean;
8
8
  speedUnit?: string;
@@ -13,6 +13,10 @@ export interface FitParserOptions {
13
13
  mode?: 'list' | 'cascade' | 'both';
14
14
  /** Retains exact developer-field bytes for all or selected global message numbers. */
15
15
  includeRawDeveloperFields?: boolean | readonly number[];
16
+ /** Retains exact native and developer fields for all or selected global message numbers. */
17
+ includeRawMessages?: boolean | readonly number[];
18
+ /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
+ rawMessagesOnly?: boolean;
16
20
  }
17
21
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
18
22
  export default class FitParser {
@@ -3,7 +3,7 @@ import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export default class FitParser {
5
5
  constructor(options = {}) {
6
- var _a;
6
+ var _a, _b, _c;
7
7
  this.options = {
8
8
  force: options.force != null ? options.force : true,
9
9
  speedUnit: options.speedUnit || 'm/s',
@@ -13,6 +13,8 @@ export default class FitParser {
13
13
  pressureUnit: options.pressureUnit || 'bar',
14
14
  mode: options.mode || 'list',
15
15
  includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
16
+ includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
17
+ rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
16
18
  };
17
19
  }
18
20
  parseAsync(content) {
@@ -119,6 +121,10 @@ export default class FitParser {
119
121
  || Array.isArray(this.options.includeRawDeveloperFields)
120
122
  ? []
121
123
  : undefined;
124
+ const rawMessages = this.options.includeRawMessages === true
125
+ || Array.isArray(this.options.includeRawMessages)
126
+ ? []
127
+ : undefined;
122
128
  const messageCountsByGlobalNumber = new Map();
123
129
  let loopIndex = headerLength;
124
130
  const messageTypes = [];
@@ -130,11 +136,24 @@ export default class FitParser {
130
136
  let lastStopTimestamp;
131
137
  let pausedTime = 0;
132
138
  while (loopIndex < crcStart) {
133
- const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
139
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
134
140
  loopIndex = nextIndex;
135
141
  if (globalMessageNumber !== undefined) {
136
142
  const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
137
143
  messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
144
+ if (recordRawFields && littleEndian !== undefined) {
145
+ rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
146
+ ? {}
147
+ : { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
148
+ field_definition_number: field.fieldDefinitionNumber,
149
+ base_type: field.baseType,
150
+ raw_value: field.rawValue,
151
+ })), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
152
+ developer_data_index: field.developerDataIndex,
153
+ field_definition_number: field.fieldDefinitionNumber,
154
+ raw_value: field.rawValue,
155
+ })) }));
156
+ }
138
157
  recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
139
158
  rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
140
159
  global_message_number: globalMessageNumber,
@@ -145,6 +164,9 @@ export default class FitParser {
145
164
  });
146
165
  });
147
166
  }
167
+ if (this.options.rawMessagesOnly) {
168
+ continue;
169
+ }
148
170
  if (messageType !== ''
149
171
  && messageType !== 'definition'
150
172
  && message !== undefined) {
@@ -264,6 +286,16 @@ export default class FitParser {
264
286
  break;
265
287
  }
266
288
  }
289
+ if (this.options.rawMessagesOnly) {
290
+ if (rawDeveloperFields) {
291
+ fitObj.raw_developer_fields = rawDeveloperFields;
292
+ }
293
+ if (rawMessages) {
294
+ fitObj.raw_messages = rawMessages;
295
+ }
296
+ callback(undefined, fitObj);
297
+ return;
298
+ }
267
299
  fitObj.hr_zone = hr_zone;
268
300
  fitObj.power_zone = power_zone;
269
301
  fitObj.dive_gases = dive_gases;
@@ -286,6 +318,9 @@ export default class FitParser {
286
318
  if (rawDeveloperFields) {
287
319
  fitObj.raw_developer_fields = rawDeveloperFields;
288
320
  }
321
+ if (rawMessages) {
322
+ fitObj.raw_messages = rawMessages;
323
+ }
289
324
  if (isCascadeNeeded) {
290
325
  laps = mapDataIntoLap(laps, 'records', records);
291
326
  laps = mapDataIntoLap(laps, 'lengths', lengths);
@@ -16,6 +16,24 @@ export interface ParsedRawDeveloperField {
16
16
  field_definition_number: number;
17
17
  raw_value: number[];
18
18
  }
19
+ export interface ParsedRawFitField {
20
+ field_definition_number: number;
21
+ base_type: number;
22
+ raw_value: number[];
23
+ }
24
+ export interface ParsedRawFitMessageDeveloperField {
25
+ developer_data_index: number;
26
+ field_definition_number: number;
27
+ raw_value: number[];
28
+ }
29
+ export interface ParsedRawFitMessage {
30
+ global_message_number: number;
31
+ message_index: number;
32
+ little_endian: boolean;
33
+ compressed_timestamp?: number;
34
+ fields: ParsedRawFitField[];
35
+ developer_fields: ParsedRawFitMessageDeveloperField[];
36
+ }
19
37
  export type Activity = 'manual' | 'auto_multi_sport' | number;
20
38
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
21
39
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2078,6 +2096,7 @@ export interface ParsedFit {
2078
2096
  profileVersion?: number;
2079
2097
  messages?: ParsedMessages;
2080
2098
  raw_developer_fields?: ParsedRawDeveloperField[];
2099
+ raw_messages?: ParsedRawFitMessage[];
2081
2100
  file_creator: ParsedFileCreator;
2082
2101
  device_settings: ParsedDeviceSettings;
2083
2102
  dive_summary?: ParsedDiveSummary;
@@ -161,6 +161,24 @@ export function generateUtilities() {
161
161
  generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
162
162
  generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
163
163
  ]));
164
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
165
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
166
+ generateProperty('base_type', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
167
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
168
+ ]));
169
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
170
+ generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
171
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
172
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
173
+ ]));
174
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
175
+ generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
176
+ generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
177
+ generateProperty('little_endian', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
178
+ generateProperty('compressed_timestamp', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
179
+ generateArrayProperty('fields', ts.factory.createTypeReferenceNode('ParsedRawFitField'), false),
180
+ generateArrayProperty('developer_fields', ts.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
181
+ ]));
164
182
  return nodes;
165
183
  }
166
184
  function generateAdditionalFields(msg) {
@@ -245,6 +263,7 @@ export function generateFitType() {
245
263
  generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
246
264
  generateProperty('messages', ts.factory.createTypeReferenceNode('ParsedMessages')),
247
265
  generateArrayProperty('raw_developer_fields', ts.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
266
+ generateArrayProperty('raw_messages', ts.factory.createTypeReferenceNode('ParsedRawFitMessage')),
248
267
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
249
268
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
250
269
  ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "5.1.0",
4
+ "version": "5.2.0",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {