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/CHANGELOG.md +41 -0
- package/README.md +190 -119
- package/dist/binary.d.ts +5 -1
- package/dist/binary.js +202 -141
- package/dist/cjs/binary.d.ts +5 -1
- package/dist/cjs/binary.js +202 -141
- package/dist/cjs/fit-parser.js +37 -19
- package/dist/cjs/fit.d.ts +6 -0
- package/dist/cjs/fit.js +167 -44
- package/dist/cjs/fit_types.d.ts +1297 -207
- package/dist/cjs/garmin_profile.generated.d.ts +9 -0
- package/dist/cjs/garmin_profile.generated.js +17838 -0
- package/dist/cjs/type_generator.d.ts +3 -0
- package/dist/cjs/type_generator.js +37 -9
- package/dist/fit-parser.js +37 -19
- package/dist/fit.d.ts +6 -0
- package/dist/fit.js +167 -44
- package/dist/fit_types.d.ts +1297 -207
- package/dist/garmin_profile.generated.d.ts +9 -0
- package/dist/garmin_profile.generated.js +17835 -0
- package/dist/type_generator.d.ts +3 -0
- package/dist/type_generator.js +36 -9
- package/package.json +9 -6
package/dist/cjs/binary.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
28
|
+
return 2;
|
|
22
29
|
case 'sint32':
|
|
23
30
|
case 'uint32':
|
|
24
31
|
case 'uint32z':
|
|
25
32
|
case 'float32':
|
|
26
|
-
return
|
|
33
|
+
return 4;
|
|
27
34
|
case 'float64':
|
|
28
|
-
return
|
|
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
|
|
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
|
|
47
|
-
var _a;
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
220
|
+
return Number.isNaN(data);
|
|
222
221
|
case 'float64':
|
|
223
|
-
|
|
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':
|
|
@@ -340,7 +361,7 @@ function applyGarminProductName(fields) {
|
|
|
340
361
|
}
|
|
341
362
|
}
|
|
342
363
|
function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
|
|
343
|
-
var _a, _b, _c, _d;
|
|
364
|
+
var _a, _b, _c, _d, _e, _f;
|
|
344
365
|
const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
|
|
345
366
|
if (!description) {
|
|
346
367
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
@@ -351,9 +372,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
351
372
|
&& developerFieldDef.resolvedFieldDef) {
|
|
352
373
|
return developerFieldDef.resolvedFieldDef;
|
|
353
374
|
}
|
|
354
|
-
const
|
|
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]);
|
|
355
379
|
const type = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
356
|
-
if (
|
|
380
|
+
if (!Number.isInteger(baseType) || type === undefined) {
|
|
357
381
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
358
382
|
developerFieldDef.resolvedFrom = undefined;
|
|
359
383
|
if (options.force) {
|
|
@@ -363,15 +387,18 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
363
387
|
}
|
|
364
388
|
const resolvedFieldDef = {
|
|
365
389
|
type,
|
|
390
|
+
rawType: type,
|
|
366
391
|
fDefNo: developerFieldDef.fieldDefinitionNumber,
|
|
367
392
|
size: developerFieldDef.size,
|
|
393
|
+
array: type !== 'string'
|
|
394
|
+
&& developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
|
|
368
395
|
endianAbility: (baseType & 128) === 128,
|
|
369
396
|
littleEndian,
|
|
370
397
|
baseTypeNo: baseType,
|
|
371
|
-
name: (
|
|
398
|
+
name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
|
|
372
399
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
373
|
-
scale: (
|
|
374
|
-
offset: (
|
|
400
|
+
scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
|
|
401
|
+
offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
|
|
375
402
|
requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
|
|
376
403
|
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
377
404
|
isDeveloperField: true,
|
|
@@ -380,11 +407,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
380
407
|
developerFieldDef.resolvedFrom = description;
|
|
381
408
|
return resolvedFieldDef;
|
|
382
409
|
}
|
|
383
|
-
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
384
|
-
var _a, _b, _c
|
|
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;
|
|
385
412
|
const recordHeader = blob[startIndex];
|
|
386
413
|
let localMessageType = recordHeader & 15;
|
|
387
|
-
|
|
414
|
+
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
415
|
+
if (isCompressedTimestamp) {
|
|
388
416
|
// compressed timestamp
|
|
389
417
|
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
390
418
|
}
|
|
@@ -412,19 +440,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
412
440
|
for (let i = 0; i < numberOfFields; i++) {
|
|
413
441
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
414
442
|
const baseType = blob[fDefIndex + 2];
|
|
415
|
-
const
|
|
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);
|
|
416
446
|
const fDef = {
|
|
417
|
-
type,
|
|
447
|
+
type: profileCompatible ? type : wireType,
|
|
448
|
+
rawType: wireType,
|
|
418
449
|
fDefNo: blob[fDefIndex],
|
|
419
450
|
size: blob[fDefIndex + 1],
|
|
451
|
+
array: profileCompatible
|
|
452
|
+
? array === true || String(type).endsWith('_array')
|
|
453
|
+
: false,
|
|
420
454
|
endianAbility: (baseType & 128) === 128,
|
|
421
455
|
littleEndian: lEnd,
|
|
422
456
|
baseTypeNo: baseType,
|
|
423
|
-
name: field,
|
|
457
|
+
name: profileCompatible ? field : '',
|
|
424
458
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
425
|
-
scale,
|
|
426
|
-
offset,
|
|
427
|
-
requiresBoundedDataView: requiresBoundedEndianDataView(
|
|
459
|
+
scale: profileCompatible ? scale : null,
|
|
460
|
+
offset: profileCompatible ? offset : 0,
|
|
461
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
|
|
462
|
+
aliases: profileCompatible ? aliases : undefined,
|
|
428
463
|
};
|
|
429
464
|
mTypeDef.fieldDefs.push(fDef);
|
|
430
465
|
}
|
|
@@ -446,8 +481,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
446
481
|
};
|
|
447
482
|
}
|
|
448
483
|
const messageType = messageTypes[localMessageType] || messageTypes[0];
|
|
449
|
-
// TODO: handle compressed header ((recordHeader & 128) == 128)
|
|
450
|
-
// uncompressed header
|
|
451
484
|
let messageSize = 0;
|
|
452
485
|
let readDataFromIndex = startIndex + 1;
|
|
453
486
|
const fields = {};
|
|
@@ -458,10 +491,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
458
491
|
let validFieldCount = 0;
|
|
459
492
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
460
493
|
const fDef = messageType.fieldDefs[i];
|
|
461
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
462
|
-
if (
|
|
494
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
495
|
+
if (data !== InvalidFieldData
|
|
496
|
+
&& !isInvalidValue(data, fDef.type)
|
|
497
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
463
498
|
rawData[i] = data;
|
|
464
499
|
validFieldCount++;
|
|
500
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
|
|
501
|
+
decoderState.lastTimestamp = data;
|
|
502
|
+
}
|
|
465
503
|
}
|
|
466
504
|
else {
|
|
467
505
|
rawData[i] = InvalidFieldData;
|
|
@@ -474,8 +512,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
474
512
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
475
513
|
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
476
514
|
if (fDef) {
|
|
477
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
478
|
-
if (
|
|
515
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
516
|
+
if (data !== InvalidFieldData
|
|
517
|
+
&& !isInvalidValue(data, fDef.type)
|
|
518
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
479
519
|
rawData[rawDataIndex] = data;
|
|
480
520
|
validFieldCount++;
|
|
481
521
|
}
|
|
@@ -495,9 +535,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
495
535
|
continue;
|
|
496
536
|
}
|
|
497
537
|
const fDef = messageType.fieldDefs[i];
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
501
547
|
}
|
|
502
548
|
}
|
|
503
549
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
@@ -517,19 +563,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
517
563
|
continue;
|
|
518
564
|
}
|
|
519
565
|
const fDef = messageType.fieldDefs[i];
|
|
520
|
-
if (fDef.
|
|
521
|
-
|
|
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);
|
|
566
|
+
if (isOutputFieldName(fDef.name)) {
|
|
567
|
+
fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
|
|
526
568
|
}
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
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
|
+
}
|
|
533
574
|
}
|
|
534
575
|
}
|
|
535
576
|
}
|
|
@@ -541,7 +582,24 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
541
582
|
}
|
|
542
583
|
const field = fDef.name;
|
|
543
584
|
if (field !== 'unknown' && field !== '') {
|
|
544
|
-
fields[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');
|
|
593
|
+
}
|
|
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++;
|
|
545
603
|
}
|
|
546
604
|
}
|
|
547
605
|
applyGarminProductName(fields);
|
|
@@ -555,17 +613,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
555
613
|
developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
|
|
556
614
|
}
|
|
557
615
|
if (message.name === 'monitoring') {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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);
|
|
563
623
|
}
|
|
564
|
-
if (fields.timestamp16
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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);
|
|
569
630
|
}
|
|
570
631
|
}
|
|
571
632
|
return {
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -31,23 +31,23 @@ class FitParser {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
parse(content, callback) {
|
|
34
|
-
var _a;
|
|
34
|
+
var _a, _b;
|
|
35
35
|
const blob = content instanceof ArrayBuffer
|
|
36
36
|
? new Uint8Array(content)
|
|
37
37
|
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
38
38
|
const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
39
39
|
if (blob.length < 12) {
|
|
40
40
|
callback('File to small to be a FIT file', undefined);
|
|
41
|
-
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
41
|
+
return;
|
|
44
42
|
}
|
|
45
43
|
const headerLength = blob[0];
|
|
46
44
|
if (headerLength !== 14 && headerLength !== 12) {
|
|
47
45
|
callback('Incorrect header size', undefined);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (blob.length < headerLength) {
|
|
49
|
+
callback('File to small to be a FIT file', undefined);
|
|
50
|
+
return;
|
|
51
51
|
}
|
|
52
52
|
let fileTypeString = '';
|
|
53
53
|
for (let i = 8; i < 12; i++) {
|
|
@@ -55,29 +55,33 @@ class FitParser {
|
|
|
55
55
|
}
|
|
56
56
|
if (fileTypeString !== '.FIT') {
|
|
57
57
|
callback('Missing \'.FIT\' in header', undefined);
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
58
|
+
return;
|
|
61
59
|
}
|
|
62
60
|
if (headerLength === 14 && !this.options.force) {
|
|
63
61
|
const crcHeader = blob[12] + (blob[13] << 8);
|
|
64
62
|
const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
|
|
65
|
-
if (crcHeader !== crcHeaderCalc) {
|
|
66
|
-
|
|
67
|
-
// TODO: fix Header CRC check
|
|
63
|
+
if (crcHeader !== 0 && crcHeader !== crcHeaderCalc) {
|
|
64
|
+
callback('Header CRC mismatch', undefined);
|
|
68
65
|
return;
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
68
|
const protocolVersion = blob[1];
|
|
72
69
|
const profileVersion = blob[2] + (blob[3] << 8);
|
|
73
|
-
const dataLength =
|
|
70
|
+
const dataLength = dataView.getUint32(4, true);
|
|
74
71
|
const crcStart = dataLength + headerLength;
|
|
72
|
+
if (crcStart > blob.length) {
|
|
73
|
+
callback('File data exceeds input length', undefined);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!this.options.force && crcStart + 2 > blob.length) {
|
|
77
|
+
callback('File CRC missing', undefined);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
75
80
|
if (!this.options.force) {
|
|
76
81
|
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
77
|
-
const crcFileCalc = (0, binary_js_1.calculateCRC)(blob,
|
|
82
|
+
const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, 0, crcStart);
|
|
78
83
|
if (crcFile !== crcFileCalc) {
|
|
79
|
-
|
|
80
|
-
// TODO: fix File CRC check
|
|
84
|
+
callback('File CRC mismatch', undefined);
|
|
81
85
|
return;
|
|
82
86
|
}
|
|
83
87
|
}
|
|
@@ -85,6 +89,7 @@ class FitParser {
|
|
|
85
89
|
profileVersion,
|
|
86
90
|
protocolVersion,
|
|
87
91
|
};
|
|
92
|
+
const messages = {};
|
|
88
93
|
let sessions = [];
|
|
89
94
|
let laps = [];
|
|
90
95
|
const records = [];
|
|
@@ -107,6 +112,7 @@ class FitParser {
|
|
|
107
112
|
const tank_updates = [];
|
|
108
113
|
const tank_summaries = [];
|
|
109
114
|
const jumps = [];
|
|
115
|
+
const sets = [];
|
|
110
116
|
const splits = [];
|
|
111
117
|
const split_summaries = [];
|
|
112
118
|
const time_in_zone = [];
|
|
@@ -115,14 +121,20 @@ class FitParser {
|
|
|
115
121
|
let loopIndex = headerLength;
|
|
116
122
|
const messageTypes = [];
|
|
117
123
|
const developerFields = [];
|
|
124
|
+
const decoderState = {};
|
|
118
125
|
const isModeCascade = this.options.mode === 'cascade';
|
|
119
126
|
const isCascadeNeeded = isModeCascade || this.options.mode === 'both';
|
|
120
127
|
let startDate;
|
|
121
128
|
let lastStopTimestamp;
|
|
122
129
|
let pausedTime = 0;
|
|
123
130
|
while (loopIndex < crcStart) {
|
|
124
|
-
const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
|
|
131
|
+
const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
|
|
125
132
|
loopIndex = nextIndex;
|
|
133
|
+
if (messageType !== ''
|
|
134
|
+
&& messageType !== 'definition'
|
|
135
|
+
&& message !== undefined) {
|
|
136
|
+
((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
|
|
137
|
+
}
|
|
126
138
|
switch (messageType) {
|
|
127
139
|
case 'lap':
|
|
128
140
|
laps.push(message);
|
|
@@ -210,6 +222,9 @@ class FitParser {
|
|
|
210
222
|
case 'jump':
|
|
211
223
|
jumps.push(message);
|
|
212
224
|
break;
|
|
225
|
+
case 'set':
|
|
226
|
+
sets.push(message);
|
|
227
|
+
break;
|
|
213
228
|
case 'split':
|
|
214
229
|
splits.push(message);
|
|
215
230
|
break;
|
|
@@ -250,15 +265,17 @@ class FitParser {
|
|
|
250
265
|
fitObj.time_in_zone = time_in_zone;
|
|
251
266
|
fitObj.activity_metrics = activity_metrics;
|
|
252
267
|
fitObj.user_metrics = user_metrics;
|
|
268
|
+
fitObj.messages = messages;
|
|
253
269
|
if (isCascadeNeeded) {
|
|
254
270
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
|
|
255
271
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
|
|
256
272
|
sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
|
|
257
|
-
fitObj.activity = Object.assign(Object.assign({}, ((
|
|
273
|
+
fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
|
|
258
274
|
sessions,
|
|
259
275
|
events,
|
|
260
276
|
hrv,
|
|
261
277
|
device_infos, developer_data_ids: applications, field_descriptions: fieldDescriptions, sports,
|
|
278
|
+
sets,
|
|
262
279
|
splits,
|
|
263
280
|
split_summaries });
|
|
264
281
|
}
|
|
@@ -272,6 +289,7 @@ class FitParser {
|
|
|
272
289
|
fitObj.developer_data_ids = applications;
|
|
273
290
|
fitObj.field_descriptions = fieldDescriptions;
|
|
274
291
|
fitObj.hrv = hrv;
|
|
292
|
+
fitObj.sets = sets;
|
|
275
293
|
}
|
|
276
294
|
callback(undefined, fitObj);
|
|
277
295
|
}
|