fit-file-parser 5.1.0 → 5.2.1

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,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.2.1
6
+
7
+ ### Fixed
8
+
9
+ - Preserve unusual vendor field definitions in lossless raw-message output so
10
+ domain consumers can validate only the messages they understand.
11
+ - Add corpus checks for raw-message-only and combined raw/decoded parsing.
12
+
13
+ ## 5.2.0
14
+
15
+ ### Added
16
+
17
+ - Add opt-in, message-filterable `raw_messages` output with exact native and
18
+ developer-field bytes, wire base types, endianness, message occurrence, and
19
+ reconstructed compressed timestamps. Existing decoded output remains
20
+ unchanged when the option is disabled.
21
+ - Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
22
+ activity collections alongside retained messages.
23
+
24
+ ### Fixed
25
+
26
+ - Decode compressed timestamp records without consuming bytes for the omitted
27
+ timestamp field.
28
+
5
29
  ## 5.1.0
6
30
 
7
31
  ### 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
@@ -345,12 +388,16 @@ aggregate-only validation command:
345
388
  ```sh
346
389
  git clone https://github.com/ThomasKuehne/FIT-test-files.git ../FIT-test-files
347
390
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery
391
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages
392
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages-with-decoded-output
348
393
  ```
349
394
 
350
395
  The command accepts any corpus path; the sibling location is only a convenient
351
- convention. The corpus contains a known header-CRC failure that is expected to
352
- recover only in force mode. It reports aggregate counts and never prints file
353
- names or parsed activity data.
396
+ convention. Add `--raw-messages` to exercise lossless raw-message-only parsing
397
+ for every message in each file, or `--raw-messages-with-decoded-output` to also
398
+ verify the ordinary decoded output path. The corpus contains a known header-CRC
399
+ failure that is expected to recover only in force mode. It reports aggregate
400
+ counts and never prints file names or parsed activity data.
354
401
 
355
402
  Do not edit `src/garmin_profile.generated.ts` or `src/fit_types.ts` manually.
356
403
  Update the pinned SDK, audited vendor extensions, or a generator, then run
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,15 @@ 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
+ function retainsRawMessages(options) {
12
+ return options.includeRawMessages === true
13
+ || Array.isArray(options.includeRawMessages);
14
+ }
15
+ function retainsRawMessage(options, globalMessageNumber) {
16
+ return options.includeRawMessages === true
17
+ || (Array.isArray(options.includeRawMessages)
18
+ && options.includeRawMessages.includes(globalMessageNumber));
19
+ }
11
20
  function baseTypeSize(type) {
12
21
  switch (type) {
13
22
  case 'enum':
@@ -368,7 +377,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
368
377
  if (!Number.isInteger(baseType) || type === undefined) {
369
378
  developerFieldDef.resolvedFieldDef = undefined;
370
379
  developerFieldDef.resolvedFrom = undefined;
371
- if (options.force) {
380
+ if (options.force || retainsRawMessages(options)) {
372
381
  return undefined;
373
382
  }
374
383
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -401,6 +410,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
401
410
  }
402
411
  export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
403
412
  var _a, _b, _c;
413
+ if (startIndex < 0 || startIndex >= dataEnd) {
414
+ throw new Error('Invalid FIT record bounds');
415
+ }
404
416
  const recordHeader = blob[startIndex];
405
417
  let localMessageType = recordHeader & 15;
406
418
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -411,12 +423,29 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
411
423
  else if ((recordHeader & 64) === 64) {
412
424
  // is definition message
413
425
  // startIndex + 1 is reserved
426
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
427
+ throw new Error('Invalid FIT definition bounds');
428
+ }
414
429
  const hasDeveloperData = (recordHeader & 32) === 32;
415
430
  const lEnd = blob[startIndex + 2] === 0;
416
431
  const numberOfFields = blob[startIndex + 5];
432
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
433
+ if (retainsRawMessages(options)
434
+ && ((recordHeader & 16) !== 0
435
+ || blob[startIndex + 1] !== 0
436
+ || blob[startIndex + 2] > 1
437
+ || nativeDefinitionsEnd > dataEnd
438
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
439
+ throw new Error('Invalid FIT definition');
440
+ }
417
441
  const numberOfDeveloperDataFields = hasDeveloperData
418
442
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
419
443
  : 0;
444
+ const definitionEnd = nativeDefinitionsEnd
445
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
446
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
447
+ throw new Error('Invalid FIT developer definition bounds');
448
+ }
420
449
  const mTypeDef = {
421
450
  littleEndian: lEnd,
422
451
  globalMessageNumber: addEndian(lEnd, [
@@ -432,14 +461,16 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
432
461
  for (let i = 0; i < numberOfFields; i++) {
433
462
  const fDefIndex = startIndex + 6 + i * 3;
434
463
  const baseType = blob[fDefIndex + 2];
464
+ const fieldNumber = blob[fDefIndex];
465
+ const fieldSize = blob[fDefIndex + 1];
435
466
  const wireType = FIT.types.fit_base_type[baseType];
436
467
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
437
468
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
438
469
  const fDef = {
439
470
  type: profileCompatible ? type : wireType,
440
471
  rawType: wireType,
441
- fDefNo: blob[fDefIndex],
442
- size: blob[fDefIndex + 1],
472
+ fDefNo: fieldNumber,
473
+ size: fieldSize,
443
474
  array: profileCompatible
444
475
  ? array === true || String(type).endsWith('_array')
445
476
  : false,
@@ -451,7 +482,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
451
482
  scale: profileCompatible ? scale : null,
452
483
  offset: profileCompatible ? offset : 0,
453
484
  units: profileCompatible ? units : '',
454
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
485
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
455
486
  };
456
487
  mTypeDef.fieldDefs.push(fDef);
457
488
  }
@@ -472,33 +503,62 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
472
503
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
473
504
  };
474
505
  }
475
- const messageType = messageTypes[localMessageType] || messageTypes[0];
506
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
507
+ throw new Error('Invalid FIT data record header');
508
+ }
509
+ const messageType = messageTypes[localMessageType];
510
+ if (!messageType) {
511
+ throw new Error('FIT data record has no local definition');
512
+ }
476
513
  let messageSize = 0;
477
514
  let readDataFromIndex = startIndex + 1;
478
515
  const fields = {};
479
516
  const message = getFitMessage(messageType.globalMessageNumber);
480
517
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
481
518
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
519
+ const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
482
520
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
483
521
  || (Array.isArray(options.includeRawDeveloperFields)
484
522
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
485
- const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
523
+ const rawFields = includeRawMessage ? [] : undefined;
524
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
525
+ if (retainsRawMessages(options)) {
526
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
527
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
528
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
529
+ throw new Error('Invalid FIT data record bounds');
530
+ }
531
+ }
486
532
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
487
533
  let validFieldCount = 0;
488
534
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
489
535
  const fDef = messageType.fieldDefs[i];
536
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
537
+ rawData[i] = InvalidFieldData;
538
+ continue;
539
+ }
540
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
541
+ rawFields.push({
542
+ fieldDefinitionNumber: fDef.fDefNo,
543
+ baseType: fDef.baseTypeNo,
544
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
545
+ });
546
+ }
490
547
  const data = readData(blob, dataView, fDef, readDataFromIndex);
491
548
  if (data !== InvalidFieldData
492
549
  && !isInvalidValue(data, fDef.type)
493
550
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
494
551
  rawData[i] = data;
495
552
  validFieldCount++;
496
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
497
- decoderState.lastTimestamp = data;
553
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
554
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
498
555
  }
499
556
  }
500
557
  else {
501
558
  rawData[i] = InvalidFieldData;
559
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
560
+ decoderState.lastTimestamp = undefined;
561
+ }
502
562
  }
503
563
  readDataFromIndex += fDef.size;
504
564
  messageSize += fDef.size;
@@ -575,6 +635,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
575
635
  fields[field] = formatFieldValue(data, fDef, options, fields);
576
636
  }
577
637
  }
638
+ let compressedTimestamp;
578
639
  if (isCompressedTimestamp) {
579
640
  const previousTimestamp = decoderState.lastTimestamp;
580
641
  if (previousTimestamp === undefined) {
@@ -586,8 +647,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
586
647
  const timeOffset = recordHeader & CompressedTimestampMask;
587
648
  const previousOffset = previousTimestamp & CompressedTimestampMask;
588
649
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
589
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
650
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
651
+ if (timestamp >= 0xFFFFFFFF) {
652
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
653
+ }
590
654
  decoderState.lastTimestamp = timestamp;
655
+ compressedTimestamp = timestamp;
591
656
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
592
657
  validFieldCount++;
593
658
  }
@@ -620,9 +685,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
620
685
  }
621
686
  return {
622
687
  globalMessageNumber: messageType.globalMessageNumber,
688
+ littleEndian: messageType.littleEndian,
689
+ compressedTimestamp,
623
690
  messageType: message.name,
624
691
  nextIndex: startIndex + messageSize + 1,
625
692
  message: fields,
693
+ rawFields,
626
694
  rawDeveloperFields,
627
695
  };
628
696
  }
@@ -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,15 @@ 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
+ function retainsRawMessages(options) {
18
+ return options.includeRawMessages === true
19
+ || Array.isArray(options.includeRawMessages);
20
+ }
21
+ function retainsRawMessage(options, globalMessageNumber) {
22
+ return options.includeRawMessages === true
23
+ || (Array.isArray(options.includeRawMessages)
24
+ && options.includeRawMessages.includes(globalMessageNumber));
25
+ }
17
26
  function baseTypeSize(type) {
18
27
  switch (type) {
19
28
  case 'enum':
@@ -374,7 +383,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
374
383
  if (!Number.isInteger(baseType) || type === undefined) {
375
384
  developerFieldDef.resolvedFieldDef = undefined;
376
385
  developerFieldDef.resolvedFrom = undefined;
377
- if (options.force) {
386
+ if (options.force || retainsRawMessages(options)) {
378
387
  return undefined;
379
388
  }
380
389
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -407,6 +416,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
407
416
  }
408
417
  function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
409
418
  var _a, _b, _c;
419
+ if (startIndex < 0 || startIndex >= dataEnd) {
420
+ throw new Error('Invalid FIT record bounds');
421
+ }
410
422
  const recordHeader = blob[startIndex];
411
423
  let localMessageType = recordHeader & 15;
412
424
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -417,12 +429,29 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
417
429
  else if ((recordHeader & 64) === 64) {
418
430
  // is definition message
419
431
  // startIndex + 1 is reserved
432
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
433
+ throw new Error('Invalid FIT definition bounds');
434
+ }
420
435
  const hasDeveloperData = (recordHeader & 32) === 32;
421
436
  const lEnd = blob[startIndex + 2] === 0;
422
437
  const numberOfFields = blob[startIndex + 5];
438
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
439
+ if (retainsRawMessages(options)
440
+ && ((recordHeader & 16) !== 0
441
+ || blob[startIndex + 1] !== 0
442
+ || blob[startIndex + 2] > 1
443
+ || nativeDefinitionsEnd > dataEnd
444
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
445
+ throw new Error('Invalid FIT definition');
446
+ }
423
447
  const numberOfDeveloperDataFields = hasDeveloperData
424
448
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
425
449
  : 0;
450
+ const definitionEnd = nativeDefinitionsEnd
451
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
452
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
453
+ throw new Error('Invalid FIT developer definition bounds');
454
+ }
426
455
  const mTypeDef = {
427
456
  littleEndian: lEnd,
428
457
  globalMessageNumber: addEndian(lEnd, [
@@ -438,14 +467,16 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
438
467
  for (let i = 0; i < numberOfFields; i++) {
439
468
  const fDefIndex = startIndex + 6 + i * 3;
440
469
  const baseType = blob[fDefIndex + 2];
470
+ const fieldNumber = blob[fDefIndex];
471
+ const fieldSize = blob[fDefIndex + 1];
441
472
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
442
473
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
443
474
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
444
475
  const fDef = {
445
476
  type: profileCompatible ? type : wireType,
446
477
  rawType: wireType,
447
- fDefNo: blob[fDefIndex],
448
- size: blob[fDefIndex + 1],
478
+ fDefNo: fieldNumber,
479
+ size: fieldSize,
449
480
  array: profileCompatible
450
481
  ? array === true || String(type).endsWith('_array')
451
482
  : false,
@@ -457,7 +488,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
457
488
  scale: profileCompatible ? scale : null,
458
489
  offset: profileCompatible ? offset : 0,
459
490
  units: profileCompatible ? units : '',
460
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
491
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
461
492
  };
462
493
  mTypeDef.fieldDefs.push(fDef);
463
494
  }
@@ -478,33 +509,62 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
478
509
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
479
510
  };
480
511
  }
481
- const messageType = messageTypes[localMessageType] || messageTypes[0];
512
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
513
+ throw new Error('Invalid FIT data record header');
514
+ }
515
+ const messageType = messageTypes[localMessageType];
516
+ if (!messageType) {
517
+ throw new Error('FIT data record has no local definition');
518
+ }
482
519
  let messageSize = 0;
483
520
  let readDataFromIndex = startIndex + 1;
484
521
  const fields = {};
485
522
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
486
523
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
487
524
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
525
+ const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
488
526
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
489
527
  || (Array.isArray(options.includeRawDeveloperFields)
490
528
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
491
- const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
529
+ const rawFields = includeRawMessage ? [] : undefined;
530
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
531
+ if (retainsRawMessages(options)) {
532
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
533
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
534
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
535
+ throw new Error('Invalid FIT data record bounds');
536
+ }
537
+ }
492
538
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
493
539
  let validFieldCount = 0;
494
540
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
495
541
  const fDef = messageType.fieldDefs[i];
542
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
543
+ rawData[i] = InvalidFieldData;
544
+ continue;
545
+ }
546
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
547
+ rawFields.push({
548
+ fieldDefinitionNumber: fDef.fDefNo,
549
+ baseType: fDef.baseTypeNo,
550
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
551
+ });
552
+ }
496
553
  const data = readData(blob, dataView, fDef, readDataFromIndex);
497
554
  if (data !== InvalidFieldData
498
555
  && !isInvalidValue(data, fDef.type)
499
556
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
500
557
  rawData[i] = data;
501
558
  validFieldCount++;
502
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
503
- decoderState.lastTimestamp = data;
559
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
560
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
504
561
  }
505
562
  }
506
563
  else {
507
564
  rawData[i] = InvalidFieldData;
565
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
566
+ decoderState.lastTimestamp = undefined;
567
+ }
508
568
  }
509
569
  readDataFromIndex += fDef.size;
510
570
  messageSize += fDef.size;
@@ -581,6 +641,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
581
641
  fields[field] = formatFieldValue(data, fDef, options, fields);
582
642
  }
583
643
  }
644
+ let compressedTimestamp;
584
645
  if (isCompressedTimestamp) {
585
646
  const previousTimestamp = decoderState.lastTimestamp;
586
647
  if (previousTimestamp === undefined) {
@@ -592,8 +653,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
592
653
  const timeOffset = recordHeader & CompressedTimestampMask;
593
654
  const previousOffset = previousTimestamp & CompressedTimestampMask;
594
655
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
595
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
656
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
657
+ if (timestamp >= 0xFFFFFFFF) {
658
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
659
+ }
596
660
  decoderState.lastTimestamp = timestamp;
661
+ compressedTimestamp = timestamp;
597
662
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
598
663
  validFieldCount++;
599
664
  }
@@ -626,9 +691,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
626
691
  }
627
692
  return {
628
693
  globalMessageNumber: messageType.globalMessageNumber,
694
+ littleEndian: messageType.littleEndian,
695
+ compressedTimestamp,
629
696
  messageType: message.name,
630
697
  nextIndex: startIndex + messageSize + 1,
631
698
  message: fields,
699
+ rawFields,
632
700
  rawDeveloperFields,
633
701
  };
634
702
  }
@@ -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.1",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {