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.
package/dist/binary.js CHANGED
@@ -3,31 +3,48 @@ 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;
29
32
  }
30
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;
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));
47
+ }
31
48
  export function addEndian(littleEndian, bytes) {
32
49
  let result = 0;
33
50
  if (!littleEndian)
@@ -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,30 +66,77 @@ 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
- function formatByType(data, type, scale, offset) {
121
+ function formatByType(data, type, scale, offset, units) {
133
122
  switch (type) {
134
123
  case 'date_time':
135
124
  case 'local_date_time':
136
125
  return new Date(data * 1000 + GarminTimeOffset);
137
126
  case 'sint32':
138
- return data * FIT.scConst;
127
+ return units === 'semicircles'
128
+ ? data * FIT.scConst
129
+ : scale ? data / scale + offset : data;
130
+ case 'sint8':
139
131
  case 'uint8':
132
+ case 'uint8z':
140
133
  case 'sint16':
141
- case 'uint32':
142
134
  case 'uint16':
135
+ case 'uint16z':
136
+ case 'uint32':
137
+ case 'uint32z':
138
+ case 'float32':
139
+ case 'float64':
143
140
  return scale ? data / scale + offset : data;
144
141
  case 'uint32_array':
145
142
  case 'uint16_array':
@@ -154,6 +151,16 @@ function formatByType(data, type, scale, offset) {
154
151
  });
155
152
  }
156
153
  return scale ? data / scale + offset : data;
154
+ case 'exercise_category_array':
155
+ if (Array.isArray(data)) {
156
+ return data.map((dataItem) => {
157
+ if (isInvalidValue(dataItem, 'uint16')) {
158
+ return null;
159
+ }
160
+ return formatByType(dataItem, 'exercise_category', scale, offset);
161
+ });
162
+ }
163
+ return formatByType(data, 'exercise_category', scale, offset);
157
164
  default:
158
165
  {
159
166
  const typeMap = FIT.types[type];
@@ -186,7 +193,7 @@ function formatByType(data, type, scale, offset) {
186
193
  dataItem.value = data & Number(key);
187
194
  }
188
195
  else {
189
- 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
196
+ dataItem[value] = (data & Number(key)) !== 0;
190
197
  }
191
198
  }
192
199
  return dataItem;
@@ -212,10 +219,9 @@ function isInvalidValue(data, type) {
212
219
  case 'string':
213
220
  return data === 0x00;
214
221
  case 'float32':
215
- return data === 0xFFFFFFFF;
222
+ return Number.isNaN(data);
216
223
  case 'float64':
217
- // eslint-disable-next-line no-loss-of-precision
218
- return data === 0xFFFFFFFFFFFFFFFF;
224
+ return Number.isNaN(data);
219
225
  case 'uint8z':
220
226
  return data === 0x00;
221
227
  case 'uint16z':
@@ -243,6 +249,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
243
249
  const baseType = FIT.types.fit_base_type[baseTypeNo];
244
250
  return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
245
251
  }
252
+ function formatFieldValue(data, fDef, options, fields) {
253
+ var _a, _b, _c;
254
+ const field = fDef.name;
255
+ const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
256
+ const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
257
+ if (Array.isArray(data)
258
+ && !String(fDef.type).endsWith('_array')
259
+ && fDef.type !== 'exercise_category_array') {
260
+ const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : FIT.types.fit_base_type[fDef.baseTypeNo];
261
+ return data.map((item) => {
262
+ if (isInvalidValue(item, rawType)) {
263
+ return null;
264
+ }
265
+ return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
266
+ });
267
+ }
268
+ return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
269
+ }
270
+ function isOutputFieldName(field) {
271
+ return field !== 'unknown' && field !== '' && field !== undefined;
272
+ }
246
273
  function convertTo(data, unitsList, unitName) {
247
274
  const options = FIT.options[unitsList];
248
275
  const unit = options[unitName];
@@ -270,7 +297,7 @@ function applyOptions(data, field, options, fields) {
270
297
  case 'vertical_speed':
271
298
  case 'avg_speed':
272
299
  case 'max_speed':
273
- case 'speed_1s':
300
+ case 'speed1s':
274
301
  case 'ball_speed':
275
302
  case 'enhanced_avg_speed':
276
303
  case 'enhanced_max_speed':
@@ -301,8 +328,10 @@ function applyOptions(data, field, options, fields) {
301
328
  return convertTo(data, 'lengthUnits', options.lengthUnit);
302
329
  case 'temperature':
303
330
  case 'min_temperature':
331
+ case 'temperature_min':
304
332
  case 'avg_temperature':
305
333
  case 'max_temperature':
334
+ case 'temperature_max':
306
335
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
307
336
  case 'pressure':
308
337
  case 'start_pressure':
@@ -319,22 +348,8 @@ function applyOptions(data, field, options, fields) {
319
348
  return data;
320
349
  }
321
350
  }
322
- function applyGarminProductName(fields) {
323
- if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
324
- return;
325
- }
326
- const product = fields.product;
327
- if (typeof product !== 'number') {
328
- return;
329
- }
330
- const productName = FIT.types.garmin_product[product];
331
- if (typeof productName === 'string') {
332
- // Keep the raw protocol ID and expose the SDK product name separately.
333
- fields.product_name = productName;
334
- }
335
- }
336
351
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
337
- var _a, _b, _c, _d;
352
+ var _a, _b, _c, _d, _e, _f, _g;
338
353
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
339
354
  if (!description) {
340
355
  developerFieldDef.resolvedFieldDef = undefined;
@@ -345,9 +360,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
345
360
  && developerFieldDef.resolvedFieldDef) {
346
361
  return developerFieldDef.resolvedFieldDef;
347
362
  }
348
- const baseType = description.fit_base_type_id;
363
+ const describedBaseType = description.fit_base_type_id;
364
+ const baseType = typeof describedBaseType === 'number'
365
+ ? describedBaseType
366
+ : Number((_b = Object.entries(FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
349
367
  const type = FIT.types.fit_base_type[baseType];
350
- if (typeof baseType !== 'number' || type === undefined) {
368
+ if (!Number.isInteger(baseType) || type === undefined) {
351
369
  developerFieldDef.resolvedFieldDef = undefined;
352
370
  developerFieldDef.resolvedFrom = undefined;
353
371
  if (options.force) {
@@ -357,15 +375,22 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
357
375
  }
358
376
  const resolvedFieldDef = {
359
377
  type,
378
+ rawType: type,
360
379
  fDefNo: developerFieldDef.fieldDefinitionNumber,
361
380
  size: developerFieldDef.size,
381
+ array: type !== 'string'
382
+ && developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
362
383
  endianAbility: (baseType & 128) === 128,
363
384
  littleEndian,
364
385
  baseTypeNo: baseType,
365
- name: (_b = description.field_name) !== null && _b !== void 0 ? _b : '',
386
+ name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
366
387
  dataType: getFitMessageBaseType(baseType & 15),
367
- scale: (_c = description.scale) !== null && _c !== void 0 ? _c : 1,
368
- offset: (_d = description.offset) !== null && _d !== void 0 ? _d : 0,
388
+ scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
389
+ // FIT developer-field descriptions use `raw / scale - offset`. This
390
+ // parser's formatter retains the legacy equivalent signed offset and adds
391
+ // it after scaling.
392
+ offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
393
+ units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
369
394
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
370
395
  developerDataIndex: developerFieldDef.developerDataIndex,
371
396
  isDeveloperField: true,
@@ -374,11 +399,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
374
399
  developerFieldDef.resolvedFrom = description;
375
400
  return resolvedFieldDef;
376
401
  }
377
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
378
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
402
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
403
+ var _a, _b, _c;
379
404
  const recordHeader = blob[startIndex];
380
405
  let localMessageType = recordHeader & 15;
381
- if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
406
+ const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
407
+ if (isCompressedTimestamp) {
382
408
  // compressed timestamp
383
409
  localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
384
410
  }
@@ -406,19 +432,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
406
432
  for (let i = 0; i < numberOfFields; i++) {
407
433
  const fDefIndex = startIndex + 6 + i * 3;
408
434
  const baseType = blob[fDefIndex + 2];
409
- const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
435
+ const wireType = FIT.types.fit_base_type[baseType];
436
+ const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
437
+ const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
410
438
  const fDef = {
411
- type,
439
+ type: profileCompatible ? type : wireType,
440
+ rawType: wireType,
412
441
  fDefNo: blob[fDefIndex],
413
442
  size: blob[fDefIndex + 1],
443
+ array: profileCompatible
444
+ ? array === true || String(type).endsWith('_array')
445
+ : false,
414
446
  endianAbility: (baseType & 128) === 128,
415
447
  littleEndian: lEnd,
416
448
  baseTypeNo: baseType,
417
- name: field,
449
+ name: profileCompatible ? field : '',
418
450
  dataType: getFitMessageBaseType(baseType & 15),
419
- scale,
420
- offset,
421
- requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
451
+ scale: profileCompatible ? scale : null,
452
+ offset: profileCompatible ? offset : 0,
453
+ units: profileCompatible ? units : '',
454
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
422
455
  };
423
456
  mTypeDef.fieldDefs.push(fDef);
424
457
  }
@@ -440,8 +473,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
440
473
  };
441
474
  }
442
475
  const messageType = messageTypes[localMessageType] || messageTypes[0];
443
- // TODO: handle compressed header ((recordHeader & 128) == 128)
444
- // uncompressed header
445
476
  let messageSize = 0;
446
477
  let readDataFromIndex = startIndex + 1;
447
478
  const fields = {};
@@ -452,10 +483,15 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
452
483
  let validFieldCount = 0;
453
484
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
454
485
  const fDef = messageType.fieldDefs[i];
455
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
456
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
486
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
487
+ if (data !== InvalidFieldData
488
+ && !isInvalidValue(data, fDef.type)
489
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
457
490
  rawData[i] = data;
458
491
  validFieldCount++;
492
+ if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
493
+ decoderState.lastTimestamp = data;
494
+ }
459
495
  }
460
496
  else {
461
497
  rawData[i] = InvalidFieldData;
@@ -468,8 +504,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
468
504
  const rawDataIndex = messageType.fieldDefs.length + i;
469
505
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
470
506
  if (fDef) {
471
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
472
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
507
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
508
+ if (data !== InvalidFieldData
509
+ && !isInvalidValue(data, fDef.type)
510
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
473
511
  rawData[rawDataIndex] = data;
474
512
  validFieldCount++;
475
513
  }
@@ -489,9 +527,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
489
527
  continue;
490
528
  }
491
529
  const fDef = messageType.fieldDefs[i];
492
- const field = fDef.name;
493
- if (field !== 'unknown' && field !== '' && field !== undefined) {
494
- fields[field] = data;
530
+ if (isOutputFieldName(fDef.name)) {
531
+ fields[fDef.name] = data;
495
532
  }
496
533
  }
497
534
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -511,20 +548,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
511
548
  continue;
512
549
  }
513
550
  const fDef = messageType.fieldDefs[i];
514
- if (fDef.isDeveloperField) {
515
- const field = fDef.name;
516
- const { type } = fDef;
517
- const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
518
- const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
519
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
520
- }
521
- else {
522
- const { name: field, type } = fDef;
523
- const scale = (_f = fDef.scale) !== null && _f !== void 0 ? _f : null;
524
- const offset = (_g = fDef.offset) !== null && _g !== void 0 ? _g : 0;
525
- if (field !== 'unknown' && field !== '' && field !== undefined) {
526
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
527
- }
551
+ if (isOutputFieldName(fDef.name)) {
552
+ fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
528
553
  }
529
554
  }
530
555
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -535,10 +560,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
535
560
  }
536
561
  const field = fDef.name;
537
562
  if (field !== 'unknown' && field !== '') {
538
- 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);
563
+ fields[field] = formatFieldValue(data, fDef, options, fields);
564
+ }
565
+ }
566
+ if (isCompressedTimestamp) {
567
+ const previousTimestamp = decoderState.lastTimestamp;
568
+ if (previousTimestamp === undefined) {
569
+ if (!options.force) {
570
+ throw new Error('Compressed timestamp requires a previous timestamp');
571
+ }
572
+ }
573
+ else {
574
+ const timeOffset = recordHeader & CompressedTimestampMask;
575
+ const previousOffset = previousTimestamp & CompressedTimestampMask;
576
+ const rollover = timeOffset < previousOffset ? 0x20 : 0;
577
+ const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
578
+ decoderState.lastTimestamp = timestamp;
579
+ fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
580
+ validFieldCount++;
539
581
  }
540
582
  }
541
- applyGarminProductName(fields);
542
583
  if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
543
584
  fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
544
585
  fields.timer_time = fields.elapsed_time - pausedTime;
@@ -549,17 +590,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
549
590
  developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
550
591
  }
551
592
  if (message.name === 'monitoring') {
552
- // TODO weirdly uses global variables?
553
- // we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
554
- if (fields.timestamp) {
555
- monitoring_timestamp = fields.timestamp;
556
- fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
593
+ const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
594
+ const rawTimestamp = timestampFieldIndex >= 0
595
+ ? rawData[timestampFieldIndex]
596
+ : InvalidFieldData;
597
+ if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
598
+ decoderState.monitoringTimestamp = rawTimestamp;
599
+ fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
557
600
  }
558
- if (fields.timestamp16 && !fields.timestamp) {
559
- monitoring_timestamp
560
- += (fields.timestamp16 - (monitoring_timestamp & 0xFFFF)) & 0xFFFF;
561
- // fields.timestamp = monitoring_timestamp;
562
- fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
601
+ else if (fields.timestamp16
602
+ && decoderState.monitoringTimestamp !== undefined
603
+ && !fields.timestamp) {
604
+ decoderState.monitoringTimestamp
605
+ += (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
606
+ fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
563
607
  }
564
608
  }
565
609
  return {
@@ -1,6 +1,5 @@
1
1
  import type { FitParserOptions } from './fit-parser.js';
2
- import type { FieldDefinition } from './fit.js';
3
- import type { MesgNum } from './fit_types.js';
2
+ import type { FieldDefinition, MessageName } from './fit.js';
4
3
  import { Buffer } from 'buffer';
5
4
  export interface MessageTypeDefinition {
6
5
  littleEndian: boolean;
@@ -17,9 +16,13 @@ export interface DeveloperFieldDefinition {
17
16
  resolvedFieldDef?: FieldDefinition;
18
17
  resolvedFrom?: unknown;
19
18
  }
19
+ export interface DecoderState {
20
+ lastTimestamp?: number;
21
+ monitoringTimestamp?: number;
22
+ }
20
23
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
21
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
22
- messageType: MesgNum | '';
24
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
25
+ messageType: MessageName | 'definition' | '';
23
26
  nextIndex: number;
24
27
  message?: any;
25
28
  };