fit-file-parser 5.2.1 → 6.1.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.
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FitMessageReaderError = void 0;
4
+ exports.getFitBaseTypeId = getFitBaseTypeId;
5
+ exports.readFitUnsignedField = readFitUnsignedField;
6
+ exports.readFitStringField = readFitStringField;
7
+ exports.readFitMessages = readFitMessages;
8
+ exports.fitTimestampToUnixMilliseconds = fitTimestampToUnixMilliseconds;
9
+ const FIT_EPOCH_MS = 631065600000;
10
+ const FIT_BASE_TYPE_WIDTHS = new Map([
11
+ [0, 1],
12
+ [1, 1],
13
+ [2, 1],
14
+ [3, 2],
15
+ [4, 2],
16
+ [5, 4],
17
+ [6, 4],
18
+ [7, 1],
19
+ [8, 4],
20
+ [9, 8],
21
+ [10, 1],
22
+ [11, 2],
23
+ [12, 4],
24
+ [13, 1],
25
+ [14, 8],
26
+ [15, 8],
27
+ [16, 8],
28
+ ]);
29
+ const FIT_UNSIGNED_SCALAR_BASE_TYPES = new Set([0, 2, 4, 6, 10, 11, 12, 13]);
30
+ const CRC_TABLE = [
31
+ 0,
32
+ 0xCC01,
33
+ 0xD801,
34
+ 0x1400,
35
+ 0xF001,
36
+ 0x3C00,
37
+ 0x2800,
38
+ 0xE401,
39
+ 0xA001,
40
+ 0x6C00,
41
+ 0x7800,
42
+ 0xB401,
43
+ 0x5000,
44
+ 0x9C01,
45
+ 0x8801,
46
+ 0x4400,
47
+ ];
48
+ /** Error thrown when the strict raw-message reader rejects a FIT input. */
49
+ class FitMessageReaderError extends Error {
50
+ constructor(code) {
51
+ super(code);
52
+ this.name = 'FitMessageReaderError';
53
+ this.code = code;
54
+ }
55
+ }
56
+ exports.FitMessageReaderError = FitMessageReaderError;
57
+ function calculateCRC(bytes) {
58
+ let value = 0;
59
+ for (const byte of bytes) {
60
+ value = (value >>> 4) ^ CRC_TABLE[value & 15] ^ CRC_TABLE[byte & 15];
61
+ value = (value >>> 4) ^ CRC_TABLE[value & 15] ^ CRC_TABLE[byte >>> 4];
62
+ }
63
+ return value;
64
+ }
65
+ /** Returns the five-bit FIT base-type identifier, or null for reserved/unknown values. */
66
+ function getFitBaseTypeId(baseType) {
67
+ if (!Number.isInteger(baseType) || baseType < 0 || baseType > 0xFF) {
68
+ return null;
69
+ }
70
+ if (baseType & 0x60) {
71
+ return null;
72
+ }
73
+ const id = baseType & 0x1F;
74
+ return FIT_BASE_TYPE_WIDTHS.has(id) ? id : null;
75
+ }
76
+ function isValidFieldDefinition(field) {
77
+ const baseTypeId = getFitBaseTypeId(field.baseType);
78
+ const width = baseTypeId === null
79
+ ? undefined
80
+ : FIT_BASE_TYPE_WIDTHS.get(baseTypeId);
81
+ return width !== undefined
82
+ && field.size > 0
83
+ && (baseTypeId === 7 || field.size % width === 0);
84
+ }
85
+ /** Reads an unsigned one-, two-, or four-byte raw FIT field. */
86
+ function readFitUnsignedField(field, expectedBaseType, size, littleEndian) {
87
+ if (!field) {
88
+ return undefined;
89
+ }
90
+ const expectedType = getFitBaseTypeId(expectedBaseType);
91
+ const actualType = getFitBaseTypeId(field.baseType);
92
+ const compatibleByte = size === 1
93
+ && expectedType !== null
94
+ && actualType !== null
95
+ && [0, 2, 13].includes(expectedType)
96
+ && [0, 2, 13].includes(actualType);
97
+ if (expectedType === null
98
+ || actualType === null
99
+ || !FIT_UNSIGNED_SCALAR_BASE_TYPES.has(expectedType)
100
+ || FIT_BASE_TYPE_WIDTHS.get(expectedType) !== size
101
+ || field.size !== size
102
+ || !(field.bytes instanceof Uint8Array)
103
+ || field.bytes.byteLength !== field.size
104
+ || (actualType !== expectedType && !compatibleByte)) {
105
+ throw new TypeError('FIT field does not match the expected unsigned type');
106
+ }
107
+ const view = new DataView(field.bytes.buffer, field.bytes.byteOffset, field.bytes.byteLength);
108
+ const value = size === 1
109
+ ? view.getUint8(0)
110
+ : size === 2
111
+ ? view.getUint16(0, littleEndian)
112
+ : view.getUint32(0, littleEndian);
113
+ const invalid = [10, 11, 12].includes(expectedType)
114
+ ? 0
115
+ : size === 1
116
+ ? 0xFF
117
+ : size === 2
118
+ ? 0xFFFF
119
+ : 0xFFFFFFFF;
120
+ return value === invalid ? undefined : value;
121
+ }
122
+ /** Reads UTF-8 bytes, removes trailing NUL padding, and preserves interior NUL separators. */
123
+ function readFitStringField(field) {
124
+ if (!field) {
125
+ return undefined;
126
+ }
127
+ if (getFitBaseTypeId(field.baseType) !== 7
128
+ || field.size <= 0
129
+ || !(field.bytes instanceof Uint8Array)
130
+ || field.bytes.byteLength !== field.size) {
131
+ throw new TypeError('FIT field is not a string');
132
+ }
133
+ const value = new TextDecoder('utf-8', {
134
+ fatal: true,
135
+ ignoreBOM: true,
136
+ }).decode(field.bytes).replace(/\0+$/, '');
137
+ return value || undefined;
138
+ }
139
+ function readTimestamp(field, bytes, littleEndian) {
140
+ if (field.size !== 4 || (field.baseType & 0x1F) !== 6) {
141
+ throw new FitMessageReaderError('invalid_structure');
142
+ }
143
+ const value = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, littleEndian);
144
+ return value === 0xFFFFFFFF ? undefined : value;
145
+ }
146
+ function validateOptions(options) {
147
+ var _a;
148
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
149
+ throw new FitMessageReaderError('invalid_input');
150
+ }
151
+ const maxInputBytes = (_a = options.maxInputBytes) !== null && _a !== void 0 ? _a : Number.POSITIVE_INFINITY;
152
+ if (maxInputBytes !== Number.POSITIVE_INFINITY
153
+ && (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 0)) {
154
+ throw new FitMessageReaderError('invalid_input');
155
+ }
156
+ if (options.messageNumbers === undefined) {
157
+ return { maxInputBytes };
158
+ }
159
+ if (!Array.isArray(options.messageNumbers)) {
160
+ throw new FitMessageReaderError('invalid_input');
161
+ }
162
+ const selectedMessages = new Set();
163
+ for (const messageNumber of options.messageNumbers) {
164
+ if (!Number.isSafeInteger(messageNumber)
165
+ || messageNumber < 0
166
+ || messageNumber > 0xFFFF) {
167
+ throw new FitMessageReaderError('invalid_input');
168
+ }
169
+ selectedMessages.add(messageNumber);
170
+ }
171
+ return { maxInputBytes, selectedMessages };
172
+ }
173
+ /**
174
+ * Strictly validates a FIT file and retains raw fields for selected messages.
175
+ * This entry point does not load the semantic FIT profile or format field values.
176
+ */
177
+ function readFitMessages(input, options = {}) {
178
+ var _a;
179
+ const { maxInputBytes, selectedMessages } = validateOptions(options);
180
+ if (!(input instanceof ArrayBuffer) && !(input instanceof Uint8Array)) {
181
+ throw new FitMessageReaderError('invalid_input');
182
+ }
183
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
184
+ if (bytes.length > maxInputBytes) {
185
+ throw new FitMessageReaderError('input_limit');
186
+ }
187
+ if (bytes.length < 14) {
188
+ throw new FitMessageReaderError('invalid_header');
189
+ }
190
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
191
+ const headerSize = bytes[0];
192
+ if (headerSize !== 12 && headerSize !== 14) {
193
+ throw new FitMessageReaderError('invalid_header');
194
+ }
195
+ const dataEnd = headerSize + view.getUint32(4, true);
196
+ const protocolMajorVersion = bytes[1] >>> 4;
197
+ if ((protocolMajorVersion !== 1 && protocolMajorVersion !== 2)
198
+ || bytes[8] !== 0x2E
199
+ || bytes[9] !== 0x46
200
+ || bytes[10] !== 0x49
201
+ || bytes[11] !== 0x54
202
+ || dataEnd + 2 !== bytes.length) {
203
+ throw new FitMessageReaderError('invalid_header');
204
+ }
205
+ if (headerSize === 14
206
+ && view.getUint16(12, true) !== 0
207
+ && calculateCRC(bytes.subarray(0, 12)) !== view.getUint16(12, true)) {
208
+ throw new FitMessageReaderError('invalid_crc');
209
+ }
210
+ if (calculateCRC(bytes.subarray(0, dataEnd)) !== view.getUint16(dataEnd, true)) {
211
+ throw new FitMessageReaderError('invalid_crc');
212
+ }
213
+ const definitions = new Map();
214
+ const messageIndexes = new Map();
215
+ const messages = [];
216
+ const issues = [];
217
+ let cursor = headerSize;
218
+ let lastTimestamp;
219
+ const take = (size) => {
220
+ if (cursor + size > dataEnd) {
221
+ throw new FitMessageReaderError('invalid_structure');
222
+ }
223
+ const value = bytes.subarray(cursor, cursor + size);
224
+ cursor += size;
225
+ return value;
226
+ };
227
+ // `take` owns bounds checking and advances the shared cursor for every record component.
228
+ // eslint-disable-next-line no-unmodified-loop-condition
229
+ while (cursor < dataEnd) {
230
+ const recordHeader = take(1)[0];
231
+ const compressed = (recordHeader & 0x80) !== 0;
232
+ const localMessageNumber = compressed
233
+ ? (recordHeader >> 5) & 3
234
+ : recordHeader & 15;
235
+ if (!compressed && (recordHeader & 0x10) !== 0) {
236
+ throw new FitMessageReaderError('invalid_structure');
237
+ }
238
+ if (!compressed && (recordHeader & 0x40) !== 0) {
239
+ const definitionHeader = take(5);
240
+ if (definitionHeader[0] !== 0 || definitionHeader[1] > 1) {
241
+ throw new FitMessageReaderError('invalid_structure');
242
+ }
243
+ const littleEndian = definitionHeader[1] === 0;
244
+ const globalMessageNumber = littleEndian
245
+ ? definitionHeader[2] | (definitionHeader[3] << 8)
246
+ : (definitionHeader[2] << 8) | definitionHeader[3];
247
+ const selected = selectedMessages === undefined
248
+ || selectedMessages.has(globalMessageNumber);
249
+ const readFields = (count) => {
250
+ const fields = [];
251
+ const fieldNumbers = new Set();
252
+ for (let index = 0; index < count; index++) {
253
+ const raw = take(3);
254
+ const field = {
255
+ fieldNumber: raw[0],
256
+ size: raw[1],
257
+ baseType: raw[2],
258
+ };
259
+ if (fieldNumbers.has(field.fieldNumber)
260
+ || field.size === 0
261
+ || (selected && !isValidFieldDefinition(field))) {
262
+ throw new FitMessageReaderError('invalid_structure');
263
+ }
264
+ fieldNumbers.add(field.fieldNumber);
265
+ fields.push(field);
266
+ }
267
+ return fields;
268
+ };
269
+ const fields = readFields(definitionHeader[4]);
270
+ const developerFields = [];
271
+ if ((recordHeader & 0x20) !== 0) {
272
+ const developerFieldKeys = new Set();
273
+ const count = take(1)[0];
274
+ for (let index = 0; index < count; index++) {
275
+ const raw = take(3);
276
+ const key = `${raw[2]}:${raw[0]}`;
277
+ if (raw[1] === 0 || developerFieldKeys.has(key)) {
278
+ throw new FitMessageReaderError('invalid_structure');
279
+ }
280
+ developerFieldKeys.add(key);
281
+ developerFields.push({
282
+ fieldNumber: raw[0],
283
+ size: raw[1],
284
+ developerDataIndex: raw[2],
285
+ });
286
+ }
287
+ }
288
+ definitions.set(localMessageNumber, {
289
+ globalMessageNumber,
290
+ littleEndian,
291
+ fields,
292
+ developerFields,
293
+ });
294
+ continue;
295
+ }
296
+ if (!compressed && (recordHeader & 0x20) !== 0) {
297
+ throw new FitMessageReaderError('invalid_structure');
298
+ }
299
+ const definition = definitions.get(localMessageNumber);
300
+ if (!definition) {
301
+ throw new FitMessageReaderError('invalid_structure');
302
+ }
303
+ const messageIndex = (_a = messageIndexes.get(definition.globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
304
+ messageIndexes.set(definition.globalMessageNumber, messageIndex + 1);
305
+ const selected = selectedMessages === undefined
306
+ || selectedMessages.has(definition.globalMessageNumber);
307
+ let timestamp;
308
+ let compressedTimestamp;
309
+ if (compressed) {
310
+ const timestampField = definition.fields[0];
311
+ if (!timestampField
312
+ || timestampField.fieldNumber !== 253
313
+ || timestampField.baseType !== 0x86
314
+ || timestampField.size !== 4
315
+ || lastTimestamp === undefined) {
316
+ throw new FitMessageReaderError('invalid_structure');
317
+ }
318
+ timestamp = Math.floor(lastTimestamp / 32) * 32 + (recordHeader & 31);
319
+ if (timestamp < lastTimestamp) {
320
+ timestamp += 32;
321
+ }
322
+ if (timestamp >= 0xFFFFFFFF) {
323
+ throw new FitMessageReaderError('invalid_structure');
324
+ }
325
+ lastTimestamp = timestamp;
326
+ compressedTimestamp = timestamp;
327
+ }
328
+ const fields = [];
329
+ for (const field of definition.fields) {
330
+ if (compressed && field.fieldNumber === 253) {
331
+ continue;
332
+ }
333
+ const raw = take(field.size);
334
+ if (field.fieldNumber === 253) {
335
+ try {
336
+ timestamp = readTimestamp(field, raw, definition.littleEndian);
337
+ lastTimestamp = timestamp;
338
+ }
339
+ catch (_b) {
340
+ lastTimestamp = undefined;
341
+ issues.push({
342
+ code: 'invalid_timestamp',
343
+ globalMessageNumber: definition.globalMessageNumber,
344
+ messageIndex,
345
+ });
346
+ }
347
+ }
348
+ if (selected) {
349
+ fields.push(Object.assign(Object.assign({}, field), { bytes: raw.slice() }));
350
+ }
351
+ }
352
+ const developerFields = [];
353
+ for (const field of definition.developerFields) {
354
+ const raw = take(field.size);
355
+ if (selected) {
356
+ developerFields.push(Object.assign(Object.assign({}, field), { bytes: raw.slice() }));
357
+ }
358
+ }
359
+ if (selected) {
360
+ messages.push(Object.assign(Object.assign(Object.assign({ globalMessageNumber: definition.globalMessageNumber, messageIndex, littleEndian: definition.littleEndian }, (timestamp === undefined ? {} : { timestamp })), (compressedTimestamp === undefined ? {} : { compressedTimestamp })), { fields,
361
+ developerFields }));
362
+ }
363
+ }
364
+ return {
365
+ protocolVersion: bytes[1],
366
+ profileVersion: view.getUint16(2, true),
367
+ messages,
368
+ issues,
369
+ };
370
+ }
371
+ /** Converts a native FIT timestamp to a JavaScript epoch millisecond value. */
372
+ function fitTimestampToUnixMilliseconds(timestamp) {
373
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 0xFFFFFFFF) {
374
+ throw new RangeError('FIT timestamp must be an integer between 0 and 4294967294');
375
+ }
376
+ return FIT_EPOCH_MS + timestamp * 1000;
377
+ }
@@ -285,6 +285,7 @@ function generateFitType() {
285
285
  generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
286
286
  generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
287
287
  generateArrayProperty('raw_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
288
+ generateArrayProperty('unmapped_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
288
289
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
289
290
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
290
291
  ? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
@@ -3,6 +3,9 @@ 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
5
  export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
6
+ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
7
+ export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
8
+ export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
6
9
  export interface FitParserOptions {
7
10
  force?: boolean;
8
11
  speedUnit?: string;
@@ -15,6 +18,8 @@ export interface FitParserOptions {
15
18
  includeRawDeveloperFields?: boolean | readonly number[];
16
19
  /** Retains exact native and developer fields for all or selected global message numbers. */
17
20
  includeRawMessages?: boolean | readonly number[];
21
+ /** Retains exact bytes for fields that have no semantic profile mapping. */
22
+ includeUnmappedMessages?: boolean;
18
23
  /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
24
  rawMessagesOnly?: boolean;
20
25
  }
@@ -1,9 +1,11 @@
1
1
  import { calculateCRC, readRecord } from './binary.js';
2
2
  import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
+ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
5
+ export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
4
6
  export default class FitParser {
5
7
  constructor(options = {}) {
6
- var _a, _b, _c;
8
+ var _a, _b, _c, _d;
7
9
  this.options = {
8
10
  force: options.force != null ? options.force : true,
9
11
  speedUnit: options.speedUnit || 'm/s',
@@ -14,7 +16,8 @@ export default class FitParser {
14
16
  mode: options.mode || 'list',
15
17
  includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
16
18
  includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
17
- rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
19
+ includeUnmappedMessages: (_c = options.includeUnmappedMessages) !== null && _c !== void 0 ? _c : false,
20
+ rawMessagesOnly: (_d = options.rawMessagesOnly) !== null && _d !== void 0 ? _d : false,
18
21
  };
19
22
  }
20
23
  parseAsync(content) {
@@ -125,6 +128,7 @@ export default class FitParser {
125
128
  || Array.isArray(this.options.includeRawMessages)
126
129
  ? []
127
130
  : undefined;
131
+ const unmappedMessages = this.options.includeUnmappedMessages ? [] : undefined;
128
132
  const messageCountsByGlobalNumber = new Map();
129
133
  let loopIndex = headerLength;
130
134
  const messageTypes = [];
@@ -136,7 +140,7 @@ export default class FitParser {
136
140
  let lastStopTimestamp;
137
141
  let pausedTime = 0;
138
142
  while (loopIndex < 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);
143
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
140
144
  loopIndex = nextIndex;
141
145
  if (globalMessageNumber !== undefined) {
142
146
  const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
@@ -163,6 +167,20 @@ export default class FitParser {
163
167
  raw_value: field.rawValue,
164
168
  });
165
169
  });
170
+ if (littleEndian !== undefined
171
+ && ((recordUnmappedFields === null || recordUnmappedFields === void 0 ? void 0 : recordUnmappedFields.length) || (recordUnmappedDeveloperFields === null || recordUnmappedDeveloperFields === void 0 ? void 0 : recordUnmappedDeveloperFields.length))) {
172
+ 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
173
+ ? {}
174
+ : { compressed_timestamp: compressedTimestamp })), { fields: (recordUnmappedFields !== null && recordUnmappedFields !== void 0 ? recordUnmappedFields : []).map(field => ({
175
+ field_definition_number: field.fieldDefinitionNumber,
176
+ base_type: field.baseType,
177
+ raw_value: field.rawValue,
178
+ })), developer_fields: (recordUnmappedDeveloperFields !== null && recordUnmappedDeveloperFields !== void 0 ? recordUnmappedDeveloperFields : []).map(field => ({
179
+ developer_data_index: field.developerDataIndex,
180
+ field_definition_number: field.fieldDefinitionNumber,
181
+ raw_value: field.rawValue,
182
+ })) }));
183
+ }
166
184
  }
167
185
  if (this.options.rawMessagesOnly) {
168
186
  continue;
@@ -293,6 +311,9 @@ export default class FitParser {
293
311
  if (rawMessages) {
294
312
  fitObj.raw_messages = rawMessages;
295
313
  }
314
+ if (unmappedMessages && unmappedMessages.length > 0) {
315
+ fitObj.unmapped_messages = unmappedMessages;
316
+ }
296
317
  callback(undefined, fitObj);
297
318
  return;
298
319
  }
@@ -321,6 +342,9 @@ export default class FitParser {
321
342
  if (rawMessages) {
322
343
  fitObj.raw_messages = rawMessages;
323
344
  }
345
+ if (unmappedMessages && unmappedMessages.length > 0) {
346
+ fitObj.unmapped_messages = unmappedMessages;
347
+ }
324
348
  if (isCascadeNeeded) {
325
349
  laps = mapDataIntoLap(laps, 'records', records);
326
350
  laps = mapDataIntoLap(laps, 'lengths', lengths);
package/dist/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/fit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GARMIN_MESSAGES, GARMIN_TYPES } from './garmin_profile.generated.js';
1
+ import { PROFILE_MESSAGES, PROFILE_TYPES } from './profile.js';
2
2
  const metersInOneKilometer = 1000;
3
3
  const secondsInOneHour = 3600;
4
4
  // according to https://en.wikipedia.org/wiki/Mile
@@ -28,119 +28,9 @@ const options = {
28
28
  psi: { multiplier: psiInOneBar, offset: 0 },
29
29
  },
30
30
  };
31
- /**
32
- * Garmin fields observed in the external FIT corpus but absent from the pinned
33
- * public SDK profile. These additions may not replace standard SDK fields.
34
- */
35
- export const FIT_VENDOR_MESSAGE_EXTENSIONS = {
36
- 18: {
37
- name: 'session',
38
- 178: field('est_sweat_loss', 'uint16', 1, 'ml'),
39
- 188: field('primary_benefit', 'uint8'),
40
- 205: field('beginning_potential_stamina', 'uint8', 1, 'percent'),
41
- 206: field('ending_potential_stamina', 'uint8', 1, 'percent'),
42
- 207: field('min_stamina', 'uint8', 1, 'percent'),
43
- },
44
- 20: {
45
- name: 'record',
46
- 90: field('garmin_performance_condition', 'sint8'),
47
- 137: field('potential_stamina', 'uint8', 1, 'percent'),
48
- 138: field('stamina', 'uint8', 1, 'percent'),
49
- },
50
- 23: {
51
- name: 'device_info',
52
- 24: field('ant_id', 'uint32z'),
53
- },
54
- // Undocumented Garmin user metrics message observed in activity FIT files.
55
- 79: {
56
- name: 'user_metrics',
57
- 0: field('vo2_max', 'uint16', 1024 / 3.5, 'ml/kg/min'),
58
- 1: field('age', 'uint8', 1, 'years'),
59
- 2: field('height', 'uint8', 100, 'm'),
60
- 3: field('weight', 'uint16', 10, 'kg'),
61
- 4: field('gender', 'gender'),
62
- 6: field('max_heart_rate', 'uint8', 1, 'bpm'),
63
- 8: field('remaining_recovery_time', 'uint16'),
64
- 11: field('lthr', 'uint16', 1, 'bpm'),
65
- 12: field('ltpower', 'uint16', 1, 'watts'),
66
- 13: field('ltspeed', 'uint16', 1000, 'm/s'),
67
- 16: field('start_of_activity', 'date_time'),
68
- 19: field('first_vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
69
- 35: field('end_of_previous_activity', 'date_time'),
70
- 253: field('timestamp', 'date_time'),
71
- },
72
- // Undocumented Garmin activity metrics message observed in activity FIT files.
73
- 140: {
74
- name: 'activity_metrics',
75
- 1: field('new_max_heart_rate', 'uint8', 1, 'bpm'),
76
- 4: field('aerobic_training_effect', 'uint8', 10),
77
- 7: field('vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
78
- 9: field('recovery_time', 'uint16', 1, 'min'),
79
- 11: field('sport', 'sport'),
80
- 20: field('anaerobic_training_effect', 'uint8', 10),
81
- 29: field('first_vo2_max', 'uint32', 65536 / 3.5, 'ml/kg/min'),
82
- 41: field('primary_benefit', 'uint8'),
83
- 60: field('total_ascent', 'uint16', 1, 'm'),
84
- 61: field('total_descent', 'uint16', 1, 'm'),
85
- 62: field('avg_power', 'uint16', 1, 'watts'),
86
- 63: field('avg_heart_rate', 'uint8', 1, 'bpm'),
87
- },
88
- 312: {
89
- name: 'split',
90
- 107: field('beginning_potential_stamina', 'uint8', 1, 'percent'),
91
- 108: field('ending_potential_stamina', 'uint8', 1, 'percent'),
92
- 109: field('min_stamina', 'uint8', 1, 'percent'),
93
- },
94
- };
95
- export const FIT_VENDOR_TYPE_EXTENSIONS = {
96
- mesg_num: {
97
- 79: 'user_metrics',
98
- 140: 'activity_metrics',
99
- },
100
- };
101
- function field(name, type, scale = 1, units = '', baseType) {
102
- return Object.assign(Object.assign({ field: name, type }, (baseType ? { baseType } : {})), { scale, offset: 0, units });
103
- }
104
- function mergeVendorMessages() {
105
- const messages = Object.assign({}, GARMIN_MESSAGES);
106
- Object.entries(FIT_VENDOR_MESSAGE_EXTENSIONS).forEach(([messageIdText, extension]) => {
107
- const messageId = Number(messageIdText);
108
- const standardMessage = messages[messageId];
109
- if (!standardMessage) {
110
- messages[messageId] = extension;
111
- return;
112
- }
113
- if (standardMessage.name !== extension.name) {
114
- throw new Error(`Vendor message ${messageId} conflicts with the Garmin SDK name`);
115
- }
116
- Object.keys(extension)
117
- .filter(key => key !== 'name')
118
- .forEach((fieldId) => {
119
- if (standardMessage[Number(fieldId)]) {
120
- throw new Error(`Vendor message ${messageId}, field ${fieldId} conflicts with the Garmin SDK profile`);
121
- }
122
- });
123
- messages[messageId] = Object.assign(Object.assign({}, standardMessage), extension);
124
- });
125
- return messages;
126
- }
127
- function mergeVendorTypes() {
128
- const types = Object.fromEntries(Object.entries(GARMIN_TYPES).map(([name, values]) => [name, Object.assign({}, values)]));
129
- Object.entries(FIT_VENDOR_TYPE_EXTENSIONS).forEach(([name, extension]) => {
130
- var _a;
131
- const standardValues = (_a = types[name]) !== null && _a !== void 0 ? _a : {};
132
- Object.keys(extension).forEach((valueId) => {
133
- if (standardValues[Number(valueId)] !== undefined) {
134
- throw new Error(`Vendor type ${name}, value ${valueId} conflicts with the Garmin SDK profile`);
135
- }
136
- });
137
- types[name] = Object.assign(Object.assign({}, standardValues), extension);
138
- });
139
- return types;
140
- }
141
31
  export const FIT = {
142
32
  scConst: 180 / Math.pow(2, 31),
143
33
  options,
144
- messages: mergeVendorMessages(),
145
- types: mergeVendorTypes(),
34
+ messages: PROFILE_MESSAGES,
35
+ types: PROFILE_TYPES,
146
36
  };
@@ -213,7 +213,7 @@ export type SportEvent = 'uncategorized' | 'geocaching' | 'fitness' | 'recreatio
213
213
  export type SquatExerciseName = 'leg_press' | 'back_squat_with_body_bar' | 'back_squats' | 'weighted_back_squats' | 'balancing_squat' | 'weighted_balancing_squat' | 'barbell_back_squat' | 'barbell_box_squat' | 'barbell_front_squat' | 'barbell_hack_squat' | 'barbell_hang_squat_snatch' | 'barbell_lateral_step_up' | 'barbell_quarter_squat' | 'barbell_siff_squat' | 'barbell_squat_snatch' | 'barbell_squat_with_heels_raised' | 'barbell_stepover' | 'barbell_step_up' | 'bench_squat_with_rotational_chop' | 'weighted_bench_squat_with_rotational_chop' | 'body_weight_wall_squat' | 'weighted_wall_squat' | 'box_step_squat' | 'weighted_box_step_squat' | 'braced_squat' | 'crossed_arm_barbell_front_squat' | 'crossover_dumbbell_step_up' | 'dumbbell_front_squat' | 'dumbbell_split_squat' | 'dumbbell_squat' | 'dumbbell_squat_clean' | 'dumbbell_stepover' | 'dumbbell_step_up' | 'elevated_single_leg_squat' | 'weighted_elevated_single_leg_squat' | 'figure_four_squats' | 'weighted_figure_four_squats' | 'goblet_squat' | 'kettlebell_squat' | 'kettlebell_swing_overhead' | 'kettlebell_swing_with_flip_to_squat' | 'lateral_dumbbell_step_up' | 'one_legged_squat' | 'overhead_dumbbell_squat' | 'overhead_squat' | 'partial_single_leg_squat' | 'weighted_partial_single_leg_squat' | 'pistol_squat' | 'weighted_pistol_squat' | 'plie_slides' | 'weighted_plie_slides' | 'plie_squat' | 'weighted_plie_squat' | 'prisoner_squat' | 'weighted_prisoner_squat' | 'single_leg_bench_get_up' | 'weighted_single_leg_bench_get_up' | 'single_leg_bench_squat' | 'weighted_single_leg_bench_squat' | 'single_leg_squat_on_swiss_ball' | 'weighted_single_leg_squat_on_swiss_ball' | 'squat' | 'weighted_squat' | 'squats_with_band' | 'staggered_squat' | 'weighted_staggered_squat' | 'step_up' | 'weighted_step_up' | 'suitcase_squats' | 'sumo_squat' | 'sumo_squat_slide_in' | 'weighted_sumo_squat_slide_in' | 'sumo_squat_to_high_pull' | 'sumo_squat_to_stand' | 'weighted_sumo_squat_to_stand' | 'sumo_squat_with_rotation' | 'weighted_sumo_squat_with_rotation' | 'swiss_ball_body_weight_wall_squat' | 'weighted_swiss_ball_wall_squat' | 'thrusters' | 'uneven_squat' | 'weighted_uneven_squat' | 'waist_slimming_squat' | 'wall_ball' | 'wide_stance_barbell_squat' | 'wide_stance_goblet_squat' | 'zercher_squat' | 'kbs_overhead' | 'squat_and_side_kick' | 'squat_jumps_in_n_out' | 'pilates_plie_squats_parallel_turned_out_flat_and_heels' | 'releve_straight_leg_and_knee_bent_with_one_leg_variation' | 'alternating_box_dumbbell_step_ups' | 'dumbbell_overhead_squat_single_arm' | 'dumbbell_squat_snatch' | 'medicine_ball_squat' | 'wall_ball_squat_and_press' | 'squat_american_swing' | 'air_squat' | 'dumbbell_thrusters' | 'overhead_barbell_squat' | number;
214
214
  export type StairStepperExerciseName = 'stair_stepper' | number;
215
215
  export type StrokeType = 'no_event' | 'other' | 'serve' | 'forehand' | 'backhand' | 'smash' | number;
216
- export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'all' | number;
216
+ export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'mountain_enduro' | 'mountain_downhill' | 'all' | number;
217
217
  export type SupportedExdScreenLayouts = 'full_screen' | 'half_vertical' | 'half_horizontal' | 'half_vertical_right_split' | 'half_horizontal_bottom_split' | 'full_quarter_split' | 'half_vertical_left_split' | 'half_horizontal_top_split' | number;
218
218
  export type SuspensionExerciseName = 'chest_fly' | 'chest_press' | 'crunch' | 'curl' | 'dip' | 'face_pull' | 'glute_bridge' | 'hamstring_curl' | 'hip_drop' | 'inverted_row' | 'knee_drive_jump' | 'knee_to_chest' | 'lat_pullover' | 'lunge' | 'mountain_climber' | 'pendulum' | 'pike' | 'plank' | 'power_pull' | 'pull_up' | 'push_up' | 'reverse_mountain_climber' | 'reverse_plank' | 'rollout' | 'row' | 'side_lunge' | 'side_plank' | 'single_leg_deadlift' | 'single_leg_squat' | 'sit_up' | 'split' | 'squat' | 'squat_jump' | 'tricep_press' | 'y_fly' | number;
219
219
  export type SwimStroke = 'freestyle' | 'backstroke' | 'breaststroke' | 'butterfly' | 'drill' | 'mixed' | 'im' | 'im_by_round' | 'rimo' | number;
@@ -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;