fit-file-parser 5.2.0 → 6.0.2

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.
@@ -14,36 +14,14 @@ 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
17
  function retainsRawMessages(options) {
37
18
  return options.includeRawMessages === true
38
19
  || Array.isArray(options.includeRawMessages);
39
20
  }
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);
21
+ function retainsRawMessage(options, globalMessageNumber) {
22
+ return options.includeRawMessages === true
23
+ || (Array.isArray(options.includeRawMessages)
24
+ && options.includeRawMessages.includes(globalMessageNumber));
47
25
  }
48
26
  function baseTypeSize(type) {
49
27
  switch (type) {
@@ -486,18 +464,11 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
486
464
  rawData: [],
487
465
  };
488
466
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
489
- const nativeFieldNumbers = new Set();
490
467
  for (let i = 0; i < numberOfFields; i++) {
491
468
  const fDefIndex = startIndex + 6 + i * 3;
492
469
  const baseType = blob[fDefIndex + 2];
493
470
  const fieldNumber = blob[fDefIndex];
494
471
  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);
501
472
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
502
473
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
503
474
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
@@ -521,15 +492,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
521
492
  };
522
493
  mTypeDef.fieldDefs.push(fDef);
523
494
  }
524
- const developerFieldNumbers = new Set();
525
495
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
526
496
  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);
533
497
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
534
498
  fieldDefinitionNumber: blob[fDefIndex],
535
499
  size: blob[fDefIndex + 1],
@@ -552,29 +516,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
552
516
  if (!messageType) {
553
517
  throw new Error('FIT data record has no local definition');
554
518
  }
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
- }
564
519
  let messageSize = 0;
565
520
  let readDataFromIndex = startIndex + 1;
566
521
  const fields = {};
567
522
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
568
523
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
569
524
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
570
- const includeRawMessage = options.includeRawMessages === true
571
- || (Array.isArray(options.includeRawMessages)
572
- && options.includeRawMessages.includes(messageType.globalMessageNumber));
525
+ const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
573
526
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
574
527
  || (Array.isArray(options.includeRawDeveloperFields)
575
528
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
576
529
  const rawFields = includeRawMessage ? [] : undefined;
577
530
  const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
531
+ const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
532
+ const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
578
533
  if (retainsRawMessages(options)) {
579
534
  const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
580
535
  const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
@@ -590,12 +545,17 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
590
545
  rawData[i] = InvalidFieldData;
591
546
  continue;
592
547
  }
593
- if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
594
- rawFields.push({
548
+ if ((rawFields || (unmappedFields && !isOutputFieldName(fDef.name)))
549
+ && readDataFromIndex + fDef.size <= dataEnd) {
550
+ const rawField = {
595
551
  fieldDefinitionNumber: fDef.fDefNo,
596
552
  baseType: fDef.baseTypeNo,
597
553
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
598
- });
554
+ };
555
+ rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
556
+ if (!isOutputFieldName(fDef.name)) {
557
+ unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
558
+ }
599
559
  }
600
560
  const data = readData(blob, dataView, fDef, readDataFromIndex);
601
561
  if (data !== InvalidFieldData
@@ -619,15 +579,21 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
619
579
  for (let i = 0; i < developerFieldDefs.length; i++) {
620
580
  const developerFieldDef = developerFieldDefs[i];
621
581
  const rawDataIndex = messageType.fieldDefs.length + i;
622
- if (rawDeveloperFields
582
+ let rawDeveloperField;
583
+ if ((rawDeveloperFields
584
+ || (unmappedDeveloperFields && developerFieldDef.resolvedFieldDef === undefined))
623
585
  && readDataFromIndex + developerFieldDef.size <= dataEnd) {
624
- rawDeveloperFields.push({
586
+ rawDeveloperField = {
625
587
  developerDataIndex: developerFieldDef.developerDataIndex,
626
588
  fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
627
589
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
628
- });
590
+ };
591
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
629
592
  }
630
593
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
594
+ if (!fDef && rawDeveloperField) {
595
+ unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
596
+ }
631
597
  if (fDef) {
632
598
  const data = readData(blob, dataView, fDef, readDataFromIndex);
633
599
  if (data !== InvalidFieldData
@@ -745,6 +711,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
745
711
  message: fields,
746
712
  rawFields,
747
713
  rawDeveloperFields,
714
+ unmappedFields: unmappedFields && unmappedFields.length > 0 ? unmappedFields : undefined,
715
+ unmappedDeveloperFields: unmappedDeveloperFields && unmappedDeveloperFields.length > 0
716
+ ? unmappedDeveloperFields
717
+ : undefined,
748
718
  };
749
719
  }
750
720
  function getArrayBuffer(buffer) {
@@ -15,6 +15,8 @@ export interface FitParserOptions {
15
15
  includeRawDeveloperFields?: boolean | readonly number[];
16
16
  /** Retains exact native and developer fields for all or selected global message numbers. */
17
17
  includeRawMessages?: boolean | readonly number[];
18
+ /** Retains exact bytes for fields that have no semantic profile mapping. */
19
+ includeUnmappedMessages?: boolean;
18
20
  /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
21
  rawMessagesOnly?: boolean;
20
22
  }
@@ -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, _b, _c;
11
+ var _a, _b, _c, _d;
12
12
  this.options = {
13
13
  force: options.force != null ? options.force : true,
14
14
  speedUnit: options.speedUnit || 'm/s',
@@ -19,7 +19,8 @@ class FitParser {
19
19
  mode: options.mode || 'list',
20
20
  includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
21
21
  includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
22
- rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
22
+ includeUnmappedMessages: (_c = options.includeUnmappedMessages) !== null && _c !== void 0 ? _c : false,
23
+ rawMessagesOnly: (_d = options.rawMessagesOnly) !== null && _d !== void 0 ? _d : false,
23
24
  };
24
25
  }
25
26
  parseAsync(content) {
@@ -130,6 +131,7 @@ class FitParser {
130
131
  || Array.isArray(this.options.includeRawMessages)
131
132
  ? []
132
133
  : undefined;
134
+ const unmappedMessages = this.options.includeUnmappedMessages ? [] : undefined;
133
135
  const messageCountsByGlobalNumber = new Map();
134
136
  let loopIndex = headerLength;
135
137
  const messageTypes = [];
@@ -141,7 +143,7 @@ class FitParser {
141
143
  let lastStopTimestamp;
142
144
  let pausedTime = 0;
143
145
  while (loopIndex < 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);
146
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
145
147
  loopIndex = nextIndex;
146
148
  if (globalMessageNumber !== undefined) {
147
149
  const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
@@ -168,6 +170,20 @@ class FitParser {
168
170
  raw_value: field.rawValue,
169
171
  });
170
172
  });
173
+ if (littleEndian !== undefined
174
+ && ((recordUnmappedFields === null || recordUnmappedFields === void 0 ? void 0 : recordUnmappedFields.length) || (recordUnmappedDeveloperFields === null || recordUnmappedDeveloperFields === void 0 ? void 0 : recordUnmappedDeveloperFields.length))) {
175
+ unmappedMessages === null || unmappedMessages === void 0 ? void 0 : unmappedMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
176
+ ? {}
177
+ : { compressed_timestamp: compressedTimestamp })), { fields: (recordUnmappedFields !== null && recordUnmappedFields !== void 0 ? recordUnmappedFields : []).map(field => ({
178
+ field_definition_number: field.fieldDefinitionNumber,
179
+ base_type: field.baseType,
180
+ raw_value: field.rawValue,
181
+ })), developer_fields: (recordUnmappedDeveloperFields !== null && recordUnmappedDeveloperFields !== void 0 ? recordUnmappedDeveloperFields : []).map(field => ({
182
+ developer_data_index: field.developerDataIndex,
183
+ field_definition_number: field.fieldDefinitionNumber,
184
+ raw_value: field.rawValue,
185
+ })) }));
186
+ }
171
187
  }
172
188
  if (this.options.rawMessagesOnly) {
173
189
  continue;
@@ -298,6 +314,9 @@ class FitParser {
298
314
  if (rawMessages) {
299
315
  fitObj.raw_messages = rawMessages;
300
316
  }
317
+ if (unmappedMessages && unmappedMessages.length > 0) {
318
+ fitObj.unmapped_messages = unmappedMessages;
319
+ }
301
320
  callback(undefined, fitObj);
302
321
  return;
303
322
  }
@@ -326,6 +345,9 @@ class FitParser {
326
345
  if (rawMessages) {
327
346
  fitObj.raw_messages = rawMessages;
328
347
  }
348
+ if (unmappedMessages && unmappedMessages.length > 0) {
349
+ fitObj.unmapped_messages = unmappedMessages;
350
+ }
329
351
  if (isCascadeNeeded) {
330
352
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
331
353
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
package/dist/cjs/fit.d.ts CHANGED
@@ -37,10 +37,4 @@ export interface FitType {
37
37
  messages: Record<number, Message>;
38
38
  types: Record<string, Record<number, string | number>>;
39
39
  }
40
- /**
41
- * Garmin fields observed in the external FIT corpus but absent from the pinned
42
- * public SDK profile. These additions may not replace standard SDK fields.
43
- */
44
- export declare const FIT_VENDOR_MESSAGE_EXTENSIONS: Record<number, Message>;
45
- export declare const FIT_VENDOR_TYPE_EXTENSIONS: Record<string, Record<number, string>>;
46
40
  export declare const FIT: FitType;
package/dist/cjs/fit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FIT = exports.FIT_VENDOR_TYPE_EXTENSIONS = exports.FIT_VENDOR_MESSAGE_EXTENSIONS = void 0;
4
- const garmin_profile_generated_js_1 = require("./garmin_profile.generated.js");
3
+ exports.FIT = void 0;
4
+ const profile_js_1 = require("./profile.js");
5
5
  const metersInOneKilometer = 1000;
6
6
  const secondsInOneHour = 3600;
7
7
  // according to https://en.wikipedia.org/wiki/Mile
@@ -31,119 +31,9 @@ const options = {
31
31
  psi: { multiplier: psiInOneBar, offset: 0 },
32
32
  },
33
33
  };
34
- /**
35
- * Garmin fields observed in the external FIT corpus but absent from the pinned
36
- * public SDK profile. These additions may not replace standard SDK fields.
37
- */
38
- exports.FIT_VENDOR_MESSAGE_EXTENSIONS = {
39
- 18: {
40
- name: 'session',
41
- 178: field('est_sweat_loss', 'uint16', 1, 'ml'),
42
- 188: field('primary_benefit', 'uint8'),
43
- 205: field('beginning_potential_stamina', 'uint8', 1, 'percent'),
44
- 206: field('ending_potential_stamina', 'uint8', 1, 'percent'),
45
- 207: field('min_stamina', 'uint8', 1, 'percent'),
46
- },
47
- 20: {
48
- name: 'record',
49
- 90: field('garmin_performance_condition', 'sint8'),
50
- 137: field('potential_stamina', 'uint8', 1, 'percent'),
51
- 138: field('stamina', 'uint8', 1, 'percent'),
52
- },
53
- 23: {
54
- name: 'device_info',
55
- 24: field('ant_id', 'uint32z'),
56
- },
57
- // Undocumented Garmin user metrics message observed in activity FIT files.
58
- 79: {
59
- name: 'user_metrics',
60
- 0: field('vo2_max', 'uint16', 1024 / 3.5, 'ml/kg/min'),
61
- 1: field('age', 'uint8', 1, 'years'),
62
- 2: field('height', 'uint8', 100, 'm'),
63
- 3: field('weight', 'uint16', 10, 'kg'),
64
- 4: field('gender', 'gender'),
65
- 6: field('max_heart_rate', 'uint8', 1, 'bpm'),
66
- 8: field('remaining_recovery_time', 'uint16'),
67
- 11: field('lthr', 'uint16', 1, 'bpm'),
68
- 12: field('ltpower', 'uint16', 1, 'watts'),
69
- 13: field('ltspeed', 'uint16', 1000, 'm/s'),
70
- 16: field('start_of_activity', 'date_time'),
71
- 19: field('first_vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
72
- 35: field('end_of_previous_activity', 'date_time'),
73
- 253: field('timestamp', 'date_time'),
74
- },
75
- // Undocumented Garmin activity metrics message observed in activity FIT files.
76
- 140: {
77
- name: 'activity_metrics',
78
- 1: field('new_max_heart_rate', 'uint8', 1, 'bpm'),
79
- 4: field('aerobic_training_effect', 'uint8', 10),
80
- 7: field('vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
81
- 9: field('recovery_time', 'uint16', 1, 'min'),
82
- 11: field('sport', 'sport'),
83
- 20: field('anaerobic_training_effect', 'uint8', 10),
84
- 29: field('first_vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
85
- 41: field('primary_benefit', 'uint8'),
86
- 60: field('total_ascent', 'uint16', 1, 'm'),
87
- 61: field('total_descent', 'uint16', 1, 'm'),
88
- 62: field('avg_power', 'uint16', 1, 'watts'),
89
- 63: field('avg_heart_rate', 'uint8', 1, 'bpm'),
90
- },
91
- 312: {
92
- name: 'split',
93
- 107: field('beginning_potential_stamina', 'uint8', 1, 'percent'),
94
- 108: field('ending_potential_stamina', 'uint8', 1, 'percent'),
95
- 109: field('min_stamina', 'uint8', 1, 'percent'),
96
- },
97
- };
98
- exports.FIT_VENDOR_TYPE_EXTENSIONS = {
99
- mesg_num: {
100
- 79: 'user_metrics',
101
- 140: 'activity_metrics',
102
- },
103
- };
104
- function field(name, type, scale = 1, units = '', baseType) {
105
- return Object.assign(Object.assign({ field: name, type }, (baseType ? { baseType } : {})), { scale, offset: 0, units });
106
- }
107
- function mergeVendorMessages() {
108
- const messages = Object.assign({}, garmin_profile_generated_js_1.GARMIN_MESSAGES);
109
- Object.entries(exports.FIT_VENDOR_MESSAGE_EXTENSIONS).forEach(([messageIdText, extension]) => {
110
- const messageId = Number(messageIdText);
111
- const standardMessage = messages[messageId];
112
- if (!standardMessage) {
113
- messages[messageId] = extension;
114
- return;
115
- }
116
- if (standardMessage.name !== extension.name) {
117
- throw new Error(`Vendor message ${messageId} conflicts with the Garmin SDK name`);
118
- }
119
- Object.keys(extension)
120
- .filter(key => key !== 'name')
121
- .forEach((fieldId) => {
122
- if (standardMessage[Number(fieldId)]) {
123
- throw new Error(`Vendor message ${messageId}, field ${fieldId} conflicts with the Garmin SDK profile`);
124
- }
125
- });
126
- messages[messageId] = Object.assign(Object.assign({}, standardMessage), extension);
127
- });
128
- return messages;
129
- }
130
- function mergeVendorTypes() {
131
- const types = Object.fromEntries(Object.entries(garmin_profile_generated_js_1.GARMIN_TYPES).map(([name, values]) => [name, Object.assign({}, values)]));
132
- Object.entries(exports.FIT_VENDOR_TYPE_EXTENSIONS).forEach(([name, extension]) => {
133
- var _a;
134
- const standardValues = (_a = types[name]) !== null && _a !== void 0 ? _a : {};
135
- Object.keys(extension).forEach((valueId) => {
136
- if (standardValues[Number(valueId)] !== undefined) {
137
- throw new Error(`Vendor type ${name}, value ${valueId} conflicts with the Garmin SDK profile`);
138
- }
139
- });
140
- types[name] = Object.assign(Object.assign({}, standardValues), extension);
141
- });
142
- return types;
143
- }
144
34
  exports.FIT = {
145
35
  scConst: 180 / Math.pow(2, 31),
146
36
  options,
147
- messages: mergeVendorMessages(),
148
- types: mergeVendorTypes(),
37
+ messages: profile_js_1.PROFILE_MESSAGES,
38
+ types: profile_js_1.PROFILE_TYPES,
149
39
  };
@@ -2097,6 +2097,7 @@ export interface ParsedFit {
2097
2097
  messages?: ParsedMessages;
2098
2098
  raw_developer_fields?: ParsedRawDeveloperField[];
2099
2099
  raw_messages?: ParsedRawFitMessage[];
2100
+ unmapped_messages?: ParsedRawFitMessage[];
2100
2101
  file_creator: ParsedFileCreator;
2101
2102
  device_settings: ParsedDeviceSettings;
2102
2103
  dive_summary?: ParsedDiveSummary;
@@ -0,0 +1,9 @@
1
+ import type { Message } from './fit.js';
2
+ /**
3
+ * Static, community-maintained FIT interoperability profile.
4
+ *
5
+ * This table is ordinary project source maintained under the repository's MIT
6
+ * license. See PROFILE.md for the maintenance and verification policy.
7
+ */
8
+ export declare const PROFILE_MESSAGES: Record<number, Message>;
9
+ export declare const PROFILE_TYPES: Record<string, Record<number, string | number>>;