fit-file-parser 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +50 -3
- package/dist/binary.d.ts +8 -0
- package/dist/binary.js +77 -9
- package/dist/cjs/binary.d.ts +8 -0
- package/dist/cjs/binary.js +77 -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,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 5.2.1
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserve unusual vendor field definitions in lossless raw-message output so
|
|
10
|
+
domain consumers can validate only the messages they understand.
|
|
11
|
+
- Add corpus checks for raw-message-only and combined raw/decoded parsing.
|
|
12
|
+
|
|
13
|
+
## 5.2.0
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Add opt-in, message-filterable `raw_messages` output with exact native and
|
|
18
|
+
developer-field bytes, wire base types, endianness, message occurrence, and
|
|
19
|
+
reconstructed compressed timestamps. Existing decoded output remains
|
|
20
|
+
unchanged when the option is disabled.
|
|
21
|
+
- Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
|
|
22
|
+
activity collections alongside retained messages.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Decode compressed timestamp records without consuming bytes for the omitted
|
|
27
|
+
timestamp field.
|
|
28
|
+
|
|
5
29
|
## 5.1.0
|
|
6
30
|
|
|
7
31
|
### 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
|
|
@@ -345,12 +388,16 @@ aggregate-only validation command:
|
|
|
345
388
|
```sh
|
|
346
389
|
git clone https://github.com/ThomasKuehne/FIT-test-files.git ../FIT-test-files
|
|
347
390
|
npm run corpus:check -- ../FIT-test-files --allow-force-recovery
|
|
391
|
+
npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages
|
|
392
|
+
npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages-with-decoded-output
|
|
348
393
|
```
|
|
349
394
|
|
|
350
395
|
The command accepts any corpus path; the sibling location is only a convenient
|
|
351
|
-
convention.
|
|
352
|
-
|
|
353
|
-
|
|
396
|
+
convention. Add `--raw-messages` to exercise lossless raw-message-only parsing
|
|
397
|
+
for every message in each file, or `--raw-messages-with-decoded-output` to also
|
|
398
|
+
verify the ordinary decoded output path. The corpus contains a known header-CRC
|
|
399
|
+
failure that is expected to recover only in force mode. It reports aggregate
|
|
400
|
+
counts and never prints file names or parsed activity data.
|
|
354
401
|
|
|
355
402
|
Do not edit `src/garmin_profile.generated.ts` or `src/fit_types.ts` manually.
|
|
356
403
|
Update the pinned SDK, audited vendor extensions, or a generator, then run
|
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,15 @@ 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
|
+
function retainsRawMessages(options) {
|
|
12
|
+
return options.includeRawMessages === true
|
|
13
|
+
|| Array.isArray(options.includeRawMessages);
|
|
14
|
+
}
|
|
15
|
+
function retainsRawMessage(options, globalMessageNumber) {
|
|
16
|
+
return options.includeRawMessages === true
|
|
17
|
+
|| (Array.isArray(options.includeRawMessages)
|
|
18
|
+
&& options.includeRawMessages.includes(globalMessageNumber));
|
|
19
|
+
}
|
|
11
20
|
function baseTypeSize(type) {
|
|
12
21
|
switch (type) {
|
|
13
22
|
case 'enum':
|
|
@@ -368,7 +377,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
368
377
|
if (!Number.isInteger(baseType) || type === undefined) {
|
|
369
378
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
370
379
|
developerFieldDef.resolvedFrom = undefined;
|
|
371
|
-
if (options.force) {
|
|
380
|
+
if (options.force || retainsRawMessages(options)) {
|
|
372
381
|
return undefined;
|
|
373
382
|
}
|
|
374
383
|
throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
|
|
@@ -401,6 +410,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
401
410
|
}
|
|
402
411
|
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
|
|
403
412
|
var _a, _b, _c;
|
|
413
|
+
if (startIndex < 0 || startIndex >= dataEnd) {
|
|
414
|
+
throw new Error('Invalid FIT record bounds');
|
|
415
|
+
}
|
|
404
416
|
const recordHeader = blob[startIndex];
|
|
405
417
|
let localMessageType = recordHeader & 15;
|
|
406
418
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
@@ -411,12 +423,29 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
411
423
|
else if ((recordHeader & 64) === 64) {
|
|
412
424
|
// is definition message
|
|
413
425
|
// startIndex + 1 is reserved
|
|
426
|
+
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
427
|
+
throw new Error('Invalid FIT definition bounds');
|
|
428
|
+
}
|
|
414
429
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
415
430
|
const lEnd = blob[startIndex + 2] === 0;
|
|
416
431
|
const numberOfFields = blob[startIndex + 5];
|
|
432
|
+
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
433
|
+
if (retainsRawMessages(options)
|
|
434
|
+
&& ((recordHeader & 16) !== 0
|
|
435
|
+
|| blob[startIndex + 1] !== 0
|
|
436
|
+
|| blob[startIndex + 2] > 1
|
|
437
|
+
|| nativeDefinitionsEnd > dataEnd
|
|
438
|
+
|| (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
|
|
439
|
+
throw new Error('Invalid FIT definition');
|
|
440
|
+
}
|
|
417
441
|
const numberOfDeveloperDataFields = hasDeveloperData
|
|
418
442
|
? blob[startIndex + 5 + numberOfFields * 3 + 1]
|
|
419
443
|
: 0;
|
|
444
|
+
const definitionEnd = nativeDefinitionsEnd
|
|
445
|
+
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
446
|
+
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
447
|
+
throw new Error('Invalid FIT developer definition bounds');
|
|
448
|
+
}
|
|
420
449
|
const mTypeDef = {
|
|
421
450
|
littleEndian: lEnd,
|
|
422
451
|
globalMessageNumber: addEndian(lEnd, [
|
|
@@ -432,14 +461,16 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
432
461
|
for (let i = 0; i < numberOfFields; i++) {
|
|
433
462
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
434
463
|
const baseType = blob[fDefIndex + 2];
|
|
464
|
+
const fieldNumber = blob[fDefIndex];
|
|
465
|
+
const fieldSize = blob[fDefIndex + 1];
|
|
435
466
|
const wireType = FIT.types.fit_base_type[baseType];
|
|
436
467
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
437
468
|
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
438
469
|
const fDef = {
|
|
439
470
|
type: profileCompatible ? type : wireType,
|
|
440
471
|
rawType: wireType,
|
|
441
|
-
fDefNo:
|
|
442
|
-
size:
|
|
472
|
+
fDefNo: fieldNumber,
|
|
473
|
+
size: fieldSize,
|
|
443
474
|
array: profileCompatible
|
|
444
475
|
? array === true || String(type).endsWith('_array')
|
|
445
476
|
: false,
|
|
@@ -451,7 +482,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
451
482
|
scale: profileCompatible ? scale : null,
|
|
452
483
|
offset: profileCompatible ? offset : 0,
|
|
453
484
|
units: profileCompatible ? units : '',
|
|
454
|
-
requiresBoundedDataView: requiresBoundedEndianDataView(wireType,
|
|
485
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
|
|
455
486
|
};
|
|
456
487
|
mTypeDef.fieldDefs.push(fDef);
|
|
457
488
|
}
|
|
@@ -472,33 +503,62 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
472
503
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
473
504
|
};
|
|
474
505
|
}
|
|
475
|
-
|
|
506
|
+
if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
|
|
507
|
+
throw new Error('Invalid FIT data record header');
|
|
508
|
+
}
|
|
509
|
+
const messageType = messageTypes[localMessageType];
|
|
510
|
+
if (!messageType) {
|
|
511
|
+
throw new Error('FIT data record has no local definition');
|
|
512
|
+
}
|
|
476
513
|
let messageSize = 0;
|
|
477
514
|
let readDataFromIndex = startIndex + 1;
|
|
478
515
|
const fields = {};
|
|
479
516
|
const message = getFitMessage(messageType.globalMessageNumber);
|
|
480
517
|
const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
|
|
481
518
|
const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
|
|
519
|
+
const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
|
|
482
520
|
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
483
521
|
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
484
522
|
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
485
|
-
const
|
|
523
|
+
const rawFields = includeRawMessage ? [] : undefined;
|
|
524
|
+
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
525
|
+
if (retainsRawMessages(options)) {
|
|
526
|
+
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
527
|
+
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
528
|
+
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
529
|
+
throw new Error('Invalid FIT data record bounds');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
486
532
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
487
533
|
let validFieldCount = 0;
|
|
488
534
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
489
535
|
const fDef = messageType.fieldDefs[i];
|
|
536
|
+
if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
|
|
537
|
+
rawData[i] = InvalidFieldData;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
|
|
541
|
+
rawFields.push({
|
|
542
|
+
fieldDefinitionNumber: fDef.fDefNo,
|
|
543
|
+
baseType: fDef.baseTypeNo,
|
|
544
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
490
547
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
491
548
|
if (data !== InvalidFieldData
|
|
492
549
|
&& !isInvalidValue(data, fDef.type)
|
|
493
550
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
494
551
|
rawData[i] = data;
|
|
495
552
|
validFieldCount++;
|
|
496
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
497
|
-
decoderState.lastTimestamp = data;
|
|
553
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
554
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
498
555
|
}
|
|
499
556
|
}
|
|
500
557
|
else {
|
|
501
558
|
rawData[i] = InvalidFieldData;
|
|
559
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
560
|
+
decoderState.lastTimestamp = undefined;
|
|
561
|
+
}
|
|
502
562
|
}
|
|
503
563
|
readDataFromIndex += fDef.size;
|
|
504
564
|
messageSize += fDef.size;
|
|
@@ -575,6 +635,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
575
635
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
576
636
|
}
|
|
577
637
|
}
|
|
638
|
+
let compressedTimestamp;
|
|
578
639
|
if (isCompressedTimestamp) {
|
|
579
640
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
580
641
|
if (previousTimestamp === undefined) {
|
|
@@ -586,8 +647,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
586
647
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
587
648
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
588
649
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
589
|
-
const timestamp =
|
|
650
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
651
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
652
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
653
|
+
}
|
|
590
654
|
decoderState.lastTimestamp = timestamp;
|
|
655
|
+
compressedTimestamp = timestamp;
|
|
591
656
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
592
657
|
validFieldCount++;
|
|
593
658
|
}
|
|
@@ -620,9 +685,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
620
685
|
}
|
|
621
686
|
return {
|
|
622
687
|
globalMessageNumber: messageType.globalMessageNumber,
|
|
688
|
+
littleEndian: messageType.littleEndian,
|
|
689
|
+
compressedTimestamp,
|
|
623
690
|
messageType: message.name,
|
|
624
691
|
nextIndex: startIndex + messageSize + 1,
|
|
625
692
|
message: fields,
|
|
693
|
+
rawFields,
|
|
626
694
|
rawDeveloperFields,
|
|
627
695
|
};
|
|
628
696
|
}
|
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,15 @@ 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
|
+
function retainsRawMessages(options) {
|
|
18
|
+
return options.includeRawMessages === true
|
|
19
|
+
|| Array.isArray(options.includeRawMessages);
|
|
20
|
+
}
|
|
21
|
+
function retainsRawMessage(options, globalMessageNumber) {
|
|
22
|
+
return options.includeRawMessages === true
|
|
23
|
+
|| (Array.isArray(options.includeRawMessages)
|
|
24
|
+
&& options.includeRawMessages.includes(globalMessageNumber));
|
|
25
|
+
}
|
|
17
26
|
function baseTypeSize(type) {
|
|
18
27
|
switch (type) {
|
|
19
28
|
case 'enum':
|
|
@@ -374,7 +383,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
374
383
|
if (!Number.isInteger(baseType) || type === undefined) {
|
|
375
384
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
376
385
|
developerFieldDef.resolvedFrom = undefined;
|
|
377
|
-
if (options.force) {
|
|
386
|
+
if (options.force || retainsRawMessages(options)) {
|
|
378
387
|
return undefined;
|
|
379
388
|
}
|
|
380
389
|
throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
|
|
@@ -407,6 +416,9 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
407
416
|
}
|
|
408
417
|
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
|
|
409
418
|
var _a, _b, _c;
|
|
419
|
+
if (startIndex < 0 || startIndex >= dataEnd) {
|
|
420
|
+
throw new Error('Invalid FIT record bounds');
|
|
421
|
+
}
|
|
410
422
|
const recordHeader = blob[startIndex];
|
|
411
423
|
let localMessageType = recordHeader & 15;
|
|
412
424
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
@@ -417,12 +429,29 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
417
429
|
else if ((recordHeader & 64) === 64) {
|
|
418
430
|
// is definition message
|
|
419
431
|
// startIndex + 1 is reserved
|
|
432
|
+
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
433
|
+
throw new Error('Invalid FIT definition bounds');
|
|
434
|
+
}
|
|
420
435
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
421
436
|
const lEnd = blob[startIndex + 2] === 0;
|
|
422
437
|
const numberOfFields = blob[startIndex + 5];
|
|
438
|
+
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
439
|
+
if (retainsRawMessages(options)
|
|
440
|
+
&& ((recordHeader & 16) !== 0
|
|
441
|
+
|| blob[startIndex + 1] !== 0
|
|
442
|
+
|| blob[startIndex + 2] > 1
|
|
443
|
+
|| nativeDefinitionsEnd > dataEnd
|
|
444
|
+
|| (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
|
|
445
|
+
throw new Error('Invalid FIT definition');
|
|
446
|
+
}
|
|
423
447
|
const numberOfDeveloperDataFields = hasDeveloperData
|
|
424
448
|
? blob[startIndex + 5 + numberOfFields * 3 + 1]
|
|
425
449
|
: 0;
|
|
450
|
+
const definitionEnd = nativeDefinitionsEnd
|
|
451
|
+
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
452
|
+
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
453
|
+
throw new Error('Invalid FIT developer definition bounds');
|
|
454
|
+
}
|
|
426
455
|
const mTypeDef = {
|
|
427
456
|
littleEndian: lEnd,
|
|
428
457
|
globalMessageNumber: addEndian(lEnd, [
|
|
@@ -438,14 +467,16 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
438
467
|
for (let i = 0; i < numberOfFields; i++) {
|
|
439
468
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
440
469
|
const baseType = blob[fDefIndex + 2];
|
|
470
|
+
const fieldNumber = blob[fDefIndex];
|
|
471
|
+
const fieldSize = blob[fDefIndex + 1];
|
|
441
472
|
const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
442
473
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
443
474
|
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
444
475
|
const fDef = {
|
|
445
476
|
type: profileCompatible ? type : wireType,
|
|
446
477
|
rawType: wireType,
|
|
447
|
-
fDefNo:
|
|
448
|
-
size:
|
|
478
|
+
fDefNo: fieldNumber,
|
|
479
|
+
size: fieldSize,
|
|
449
480
|
array: profileCompatible
|
|
450
481
|
? array === true || String(type).endsWith('_array')
|
|
451
482
|
: false,
|
|
@@ -457,7 +488,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
457
488
|
scale: profileCompatible ? scale : null,
|
|
458
489
|
offset: profileCompatible ? offset : 0,
|
|
459
490
|
units: profileCompatible ? units : '',
|
|
460
|
-
requiresBoundedDataView: requiresBoundedEndianDataView(wireType,
|
|
491
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
|
|
461
492
|
};
|
|
462
493
|
mTypeDef.fieldDefs.push(fDef);
|
|
463
494
|
}
|
|
@@ -478,33 +509,62 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
478
509
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
479
510
|
};
|
|
480
511
|
}
|
|
481
|
-
|
|
512
|
+
if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
|
|
513
|
+
throw new Error('Invalid FIT data record header');
|
|
514
|
+
}
|
|
515
|
+
const messageType = messageTypes[localMessageType];
|
|
516
|
+
if (!messageType) {
|
|
517
|
+
throw new Error('FIT data record has no local definition');
|
|
518
|
+
}
|
|
482
519
|
let messageSize = 0;
|
|
483
520
|
let readDataFromIndex = startIndex + 1;
|
|
484
521
|
const fields = {};
|
|
485
522
|
const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
|
|
486
523
|
const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
|
|
487
524
|
const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
|
|
525
|
+
const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
|
|
488
526
|
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
489
527
|
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
490
528
|
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
491
|
-
const
|
|
529
|
+
const rawFields = includeRawMessage ? [] : undefined;
|
|
530
|
+
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
531
|
+
if (retainsRawMessages(options)) {
|
|
532
|
+
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
533
|
+
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
534
|
+
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
535
|
+
throw new Error('Invalid FIT data record bounds');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
492
538
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
493
539
|
let validFieldCount = 0;
|
|
494
540
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
495
541
|
const fDef = messageType.fieldDefs[i];
|
|
542
|
+
if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
|
|
543
|
+
rawData[i] = InvalidFieldData;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
|
|
547
|
+
rawFields.push({
|
|
548
|
+
fieldDefinitionNumber: fDef.fDefNo,
|
|
549
|
+
baseType: fDef.baseTypeNo,
|
|
550
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
496
553
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
497
554
|
if (data !== InvalidFieldData
|
|
498
555
|
&& !isInvalidValue(data, fDef.type)
|
|
499
556
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
500
557
|
rawData[i] = data;
|
|
501
558
|
validFieldCount++;
|
|
502
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
503
|
-
decoderState.lastTimestamp = data;
|
|
559
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
560
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
504
561
|
}
|
|
505
562
|
}
|
|
506
563
|
else {
|
|
507
564
|
rawData[i] = InvalidFieldData;
|
|
565
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
566
|
+
decoderState.lastTimestamp = undefined;
|
|
567
|
+
}
|
|
508
568
|
}
|
|
509
569
|
readDataFromIndex += fDef.size;
|
|
510
570
|
messageSize += fDef.size;
|
|
@@ -581,6 +641,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
581
641
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
582
642
|
}
|
|
583
643
|
}
|
|
644
|
+
let compressedTimestamp;
|
|
584
645
|
if (isCompressedTimestamp) {
|
|
585
646
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
586
647
|
if (previousTimestamp === undefined) {
|
|
@@ -592,8 +653,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
592
653
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
593
654
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
594
655
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
595
|
-
const timestamp =
|
|
656
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
657
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
658
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
659
|
+
}
|
|
596
660
|
decoderState.lastTimestamp = timestamp;
|
|
661
|
+
compressedTimestamp = timestamp;
|
|
597
662
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
598
663
|
validFieldCount++;
|
|
599
664
|
}
|
|
@@ -626,9 +691,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
626
691
|
}
|
|
627
692
|
return {
|
|
628
693
|
globalMessageNumber: messageType.globalMessageNumber,
|
|
694
|
+
littleEndian: messageType.littleEndian,
|
|
695
|
+
compressedTimestamp,
|
|
629
696
|
messageType: message.name,
|
|
630
697
|
nextIndex: startIndex + messageSize + 1,
|
|
631
698
|
message: fields,
|
|
699
|
+
rawFields,
|
|
632
700
|
rawDeveloperFields,
|
|
633
701
|
};
|
|
634
702
|
}
|
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)
|