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