fit-file-parser 4.0.2 → 5.0.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.
@@ -9,31 +9,48 @@ 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;
35
38
  }
36
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;
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));
53
+ }
37
54
  function addEndian(littleEndian, bytes) {
38
55
  let result = 0;
39
56
  if (!littleEndian)
@@ -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,30 +72,77 @@ 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
- function formatByType(data, type, scale, offset) {
127
+ function formatByType(data, type, scale, offset, units) {
139
128
  switch (type) {
140
129
  case 'date_time':
141
130
  case 'local_date_time':
142
131
  return new Date(data * 1000 + GarminTimeOffset);
143
132
  case 'sint32':
144
- return data * fit_js_1.FIT.scConst;
133
+ return units === 'semicircles'
134
+ ? data * fit_js_1.FIT.scConst
135
+ : scale ? data / scale + offset : data;
136
+ case 'sint8':
145
137
  case 'uint8':
138
+ case 'uint8z':
146
139
  case 'sint16':
147
- case 'uint32':
148
140
  case 'uint16':
141
+ case 'uint16z':
142
+ case 'uint32':
143
+ case 'uint32z':
144
+ case 'float32':
145
+ case 'float64':
149
146
  return scale ? data / scale + offset : data;
150
147
  case 'uint32_array':
151
148
  case 'uint16_array':
@@ -160,6 +157,16 @@ function formatByType(data, type, scale, offset) {
160
157
  });
161
158
  }
162
159
  return scale ? data / scale + offset : data;
160
+ case 'exercise_category_array':
161
+ if (Array.isArray(data)) {
162
+ return data.map((dataItem) => {
163
+ if (isInvalidValue(dataItem, 'uint16')) {
164
+ return null;
165
+ }
166
+ return formatByType(dataItem, 'exercise_category', scale, offset);
167
+ });
168
+ }
169
+ return formatByType(data, 'exercise_category', scale, offset);
163
170
  default:
164
171
  {
165
172
  const typeMap = fit_js_1.FIT.types[type];
@@ -192,7 +199,7 @@ function formatByType(data, type, scale, offset) {
192
199
  dataItem.value = data & Number(key);
193
200
  }
194
201
  else {
195
- dataItem[value] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
202
+ dataItem[value] = (data & Number(key)) !== 0;
196
203
  }
197
204
  }
198
205
  return dataItem;
@@ -218,10 +225,9 @@ function isInvalidValue(data, type) {
218
225
  case 'string':
219
226
  return data === 0x00;
220
227
  case 'float32':
221
- return data === 0xFFFFFFFF;
228
+ return Number.isNaN(data);
222
229
  case 'float64':
223
- // eslint-disable-next-line no-loss-of-precision
224
- return data === 0xFFFFFFFFFFFFFFFF;
230
+ return Number.isNaN(data);
225
231
  case 'uint8z':
226
232
  return data === 0x00;
227
233
  case 'uint16z':
@@ -249,6 +255,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
249
255
  const baseType = fit_js_1.FIT.types.fit_base_type[baseTypeNo];
250
256
  return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
251
257
  }
258
+ function formatFieldValue(data, fDef, options, fields) {
259
+ var _a, _b, _c;
260
+ const field = fDef.name;
261
+ const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
262
+ const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
263
+ if (Array.isArray(data)
264
+ && !String(fDef.type).endsWith('_array')
265
+ && fDef.type !== 'exercise_category_array') {
266
+ const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : fit_js_1.FIT.types.fit_base_type[fDef.baseTypeNo];
267
+ return data.map((item) => {
268
+ if (isInvalidValue(item, rawType)) {
269
+ return null;
270
+ }
271
+ return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
272
+ });
273
+ }
274
+ return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
275
+ }
276
+ function isOutputFieldName(field) {
277
+ return field !== 'unknown' && field !== '' && field !== undefined;
278
+ }
252
279
  function convertTo(data, unitsList, unitName) {
253
280
  const options = fit_js_1.FIT.options[unitsList];
254
281
  const unit = options[unitName];
@@ -276,7 +303,7 @@ function applyOptions(data, field, options, fields) {
276
303
  case 'vertical_speed':
277
304
  case 'avg_speed':
278
305
  case 'max_speed':
279
- case 'speed_1s':
306
+ case 'speed1s':
280
307
  case 'ball_speed':
281
308
  case 'enhanced_avg_speed':
282
309
  case 'enhanced_max_speed':
@@ -307,8 +334,10 @@ function applyOptions(data, field, options, fields) {
307
334
  return convertTo(data, 'lengthUnits', options.lengthUnit);
308
335
  case 'temperature':
309
336
  case 'min_temperature':
337
+ case 'temperature_min':
310
338
  case 'avg_temperature':
311
339
  case 'max_temperature':
340
+ case 'temperature_max':
312
341
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
313
342
  case 'pressure':
314
343
  case 'start_pressure':
@@ -325,22 +354,8 @@ function applyOptions(data, field, options, fields) {
325
354
  return data;
326
355
  }
327
356
  }
328
- function applyGarminProductName(fields) {
329
- if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
330
- return;
331
- }
332
- const product = fields.product;
333
- if (typeof product !== 'number') {
334
- return;
335
- }
336
- const productName = fit_js_1.FIT.types.garmin_product[product];
337
- if (typeof productName === 'string') {
338
- // Keep the raw protocol ID and expose the SDK product name separately.
339
- fields.product_name = productName;
340
- }
341
- }
342
357
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
343
- var _a, _b, _c, _d;
358
+ var _a, _b, _c, _d, _e, _f, _g;
344
359
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
345
360
  if (!description) {
346
361
  developerFieldDef.resolvedFieldDef = undefined;
@@ -351,9 +366,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
351
366
  && developerFieldDef.resolvedFieldDef) {
352
367
  return developerFieldDef.resolvedFieldDef;
353
368
  }
354
- const baseType = description.fit_base_type_id;
369
+ const describedBaseType = description.fit_base_type_id;
370
+ const baseType = typeof describedBaseType === 'number'
371
+ ? describedBaseType
372
+ : Number((_b = Object.entries(fit_js_1.FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
355
373
  const type = fit_js_1.FIT.types.fit_base_type[baseType];
356
- if (typeof baseType !== 'number' || type === undefined) {
374
+ if (!Number.isInteger(baseType) || type === undefined) {
357
375
  developerFieldDef.resolvedFieldDef = undefined;
358
376
  developerFieldDef.resolvedFrom = undefined;
359
377
  if (options.force) {
@@ -363,15 +381,22 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
363
381
  }
364
382
  const resolvedFieldDef = {
365
383
  type,
384
+ rawType: type,
366
385
  fDefNo: developerFieldDef.fieldDefinitionNumber,
367
386
  size: developerFieldDef.size,
387
+ array: type !== 'string'
388
+ && developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
368
389
  endianAbility: (baseType & 128) === 128,
369
390
  littleEndian,
370
391
  baseTypeNo: baseType,
371
- name: (_b = description.field_name) !== null && _b !== void 0 ? _b : '',
392
+ name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
372
393
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
373
- scale: (_c = description.scale) !== null && _c !== void 0 ? _c : 1,
374
- offset: (_d = description.offset) !== null && _d !== void 0 ? _d : 0,
394
+ scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
395
+ // FIT developer-field descriptions use `raw / scale - offset`. This
396
+ // parser's formatter retains the legacy equivalent signed offset and adds
397
+ // it after scaling.
398
+ offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
399
+ units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
375
400
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
376
401
  developerDataIndex: developerFieldDef.developerDataIndex,
377
402
  isDeveloperField: true,
@@ -380,11 +405,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
380
405
  developerFieldDef.resolvedFrom = description;
381
406
  return resolvedFieldDef;
382
407
  }
383
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
384
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
408
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
409
+ var _a, _b, _c;
385
410
  const recordHeader = blob[startIndex];
386
411
  let localMessageType = recordHeader & 15;
387
- if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
412
+ const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
413
+ if (isCompressedTimestamp) {
388
414
  // compressed timestamp
389
415
  localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
390
416
  }
@@ -412,19 +438,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
412
438
  for (let i = 0; i < numberOfFields; i++) {
413
439
  const fDefIndex = startIndex + 6 + i * 3;
414
440
  const baseType = blob[fDefIndex + 2];
415
- const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
441
+ const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
442
+ const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
443
+ const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
416
444
  const fDef = {
417
- type,
445
+ type: profileCompatible ? type : wireType,
446
+ rawType: wireType,
418
447
  fDefNo: blob[fDefIndex],
419
448
  size: blob[fDefIndex + 1],
449
+ array: profileCompatible
450
+ ? array === true || String(type).endsWith('_array')
451
+ : false,
420
452
  endianAbility: (baseType & 128) === 128,
421
453
  littleEndian: lEnd,
422
454
  baseTypeNo: baseType,
423
- name: field,
455
+ name: profileCompatible ? field : '',
424
456
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
425
- scale,
426
- offset,
427
- requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
457
+ scale: profileCompatible ? scale : null,
458
+ offset: profileCompatible ? offset : 0,
459
+ units: profileCompatible ? units : '',
460
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
428
461
  };
429
462
  mTypeDef.fieldDefs.push(fDef);
430
463
  }
@@ -446,8 +479,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
446
479
  };
447
480
  }
448
481
  const messageType = messageTypes[localMessageType] || messageTypes[0];
449
- // TODO: handle compressed header ((recordHeader & 128) == 128)
450
- // uncompressed header
451
482
  let messageSize = 0;
452
483
  let readDataFromIndex = startIndex + 1;
453
484
  const fields = {};
@@ -458,10 +489,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
458
489
  let validFieldCount = 0;
459
490
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
460
491
  const fDef = messageType.fieldDefs[i];
461
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
462
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
492
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
493
+ if (data !== InvalidFieldData
494
+ && !isInvalidValue(data, fDef.type)
495
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
463
496
  rawData[i] = data;
464
497
  validFieldCount++;
498
+ if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
499
+ decoderState.lastTimestamp = data;
500
+ }
465
501
  }
466
502
  else {
467
503
  rawData[i] = InvalidFieldData;
@@ -474,8 +510,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
474
510
  const rawDataIndex = messageType.fieldDefs.length + i;
475
511
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
476
512
  if (fDef) {
477
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
478
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
513
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
514
+ if (data !== InvalidFieldData
515
+ && !isInvalidValue(data, fDef.type)
516
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
479
517
  rawData[rawDataIndex] = data;
480
518
  validFieldCount++;
481
519
  }
@@ -495,9 +533,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
495
533
  continue;
496
534
  }
497
535
  const fDef = messageType.fieldDefs[i];
498
- const field = fDef.name;
499
- if (field !== 'unknown' && field !== '' && field !== undefined) {
500
- fields[field] = data;
536
+ if (isOutputFieldName(fDef.name)) {
537
+ fields[fDef.name] = data;
501
538
  }
502
539
  }
503
540
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -517,20 +554,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
517
554
  continue;
518
555
  }
519
556
  const fDef = messageType.fieldDefs[i];
520
- if (fDef.isDeveloperField) {
521
- const field = fDef.name;
522
- const { type } = fDef;
523
- const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
524
- const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
525
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
526
- }
527
- else {
528
- const { name: field, type } = fDef;
529
- const scale = (_f = fDef.scale) !== null && _f !== void 0 ? _f : null;
530
- const offset = (_g = fDef.offset) !== null && _g !== void 0 ? _g : 0;
531
- if (field !== 'unknown' && field !== '' && field !== undefined) {
532
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
533
- }
557
+ if (isOutputFieldName(fDef.name)) {
558
+ fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
534
559
  }
535
560
  }
536
561
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -541,10 +566,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
541
566
  }
542
567
  const field = fDef.name;
543
568
  if (field !== 'unknown' && field !== '') {
544
- fields[field] = applyOptions(formatByType(data, fDef.type, (_h = fDef.scale) !== null && _h !== void 0 ? _h : null, (_j = fDef.offset) !== null && _j !== void 0 ? _j : 0), field, options, fields);
569
+ fields[field] = formatFieldValue(data, fDef, options, fields);
570
+ }
571
+ }
572
+ if (isCompressedTimestamp) {
573
+ const previousTimestamp = decoderState.lastTimestamp;
574
+ if (previousTimestamp === undefined) {
575
+ if (!options.force) {
576
+ throw new Error('Compressed timestamp requires a previous timestamp');
577
+ }
578
+ }
579
+ else {
580
+ const timeOffset = recordHeader & CompressedTimestampMask;
581
+ const previousOffset = previousTimestamp & CompressedTimestampMask;
582
+ const rollover = timeOffset < previousOffset ? 0x20 : 0;
583
+ const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
584
+ decoderState.lastTimestamp = timestamp;
585
+ fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
586
+ validFieldCount++;
545
587
  }
546
588
  }
547
- applyGarminProductName(fields);
548
589
  if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
549
590
  fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
550
591
  fields.timer_time = fields.elapsed_time - pausedTime;
@@ -555,17 +596,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
555
596
  developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
556
597
  }
557
598
  if (message.name === 'monitoring') {
558
- // TODO weirdly uses global variables?
559
- // we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
560
- if (fields.timestamp) {
561
- monitoring_timestamp = fields.timestamp;
562
- fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
599
+ const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
600
+ const rawTimestamp = timestampFieldIndex >= 0
601
+ ? rawData[timestampFieldIndex]
602
+ : InvalidFieldData;
603
+ if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
604
+ decoderState.monitoringTimestamp = rawTimestamp;
605
+ fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
563
606
  }
564
- if (fields.timestamp16 && !fields.timestamp) {
565
- monitoring_timestamp
566
- += (fields.timestamp16 - (monitoring_timestamp & 0xFFFF)) & 0xFFFF;
567
- // fields.timestamp = monitoring_timestamp;
568
- fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
607
+ else if (fields.timestamp16
608
+ && decoderState.monitoringTimestamp !== undefined
609
+ && !fields.timestamp) {
610
+ decoderState.monitoringTimestamp
611
+ += (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
612
+ fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
569
613
  }
570
614
  }
571
615
  return {