fit-file-parser 5.1.0 → 5.2.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 +14 -0
- package/README.md +43 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.js +124 -9
- package/dist/cjs/binary.d.ts +8 -0
- package/dist/cjs/binary.js +124 -9
- package/dist/cjs/fit-parser.d.ts +5 -1
- package/dist/cjs/fit-parser.js +37 -2
- package/dist/cjs/fit_types.d.ts +19 -0
- package/dist/cjs/type_generator.js +19 -0
- package/dist/fit-parser.d.ts +5 -1
- package/dist/fit-parser.js +37 -2
- package/dist/fit_types.d.ts +19 -0
- package/dist/type_generator.js +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add opt-in, message-filterable `raw_messages` output with exact native and
|
|
8
|
+
developer-field bytes, wire base types, endianness, message occurrence, and
|
|
9
|
+
reconstructed compressed timestamps. Existing decoded output remains
|
|
10
|
+
unchanged when the option is disabled.
|
|
11
|
+
- Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
|
|
12
|
+
activity collections alongside retained messages.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Decode compressed timestamp records without consuming bytes for the omitted
|
|
17
|
+
timestamp field.
|
|
18
|
+
|
|
5
19
|
## 5.1.0
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -145,6 +145,8 @@ All options are optional.
|
|
|
145
145
|
| `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
|
|
146
146
|
| `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
|
|
147
147
|
| `includeRawDeveloperFields` | `true`, `false`, message-number array | `false` | Adds a lossless, message-associated view of developer-field bytes at `raw_developer_fields`. |
|
|
148
|
+
| `includeRawMessages` | `true`, `false`, message-number array | `false` | Adds selected FIT messages with exact native/developer bytes and wire metadata at `raw_messages`. |
|
|
149
|
+
| `rawMessagesOnly` | `true`, `false` | `false` | Omits decoded activity collections when a consumer needs only the opt-in raw-message representation. |
|
|
148
150
|
|
|
149
151
|
`force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
|
|
150
152
|
too short, have an invalid header size or signature, or declare data beyond the
|
|
@@ -184,6 +186,47 @@ provider-specific semantics. Pass `true` to retain developer fields from every
|
|
|
184
186
|
global message, or an array such as `[18]` to bound collection to specific FIT
|
|
185
187
|
message numbers.
|
|
186
188
|
|
|
189
|
+
### Lossless selected messages
|
|
190
|
+
|
|
191
|
+
Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
|
|
192
|
+
than the parser's intentionally formatted enum names, or needs native and
|
|
193
|
+
developer fields grouped exactly by message occurrence:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
const data = await new FitParser({
|
|
197
|
+
force: false,
|
|
198
|
+
includeRawMessages: [18, 26, 72, 206, 207],
|
|
199
|
+
rawMessagesOnly: true,
|
|
200
|
+
}).parseAsync(content)
|
|
201
|
+
|
|
202
|
+
for (const message of data.raw_messages ?? []) {
|
|
203
|
+
console.log({
|
|
204
|
+
globalMessageNumber: message.global_message_number,
|
|
205
|
+
messageIndex: message.message_index,
|
|
206
|
+
littleEndian: message.little_endian,
|
|
207
|
+
fields: message.fields,
|
|
208
|
+
developerFields: message.developer_fields,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Each native field includes its field-definition number, wire base-type byte,
|
|
214
|
+
and a defensive plain-number copy of its exact bytes. Developer fields retain
|
|
215
|
+
their developer-data index, field-definition number, and exact bytes. Empty
|
|
216
|
+
selected messages are retained with empty field arrays. For a compressed
|
|
217
|
+
timestamp record, the timestamp is not present on the wire and therefore is
|
|
218
|
+
not invented in `fields`; its reconstructed native FIT timestamp is exposed as
|
|
219
|
+
`compressed_timestamp` instead.
|
|
220
|
+
|
|
221
|
+
The parser does not apply profile enum formatting, scaling, invalid-sentinel
|
|
222
|
+
filtering, or provider semantics to this opt-in representation. Its normal
|
|
223
|
+
decoded output remains unchanged. Pass `true` to retain every message or a
|
|
224
|
+
global-message-number array to keep memory use bounded. `raw_messages` and
|
|
225
|
+
`raw_developer_fields` are independent opt-ins; enabling `raw_messages` does
|
|
226
|
+
not add the flattened `raw_developer_fields` property. Set `rawMessagesOnly`
|
|
227
|
+
when only this representation is needed; the parser still validates and walks
|
|
228
|
+
the complete FIT data section but does not retain decoded activity collections.
|
|
229
|
+
|
|
187
230
|
## Output modes
|
|
188
231
|
|
|
189
232
|
The mode controls where sessions, laps, records, and related activity
|
package/dist/binary.d.ts
CHANGED
|
@@ -25,12 +25,20 @@ export interface RawDeveloperFieldValue {
|
|
|
25
25
|
fieldDefinitionNumber: number;
|
|
26
26
|
rawValue: number[];
|
|
27
27
|
}
|
|
28
|
+
export interface RawFieldValue {
|
|
29
|
+
fieldDefinitionNumber: number;
|
|
30
|
+
baseType: number;
|
|
31
|
+
rawValue: number[];
|
|
32
|
+
}
|
|
28
33
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
29
34
|
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
|
|
30
35
|
messageType: MessageName | 'definition' | '';
|
|
31
36
|
nextIndex: number;
|
|
32
37
|
message?: any;
|
|
33
38
|
globalMessageNumber?: number;
|
|
39
|
+
littleEndian?: boolean;
|
|
40
|
+
compressedTimestamp?: number;
|
|
41
|
+
rawFields?: RawFieldValue[];
|
|
34
42
|
rawDeveloperFields?: RawDeveloperFieldValue[];
|
|
35
43
|
};
|
|
36
44
|
export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
|
package/dist/binary.js
CHANGED
|
@@ -8,6 +8,37 @@ const GarminTimeOffset = 631065600000;
|
|
|
8
8
|
const InvalidFieldData = Symbol('invalid FIT field data');
|
|
9
9
|
const formatTypeMetadata = new Map();
|
|
10
10
|
const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
|
|
11
|
+
const fitBaseTypeWidths = new Map([
|
|
12
|
+
[0, 1],
|
|
13
|
+
[1, 1],
|
|
14
|
+
[2, 1],
|
|
15
|
+
[3, 2],
|
|
16
|
+
[4, 2],
|
|
17
|
+
[5, 4],
|
|
18
|
+
[6, 4],
|
|
19
|
+
[7, 1],
|
|
20
|
+
[8, 4],
|
|
21
|
+
[9, 8],
|
|
22
|
+
[10, 1],
|
|
23
|
+
[11, 2],
|
|
24
|
+
[12, 4],
|
|
25
|
+
[13, 1],
|
|
26
|
+
[14, 8],
|
|
27
|
+
[15, 8],
|
|
28
|
+
[16, 8],
|
|
29
|
+
]);
|
|
30
|
+
function retainsRawMessages(options) {
|
|
31
|
+
return options.includeRawMessages === true
|
|
32
|
+
|| Array.isArray(options.includeRawMessages);
|
|
33
|
+
}
|
|
34
|
+
function isValidRawFieldDefinition(size, baseType) {
|
|
35
|
+
if (size <= 0 || (baseType & 0x60) !== 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const typeId = baseType & 0x1F;
|
|
39
|
+
const width = fitBaseTypeWidths.get(typeId);
|
|
40
|
+
return width !== undefined && (typeId === 7 || size % width === 0);
|
|
41
|
+
}
|
|
11
42
|
function baseTypeSize(type) {
|
|
12
43
|
switch (type) {
|
|
13
44
|
case 'enum':
|
|
@@ -368,7 +399,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
368
399
|
if (!Number.isInteger(baseType) || type === undefined) {
|
|
369
400
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
370
401
|
developerFieldDef.resolvedFrom = undefined;
|
|
371
|
-
if (options.force) {
|
|
402
|
+
if (options.force || retainsRawMessages(options)) {
|
|
372
403
|
return undefined;
|
|
373
404
|
}
|
|
374
405
|
throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
|
|
@@ -401,6 +432,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
401
432
|
}
|
|
402
433
|
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
|
|
403
434
|
var _a, _b, _c;
|
|
435
|
+
if (startIndex < 0 || startIndex >= dataEnd) {
|
|
436
|
+
throw new Error('Invalid FIT record bounds');
|
|
437
|
+
}
|
|
404
438
|
const recordHeader = blob[startIndex];
|
|
405
439
|
let localMessageType = recordHeader & 15;
|
|
406
440
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
@@ -411,12 +445,29 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
411
445
|
else if ((recordHeader & 64) === 64) {
|
|
412
446
|
// is definition message
|
|
413
447
|
// startIndex + 1 is reserved
|
|
448
|
+
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
449
|
+
throw new Error('Invalid FIT definition bounds');
|
|
450
|
+
}
|
|
414
451
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
415
452
|
const lEnd = blob[startIndex + 2] === 0;
|
|
416
453
|
const numberOfFields = blob[startIndex + 5];
|
|
454
|
+
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
455
|
+
if (retainsRawMessages(options)
|
|
456
|
+
&& ((recordHeader & 16) !== 0
|
|
457
|
+
|| blob[startIndex + 1] !== 0
|
|
458
|
+
|| blob[startIndex + 2] > 1
|
|
459
|
+
|| nativeDefinitionsEnd > dataEnd
|
|
460
|
+
|| (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
|
|
461
|
+
throw new Error('Invalid FIT definition');
|
|
462
|
+
}
|
|
417
463
|
const numberOfDeveloperDataFields = hasDeveloperData
|
|
418
464
|
? blob[startIndex + 5 + numberOfFields * 3 + 1]
|
|
419
465
|
: 0;
|
|
466
|
+
const definitionEnd = nativeDefinitionsEnd
|
|
467
|
+
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
468
|
+
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
469
|
+
throw new Error('Invalid FIT developer definition bounds');
|
|
470
|
+
}
|
|
420
471
|
const mTypeDef = {
|
|
421
472
|
littleEndian: lEnd,
|
|
422
473
|
globalMessageNumber: addEndian(lEnd, [
|
|
@@ -429,17 +480,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
429
480
|
rawData: [],
|
|
430
481
|
};
|
|
431
482
|
const message = getFitMessage(mTypeDef.globalMessageNumber);
|
|
483
|
+
const nativeFieldNumbers = new Set();
|
|
432
484
|
for (let i = 0; i < numberOfFields; i++) {
|
|
433
485
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
434
486
|
const baseType = blob[fDefIndex + 2];
|
|
487
|
+
const fieldNumber = blob[fDefIndex];
|
|
488
|
+
const fieldSize = blob[fDefIndex + 1];
|
|
489
|
+
if (retainsRawMessages(options)
|
|
490
|
+
&& (nativeFieldNumbers.has(fieldNumber)
|
|
491
|
+
|| !isValidRawFieldDefinition(fieldSize, baseType))) {
|
|
492
|
+
throw new Error('Invalid FIT native field definition');
|
|
493
|
+
}
|
|
494
|
+
nativeFieldNumbers.add(fieldNumber);
|
|
435
495
|
const wireType = FIT.types.fit_base_type[baseType];
|
|
436
496
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
437
497
|
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
438
498
|
const fDef = {
|
|
439
499
|
type: profileCompatible ? type : wireType,
|
|
440
500
|
rawType: wireType,
|
|
441
|
-
fDefNo:
|
|
442
|
-
size:
|
|
501
|
+
fDefNo: fieldNumber,
|
|
502
|
+
size: fieldSize,
|
|
443
503
|
array: profileCompatible
|
|
444
504
|
? array === true || String(type).endsWith('_array')
|
|
445
505
|
: false,
|
|
@@ -451,12 +511,19 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
451
511
|
scale: profileCompatible ? scale : null,
|
|
452
512
|
offset: profileCompatible ? offset : 0,
|
|
453
513
|
units: profileCompatible ? units : '',
|
|
454
|
-
requiresBoundedDataView: requiresBoundedEndianDataView(wireType,
|
|
514
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
|
|
455
515
|
};
|
|
456
516
|
mTypeDef.fieldDefs.push(fDef);
|
|
457
517
|
}
|
|
518
|
+
const developerFieldNumbers = new Set();
|
|
458
519
|
for (let i = 0; i < numberOfDeveloperDataFields; i++) {
|
|
459
520
|
const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
|
|
521
|
+
const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
|
|
522
|
+
if (retainsRawMessages(options)
|
|
523
|
+
&& (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
|
|
524
|
+
throw new Error('Invalid FIT developer field definition');
|
|
525
|
+
}
|
|
526
|
+
developerFieldNumbers.add(developerFieldKey);
|
|
460
527
|
(_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
|
|
461
528
|
fieldDefinitionNumber: blob[fDefIndex],
|
|
462
529
|
size: blob[fDefIndex + 1],
|
|
@@ -472,33 +539,73 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
472
539
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
473
540
|
};
|
|
474
541
|
}
|
|
475
|
-
|
|
542
|
+
if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
|
|
543
|
+
throw new Error('Invalid FIT data record header');
|
|
544
|
+
}
|
|
545
|
+
const messageType = messageTypes[localMessageType];
|
|
546
|
+
if (!messageType) {
|
|
547
|
+
throw new Error('FIT data record has no local definition');
|
|
548
|
+
}
|
|
549
|
+
if (isCompressedTimestamp && retainsRawMessages(options)) {
|
|
550
|
+
const timestampField = messageType.fieldDefs[0];
|
|
551
|
+
if (!timestampField
|
|
552
|
+
|| timestampField.fDefNo !== 253
|
|
553
|
+
|| timestampField.size !== 4
|
|
554
|
+
|| (timestampField.baseTypeNo & 0x1F) !== 6) {
|
|
555
|
+
throw new Error('Invalid FIT compressed timestamp definition');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
476
558
|
let messageSize = 0;
|
|
477
559
|
let readDataFromIndex = startIndex + 1;
|
|
478
560
|
const fields = {};
|
|
479
561
|
const message = getFitMessage(messageType.globalMessageNumber);
|
|
480
562
|
const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
|
|
481
563
|
const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
|
|
564
|
+
const includeRawMessage = options.includeRawMessages === true
|
|
565
|
+
|| (Array.isArray(options.includeRawMessages)
|
|
566
|
+
&& options.includeRawMessages.includes(messageType.globalMessageNumber));
|
|
482
567
|
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
483
568
|
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
484
569
|
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
485
|
-
const
|
|
570
|
+
const rawFields = includeRawMessage ? [] : undefined;
|
|
571
|
+
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
572
|
+
if (retainsRawMessages(options)) {
|
|
573
|
+
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
574
|
+
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
575
|
+
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
576
|
+
throw new Error('Invalid FIT data record bounds');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
486
579
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
487
580
|
let validFieldCount = 0;
|
|
488
581
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
489
582
|
const fDef = messageType.fieldDefs[i];
|
|
583
|
+
if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
|
|
584
|
+
rawData[i] = InvalidFieldData;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
|
|
588
|
+
rawFields.push({
|
|
589
|
+
fieldDefinitionNumber: fDef.fDefNo,
|
|
590
|
+
baseType: fDef.baseTypeNo,
|
|
591
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
490
594
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
491
595
|
if (data !== InvalidFieldData
|
|
492
596
|
&& !isInvalidValue(data, fDef.type)
|
|
493
597
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
494
598
|
rawData[i] = data;
|
|
495
599
|
validFieldCount++;
|
|
496
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
497
|
-
decoderState.lastTimestamp = data;
|
|
600
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
601
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
498
602
|
}
|
|
499
603
|
}
|
|
500
604
|
else {
|
|
501
605
|
rawData[i] = InvalidFieldData;
|
|
606
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
607
|
+
decoderState.lastTimestamp = undefined;
|
|
608
|
+
}
|
|
502
609
|
}
|
|
503
610
|
readDataFromIndex += fDef.size;
|
|
504
611
|
messageSize += fDef.size;
|
|
@@ -575,6 +682,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
575
682
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
576
683
|
}
|
|
577
684
|
}
|
|
685
|
+
let compressedTimestamp;
|
|
578
686
|
if (isCompressedTimestamp) {
|
|
579
687
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
580
688
|
if (previousTimestamp === undefined) {
|
|
@@ -586,8 +694,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
586
694
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
587
695
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
588
696
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
589
|
-
const timestamp =
|
|
697
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
698
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
699
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
700
|
+
}
|
|
590
701
|
decoderState.lastTimestamp = timestamp;
|
|
702
|
+
compressedTimestamp = timestamp;
|
|
591
703
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
592
704
|
validFieldCount++;
|
|
593
705
|
}
|
|
@@ -620,9 +732,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
620
732
|
}
|
|
621
733
|
return {
|
|
622
734
|
globalMessageNumber: messageType.globalMessageNumber,
|
|
735
|
+
littleEndian: messageType.littleEndian,
|
|
736
|
+
compressedTimestamp,
|
|
623
737
|
messageType: message.name,
|
|
624
738
|
nextIndex: startIndex + messageSize + 1,
|
|
625
739
|
message: fields,
|
|
740
|
+
rawFields,
|
|
626
741
|
rawDeveloperFields,
|
|
627
742
|
};
|
|
628
743
|
}
|
package/dist/cjs/binary.d.ts
CHANGED
|
@@ -25,12 +25,20 @@ export interface RawDeveloperFieldValue {
|
|
|
25
25
|
fieldDefinitionNumber: number;
|
|
26
26
|
rawValue: number[];
|
|
27
27
|
}
|
|
28
|
+
export interface RawFieldValue {
|
|
29
|
+
fieldDefinitionNumber: number;
|
|
30
|
+
baseType: number;
|
|
31
|
+
rawValue: number[];
|
|
32
|
+
}
|
|
28
33
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
29
34
|
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
|
|
30
35
|
messageType: MessageName | 'definition' | '';
|
|
31
36
|
nextIndex: number;
|
|
32
37
|
message?: any;
|
|
33
38
|
globalMessageNumber?: number;
|
|
39
|
+
littleEndian?: boolean;
|
|
40
|
+
compressedTimestamp?: number;
|
|
41
|
+
rawFields?: RawFieldValue[];
|
|
34
42
|
rawDeveloperFields?: RawDeveloperFieldValue[];
|
|
35
43
|
};
|
|
36
44
|
export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
|
package/dist/cjs/binary.js
CHANGED
|
@@ -14,6 +14,37 @@ const GarminTimeOffset = 631065600000;
|
|
|
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
|
+
const fitBaseTypeWidths = new Map([
|
|
18
|
+
[0, 1],
|
|
19
|
+
[1, 1],
|
|
20
|
+
[2, 1],
|
|
21
|
+
[3, 2],
|
|
22
|
+
[4, 2],
|
|
23
|
+
[5, 4],
|
|
24
|
+
[6, 4],
|
|
25
|
+
[7, 1],
|
|
26
|
+
[8, 4],
|
|
27
|
+
[9, 8],
|
|
28
|
+
[10, 1],
|
|
29
|
+
[11, 2],
|
|
30
|
+
[12, 4],
|
|
31
|
+
[13, 1],
|
|
32
|
+
[14, 8],
|
|
33
|
+
[15, 8],
|
|
34
|
+
[16, 8],
|
|
35
|
+
]);
|
|
36
|
+
function retainsRawMessages(options) {
|
|
37
|
+
return options.includeRawMessages === true
|
|
38
|
+
|| Array.isArray(options.includeRawMessages);
|
|
39
|
+
}
|
|
40
|
+
function isValidRawFieldDefinition(size, baseType) {
|
|
41
|
+
if (size <= 0 || (baseType & 0x60) !== 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const typeId = baseType & 0x1F;
|
|
45
|
+
const width = fitBaseTypeWidths.get(typeId);
|
|
46
|
+
return width !== undefined && (typeId === 7 || size % width === 0);
|
|
47
|
+
}
|
|
17
48
|
function baseTypeSize(type) {
|
|
18
49
|
switch (type) {
|
|
19
50
|
case 'enum':
|
|
@@ -374,7 +405,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
374
405
|
if (!Number.isInteger(baseType) || type === undefined) {
|
|
375
406
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
376
407
|
developerFieldDef.resolvedFrom = undefined;
|
|
377
|
-
if (options.force) {
|
|
408
|
+
if (options.force || retainsRawMessages(options)) {
|
|
378
409
|
return undefined;
|
|
379
410
|
}
|
|
380
411
|
throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
|
|
@@ -407,6 +438,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
407
438
|
}
|
|
408
439
|
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
|
|
409
440
|
var _a, _b, _c;
|
|
441
|
+
if (startIndex < 0 || startIndex >= dataEnd) {
|
|
442
|
+
throw new Error('Invalid FIT record bounds');
|
|
443
|
+
}
|
|
410
444
|
const recordHeader = blob[startIndex];
|
|
411
445
|
let localMessageType = recordHeader & 15;
|
|
412
446
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
@@ -417,12 +451,29 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
417
451
|
else if ((recordHeader & 64) === 64) {
|
|
418
452
|
// is definition message
|
|
419
453
|
// startIndex + 1 is reserved
|
|
454
|
+
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
455
|
+
throw new Error('Invalid FIT definition bounds');
|
|
456
|
+
}
|
|
420
457
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
421
458
|
const lEnd = blob[startIndex + 2] === 0;
|
|
422
459
|
const numberOfFields = blob[startIndex + 5];
|
|
460
|
+
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
461
|
+
if (retainsRawMessages(options)
|
|
462
|
+
&& ((recordHeader & 16) !== 0
|
|
463
|
+
|| blob[startIndex + 1] !== 0
|
|
464
|
+
|| blob[startIndex + 2] > 1
|
|
465
|
+
|| nativeDefinitionsEnd > dataEnd
|
|
466
|
+
|| (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
|
|
467
|
+
throw new Error('Invalid FIT definition');
|
|
468
|
+
}
|
|
423
469
|
const numberOfDeveloperDataFields = hasDeveloperData
|
|
424
470
|
? blob[startIndex + 5 + numberOfFields * 3 + 1]
|
|
425
471
|
: 0;
|
|
472
|
+
const definitionEnd = nativeDefinitionsEnd
|
|
473
|
+
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
474
|
+
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
475
|
+
throw new Error('Invalid FIT developer definition bounds');
|
|
476
|
+
}
|
|
426
477
|
const mTypeDef = {
|
|
427
478
|
littleEndian: lEnd,
|
|
428
479
|
globalMessageNumber: addEndian(lEnd, [
|
|
@@ -435,17 +486,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
435
486
|
rawData: [],
|
|
436
487
|
};
|
|
437
488
|
const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
|
|
489
|
+
const nativeFieldNumbers = new Set();
|
|
438
490
|
for (let i = 0; i < numberOfFields; i++) {
|
|
439
491
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
440
492
|
const baseType = blob[fDefIndex + 2];
|
|
493
|
+
const fieldNumber = blob[fDefIndex];
|
|
494
|
+
const fieldSize = blob[fDefIndex + 1];
|
|
495
|
+
if (retainsRawMessages(options)
|
|
496
|
+
&& (nativeFieldNumbers.has(fieldNumber)
|
|
497
|
+
|| !isValidRawFieldDefinition(fieldSize, baseType))) {
|
|
498
|
+
throw new Error('Invalid FIT native field definition');
|
|
499
|
+
}
|
|
500
|
+
nativeFieldNumbers.add(fieldNumber);
|
|
441
501
|
const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
442
502
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
443
503
|
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
444
504
|
const fDef = {
|
|
445
505
|
type: profileCompatible ? type : wireType,
|
|
446
506
|
rawType: wireType,
|
|
447
|
-
fDefNo:
|
|
448
|
-
size:
|
|
507
|
+
fDefNo: fieldNumber,
|
|
508
|
+
size: fieldSize,
|
|
449
509
|
array: profileCompatible
|
|
450
510
|
? array === true || String(type).endsWith('_array')
|
|
451
511
|
: false,
|
|
@@ -457,12 +517,19 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
457
517
|
scale: profileCompatible ? scale : null,
|
|
458
518
|
offset: profileCompatible ? offset : 0,
|
|
459
519
|
units: profileCompatible ? units : '',
|
|
460
|
-
requiresBoundedDataView: requiresBoundedEndianDataView(wireType,
|
|
520
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
|
|
461
521
|
};
|
|
462
522
|
mTypeDef.fieldDefs.push(fDef);
|
|
463
523
|
}
|
|
524
|
+
const developerFieldNumbers = new Set();
|
|
464
525
|
for (let i = 0; i < numberOfDeveloperDataFields; i++) {
|
|
465
526
|
const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
|
|
527
|
+
const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
|
|
528
|
+
if (retainsRawMessages(options)
|
|
529
|
+
&& (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
|
|
530
|
+
throw new Error('Invalid FIT developer field definition');
|
|
531
|
+
}
|
|
532
|
+
developerFieldNumbers.add(developerFieldKey);
|
|
466
533
|
(_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
|
|
467
534
|
fieldDefinitionNumber: blob[fDefIndex],
|
|
468
535
|
size: blob[fDefIndex + 1],
|
|
@@ -478,33 +545,73 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
478
545
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
479
546
|
};
|
|
480
547
|
}
|
|
481
|
-
|
|
548
|
+
if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
|
|
549
|
+
throw new Error('Invalid FIT data record header');
|
|
550
|
+
}
|
|
551
|
+
const messageType = messageTypes[localMessageType];
|
|
552
|
+
if (!messageType) {
|
|
553
|
+
throw new Error('FIT data record has no local definition');
|
|
554
|
+
}
|
|
555
|
+
if (isCompressedTimestamp && retainsRawMessages(options)) {
|
|
556
|
+
const timestampField = messageType.fieldDefs[0];
|
|
557
|
+
if (!timestampField
|
|
558
|
+
|| timestampField.fDefNo !== 253
|
|
559
|
+
|| timestampField.size !== 4
|
|
560
|
+
|| (timestampField.baseTypeNo & 0x1F) !== 6) {
|
|
561
|
+
throw new Error('Invalid FIT compressed timestamp definition');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
482
564
|
let messageSize = 0;
|
|
483
565
|
let readDataFromIndex = startIndex + 1;
|
|
484
566
|
const fields = {};
|
|
485
567
|
const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
|
|
486
568
|
const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
|
|
487
569
|
const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
|
|
570
|
+
const includeRawMessage = options.includeRawMessages === true
|
|
571
|
+
|| (Array.isArray(options.includeRawMessages)
|
|
572
|
+
&& options.includeRawMessages.includes(messageType.globalMessageNumber));
|
|
488
573
|
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
489
574
|
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
490
575
|
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
491
|
-
const
|
|
576
|
+
const rawFields = includeRawMessage ? [] : undefined;
|
|
577
|
+
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
578
|
+
if (retainsRawMessages(options)) {
|
|
579
|
+
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
580
|
+
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
581
|
+
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
582
|
+
throw new Error('Invalid FIT data record bounds');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
492
585
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
493
586
|
let validFieldCount = 0;
|
|
494
587
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
495
588
|
const fDef = messageType.fieldDefs[i];
|
|
589
|
+
if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
|
|
590
|
+
rawData[i] = InvalidFieldData;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
|
|
594
|
+
rawFields.push({
|
|
595
|
+
fieldDefinitionNumber: fDef.fDefNo,
|
|
596
|
+
baseType: fDef.baseTypeNo,
|
|
597
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
496
600
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
497
601
|
if (data !== InvalidFieldData
|
|
498
602
|
&& !isInvalidValue(data, fDef.type)
|
|
499
603
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
500
604
|
rawData[i] = data;
|
|
501
605
|
validFieldCount++;
|
|
502
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
503
|
-
decoderState.lastTimestamp = data;
|
|
606
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
607
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
504
608
|
}
|
|
505
609
|
}
|
|
506
610
|
else {
|
|
507
611
|
rawData[i] = InvalidFieldData;
|
|
612
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
613
|
+
decoderState.lastTimestamp = undefined;
|
|
614
|
+
}
|
|
508
615
|
}
|
|
509
616
|
readDataFromIndex += fDef.size;
|
|
510
617
|
messageSize += fDef.size;
|
|
@@ -581,6 +688,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
581
688
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
582
689
|
}
|
|
583
690
|
}
|
|
691
|
+
let compressedTimestamp;
|
|
584
692
|
if (isCompressedTimestamp) {
|
|
585
693
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
586
694
|
if (previousTimestamp === undefined) {
|
|
@@ -592,8 +700,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
592
700
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
593
701
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
594
702
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
595
|
-
const timestamp =
|
|
703
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
704
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
705
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
706
|
+
}
|
|
596
707
|
decoderState.lastTimestamp = timestamp;
|
|
708
|
+
compressedTimestamp = timestamp;
|
|
597
709
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
598
710
|
validFieldCount++;
|
|
599
711
|
}
|
|
@@ -626,9 +738,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
626
738
|
}
|
|
627
739
|
return {
|
|
628
740
|
globalMessageNumber: messageType.globalMessageNumber,
|
|
741
|
+
littleEndian: messageType.littleEndian,
|
|
742
|
+
compressedTimestamp,
|
|
629
743
|
messageType: message.name,
|
|
630
744
|
nextIndex: startIndex + messageSize + 1,
|
|
631
745
|
message: fields,
|
|
746
|
+
rawFields,
|
|
632
747
|
rawDeveloperFields,
|
|
633
748
|
};
|
|
634
749
|
}
|
package/dist/cjs/fit-parser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Buffer } from 'buffer';
|
|
|
2
2
|
import type { ParsedFit } from './fit_types.js';
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
4
|
export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
|
|
5
|
-
export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
|
|
5
|
+
export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
|
|
6
6
|
export interface FitParserOptions {
|
|
7
7
|
force?: boolean;
|
|
8
8
|
speedUnit?: string;
|
|
@@ -13,6 +13,10 @@ export interface FitParserOptions {
|
|
|
13
13
|
mode?: 'list' | 'cascade' | 'both';
|
|
14
14
|
/** Retains exact developer-field bytes for all or selected global message numbers. */
|
|
15
15
|
includeRawDeveloperFields?: boolean | readonly number[];
|
|
16
|
+
/** Retains exact native and developer fields for all or selected global message numbers. */
|
|
17
|
+
includeRawMessages?: boolean | readonly number[];
|
|
18
|
+
/** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
|
|
19
|
+
rawMessagesOnly?: boolean;
|
|
16
20
|
}
|
|
17
21
|
type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
|
|
18
22
|
export default class FitParser {
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -8,7 +8,7 @@ Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function
|
|
|
8
8
|
Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
|
|
9
9
|
class FitParser {
|
|
10
10
|
constructor(options = {}) {
|
|
11
|
-
var _a;
|
|
11
|
+
var _a, _b, _c;
|
|
12
12
|
this.options = {
|
|
13
13
|
force: options.force != null ? options.force : true,
|
|
14
14
|
speedUnit: options.speedUnit || 'm/s',
|
|
@@ -18,6 +18,8 @@ class FitParser {
|
|
|
18
18
|
pressureUnit: options.pressureUnit || 'bar',
|
|
19
19
|
mode: options.mode || 'list',
|
|
20
20
|
includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
|
|
21
|
+
includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
|
|
22
|
+
rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
parseAsync(content) {
|
|
@@ -124,6 +126,10 @@ class FitParser {
|
|
|
124
126
|
|| Array.isArray(this.options.includeRawDeveloperFields)
|
|
125
127
|
? []
|
|
126
128
|
: undefined;
|
|
129
|
+
const rawMessages = this.options.includeRawMessages === true
|
|
130
|
+
|| Array.isArray(this.options.includeRawMessages)
|
|
131
|
+
? []
|
|
132
|
+
: undefined;
|
|
127
133
|
const messageCountsByGlobalNumber = new Map();
|
|
128
134
|
let loopIndex = headerLength;
|
|
129
135
|
const messageTypes = [];
|
|
@@ -135,11 +141,24 @@ class FitParser {
|
|
|
135
141
|
let lastStopTimestamp;
|
|
136
142
|
let pausedTime = 0;
|
|
137
143
|
while (loopIndex < crcStart) {
|
|
138
|
-
const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
144
|
+
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
139
145
|
loopIndex = nextIndex;
|
|
140
146
|
if (globalMessageNumber !== undefined) {
|
|
141
147
|
const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
142
148
|
messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
|
|
149
|
+
if (recordRawFields && littleEndian !== undefined) {
|
|
150
|
+
rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
|
|
153
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
154
|
+
base_type: field.baseType,
|
|
155
|
+
raw_value: field.rawValue,
|
|
156
|
+
})), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
|
|
157
|
+
developer_data_index: field.developerDataIndex,
|
|
158
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
159
|
+
raw_value: field.rawValue,
|
|
160
|
+
})) }));
|
|
161
|
+
}
|
|
143
162
|
recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
|
|
144
163
|
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
|
|
145
164
|
global_message_number: globalMessageNumber,
|
|
@@ -150,6 +169,9 @@ class FitParser {
|
|
|
150
169
|
});
|
|
151
170
|
});
|
|
152
171
|
}
|
|
172
|
+
if (this.options.rawMessagesOnly) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
153
175
|
if (messageType !== ''
|
|
154
176
|
&& messageType !== 'definition'
|
|
155
177
|
&& message !== undefined) {
|
|
@@ -269,6 +291,16 @@ class FitParser {
|
|
|
269
291
|
break;
|
|
270
292
|
}
|
|
271
293
|
}
|
|
294
|
+
if (this.options.rawMessagesOnly) {
|
|
295
|
+
if (rawDeveloperFields) {
|
|
296
|
+
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
297
|
+
}
|
|
298
|
+
if (rawMessages) {
|
|
299
|
+
fitObj.raw_messages = rawMessages;
|
|
300
|
+
}
|
|
301
|
+
callback(undefined, fitObj);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
272
304
|
fitObj.hr_zone = hr_zone;
|
|
273
305
|
fitObj.power_zone = power_zone;
|
|
274
306
|
fitObj.dive_gases = dive_gases;
|
|
@@ -291,6 +323,9 @@ class FitParser {
|
|
|
291
323
|
if (rawDeveloperFields) {
|
|
292
324
|
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
293
325
|
}
|
|
326
|
+
if (rawMessages) {
|
|
327
|
+
fitObj.raw_messages = rawMessages;
|
|
328
|
+
}
|
|
294
329
|
if (isCascadeNeeded) {
|
|
295
330
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
|
|
296
331
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
|
package/dist/cjs/fit_types.d.ts
CHANGED
|
@@ -16,6 +16,24 @@ export interface ParsedRawDeveloperField {
|
|
|
16
16
|
field_definition_number: number;
|
|
17
17
|
raw_value: number[];
|
|
18
18
|
}
|
|
19
|
+
export interface ParsedRawFitField {
|
|
20
|
+
field_definition_number: number;
|
|
21
|
+
base_type: number;
|
|
22
|
+
raw_value: number[];
|
|
23
|
+
}
|
|
24
|
+
export interface ParsedRawFitMessageDeveloperField {
|
|
25
|
+
developer_data_index: number;
|
|
26
|
+
field_definition_number: number;
|
|
27
|
+
raw_value: number[];
|
|
28
|
+
}
|
|
29
|
+
export interface ParsedRawFitMessage {
|
|
30
|
+
global_message_number: number;
|
|
31
|
+
message_index: number;
|
|
32
|
+
little_endian: boolean;
|
|
33
|
+
compressed_timestamp?: number;
|
|
34
|
+
fields: ParsedRawFitField[];
|
|
35
|
+
developer_fields: ParsedRawFitMessageDeveloperField[];
|
|
36
|
+
}
|
|
19
37
|
export type Activity = 'manual' | 'auto_multi_sport' | number;
|
|
20
38
|
export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
|
|
21
39
|
export type ActivityLevel = 'low' | 'medium' | 'high' | number;
|
|
@@ -2078,6 +2096,7 @@ export interface ParsedFit {
|
|
|
2078
2096
|
profileVersion?: number;
|
|
2079
2097
|
messages?: ParsedMessages;
|
|
2080
2098
|
raw_developer_fields?: ParsedRawDeveloperField[];
|
|
2099
|
+
raw_messages?: ParsedRawFitMessage[];
|
|
2081
2100
|
file_creator: ParsedFileCreator;
|
|
2082
2101
|
device_settings: ParsedDeviceSettings;
|
|
2083
2102
|
dive_summary?: ParsedDiveSummary;
|
|
@@ -182,6 +182,24 @@ function generateUtilities() {
|
|
|
182
182
|
generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
183
183
|
generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
184
184
|
]));
|
|
185
|
+
nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
|
|
186
|
+
generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
187
|
+
generateProperty('base_type', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
188
|
+
generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
189
|
+
]));
|
|
190
|
+
nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
|
|
191
|
+
generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
192
|
+
generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
193
|
+
generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
194
|
+
]));
|
|
195
|
+
nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
|
|
196
|
+
generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
197
|
+
generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
198
|
+
generateProperty('little_endian', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.BooleanKeyword), false),
|
|
199
|
+
generateProperty('compressed_timestamp', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
|
|
200
|
+
generateArrayProperty('fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitField'), false),
|
|
201
|
+
generateArrayProperty('developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
|
|
202
|
+
]));
|
|
185
203
|
return nodes;
|
|
186
204
|
}
|
|
187
205
|
function generateAdditionalFields(msg) {
|
|
@@ -266,6 +284,7 @@ function generateFitType() {
|
|
|
266
284
|
generateProperty('profileVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
|
|
267
285
|
generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
|
|
268
286
|
generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
|
|
287
|
+
generateArrayProperty('raw_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
|
|
269
288
|
...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
|
|
270
289
|
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
|
|
271
290
|
? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
|
package/dist/fit-parser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Buffer } from 'buffer';
|
|
|
2
2
|
import type { ParsedFit } from './fit_types.js';
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
4
|
export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
|
|
5
|
-
export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
|
|
5
|
+
export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
|
|
6
6
|
export interface FitParserOptions {
|
|
7
7
|
force?: boolean;
|
|
8
8
|
speedUnit?: string;
|
|
@@ -13,6 +13,10 @@ export interface FitParserOptions {
|
|
|
13
13
|
mode?: 'list' | 'cascade' | 'both';
|
|
14
14
|
/** Retains exact developer-field bytes for all or selected global message numbers. */
|
|
15
15
|
includeRawDeveloperFields?: boolean | readonly number[];
|
|
16
|
+
/** Retains exact native and developer fields for all or selected global message numbers. */
|
|
17
|
+
includeRawMessages?: boolean | readonly number[];
|
|
18
|
+
/** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
|
|
19
|
+
rawMessagesOnly?: boolean;
|
|
16
20
|
}
|
|
17
21
|
type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
|
|
18
22
|
export default class FitParser {
|
package/dist/fit-parser.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
|
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
4
|
export default class FitParser {
|
|
5
5
|
constructor(options = {}) {
|
|
6
|
-
var _a;
|
|
6
|
+
var _a, _b, _c;
|
|
7
7
|
this.options = {
|
|
8
8
|
force: options.force != null ? options.force : true,
|
|
9
9
|
speedUnit: options.speedUnit || 'm/s',
|
|
@@ -13,6 +13,8 @@ export default class FitParser {
|
|
|
13
13
|
pressureUnit: options.pressureUnit || 'bar',
|
|
14
14
|
mode: options.mode || 'list',
|
|
15
15
|
includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
|
|
16
|
+
includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
|
|
17
|
+
rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
parseAsync(content) {
|
|
@@ -119,6 +121,10 @@ export default class FitParser {
|
|
|
119
121
|
|| Array.isArray(this.options.includeRawDeveloperFields)
|
|
120
122
|
? []
|
|
121
123
|
: undefined;
|
|
124
|
+
const rawMessages = this.options.includeRawMessages === true
|
|
125
|
+
|| Array.isArray(this.options.includeRawMessages)
|
|
126
|
+
? []
|
|
127
|
+
: undefined;
|
|
122
128
|
const messageCountsByGlobalNumber = new Map();
|
|
123
129
|
let loopIndex = headerLength;
|
|
124
130
|
const messageTypes = [];
|
|
@@ -130,11 +136,24 @@ export default class FitParser {
|
|
|
130
136
|
let lastStopTimestamp;
|
|
131
137
|
let pausedTime = 0;
|
|
132
138
|
while (loopIndex < crcStart) {
|
|
133
|
-
const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
139
|
+
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
134
140
|
loopIndex = nextIndex;
|
|
135
141
|
if (globalMessageNumber !== undefined) {
|
|
136
142
|
const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
137
143
|
messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
|
|
144
|
+
if (recordRawFields && littleEndian !== undefined) {
|
|
145
|
+
rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
|
|
148
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
149
|
+
base_type: field.baseType,
|
|
150
|
+
raw_value: field.rawValue,
|
|
151
|
+
})), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
|
|
152
|
+
developer_data_index: field.developerDataIndex,
|
|
153
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
154
|
+
raw_value: field.rawValue,
|
|
155
|
+
})) }));
|
|
156
|
+
}
|
|
138
157
|
recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
|
|
139
158
|
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
|
|
140
159
|
global_message_number: globalMessageNumber,
|
|
@@ -145,6 +164,9 @@ export default class FitParser {
|
|
|
145
164
|
});
|
|
146
165
|
});
|
|
147
166
|
}
|
|
167
|
+
if (this.options.rawMessagesOnly) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
148
170
|
if (messageType !== ''
|
|
149
171
|
&& messageType !== 'definition'
|
|
150
172
|
&& message !== undefined) {
|
|
@@ -264,6 +286,16 @@ export default class FitParser {
|
|
|
264
286
|
break;
|
|
265
287
|
}
|
|
266
288
|
}
|
|
289
|
+
if (this.options.rawMessagesOnly) {
|
|
290
|
+
if (rawDeveloperFields) {
|
|
291
|
+
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
292
|
+
}
|
|
293
|
+
if (rawMessages) {
|
|
294
|
+
fitObj.raw_messages = rawMessages;
|
|
295
|
+
}
|
|
296
|
+
callback(undefined, fitObj);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
267
299
|
fitObj.hr_zone = hr_zone;
|
|
268
300
|
fitObj.power_zone = power_zone;
|
|
269
301
|
fitObj.dive_gases = dive_gases;
|
|
@@ -286,6 +318,9 @@ export default class FitParser {
|
|
|
286
318
|
if (rawDeveloperFields) {
|
|
287
319
|
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
288
320
|
}
|
|
321
|
+
if (rawMessages) {
|
|
322
|
+
fitObj.raw_messages = rawMessages;
|
|
323
|
+
}
|
|
289
324
|
if (isCascadeNeeded) {
|
|
290
325
|
laps = mapDataIntoLap(laps, 'records', records);
|
|
291
326
|
laps = mapDataIntoLap(laps, 'lengths', lengths);
|
package/dist/fit_types.d.ts
CHANGED
|
@@ -16,6 +16,24 @@ export interface ParsedRawDeveloperField {
|
|
|
16
16
|
field_definition_number: number;
|
|
17
17
|
raw_value: number[];
|
|
18
18
|
}
|
|
19
|
+
export interface ParsedRawFitField {
|
|
20
|
+
field_definition_number: number;
|
|
21
|
+
base_type: number;
|
|
22
|
+
raw_value: number[];
|
|
23
|
+
}
|
|
24
|
+
export interface ParsedRawFitMessageDeveloperField {
|
|
25
|
+
developer_data_index: number;
|
|
26
|
+
field_definition_number: number;
|
|
27
|
+
raw_value: number[];
|
|
28
|
+
}
|
|
29
|
+
export interface ParsedRawFitMessage {
|
|
30
|
+
global_message_number: number;
|
|
31
|
+
message_index: number;
|
|
32
|
+
little_endian: boolean;
|
|
33
|
+
compressed_timestamp?: number;
|
|
34
|
+
fields: ParsedRawFitField[];
|
|
35
|
+
developer_fields: ParsedRawFitMessageDeveloperField[];
|
|
36
|
+
}
|
|
19
37
|
export type Activity = 'manual' | 'auto_multi_sport' | number;
|
|
20
38
|
export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
|
|
21
39
|
export type ActivityLevel = 'low' | 'medium' | 'high' | number;
|
|
@@ -2078,6 +2096,7 @@ export interface ParsedFit {
|
|
|
2078
2096
|
profileVersion?: number;
|
|
2079
2097
|
messages?: ParsedMessages;
|
|
2080
2098
|
raw_developer_fields?: ParsedRawDeveloperField[];
|
|
2099
|
+
raw_messages?: ParsedRawFitMessage[];
|
|
2081
2100
|
file_creator: ParsedFileCreator;
|
|
2082
2101
|
device_settings: ParsedDeviceSettings;
|
|
2083
2102
|
dive_summary?: ParsedDiveSummary;
|
package/dist/type_generator.js
CHANGED
|
@@ -161,6 +161,24 @@ export function generateUtilities() {
|
|
|
161
161
|
generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
162
162
|
generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
163
163
|
]));
|
|
164
|
+
nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
|
|
165
|
+
generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
166
|
+
generateProperty('base_type', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
167
|
+
generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
168
|
+
]));
|
|
169
|
+
nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
|
|
170
|
+
generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
171
|
+
generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
172
|
+
generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
173
|
+
]));
|
|
174
|
+
nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
|
|
175
|
+
generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
176
|
+
generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
177
|
+
generateProperty('little_endian', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
|
|
178
|
+
generateProperty('compressed_timestamp', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
179
|
+
generateArrayProperty('fields', ts.factory.createTypeReferenceNode('ParsedRawFitField'), false),
|
|
180
|
+
generateArrayProperty('developer_fields', ts.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
|
|
181
|
+
]));
|
|
164
182
|
return nodes;
|
|
165
183
|
}
|
|
166
184
|
function generateAdditionalFields(msg) {
|
|
@@ -245,6 +263,7 @@ export function generateFitType() {
|
|
|
245
263
|
generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
246
264
|
generateProperty('messages', ts.factory.createTypeReferenceNode('ParsedMessages')),
|
|
247
265
|
generateArrayProperty('raw_developer_fields', ts.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
|
|
266
|
+
generateArrayProperty('raw_messages', ts.factory.createTypeReferenceNode('ParsedRawFitMessage')),
|
|
248
267
|
...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
|
|
249
268
|
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
|
|
250
269
|
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
|