fit-file-parser 3.1.3 → 4.0.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 +22 -0
- package/dist/binary.d.ts +9 -1
- package/dist/binary.js +116 -47
- package/dist/cjs/binary.d.ts +9 -1
- package/dist/cjs/binary.js +116 -47
- package/dist/cjs/fit-parser.js +13 -12
- package/dist/cjs/fit.d.ts +2 -1
- package/dist/fit-parser.js +14 -13
- package/dist/fit.d.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### FIT decoder performance
|
|
6
|
+
|
|
7
|
+
- Parse `ArrayBuffer` inputs and exact Node.js `Buffer` views directly, avoiding a full copy of the source file.
|
|
8
|
+
- Reuse one parse-local `DataView` instead of allocating temporary views for each supported endian field.
|
|
9
|
+
- Cache standard field metadata and enum/mask lookups on reusable message definitions.
|
|
10
|
+
- Reuse raw field storage for records that share a local message definition.
|
|
11
|
+
- Generate elapsed and timer fields once per record instead of once per decoded field.
|
|
12
|
+
- Skip header and file CRC scans in `force: true` mode, where CRC mismatches are intentionally ignored; strict mode continues to validate both CRCs.
|
|
13
|
+
- Preserve legacy malformed-field zero-padding, field-boundary, developer-field, invalid-value, and offset-buffer behavior.
|
|
14
|
+
|
|
15
|
+
### Measured benefit
|
|
16
|
+
|
|
17
|
+
- Suunto 93-hour / 7.5 MB FIT decoding: 1.057 s to 0.375 s, a 64.5% reduction.
|
|
18
|
+
- Garmin 110-hour / 28.6 MB FIT decoding: 3.643 s to 1.241 s, a 65.9% reduction.
|
|
19
|
+
- Input-related array-buffer memory is approximately halved by removing the full source copy:
|
|
20
|
+
- Suunto: 15.0 MB to 7.5 MB.
|
|
21
|
+
- Garmin: 57.2 MB to 28.6 MB.
|
|
22
|
+
|
|
23
|
+
There are no intentional parsed-output or public API changes in this release. Output parity was verified across 162 checked-in fixture/mode combinations, 8,320 generated malformed endian-definition cases, and both private long-duration benchmark files.
|
|
24
|
+
|
|
3
25
|
## 3.1.0
|
|
4
26
|
|
|
5
27
|
- Add the public `FitEncoder` API for writing FIT headers, definitions, data messages, and CRCs.
|
package/dist/binary.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { FitParserOptions } from './fit-parser.js';
|
|
2
|
+
import type { FieldDefinition } from './fit.js';
|
|
2
3
|
import type { MesgNum } from './fit_types.js';
|
|
3
4
|
import { Buffer } from 'buffer';
|
|
5
|
+
export interface MessageTypeDefinition {
|
|
6
|
+
littleEndian: boolean;
|
|
7
|
+
globalMessageNumber: number;
|
|
8
|
+
numberOfFields: number;
|
|
9
|
+
fieldDefs: FieldDefinition[];
|
|
10
|
+
rawData?: any[];
|
|
11
|
+
}
|
|
4
12
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
5
|
-
export declare function readRecord(blob: Uint8Array, messageTypes:
|
|
13
|
+
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
|
|
6
14
|
messageType: MesgNum | '';
|
|
7
15
|
nextIndex: number;
|
|
8
16
|
message?: any;
|
package/dist/binary.js
CHANGED
|
@@ -5,6 +5,29 @@ const CompressedLocalMsgNumMask = 0x60;
|
|
|
5
5
|
const CompressedHeaderMask = 0x80;
|
|
6
6
|
const GarminTimeOffset = 631065600000;
|
|
7
7
|
let monitoring_timestamp = 0;
|
|
8
|
+
const InvalidFieldData = Symbol('invalid FIT field data');
|
|
9
|
+
const formatTypeMetadata = new Map();
|
|
10
|
+
function requiresBoundedEndianDataView(type, size) {
|
|
11
|
+
switch (type) {
|
|
12
|
+
case 'sint16':
|
|
13
|
+
case 'uint16':
|
|
14
|
+
case 'uint16z':
|
|
15
|
+
return size < 2;
|
|
16
|
+
case 'sint32':
|
|
17
|
+
case 'uint32':
|
|
18
|
+
case 'uint32z':
|
|
19
|
+
case 'float32':
|
|
20
|
+
return size < 4;
|
|
21
|
+
case 'float64':
|
|
22
|
+
return size < 8;
|
|
23
|
+
case 'uint16_array':
|
|
24
|
+
return size % 2 !== 0;
|
|
25
|
+
case 'uint32_array':
|
|
26
|
+
return size % 4 !== 0;
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
8
31
|
export function addEndian(littleEndian, bytes) {
|
|
9
32
|
let result = 0;
|
|
10
33
|
if (!littleEndian)
|
|
@@ -14,7 +37,8 @@ export function addEndian(littleEndian, bytes) {
|
|
|
14
37
|
}
|
|
15
38
|
return result;
|
|
16
39
|
}
|
|
17
|
-
function readData(blob, fDef, startIndex, options) {
|
|
40
|
+
function readData(blob, dataView, fDef, startIndex, options) {
|
|
41
|
+
var _a;
|
|
18
42
|
if (fDef.type === 'uint8_array') {
|
|
19
43
|
const array8 = [];
|
|
20
44
|
for (let i = 0; i < fDef.size; i++) {
|
|
@@ -23,39 +47,50 @@ function readData(blob, fDef, startIndex, options) {
|
|
|
23
47
|
return array8;
|
|
24
48
|
}
|
|
25
49
|
if (fDef.endianAbility) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
29
66
|
}
|
|
30
|
-
const { buffer } = new Uint8Array(temp);
|
|
31
|
-
const dataView = new DataView(buffer);
|
|
32
67
|
try {
|
|
33
68
|
switch (fDef.type) {
|
|
34
69
|
case 'sint16':
|
|
35
|
-
return
|
|
70
|
+
return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
|
|
36
71
|
case 'uint16':
|
|
37
72
|
case 'uint16z':
|
|
38
|
-
return
|
|
73
|
+
return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
|
|
39
74
|
case 'sint32':
|
|
40
|
-
return
|
|
75
|
+
return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
|
|
41
76
|
case 'uint32':
|
|
42
77
|
case 'uint32z':
|
|
43
|
-
return
|
|
78
|
+
return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
|
|
44
79
|
case 'float32':
|
|
45
|
-
return
|
|
80
|
+
return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
|
|
46
81
|
case 'float64':
|
|
47
|
-
return
|
|
82
|
+
return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
|
|
48
83
|
case 'uint32_array': {
|
|
49
84
|
const array32 = [];
|
|
50
85
|
for (let i = 0; i < fDef.size; i += 4) {
|
|
51
|
-
array32.push(
|
|
86
|
+
array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
|
|
52
87
|
}
|
|
53
88
|
return array32;
|
|
54
89
|
}
|
|
55
90
|
case 'uint16_array': {
|
|
56
91
|
const array16 = [];
|
|
57
92
|
for (let i = 0; i < fDef.size; i += 2) {
|
|
58
|
-
array16.push(
|
|
93
|
+
array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
|
|
59
94
|
}
|
|
60
95
|
return array16;
|
|
61
96
|
}
|
|
@@ -66,6 +101,10 @@ function readData(blob, fDef, startIndex, options) {
|
|
|
66
101
|
throw e;
|
|
67
102
|
}
|
|
68
103
|
}
|
|
104
|
+
const temp = [];
|
|
105
|
+
for (let i = 0; i < fDef.size; i++) {
|
|
106
|
+
temp.push(blob[startIndex + i]);
|
|
107
|
+
}
|
|
69
108
|
return addEndian(fDef.littleEndian, temp);
|
|
70
109
|
}
|
|
71
110
|
if (fDef.type === 'string') {
|
|
@@ -117,30 +156,37 @@ function formatByType(data, type, scale, offset) {
|
|
|
117
156
|
return scale ? data / scale + offset : data;
|
|
118
157
|
default:
|
|
119
158
|
{
|
|
120
|
-
|
|
159
|
+
const typeMap = FIT.types[type];
|
|
160
|
+
if (!typeMap) {
|
|
121
161
|
return data;
|
|
122
162
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
let metadata = formatTypeMetadata.get(type);
|
|
164
|
+
if (!metadata) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
let hasMask = false;
|
|
167
|
+
for (const key in typeMap) {
|
|
168
|
+
if (key in typeMap) {
|
|
169
|
+
const value = typeMap[key];
|
|
170
|
+
entries.push([key, value]);
|
|
171
|
+
if (String(value) === 'mask') {
|
|
172
|
+
hasMask = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
128
175
|
}
|
|
176
|
+
metadata = { entries, hasMask, typeMap };
|
|
177
|
+
formatTypeMetadata.set(type, metadata);
|
|
129
178
|
}
|
|
130
|
-
if (!
|
|
131
|
-
const
|
|
132
|
-
const mapped = typeMap[String(data)];
|
|
179
|
+
if (!metadata.hasMask) {
|
|
180
|
+
const mapped = metadata.typeMap[String(data)];
|
|
133
181
|
return mapped === undefined ? data : mapped;
|
|
134
182
|
}
|
|
135
183
|
const dataItem = {};
|
|
136
|
-
for (const key
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
dataItem[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
|
|
143
|
-
}
|
|
184
|
+
for (const [key, value] of metadata.entries) {
|
|
185
|
+
if (value === 'mask') {
|
|
186
|
+
dataItem.value = data & Number(key);
|
|
187
|
+
}
|
|
188
|
+
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
|
|
144
190
|
}
|
|
145
191
|
}
|
|
146
192
|
return dataItem;
|
|
@@ -273,8 +319,8 @@ function applyOptions(data, field, options, fields) {
|
|
|
273
319
|
return data;
|
|
274
320
|
}
|
|
275
321
|
}
|
|
276
|
-
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
|
|
277
|
-
var _a, _b;
|
|
322
|
+
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
323
|
+
var _a, _b, _c, _d, _e;
|
|
278
324
|
const recordHeader = blob[startIndex];
|
|
279
325
|
let localMessageType = recordHeader & 15;
|
|
280
326
|
if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
|
|
@@ -298,12 +344,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
298
344
|
]),
|
|
299
345
|
numberOfFields: numberOfFields + numberOfDeveloperDataFields,
|
|
300
346
|
fieldDefs: [],
|
|
347
|
+
rawData: [],
|
|
301
348
|
};
|
|
302
349
|
const message = getFitMessage(mTypeDef.globalMessageNumber);
|
|
303
350
|
for (let i = 0; i < numberOfFields; i++) {
|
|
304
351
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
305
352
|
const baseType = blob[fDefIndex + 2];
|
|
306
|
-
const { field, type } = message.getAttributes(blob[fDefIndex]);
|
|
353
|
+
const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
|
|
307
354
|
const fDef = {
|
|
308
355
|
type,
|
|
309
356
|
fDefNo: blob[fDefIndex],
|
|
@@ -313,6 +360,9 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
313
360
|
baseTypeNo: baseType,
|
|
314
361
|
name: field,
|
|
315
362
|
dataType: getFitMessageBaseType(baseType & 15),
|
|
363
|
+
scale,
|
|
364
|
+
offset,
|
|
365
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
|
|
316
366
|
};
|
|
317
367
|
mTypeDef.fieldDefs.push(fDef);
|
|
318
368
|
}
|
|
@@ -337,6 +387,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
337
387
|
dataType: getFitMessageBaseType(baseType & 15),
|
|
338
388
|
scale: devDef.scale || 1,
|
|
339
389
|
offset: devDef.offset || 0,
|
|
390
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(FIT.types.fit_base_type[baseType], size),
|
|
340
391
|
developerDataIndex: devDataIndex,
|
|
341
392
|
isDeveloperField: true,
|
|
342
393
|
};
|
|
@@ -349,6 +400,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
349
400
|
throw e;
|
|
350
401
|
}
|
|
351
402
|
}
|
|
403
|
+
mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
|
|
352
404
|
messageTypes[localMessageType] = mTypeDef;
|
|
353
405
|
const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
|
|
354
406
|
const nextIndexWithDeveloperData = nextIndex + 1;
|
|
@@ -364,40 +416,57 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
364
416
|
let readDataFromIndex = startIndex + 1;
|
|
365
417
|
const fields = {};
|
|
366
418
|
const message = getFitMessage(messageType.globalMessageNumber);
|
|
367
|
-
const
|
|
419
|
+
const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
|
|
420
|
+
let validFieldCount = 0;
|
|
368
421
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
369
422
|
const fDef = messageType.fieldDefs[i];
|
|
370
|
-
const data = readData(blob, fDef, readDataFromIndex, options);
|
|
423
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex, options);
|
|
371
424
|
if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
372
|
-
|
|
425
|
+
rawData[i] = data;
|
|
426
|
+
validFieldCount++;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
rawData[i] = InvalidFieldData;
|
|
373
430
|
}
|
|
374
431
|
readDataFromIndex += fDef.size;
|
|
375
432
|
messageSize += fDef.size;
|
|
376
433
|
}
|
|
377
|
-
for (
|
|
378
|
-
const
|
|
434
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
435
|
+
const data = rawData[i];
|
|
436
|
+
if (data === InvalidFieldData) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const fDef = messageType.fieldDefs[i];
|
|
440
|
+
const field = fDef.name;
|
|
379
441
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
380
442
|
fields[field] = data;
|
|
381
443
|
}
|
|
382
444
|
}
|
|
383
|
-
for (
|
|
445
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
446
|
+
const data = rawData[i];
|
|
447
|
+
if (data === InvalidFieldData) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const fDef = messageType.fieldDefs[i];
|
|
384
451
|
if (fDef.isDeveloperField) {
|
|
385
452
|
const field = fDef.name;
|
|
386
453
|
const { type } = fDef;
|
|
387
|
-
const scale = (
|
|
388
|
-
const offset = (
|
|
454
|
+
const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
|
|
455
|
+
const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
|
|
389
456
|
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
390
457
|
}
|
|
391
458
|
else {
|
|
392
|
-
const { field, type
|
|
459
|
+
const { name: field, type } = fDef;
|
|
460
|
+
const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
|
|
461
|
+
const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
|
|
393
462
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
394
463
|
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
395
464
|
}
|
|
396
465
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
466
|
+
}
|
|
467
|
+
if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
|
|
468
|
+
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
469
|
+
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
401
470
|
}
|
|
402
471
|
if (message.name === 'field_description') {
|
|
403
472
|
developerFields[fields.developer_data_index]
|
package/dist/cjs/binary.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { FitParserOptions } from './fit-parser.js';
|
|
2
|
+
import type { FieldDefinition } from './fit.js';
|
|
2
3
|
import type { MesgNum } from './fit_types.js';
|
|
3
4
|
import { Buffer } from 'buffer';
|
|
5
|
+
export interface MessageTypeDefinition {
|
|
6
|
+
littleEndian: boolean;
|
|
7
|
+
globalMessageNumber: number;
|
|
8
|
+
numberOfFields: number;
|
|
9
|
+
fieldDefs: FieldDefinition[];
|
|
10
|
+
rawData?: any[];
|
|
11
|
+
}
|
|
4
12
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
5
|
-
export declare function readRecord(blob: Uint8Array, messageTypes:
|
|
13
|
+
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
|
|
6
14
|
messageType: MesgNum | '';
|
|
7
15
|
nextIndex: number;
|
|
8
16
|
message?: any;
|
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,8 @@ function applyOptions(data, field, options, fields) {
|
|
|
279
325
|
return data;
|
|
280
326
|
}
|
|
281
327
|
}
|
|
282
|
-
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
|
|
283
|
-
var _a, _b;
|
|
328
|
+
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
329
|
+
var _a, _b, _c, _d, _e;
|
|
284
330
|
const recordHeader = blob[startIndex];
|
|
285
331
|
let localMessageType = recordHeader & 15;
|
|
286
332
|
if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
|
|
@@ -304,12 +350,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
304
350
|
]),
|
|
305
351
|
numberOfFields: numberOfFields + numberOfDeveloperDataFields,
|
|
306
352
|
fieldDefs: [],
|
|
353
|
+
rawData: [],
|
|
307
354
|
};
|
|
308
355
|
const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
|
|
309
356
|
for (let i = 0; i < numberOfFields; i++) {
|
|
310
357
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
311
358
|
const baseType = blob[fDefIndex + 2];
|
|
312
|
-
const { field, type } = message.getAttributes(blob[fDefIndex]);
|
|
359
|
+
const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
|
|
313
360
|
const fDef = {
|
|
314
361
|
type,
|
|
315
362
|
fDefNo: blob[fDefIndex],
|
|
@@ -319,6 +366,9 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
319
366
|
baseTypeNo: baseType,
|
|
320
367
|
name: field,
|
|
321
368
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
369
|
+
scale,
|
|
370
|
+
offset,
|
|
371
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
|
|
322
372
|
};
|
|
323
373
|
mTypeDef.fieldDefs.push(fDef);
|
|
324
374
|
}
|
|
@@ -343,6 +393,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
343
393
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
344
394
|
scale: devDef.scale || 1,
|
|
345
395
|
offset: devDef.offset || 0,
|
|
396
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(fit_js_1.FIT.types.fit_base_type[baseType], size),
|
|
346
397
|
developerDataIndex: devDataIndex,
|
|
347
398
|
isDeveloperField: true,
|
|
348
399
|
};
|
|
@@ -355,6 +406,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
355
406
|
throw e;
|
|
356
407
|
}
|
|
357
408
|
}
|
|
409
|
+
mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
|
|
358
410
|
messageTypes[localMessageType] = mTypeDef;
|
|
359
411
|
const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
|
|
360
412
|
const nextIndexWithDeveloperData = nextIndex + 1;
|
|
@@ -370,40 +422,57 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
370
422
|
let readDataFromIndex = startIndex + 1;
|
|
371
423
|
const fields = {};
|
|
372
424
|
const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
|
|
373
|
-
const
|
|
425
|
+
const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
|
|
426
|
+
let validFieldCount = 0;
|
|
374
427
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
375
428
|
const fDef = messageType.fieldDefs[i];
|
|
376
|
-
const data = readData(blob, fDef, readDataFromIndex, options);
|
|
429
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex, options);
|
|
377
430
|
if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
378
|
-
|
|
431
|
+
rawData[i] = data;
|
|
432
|
+
validFieldCount++;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
rawData[i] = InvalidFieldData;
|
|
379
436
|
}
|
|
380
437
|
readDataFromIndex += fDef.size;
|
|
381
438
|
messageSize += fDef.size;
|
|
382
439
|
}
|
|
383
|
-
for (
|
|
384
|
-
const
|
|
440
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
441
|
+
const data = rawData[i];
|
|
442
|
+
if (data === InvalidFieldData) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const fDef = messageType.fieldDefs[i];
|
|
446
|
+
const field = fDef.name;
|
|
385
447
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
386
448
|
fields[field] = data;
|
|
387
449
|
}
|
|
388
450
|
}
|
|
389
|
-
for (
|
|
451
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
452
|
+
const data = rawData[i];
|
|
453
|
+
if (data === InvalidFieldData) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const fDef = messageType.fieldDefs[i];
|
|
390
457
|
if (fDef.isDeveloperField) {
|
|
391
458
|
const field = fDef.name;
|
|
392
459
|
const { type } = fDef;
|
|
393
|
-
const scale = (
|
|
394
|
-
const offset = (
|
|
460
|
+
const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
|
|
461
|
+
const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
|
|
395
462
|
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
396
463
|
}
|
|
397
464
|
else {
|
|
398
|
-
const { field, type
|
|
465
|
+
const { name: field, type } = fDef;
|
|
466
|
+
const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
|
|
467
|
+
const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
|
|
399
468
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
400
469
|
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
401
470
|
}
|
|
402
471
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
472
|
+
}
|
|
473
|
+
if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
|
|
474
|
+
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
475
|
+
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
407
476
|
}
|
|
408
477
|
if (message.name === 'field_description') {
|
|
409
478
|
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
|
}
|
package/dist/fit-parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { calculateCRC,
|
|
1
|
+
import { calculateCRC, readRecord } from './binary.js';
|
|
2
2
|
import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
4
|
export default class FitParser {
|
|
@@ -27,7 +27,10 @@ export default class FitParser {
|
|
|
27
27
|
}
|
|
28
28
|
parse(content, callback) {
|
|
29
29
|
var _a;
|
|
30
|
-
const blob =
|
|
30
|
+
const blob = content instanceof ArrayBuffer
|
|
31
|
+
? new Uint8Array(content)
|
|
32
|
+
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
33
|
+
const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
31
34
|
if (blob.length < 12) {
|
|
32
35
|
callback('File to small to be a FIT file', undefined);
|
|
33
36
|
if (!this.options.force) {
|
|
@@ -51,27 +54,25 @@ export default class FitParser {
|
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
if (headerLength === 14) {
|
|
57
|
+
if (headerLength === 14 && !this.options.force) {
|
|
55
58
|
const crcHeader = blob[12] + (blob[13] << 8);
|
|
56
59
|
const crcHeaderCalc = calculateCRC(blob, 0, 12);
|
|
57
60
|
if (crcHeader !== crcHeaderCalc) {
|
|
58
61
|
// callback('Header CRC mismatch', {});
|
|
59
62
|
// TODO: fix Header CRC check
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
+
return;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
const protocolVersion = blob[1];
|
|
66
67
|
const profileVersion = blob[2] + (blob[3] << 8);
|
|
67
68
|
const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
|
|
68
69
|
const crcStart = dataLength + headerLength;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
if (!this.options.force) {
|
|
71
|
+
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
72
|
+
const crcFileCalc = calculateCRC(blob, headerLength === 12 ? 0 : headerLength, crcStart);
|
|
73
|
+
if (crcFile !== crcFileCalc) {
|
|
74
|
+
// callback('File CRC mismatch', {});
|
|
75
|
+
// TODO: fix File CRC check
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -115,7 +116,7 @@ export default class FitParser {
|
|
|
115
116
|
let lastStopTimestamp;
|
|
116
117
|
let pausedTime = 0;
|
|
117
118
|
while (loopIndex < crcStart) {
|
|
118
|
-
const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime);
|
|
119
|
+
const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
|
|
119
120
|
loopIndex = nextIndex;
|
|
120
121
|
switch (messageType) {
|
|
121
122
|
case 'lap':
|
package/dist/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
|
}
|