fit-file-parser 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/binary.js CHANGED
@@ -3,30 +3,47 @@ import { FIT } from './fit.js';
3
3
  import { getFitMessage, getFitMessageBaseType } from './messages.js';
4
4
  const CompressedLocalMsgNumMask = 0x60;
5
5
  const CompressedHeaderMask = 0x80;
6
+ const CompressedTimestampMask = 0x1F;
6
7
  const GarminTimeOffset = 631065600000;
7
- let monitoring_timestamp = 0;
8
8
  const InvalidFieldData = Symbol('invalid FIT field data');
9
9
  const formatTypeMetadata = new Map();
10
- function requiresBoundedEndianDataView(type, size) {
10
+ const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
11
+ function baseTypeSize(type) {
11
12
  switch (type) {
13
+ case 'enum':
14
+ case 'sint8':
15
+ case 'uint8':
16
+ case 'uint8z':
17
+ case 'byte':
18
+ return 1;
12
19
  case 'sint16':
13
20
  case 'uint16':
14
21
  case 'uint16z':
15
- return size < 2;
22
+ return 2;
16
23
  case 'sint32':
17
24
  case 'uint32':
18
25
  case 'uint32z':
19
26
  case 'float32':
20
- return size < 4;
27
+ return 4;
21
28
  case 'float64':
22
- return size < 8;
23
- case 'uint16_array':
24
- return size % 2 !== 0;
25
- case 'uint32_array':
26
- return size % 4 !== 0;
29
+ return 8;
27
30
  default:
28
- return false;
31
+ return undefined;
32
+ }
33
+ }
34
+ function requiresBoundedEndianDataView(type, size) {
35
+ const elementSize = baseTypeSize(type);
36
+ return elementSize !== undefined && size % elementSize !== 0;
37
+ }
38
+ function areProfileBaseTypesCompatible(profileType, wireType) {
39
+ if (profileType === undefined || profileType === wireType) {
40
+ return true;
29
41
  }
42
+ // FIT producers commonly interchange enum, uint8, and byte definitions.
43
+ // They have the same width and value representation; keeping the semantic
44
+ // profile type is safe while still rejecting width and sign conflicts.
45
+ return uint8CompatibleTypes.has(String(profileType))
46
+ && uint8CompatibleTypes.has(String(wireType));
30
47
  }
31
48
  export function addEndian(littleEndian, bytes) {
32
49
  let result = 0;
@@ -37,77 +54,10 @@ export function addEndian(littleEndian, bytes) {
37
54
  }
38
55
  return result;
39
56
  }
40
- function readData(blob, dataView, fDef, startIndex, options) {
41
- var _a;
42
- if (fDef.type === 'uint8_array') {
43
- const array8 = [];
44
- for (let i = 0; i < fDef.size; i++) {
45
- array8.push(blob[startIndex + i]);
46
- }
47
- return array8;
48
- }
49
- if (fDef.endianAbility) {
50
- const requiresBoundedDataView = (_a = fDef.requiresBoundedDataView) !== null && _a !== void 0 ? _a : (fDef.requiresBoundedDataView = requiresBoundedEndianDataView(fDef.type, fDef.size));
51
- let fieldDataView = dataView;
52
- let fieldStartIndex = startIndex;
53
- if (startIndex < 0
54
- || startIndex + fDef.size > blob.length
55
- || startIndex + fDef.size > dataView.byteLength) {
56
- const paddedField = new Uint8Array(fDef.size);
57
- for (let index = 0; index < fDef.size; index++) {
58
- paddedField[index] = blob[startIndex + index];
59
- }
60
- fieldDataView = new DataView(paddedField.buffer);
61
- fieldStartIndex = 0;
62
- }
63
- else if (requiresBoundedDataView) {
64
- fieldDataView = new DataView(dataView.buffer, dataView.byteOffset + startIndex, fDef.size);
65
- fieldStartIndex = 0;
66
- }
67
- try {
68
- switch (fDef.type) {
69
- case 'sint16':
70
- return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
71
- case 'uint16':
72
- case 'uint16z':
73
- return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
74
- case 'sint32':
75
- return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
76
- case 'uint32':
77
- case 'uint32z':
78
- return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
79
- case 'float32':
80
- return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
81
- case 'float64':
82
- return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
83
- case 'uint32_array': {
84
- const array32 = [];
85
- for (let i = 0; i < fDef.size; i += 4) {
86
- array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
87
- }
88
- return array32;
89
- }
90
- case 'uint16_array': {
91
- const array16 = [];
92
- for (let i = 0; i < fDef.size; i += 2) {
93
- array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
94
- }
95
- return array16;
96
- }
97
- }
98
- }
99
- catch (e) {
100
- if (!options.force) {
101
- throw e;
102
- }
103
- }
104
- const temp = [];
105
- for (let i = 0; i < fDef.size; i++) {
106
- temp.push(blob[startIndex + i]);
107
- }
108
- return addEndian(fDef.littleEndian, temp);
109
- }
110
- if (fDef.type === 'string') {
57
+ function readData(blob, dataView, fDef, startIndex) {
58
+ var _a, _b;
59
+ const rawType = (_b = (_a = fDef.rawType) !== null && _a !== void 0 ? _a : FIT.types.fit_base_type[fDef.baseTypeNo]) !== null && _b !== void 0 ? _b : fDef.type;
60
+ if (rawType === 'string') {
111
61
  const temp = [];
112
62
  for (let i = 0; i < fDef.size; i++) {
113
63
  if (blob[startIndex + i]) {
@@ -116,18 +66,57 @@ function readData(blob, dataView, fDef, startIndex, options) {
116
66
  }
117
67
  return Buffer.from(temp).toString('utf-8');
118
68
  }
119
- if (fDef.type === 'byte_array') {
120
- const temp = [];
121
- for (let i = 0; i < fDef.size; i++) {
122
- temp.push(blob[startIndex + i]);
123
- }
124
- return temp;
69
+ const elementSize = baseTypeSize(rawType);
70
+ if (elementSize === undefined) {
71
+ return InvalidFieldData;
72
+ }
73
+ if (fDef.size < elementSize
74
+ || fDef.size % elementSize !== 0
75
+ || startIndex < 0
76
+ || startIndex + fDef.size > blob.length
77
+ || startIndex + fDef.size > dataView.byteLength) {
78
+ return InvalidFieldData;
125
79
  }
126
- if (fDef.type === 'sint8') {
127
- const val = blob[startIndex];
128
- return val > 127 ? val - 256 : val;
80
+ const isArray = fDef.array === true
81
+ || String(fDef.type).endsWith('_array')
82
+ || fDef.type === 'exercise_category_array'
83
+ || (fDef.isDeveloperField === true && fDef.size > elementSize);
84
+ const elementCount = isArray ? fDef.size / elementSize : 1;
85
+ const values = [];
86
+ for (let elementIndex = 0; elementIndex < elementCount; elementIndex++) {
87
+ const offset = startIndex + elementIndex * elementSize;
88
+ switch (rawType) {
89
+ case 'sint8': {
90
+ const value = blob[offset];
91
+ values.push(value > 127 ? value - 256 : value);
92
+ break;
93
+ }
94
+ case 'sint16':
95
+ values.push(dataView.getInt16(offset, fDef.littleEndian));
96
+ break;
97
+ case 'uint16':
98
+ case 'uint16z':
99
+ values.push(dataView.getUint16(offset, fDef.littleEndian));
100
+ break;
101
+ case 'sint32':
102
+ values.push(dataView.getInt32(offset, fDef.littleEndian));
103
+ break;
104
+ case 'uint32':
105
+ case 'uint32z':
106
+ values.push(dataView.getUint32(offset, fDef.littleEndian));
107
+ break;
108
+ case 'float32':
109
+ values.push(dataView.getFloat32(offset, fDef.littleEndian));
110
+ break;
111
+ case 'float64':
112
+ values.push(dataView.getFloat64(offset, fDef.littleEndian));
113
+ break;
114
+ default:
115
+ values.push(blob[offset]);
116
+ break;
117
+ }
129
118
  }
130
- return blob[startIndex];
119
+ return isArray ? values : values[0];
131
120
  }
132
121
  function formatByType(data, type, scale, offset) {
133
122
  switch (type) {
@@ -154,6 +143,16 @@ function formatByType(data, type, scale, offset) {
154
143
  });
155
144
  }
156
145
  return scale ? data / scale + offset : data;
146
+ case 'exercise_category_array':
147
+ if (Array.isArray(data)) {
148
+ return data.map((dataItem) => {
149
+ if (isInvalidValue(dataItem, 'uint16')) {
150
+ return null;
151
+ }
152
+ return formatByType(dataItem, 'exercise_category', scale, offset);
153
+ });
154
+ }
155
+ return formatByType(data, 'exercise_category', scale, offset);
157
156
  default:
158
157
  {
159
158
  const typeMap = FIT.types[type];
@@ -212,10 +211,9 @@ function isInvalidValue(data, type) {
212
211
  case 'string':
213
212
  return data === 0x00;
214
213
  case 'float32':
215
- return data === 0xFFFFFFFF;
214
+ return Number.isNaN(data);
216
215
  case 'float64':
217
- // eslint-disable-next-line no-loss-of-precision
218
- return data === 0xFFFFFFFFFFFFFFFF;
216
+ return Number.isNaN(data);
219
217
  case 'uint8z':
220
218
  return data === 0x00;
221
219
  case 'uint16z':
@@ -243,6 +241,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
243
241
  const baseType = FIT.types.fit_base_type[baseTypeNo];
244
242
  return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
245
243
  }
244
+ function formatFieldValue(data, fDef, options, fields) {
245
+ var _a, _b, _c;
246
+ const field = fDef.name;
247
+ const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
248
+ const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
249
+ if (Array.isArray(data)
250
+ && !String(fDef.type).endsWith('_array')
251
+ && fDef.type !== 'exercise_category_array') {
252
+ const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : FIT.types.fit_base_type[fDef.baseTypeNo];
253
+ return data.map((item) => {
254
+ if (isInvalidValue(item, rawType)) {
255
+ return null;
256
+ }
257
+ return applyOptions(formatByType(item, fDef.type, scale, offset), field, options, fields);
258
+ });
259
+ }
260
+ return applyOptions(formatByType(data, fDef.type, scale, offset), field, options, fields);
261
+ }
262
+ function isOutputFieldName(field) {
263
+ return field !== 'unknown' && field !== '' && field !== undefined;
264
+ }
246
265
  function convertTo(data, unitsList, unitName) {
247
266
  const options = FIT.options[unitsList];
248
267
  const unit = options[unitName];
@@ -301,8 +320,10 @@ function applyOptions(data, field, options, fields) {
301
320
  return convertTo(data, 'lengthUnits', options.lengthUnit);
302
321
  case 'temperature':
303
322
  case 'min_temperature':
323
+ case 'temperature_min':
304
324
  case 'avg_temperature':
305
325
  case 'max_temperature':
326
+ case 'temperature_max':
306
327
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
307
328
  case 'pressure':
308
329
  case 'start_pressure':
@@ -333,11 +354,59 @@ function applyGarminProductName(fields) {
333
354
  fields.product_name = productName;
334
355
  }
335
356
  }
336
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
337
- var _a, _b, _c, _d, _e;
357
+ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
358
+ var _a, _b, _c, _d, _e, _f;
359
+ const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
360
+ if (!description) {
361
+ developerFieldDef.resolvedFieldDef = undefined;
362
+ developerFieldDef.resolvedFrom = undefined;
363
+ return undefined;
364
+ }
365
+ if (developerFieldDef.resolvedFrom === description
366
+ && developerFieldDef.resolvedFieldDef) {
367
+ return developerFieldDef.resolvedFieldDef;
368
+ }
369
+ const describedBaseType = description.fit_base_type_id;
370
+ const baseType = typeof describedBaseType === 'number'
371
+ ? describedBaseType
372
+ : Number((_b = Object.entries(FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
373
+ const type = FIT.types.fit_base_type[baseType];
374
+ if (!Number.isInteger(baseType) || type === undefined) {
375
+ developerFieldDef.resolvedFieldDef = undefined;
376
+ developerFieldDef.resolvedFrom = undefined;
377
+ if (options.force) {
378
+ return undefined;
379
+ }
380
+ throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
381
+ }
382
+ const resolvedFieldDef = {
383
+ type,
384
+ rawType: type,
385
+ fDefNo: developerFieldDef.fieldDefinitionNumber,
386
+ size: developerFieldDef.size,
387
+ array: type !== 'string'
388
+ && developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
389
+ endianAbility: (baseType & 128) === 128,
390
+ littleEndian,
391
+ baseTypeNo: baseType,
392
+ name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
393
+ dataType: getFitMessageBaseType(baseType & 15),
394
+ scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
395
+ offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
396
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
397
+ developerDataIndex: developerFieldDef.developerDataIndex,
398
+ isDeveloperField: true,
399
+ };
400
+ developerFieldDef.resolvedFieldDef = resolvedFieldDef;
401
+ developerFieldDef.resolvedFrom = description;
402
+ return resolvedFieldDef;
403
+ }
404
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
405
+ var _a, _b, _c;
338
406
  const recordHeader = blob[startIndex];
339
407
  let localMessageType = recordHeader & 15;
340
- if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
408
+ const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
409
+ if (isCompressedTimestamp) {
341
410
  // compressed timestamp
342
411
  localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
343
412
  }
@@ -358,63 +427,45 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
358
427
  ]),
359
428
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
360
429
  fieldDefs: [],
430
+ developerFieldDefs: [],
361
431
  rawData: [],
362
432
  };
363
433
  const message = getFitMessage(mTypeDef.globalMessageNumber);
364
434
  for (let i = 0; i < numberOfFields; i++) {
365
435
  const fDefIndex = startIndex + 6 + i * 3;
366
436
  const baseType = blob[fDefIndex + 2];
367
- const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
437
+ const wireType = FIT.types.fit_base_type[baseType];
438
+ const { field, type, baseType: profileBaseType, array, scale, offset, aliases, } = message.getAttributes(blob[fDefIndex]);
439
+ const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
368
440
  const fDef = {
369
- type,
441
+ type: profileCompatible ? type : wireType,
442
+ rawType: wireType,
370
443
  fDefNo: blob[fDefIndex],
371
444
  size: blob[fDefIndex + 1],
445
+ array: profileCompatible
446
+ ? array === true || String(type).endsWith('_array')
447
+ : false,
372
448
  endianAbility: (baseType & 128) === 128,
373
449
  littleEndian: lEnd,
374
450
  baseTypeNo: baseType,
375
- name: field,
451
+ name: profileCompatible ? field : '',
376
452
  dataType: getFitMessageBaseType(baseType & 15),
377
- scale,
378
- offset,
379
- requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
453
+ scale: profileCompatible ? scale : null,
454
+ offset: profileCompatible ? offset : 0,
455
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
456
+ aliases: profileCompatible ? aliases : undefined,
380
457
  };
381
458
  mTypeDef.fieldDefs.push(fDef);
382
459
  }
383
- // numberOfDeveloperDataFields = 0 so it wont crash here and wont loop
384
460
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
385
- // If we fail to parse then try catch
386
- try {
387
- const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
388
- const fieldNum = blob[fDefIndex];
389
- const size = blob[fDefIndex + 1];
390
- const devDataIndex = blob[fDefIndex + 2];
391
- const devDef = developerFields[devDataIndex][fieldNum];
392
- const baseType = devDef.fit_base_type_id;
393
- const fDef = {
394
- type: FIT.types.fit_base_type[baseType],
395
- fDefNo: fieldNum,
396
- size,
397
- endianAbility: (baseType & 128) === 128,
398
- littleEndian: lEnd,
399
- baseTypeNo: baseType,
400
- name: devDef.field_name,
401
- dataType: getFitMessageBaseType(baseType & 15),
402
- scale: devDef.scale || 1,
403
- offset: devDef.offset || 0,
404
- requiresBoundedDataView: requiresBoundedEndianDataView(FIT.types.fit_base_type[baseType], size),
405
- developerDataIndex: devDataIndex,
406
- isDeveloperField: true,
407
- };
408
- mTypeDef.fieldDefs.push(fDef);
409
- }
410
- catch (e) {
411
- if (options.force) {
412
- continue;
413
- }
414
- throw e;
415
- }
461
+ const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
462
+ (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
463
+ fieldDefinitionNumber: blob[fDefIndex],
464
+ size: blob[fDefIndex + 1],
465
+ developerDataIndex: blob[fDefIndex + 2],
466
+ });
416
467
  }
417
- mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
468
+ mTypeDef.rawData = Array.from({ length: mTypeDef.numberOfFields }, () => InvalidFieldData);
418
469
  messageTypes[localMessageType] = mTypeDef;
419
470
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
420
471
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -424,20 +475,25 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
424
475
  };
425
476
  }
426
477
  const messageType = messageTypes[localMessageType] || messageTypes[0];
427
- // TODO: handle compressed header ((recordHeader & 128) == 128)
428
- // uncompressed header
429
478
  let messageSize = 0;
430
479
  let readDataFromIndex = startIndex + 1;
431
480
  const fields = {};
432
481
  const message = getFitMessage(messageType.globalMessageNumber);
433
- const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
482
+ const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
483
+ const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
484
+ const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
434
485
  let validFieldCount = 0;
435
486
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
436
487
  const fDef = messageType.fieldDefs[i];
437
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
438
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
488
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
489
+ if (data !== InvalidFieldData
490
+ && !isInvalidValue(data, fDef.type)
491
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
439
492
  rawData[i] = data;
440
493
  validFieldCount++;
494
+ if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
495
+ decoderState.lastTimestamp = data;
496
+ }
441
497
  }
442
498
  else {
443
499
  rawData[i] = InvalidFieldData;
@@ -445,14 +501,53 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
445
501
  readDataFromIndex += fDef.size;
446
502
  messageSize += fDef.size;
447
503
  }
504
+ for (let i = 0; i < developerFieldDefs.length; i++) {
505
+ const developerFieldDef = developerFieldDefs[i];
506
+ const rawDataIndex = messageType.fieldDefs.length + i;
507
+ const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
508
+ if (fDef) {
509
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
510
+ if (data !== InvalidFieldData
511
+ && !isInvalidValue(data, fDef.type)
512
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
513
+ rawData[rawDataIndex] = data;
514
+ validFieldCount++;
515
+ }
516
+ else {
517
+ rawData[rawDataIndex] = InvalidFieldData;
518
+ }
519
+ }
520
+ else {
521
+ rawData[rawDataIndex] = InvalidFieldData;
522
+ }
523
+ readDataFromIndex += developerFieldDef.size;
524
+ messageSize += developerFieldDef.size;
525
+ }
448
526
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
449
527
  const data = rawData[i];
450
528
  if (data === InvalidFieldData) {
451
529
  continue;
452
530
  }
453
531
  const fDef = messageType.fieldDefs[i];
532
+ if (isOutputFieldName(fDef.name)) {
533
+ fields[fDef.name] = data;
534
+ }
535
+ if (fDef.aliases) {
536
+ for (const alias of fDef.aliases) {
537
+ if (isOutputFieldName(alias.field)) {
538
+ fields[alias.field] = data;
539
+ }
540
+ }
541
+ }
542
+ }
543
+ for (let i = 0; i < developerFieldDefs.length; i++) {
544
+ const data = rawData[messageType.fieldDefs.length + i];
545
+ const fDef = developerFieldDefs[i].resolvedFieldDef;
546
+ if (data === InvalidFieldData || !fDef) {
547
+ continue;
548
+ }
454
549
  const field = fDef.name;
455
- if (field !== 'unknown' && field !== '' && field !== undefined) {
550
+ if (field !== 'unknown' && field !== '') {
456
551
  fields[field] = data;
457
552
  }
458
553
  }
@@ -462,21 +557,44 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
462
557
  continue;
463
558
  }
464
559
  const fDef = messageType.fieldDefs[i];
465
- if (fDef.isDeveloperField) {
466
- const field = fDef.name;
467
- const { type } = fDef;
468
- const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
469
- const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
470
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
560
+ if (isOutputFieldName(fDef.name)) {
561
+ fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
471
562
  }
472
- else {
473
- const { name: field, type } = fDef;
474
- const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
475
- const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
476
- if (field !== 'unknown' && field !== '' && field !== undefined) {
477
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
563
+ if (fDef.aliases) {
564
+ for (const alias of fDef.aliases) {
565
+ if (isOutputFieldName(alias.field)) {
566
+ fields[alias.field] = formatFieldValue(data, Object.assign(Object.assign(Object.assign({}, fDef), alias), { name: alias.field, aliases: undefined }), options, fields);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ for (let i = 0; i < developerFieldDefs.length; i++) {
572
+ const data = rawData[messageType.fieldDefs.length + i];
573
+ const fDef = developerFieldDefs[i].resolvedFieldDef;
574
+ if (data === InvalidFieldData || !fDef) {
575
+ continue;
576
+ }
577
+ const field = fDef.name;
578
+ if (field !== 'unknown' && field !== '') {
579
+ fields[field] = formatFieldValue(data, fDef, options, fields);
580
+ }
581
+ }
582
+ if (isCompressedTimestamp) {
583
+ const previousTimestamp = decoderState.lastTimestamp;
584
+ if (previousTimestamp === undefined) {
585
+ if (!options.force) {
586
+ throw new Error('Compressed timestamp requires a previous timestamp');
478
587
  }
479
588
  }
589
+ else {
590
+ const timeOffset = recordHeader & CompressedTimestampMask;
591
+ const previousOffset = previousTimestamp & CompressedTimestampMask;
592
+ const rollover = timeOffset < previousOffset ? 0x20 : 0;
593
+ const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
594
+ decoderState.lastTimestamp = timestamp;
595
+ fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
596
+ validFieldCount++;
597
+ }
480
598
  }
481
599
  applyGarminProductName(fields);
482
600
  if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
@@ -489,17 +607,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
489
607
  developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
490
608
  }
491
609
  if (message.name === 'monitoring') {
492
- // TODO weirdly uses global variables?
493
- // we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
494
- if (fields.timestamp) {
495
- monitoring_timestamp = fields.timestamp;
496
- fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
610
+ const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
611
+ const rawTimestamp = timestampFieldIndex >= 0
612
+ ? rawData[timestampFieldIndex]
613
+ : InvalidFieldData;
614
+ if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
615
+ decoderState.monitoringTimestamp = rawTimestamp;
616
+ fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
497
617
  }
498
- if (fields.timestamp16 && !fields.timestamp) {
499
- monitoring_timestamp
500
- += (fields.timestamp16 - (monitoring_timestamp & 0xFFFF)) & 0xFFFF;
501
- // fields.timestamp = monitoring_timestamp;
502
- fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
618
+ else if (fields.timestamp16
619
+ && decoderState.monitoringTimestamp !== undefined
620
+ && !fields.timestamp) {
621
+ decoderState.monitoringTimestamp
622
+ += (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
623
+ fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
503
624
  }
504
625
  }
505
626
  return {
@@ -7,10 +7,22 @@ export interface MessageTypeDefinition {
7
7
  globalMessageNumber: number;
8
8
  numberOfFields: number;
9
9
  fieldDefs: FieldDefinition[];
10
+ developerFieldDefs?: DeveloperFieldDefinition[];
10
11
  rawData?: any[];
11
12
  }
13
+ export interface DeveloperFieldDefinition {
14
+ developerDataIndex: number;
15
+ fieldDefinitionNumber: number;
16
+ size: number;
17
+ resolvedFieldDef?: FieldDefinition;
18
+ resolvedFrom?: unknown;
19
+ }
20
+ export interface DecoderState {
21
+ lastTimestamp?: number;
22
+ monitoringTimestamp?: number;
23
+ }
12
24
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
13
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
25
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
14
26
  messageType: MesgNum | '';
15
27
  nextIndex: number;
16
28
  message?: any;