fit-file-parser 4.0.2 → 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':
@@ -334,7 +355,7 @@ function applyGarminProductName(fields) {
334
355
  }
335
356
  }
336
357
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
337
- var _a, _b, _c, _d;
358
+ var _a, _b, _c, _d, _e, _f;
338
359
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
339
360
  if (!description) {
340
361
  developerFieldDef.resolvedFieldDef = undefined;
@@ -345,9 +366,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
345
366
  && developerFieldDef.resolvedFieldDef) {
346
367
  return developerFieldDef.resolvedFieldDef;
347
368
  }
348
- 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.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
349
373
  const type = FIT.types.fit_base_type[baseType];
350
- if (typeof baseType !== 'number' || type === undefined) {
374
+ if (!Number.isInteger(baseType) || type === undefined) {
351
375
  developerFieldDef.resolvedFieldDef = undefined;
352
376
  developerFieldDef.resolvedFrom = undefined;
353
377
  if (options.force) {
@@ -357,15 +381,18 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
357
381
  }
358
382
  const resolvedFieldDef = {
359
383
  type,
384
+ rawType: type,
360
385
  fDefNo: developerFieldDef.fieldDefinitionNumber,
361
386
  size: developerFieldDef.size,
387
+ array: type !== 'string'
388
+ && developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
362
389
  endianAbility: (baseType & 128) === 128,
363
390
  littleEndian,
364
391
  baseTypeNo: baseType,
365
- name: (_b = description.field_name) !== null && _b !== void 0 ? _b : '',
392
+ name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
366
393
  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,
394
+ scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
395
+ offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
369
396
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
370
397
  developerDataIndex: developerFieldDef.developerDataIndex,
371
398
  isDeveloperField: true,
@@ -374,11 +401,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
374
401
  developerFieldDef.resolvedFrom = description;
375
402
  return resolvedFieldDef;
376
403
  }
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;
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;
379
406
  const recordHeader = blob[startIndex];
380
407
  let localMessageType = recordHeader & 15;
381
- if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
408
+ const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
409
+ if (isCompressedTimestamp) {
382
410
  // compressed timestamp
383
411
  localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
384
412
  }
@@ -406,19 +434,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
406
434
  for (let i = 0; i < numberOfFields; i++) {
407
435
  const fDefIndex = startIndex + 6 + i * 3;
408
436
  const baseType = blob[fDefIndex + 2];
409
- 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);
410
440
  const fDef = {
411
- type,
441
+ type: profileCompatible ? type : wireType,
442
+ rawType: wireType,
412
443
  fDefNo: blob[fDefIndex],
413
444
  size: blob[fDefIndex + 1],
445
+ array: profileCompatible
446
+ ? array === true || String(type).endsWith('_array')
447
+ : false,
414
448
  endianAbility: (baseType & 128) === 128,
415
449
  littleEndian: lEnd,
416
450
  baseTypeNo: baseType,
417
- name: field,
451
+ name: profileCompatible ? field : '',
418
452
  dataType: getFitMessageBaseType(baseType & 15),
419
- scale,
420
- offset,
421
- 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,
422
457
  };
423
458
  mTypeDef.fieldDefs.push(fDef);
424
459
  }
@@ -440,8 +475,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
440
475
  };
441
476
  }
442
477
  const messageType = messageTypes[localMessageType] || messageTypes[0];
443
- // TODO: handle compressed header ((recordHeader & 128) == 128)
444
- // uncompressed header
445
478
  let messageSize = 0;
446
479
  let readDataFromIndex = startIndex + 1;
447
480
  const fields = {};
@@ -452,10 +485,15 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
452
485
  let validFieldCount = 0;
453
486
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
454
487
  const fDef = messageType.fieldDefs[i];
455
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
456
- 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)) {
457
492
  rawData[i] = data;
458
493
  validFieldCount++;
494
+ if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
495
+ decoderState.lastTimestamp = data;
496
+ }
459
497
  }
460
498
  else {
461
499
  rawData[i] = InvalidFieldData;
@@ -468,8 +506,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
468
506
  const rawDataIndex = messageType.fieldDefs.length + i;
469
507
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
470
508
  if (fDef) {
471
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
472
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
509
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
510
+ if (data !== InvalidFieldData
511
+ && !isInvalidValue(data, fDef.type)
512
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
473
513
  rawData[rawDataIndex] = data;
474
514
  validFieldCount++;
475
515
  }
@@ -489,9 +529,15 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
489
529
  continue;
490
530
  }
491
531
  const fDef = messageType.fieldDefs[i];
492
- const field = fDef.name;
493
- if (field !== 'unknown' && field !== '' && field !== undefined) {
494
- fields[field] = data;
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
+ }
495
541
  }
496
542
  }
497
543
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -511,19 +557,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
511
557
  continue;
512
558
  }
513
559
  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);
560
+ if (isOutputFieldName(fDef.name)) {
561
+ fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
520
562
  }
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);
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
+ }
527
568
  }
528
569
  }
529
570
  }
@@ -535,7 +576,24 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
535
576
  }
536
577
  const field = fDef.name;
537
578
  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);
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');
587
+ }
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++;
539
597
  }
540
598
  }
541
599
  applyGarminProductName(fields);
@@ -549,17 +607,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
549
607
  developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
550
608
  }
551
609
  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);
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);
557
617
  }
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);
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);
563
624
  }
564
625
  }
565
626
  return {
@@ -17,8 +17,12 @@ export interface DeveloperFieldDefinition {
17
17
  resolvedFieldDef?: FieldDefinition;
18
18
  resolvedFrom?: unknown;
19
19
  }
20
+ export interface DecoderState {
21
+ lastTimestamp?: number;
22
+ monitoringTimestamp?: number;
23
+ }
20
24
  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): {
25
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
22
26
  messageType: MesgNum | '';
23
27
  nextIndex: number;
24
28
  message?: any;