fit-file-parser 3.1.3 → 4.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/CHANGELOG.md +22 -0
- package/dist/binary.d.ts +9 -1
- package/dist/binary.js +131 -47
- package/dist/cjs/binary.d.ts +9 -1
- package/dist/cjs/binary.js +131 -47
- package/dist/cjs/fit-parser.js +13 -12
- package/dist/cjs/fit.d.ts +2 -1
- package/dist/cjs/fit.js +420 -100
- package/dist/cjs/fit_types.d.ts +1 -1
- package/dist/fit-parser.js +14 -13
- package/dist/fit.d.ts +2 -1
- package/dist/fit.js +420 -100
- package/dist/fit_types.d.ts +1 -1
- package/package.json +4 -1
package/dist/cjs/binary.js
CHANGED
|
@@ -11,6 +11,29 @@ const CompressedLocalMsgNumMask = 0x60;
|
|
|
11
11
|
const CompressedHeaderMask = 0x80;
|
|
12
12
|
const GarminTimeOffset = 631065600000;
|
|
13
13
|
let monitoring_timestamp = 0;
|
|
14
|
+
const InvalidFieldData = Symbol('invalid FIT field data');
|
|
15
|
+
const formatTypeMetadata = new Map();
|
|
16
|
+
function requiresBoundedEndianDataView(type, size) {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case 'sint16':
|
|
19
|
+
case 'uint16':
|
|
20
|
+
case 'uint16z':
|
|
21
|
+
return size < 2;
|
|
22
|
+
case 'sint32':
|
|
23
|
+
case 'uint32':
|
|
24
|
+
case 'uint32z':
|
|
25
|
+
case 'float32':
|
|
26
|
+
return size < 4;
|
|
27
|
+
case 'float64':
|
|
28
|
+
return size < 8;
|
|
29
|
+
case 'uint16_array':
|
|
30
|
+
return size % 2 !== 0;
|
|
31
|
+
case 'uint32_array':
|
|
32
|
+
return size % 4 !== 0;
|
|
33
|
+
default:
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
14
37
|
function addEndian(littleEndian, bytes) {
|
|
15
38
|
let result = 0;
|
|
16
39
|
if (!littleEndian)
|
|
@@ -20,7 +43,8 @@ function addEndian(littleEndian, bytes) {
|
|
|
20
43
|
}
|
|
21
44
|
return result;
|
|
22
45
|
}
|
|
23
|
-
function readData(blob, fDef, startIndex, options) {
|
|
46
|
+
function readData(blob, dataView, fDef, startIndex, options) {
|
|
47
|
+
var _a;
|
|
24
48
|
if (fDef.type === 'uint8_array') {
|
|
25
49
|
const array8 = [];
|
|
26
50
|
for (let i = 0; i < fDef.size; i++) {
|
|
@@ -29,39 +53,50 @@ function readData(blob, fDef, startIndex, options) {
|
|
|
29
53
|
return array8;
|
|
30
54
|
}
|
|
31
55
|
if (fDef.endianAbility) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
35
72
|
}
|
|
36
|
-
const { buffer } = new Uint8Array(temp);
|
|
37
|
-
const dataView = new DataView(buffer);
|
|
38
73
|
try {
|
|
39
74
|
switch (fDef.type) {
|
|
40
75
|
case 'sint16':
|
|
41
|
-
return
|
|
76
|
+
return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
|
|
42
77
|
case 'uint16':
|
|
43
78
|
case 'uint16z':
|
|
44
|
-
return
|
|
79
|
+
return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
|
|
45
80
|
case 'sint32':
|
|
46
|
-
return
|
|
81
|
+
return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
|
|
47
82
|
case 'uint32':
|
|
48
83
|
case 'uint32z':
|
|
49
|
-
return
|
|
84
|
+
return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
|
|
50
85
|
case 'float32':
|
|
51
|
-
return
|
|
86
|
+
return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
|
|
52
87
|
case 'float64':
|
|
53
|
-
return
|
|
88
|
+
return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
|
|
54
89
|
case 'uint32_array': {
|
|
55
90
|
const array32 = [];
|
|
56
91
|
for (let i = 0; i < fDef.size; i += 4) {
|
|
57
|
-
array32.push(
|
|
92
|
+
array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
|
|
58
93
|
}
|
|
59
94
|
return array32;
|
|
60
95
|
}
|
|
61
96
|
case 'uint16_array': {
|
|
62
97
|
const array16 = [];
|
|
63
98
|
for (let i = 0; i < fDef.size; i += 2) {
|
|
64
|
-
array16.push(
|
|
99
|
+
array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
|
|
65
100
|
}
|
|
66
101
|
return array16;
|
|
67
102
|
}
|
|
@@ -72,6 +107,10 @@ function readData(blob, fDef, startIndex, options) {
|
|
|
72
107
|
throw e;
|
|
73
108
|
}
|
|
74
109
|
}
|
|
110
|
+
const temp = [];
|
|
111
|
+
for (let i = 0; i < fDef.size; i++) {
|
|
112
|
+
temp.push(blob[startIndex + i]);
|
|
113
|
+
}
|
|
75
114
|
return addEndian(fDef.littleEndian, temp);
|
|
76
115
|
}
|
|
77
116
|
if (fDef.type === 'string') {
|
|
@@ -123,30 +162,37 @@ function formatByType(data, type, scale, offset) {
|
|
|
123
162
|
return scale ? data / scale + offset : data;
|
|
124
163
|
default:
|
|
125
164
|
{
|
|
126
|
-
|
|
165
|
+
const typeMap = fit_js_1.FIT.types[type];
|
|
166
|
+
if (!typeMap) {
|
|
127
167
|
return data;
|
|
128
168
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
let metadata = formatTypeMetadata.get(type);
|
|
170
|
+
if (!metadata) {
|
|
171
|
+
const entries = [];
|
|
172
|
+
let hasMask = false;
|
|
173
|
+
for (const key in typeMap) {
|
|
174
|
+
if (key in typeMap) {
|
|
175
|
+
const value = typeMap[key];
|
|
176
|
+
entries.push([key, value]);
|
|
177
|
+
if (String(value) === 'mask') {
|
|
178
|
+
hasMask = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
134
181
|
}
|
|
182
|
+
metadata = { entries, hasMask, typeMap };
|
|
183
|
+
formatTypeMetadata.set(type, metadata);
|
|
135
184
|
}
|
|
136
|
-
if (!
|
|
137
|
-
const
|
|
138
|
-
const mapped = typeMap[String(data)];
|
|
185
|
+
if (!metadata.hasMask) {
|
|
186
|
+
const mapped = metadata.typeMap[String(data)];
|
|
139
187
|
return mapped === undefined ? data : mapped;
|
|
140
188
|
}
|
|
141
189
|
const dataItem = {};
|
|
142
|
-
for (const key
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
dataItem[fit_js_1.FIT.types[type][key]] = !!((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
|
|
149
|
-
}
|
|
190
|
+
for (const [key, value] of metadata.entries) {
|
|
191
|
+
if (value === 'mask') {
|
|
192
|
+
dataItem.value = data & Number(key);
|
|
193
|
+
}
|
|
194
|
+
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
|
|
150
196
|
}
|
|
151
197
|
}
|
|
152
198
|
return dataItem;
|
|
@@ -279,8 +325,22 @@ function applyOptions(data, field, options, fields) {
|
|
|
279
325
|
return data;
|
|
280
326
|
}
|
|
281
327
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
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
|
+
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
343
|
+
var _a, _b, _c, _d, _e;
|
|
284
344
|
const recordHeader = blob[startIndex];
|
|
285
345
|
let localMessageType = recordHeader & 15;
|
|
286
346
|
if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
|
|
@@ -304,12 +364,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
304
364
|
]),
|
|
305
365
|
numberOfFields: numberOfFields + numberOfDeveloperDataFields,
|
|
306
366
|
fieldDefs: [],
|
|
367
|
+
rawData: [],
|
|
307
368
|
};
|
|
308
369
|
const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
|
|
309
370
|
for (let i = 0; i < numberOfFields; i++) {
|
|
310
371
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
311
372
|
const baseType = blob[fDefIndex + 2];
|
|
312
|
-
const { field, type } = message.getAttributes(blob[fDefIndex]);
|
|
373
|
+
const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
|
|
313
374
|
const fDef = {
|
|
314
375
|
type,
|
|
315
376
|
fDefNo: blob[fDefIndex],
|
|
@@ -319,6 +380,9 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
319
380
|
baseTypeNo: baseType,
|
|
320
381
|
name: field,
|
|
321
382
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
383
|
+
scale,
|
|
384
|
+
offset,
|
|
385
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
|
|
322
386
|
};
|
|
323
387
|
mTypeDef.fieldDefs.push(fDef);
|
|
324
388
|
}
|
|
@@ -343,6 +407,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
343
407
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
344
408
|
scale: devDef.scale || 1,
|
|
345
409
|
offset: devDef.offset || 0,
|
|
410
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(fit_js_1.FIT.types.fit_base_type[baseType], size),
|
|
346
411
|
developerDataIndex: devDataIndex,
|
|
347
412
|
isDeveloperField: true,
|
|
348
413
|
};
|
|
@@ -355,6 +420,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
355
420
|
throw e;
|
|
356
421
|
}
|
|
357
422
|
}
|
|
423
|
+
mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
|
|
358
424
|
messageTypes[localMessageType] = mTypeDef;
|
|
359
425
|
const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
|
|
360
426
|
const nextIndexWithDeveloperData = nextIndex + 1;
|
|
@@ -370,40 +436,58 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
370
436
|
let readDataFromIndex = startIndex + 1;
|
|
371
437
|
const fields = {};
|
|
372
438
|
const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
|
|
373
|
-
const
|
|
439
|
+
const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
|
|
440
|
+
let validFieldCount = 0;
|
|
374
441
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
375
442
|
const fDef = messageType.fieldDefs[i];
|
|
376
|
-
const data = readData(blob, fDef, readDataFromIndex, options);
|
|
443
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex, options);
|
|
377
444
|
if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
378
|
-
|
|
445
|
+
rawData[i] = data;
|
|
446
|
+
validFieldCount++;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
rawData[i] = InvalidFieldData;
|
|
379
450
|
}
|
|
380
451
|
readDataFromIndex += fDef.size;
|
|
381
452
|
messageSize += fDef.size;
|
|
382
453
|
}
|
|
383
|
-
for (
|
|
384
|
-
const
|
|
454
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
455
|
+
const data = rawData[i];
|
|
456
|
+
if (data === InvalidFieldData) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const fDef = messageType.fieldDefs[i];
|
|
460
|
+
const field = fDef.name;
|
|
385
461
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
386
462
|
fields[field] = data;
|
|
387
463
|
}
|
|
388
464
|
}
|
|
389
|
-
for (
|
|
465
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
466
|
+
const data = rawData[i];
|
|
467
|
+
if (data === InvalidFieldData) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const fDef = messageType.fieldDefs[i];
|
|
390
471
|
if (fDef.isDeveloperField) {
|
|
391
472
|
const field = fDef.name;
|
|
392
473
|
const { type } = fDef;
|
|
393
|
-
const scale = (
|
|
394
|
-
const offset = (
|
|
474
|
+
const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
|
|
475
|
+
const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
|
|
395
476
|
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
396
477
|
}
|
|
397
478
|
else {
|
|
398
|
-
const { field, type
|
|
479
|
+
const { name: field, type } = fDef;
|
|
480
|
+
const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
|
|
481
|
+
const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
|
|
399
482
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
400
483
|
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
401
484
|
}
|
|
402
485
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
486
|
+
}
|
|
487
|
+
applyGarminProductName(fields);
|
|
488
|
+
if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
|
|
489
|
+
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
490
|
+
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
407
491
|
}
|
|
408
492
|
if (message.name === 'field_description') {
|
|
409
493
|
developerFields[fields.developer_data_index]
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -32,7 +32,10 @@ class FitParser {
|
|
|
32
32
|
}
|
|
33
33
|
parse(content, callback) {
|
|
34
34
|
var _a;
|
|
35
|
-
const blob =
|
|
35
|
+
const blob = content instanceof ArrayBuffer
|
|
36
|
+
? new Uint8Array(content)
|
|
37
|
+
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
38
|
+
const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
36
39
|
if (blob.length < 12) {
|
|
37
40
|
callback('File to small to be a FIT file', undefined);
|
|
38
41
|
if (!this.options.force) {
|
|
@@ -56,27 +59,25 @@ class FitParser {
|
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
|
-
if (headerLength === 14) {
|
|
62
|
+
if (headerLength === 14 && !this.options.force) {
|
|
60
63
|
const crcHeader = blob[12] + (blob[13] << 8);
|
|
61
64
|
const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
|
|
62
65
|
if (crcHeader !== crcHeaderCalc) {
|
|
63
66
|
// callback('Header CRC mismatch', {});
|
|
64
67
|
// TODO: fix Header CRC check
|
|
65
|
-
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
+
return;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
const protocolVersion = blob[1];
|
|
71
72
|
const profileVersion = blob[2] + (blob[3] << 8);
|
|
72
73
|
const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
|
|
73
74
|
const crcStart = dataLength + headerLength;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
if (!this.options.force) {
|
|
76
|
+
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
77
|
+
const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
|
|
78
|
+
if (crcFile !== crcFileCalc) {
|
|
79
|
+
// callback('File CRC mismatch', {});
|
|
80
|
+
// TODO: fix File CRC check
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -120,7 +121,7 @@ class FitParser {
|
|
|
120
121
|
let lastStopTimestamp;
|
|
121
122
|
let pausedTime = 0;
|
|
122
123
|
while (loopIndex < crcStart) {
|
|
123
|
-
const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime);
|
|
124
|
+
const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
|
|
124
125
|
loopIndex = nextIndex;
|
|
125
126
|
switch (messageType) {
|
|
126
127
|
case 'lap':
|
package/dist/cjs/fit.d.ts
CHANGED
|
@@ -8,8 +8,9 @@ export interface FieldDefinition {
|
|
|
8
8
|
baseTypeNo: number;
|
|
9
9
|
name: string;
|
|
10
10
|
dataType: string;
|
|
11
|
-
scale?: number;
|
|
11
|
+
scale?: number | null;
|
|
12
12
|
offset?: number;
|
|
13
|
+
requiresBoundedDataView?: boolean;
|
|
13
14
|
developerDataIndex?: number;
|
|
14
15
|
isDeveloperField?: boolean;
|
|
15
16
|
}
|