fit-file-parser 5.0.2 → 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 +23 -0
- package/README.md +87 -9
- package/dist/binary.d.ts +16 -1
- package/dist/binary.js +138 -9
- package/dist/cjs/binary.d.ts +16 -1
- package/dist/cjs/binary.js +138 -9
- package/dist/cjs/fit-parser.d.ts +7 -0
- package/dist/cjs/fit-parser.js +62 -4
- package/dist/cjs/fit_types.d.ts +27 -0
- package/dist/cjs/type_generator.js +27 -0
- package/dist/fit-parser.d.ts +7 -0
- package/dist/fit-parser.js +62 -4
- package/dist/fit_types.d.ts +27 -0
- package/dist/type_generator.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
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
|
+
|
|
19
|
+
## 5.1.0
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Add opt-in, message-filterable `raw_developer_fields` output with exact
|
|
24
|
+
developer-field bytes, global-message occurrence, developer-data index, and
|
|
25
|
+
field-definition number. Existing decoded output remains unchanged when the
|
|
26
|
+
option is disabled.
|
|
27
|
+
|
|
5
28
|
## 5.0.2
|
|
6
29
|
|
|
7
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -135,20 +135,98 @@ callback API receives as its first argument.
|
|
|
135
135
|
|
|
136
136
|
All options are optional.
|
|
137
137
|
|
|
138
|
-
| Option
|
|
139
|
-
|
|
|
140
|
-
| `mode`
|
|
141
|
-
| `force`
|
|
142
|
-
| `speedUnit`
|
|
143
|
-
| `lengthUnit`
|
|
144
|
-
| `temperatureUnit`
|
|
145
|
-
| `pressureUnit`
|
|
146
|
-
| `elapsedRecordField`
|
|
138
|
+
| Option | Values | Default | Behavior |
|
|
139
|
+
| --------------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
|
|
141
|
+
| `force` | `true`, `false` | `true` | Skips header and file CRC validation and enables supported best-effort field recovery. Structural header and data bounds are always validated. |
|
|
142
|
+
| `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
|
|
143
|
+
| `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
|
|
144
|
+
| `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
|
|
145
|
+
| `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
|
|
146
|
+
| `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
|
|
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. |
|
|
147
150
|
|
|
148
151
|
`force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
|
|
149
152
|
too short, have an invalid header size or signature, or declare data beyond the
|
|
150
153
|
available bytes are rejected in both modes.
|
|
151
154
|
|
|
155
|
+
### Lossless developer fields
|
|
156
|
+
|
|
157
|
+
By default, developer fields retain the existing decoded, name-keyed output.
|
|
158
|
+
Set `includeRawDeveloperFields: true` when a consumer also needs exact field
|
|
159
|
+
bytes or must distinguish same-named fields from different developer-data
|
|
160
|
+
indexes:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const data = await new FitParser({
|
|
164
|
+
force: false,
|
|
165
|
+
includeRawDeveloperFields: [18],
|
|
166
|
+
}).parseAsync(content)
|
|
167
|
+
|
|
168
|
+
for (const field of data.raw_developer_fields ?? []) {
|
|
169
|
+
console.log({
|
|
170
|
+
globalMessageNumber: field.global_message_number,
|
|
171
|
+
messageIndex: field.message_index,
|
|
172
|
+
developerDataIndex: field.developer_data_index,
|
|
173
|
+
fieldDefinitionNumber: field.field_definition_number,
|
|
174
|
+
rawBytes: field.raw_value,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`message_index` is the zero-based occurrence of that global message number in
|
|
180
|
+
file order. `raw_value` is a defensive plain-number copy of the exact FIT field
|
|
181
|
+
bytes, including interior NUL bytes and invalid sentinels. Join
|
|
182
|
+
`developer_data_index` and `field_definition_number` to the existing
|
|
183
|
+
`developer_data_ids` and `field_descriptions` collections to interpret an
|
|
184
|
+
application and field. The parser deliberately does not infer application- or
|
|
185
|
+
provider-specific semantics. Pass `true` to retain developer fields from every
|
|
186
|
+
global message, or an array such as `[18]` to bound collection to specific FIT
|
|
187
|
+
message numbers.
|
|
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
|
+
|
|
152
230
|
## Output modes
|
|
153
231
|
|
|
154
232
|
The mode controls where sessions, laps, records, and related activity
|
package/dist/binary.d.ts
CHANGED
|
@@ -20,11 +20,26 @@ export interface DecoderState {
|
|
|
20
20
|
lastTimestamp?: number;
|
|
21
21
|
monitoringTimestamp?: number;
|
|
22
22
|
}
|
|
23
|
+
export interface RawDeveloperFieldValue {
|
|
24
|
+
developerDataIndex: number;
|
|
25
|
+
fieldDefinitionNumber: number;
|
|
26
|
+
rawValue: number[];
|
|
27
|
+
}
|
|
28
|
+
export interface RawFieldValue {
|
|
29
|
+
fieldDefinitionNumber: number;
|
|
30
|
+
baseType: number;
|
|
31
|
+
rawValue: number[];
|
|
32
|
+
}
|
|
23
33
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
24
|
-
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
|
|
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): {
|
|
25
35
|
messageType: MessageName | 'definition' | '';
|
|
26
36
|
nextIndex: number;
|
|
27
37
|
message?: any;
|
|
38
|
+
globalMessageNumber?: number;
|
|
39
|
+
littleEndian?: boolean;
|
|
40
|
+
compressedTimestamp?: number;
|
|
41
|
+
rawFields?: RawFieldValue[];
|
|
42
|
+
rawDeveloperFields?: RawDeveloperFieldValue[];
|
|
28
43
|
};
|
|
29
44
|
export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
|
|
30
45
|
export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
|
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}`);
|
|
@@ -399,8 +430,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
399
430
|
developerFieldDef.resolvedFrom = description;
|
|
400
431
|
return resolvedFieldDef;
|
|
401
432
|
}
|
|
402
|
-
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
|
|
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,29 +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));
|
|
567
|
+
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
568
|
+
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
569
|
+
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
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
|
+
}
|
|
482
579
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
483
580
|
let validFieldCount = 0;
|
|
484
581
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
485
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
|
+
}
|
|
486
594
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
487
595
|
if (data !== InvalidFieldData
|
|
488
596
|
&& !isInvalidValue(data, fDef.type)
|
|
489
597
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
490
598
|
rawData[i] = data;
|
|
491
599
|
validFieldCount++;
|
|
492
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
493
|
-
decoderState.lastTimestamp = data;
|
|
600
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
601
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
494
602
|
}
|
|
495
603
|
}
|
|
496
604
|
else {
|
|
497
605
|
rawData[i] = InvalidFieldData;
|
|
606
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
607
|
+
decoderState.lastTimestamp = undefined;
|
|
608
|
+
}
|
|
498
609
|
}
|
|
499
610
|
readDataFromIndex += fDef.size;
|
|
500
611
|
messageSize += fDef.size;
|
|
@@ -502,6 +613,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
502
613
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
503
614
|
const developerFieldDef = developerFieldDefs[i];
|
|
504
615
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
616
|
+
if (rawDeveloperFields
|
|
617
|
+
&& readDataFromIndex + developerFieldDef.size <= dataEnd) {
|
|
618
|
+
rawDeveloperFields.push({
|
|
619
|
+
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
620
|
+
fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
|
|
621
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
505
624
|
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
506
625
|
if (fDef) {
|
|
507
626
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
@@ -563,6 +682,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
563
682
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
564
683
|
}
|
|
565
684
|
}
|
|
685
|
+
let compressedTimestamp;
|
|
566
686
|
if (isCompressedTimestamp) {
|
|
567
687
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
568
688
|
if (previousTimestamp === undefined) {
|
|
@@ -574,8 +694,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
574
694
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
575
695
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
576
696
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
577
|
-
const timestamp =
|
|
697
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
698
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
699
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
700
|
+
}
|
|
578
701
|
decoderState.lastTimestamp = timestamp;
|
|
702
|
+
compressedTimestamp = timestamp;
|
|
579
703
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
580
704
|
validFieldCount++;
|
|
581
705
|
}
|
|
@@ -607,9 +731,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
607
731
|
}
|
|
608
732
|
}
|
|
609
733
|
return {
|
|
734
|
+
globalMessageNumber: messageType.globalMessageNumber,
|
|
735
|
+
littleEndian: messageType.littleEndian,
|
|
736
|
+
compressedTimestamp,
|
|
610
737
|
messageType: message.name,
|
|
611
738
|
nextIndex: startIndex + messageSize + 1,
|
|
612
739
|
message: fields,
|
|
740
|
+
rawFields,
|
|
741
|
+
rawDeveloperFields,
|
|
613
742
|
};
|
|
614
743
|
}
|
|
615
744
|
export function getArrayBuffer(buffer) {
|
package/dist/cjs/binary.d.ts
CHANGED
|
@@ -20,11 +20,26 @@ export interface DecoderState {
|
|
|
20
20
|
lastTimestamp?: number;
|
|
21
21
|
monitoringTimestamp?: number;
|
|
22
22
|
}
|
|
23
|
+
export interface RawDeveloperFieldValue {
|
|
24
|
+
developerDataIndex: number;
|
|
25
|
+
fieldDefinitionNumber: number;
|
|
26
|
+
rawValue: number[];
|
|
27
|
+
}
|
|
28
|
+
export interface RawFieldValue {
|
|
29
|
+
fieldDefinitionNumber: number;
|
|
30
|
+
baseType: number;
|
|
31
|
+
rawValue: number[];
|
|
32
|
+
}
|
|
23
33
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
24
|
-
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
|
|
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): {
|
|
25
35
|
messageType: MessageName | 'definition' | '';
|
|
26
36
|
nextIndex: number;
|
|
27
37
|
message?: any;
|
|
38
|
+
globalMessageNumber?: number;
|
|
39
|
+
littleEndian?: boolean;
|
|
40
|
+
compressedTimestamp?: number;
|
|
41
|
+
rawFields?: RawFieldValue[];
|
|
42
|
+
rawDeveloperFields?: RawDeveloperFieldValue[];
|
|
28
43
|
};
|
|
29
44
|
export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
|
|
30
45
|
export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
|
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}`);
|
|
@@ -405,8 +436,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
405
436
|
developerFieldDef.resolvedFrom = description;
|
|
406
437
|
return resolvedFieldDef;
|
|
407
438
|
}
|
|
408
|
-
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
|
|
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,29 +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));
|
|
573
|
+
const includeRawDeveloperFields = options.includeRawDeveloperFields === true
|
|
574
|
+
|| (Array.isArray(options.includeRawDeveloperFields)
|
|
575
|
+
&& options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
|
|
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
|
+
}
|
|
488
585
|
const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
|
|
489
586
|
let validFieldCount = 0;
|
|
490
587
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
491
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
|
+
}
|
|
492
600
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
493
601
|
if (data !== InvalidFieldData
|
|
494
602
|
&& !isInvalidValue(data, fDef.type)
|
|
495
603
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
496
604
|
rawData[i] = data;
|
|
497
605
|
validFieldCount++;
|
|
498
|
-
if (!isCompressedTimestamp && fDef.fDefNo === 253
|
|
499
|
-
decoderState.lastTimestamp = data;
|
|
606
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
607
|
+
decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
|
|
500
608
|
}
|
|
501
609
|
}
|
|
502
610
|
else {
|
|
503
611
|
rawData[i] = InvalidFieldData;
|
|
612
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253) {
|
|
613
|
+
decoderState.lastTimestamp = undefined;
|
|
614
|
+
}
|
|
504
615
|
}
|
|
505
616
|
readDataFromIndex += fDef.size;
|
|
506
617
|
messageSize += fDef.size;
|
|
@@ -508,6 +619,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
508
619
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
509
620
|
const developerFieldDef = developerFieldDefs[i];
|
|
510
621
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
622
|
+
if (rawDeveloperFields
|
|
623
|
+
&& readDataFromIndex + developerFieldDef.size <= dataEnd) {
|
|
624
|
+
rawDeveloperFields.push({
|
|
625
|
+
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
626
|
+
fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
|
|
627
|
+
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
511
630
|
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
512
631
|
if (fDef) {
|
|
513
632
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
@@ -569,6 +688,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
569
688
|
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
570
689
|
}
|
|
571
690
|
}
|
|
691
|
+
let compressedTimestamp;
|
|
572
692
|
if (isCompressedTimestamp) {
|
|
573
693
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
574
694
|
if (previousTimestamp === undefined) {
|
|
@@ -580,8 +700,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
580
700
|
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
581
701
|
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
582
702
|
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
583
|
-
const timestamp =
|
|
703
|
+
const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
|
|
704
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
705
|
+
throw new Error('Compressed timestamp exceeds FIT uint32 range');
|
|
706
|
+
}
|
|
584
707
|
decoderState.lastTimestamp = timestamp;
|
|
708
|
+
compressedTimestamp = timestamp;
|
|
585
709
|
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
586
710
|
validFieldCount++;
|
|
587
711
|
}
|
|
@@ -613,9 +737,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
613
737
|
}
|
|
614
738
|
}
|
|
615
739
|
return {
|
|
740
|
+
globalMessageNumber: messageType.globalMessageNumber,
|
|
741
|
+
littleEndian: messageType.littleEndian,
|
|
742
|
+
compressedTimestamp,
|
|
616
743
|
messageType: message.name,
|
|
617
744
|
nextIndex: startIndex + messageSize + 1,
|
|
618
745
|
message: fields,
|
|
746
|
+
rawFields,
|
|
747
|
+
rawDeveloperFields,
|
|
619
748
|
};
|
|
620
749
|
}
|
|
621
750
|
function getArrayBuffer(buffer) {
|
package/dist/cjs/fit-parser.d.ts
CHANGED
|
@@ -2,6 +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, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
|
|
5
6
|
export interface FitParserOptions {
|
|
6
7
|
force?: boolean;
|
|
7
8
|
speedUnit?: string;
|
|
@@ -10,6 +11,12 @@ export interface FitParserOptions {
|
|
|
10
11
|
elapsedRecordField?: boolean;
|
|
11
12
|
pressureUnit?: string;
|
|
12
13
|
mode?: 'list' | 'cascade' | 'both';
|
|
14
|
+
/** Retains exact developer-field bytes for all or selected global message numbers. */
|
|
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;
|
|
13
20
|
}
|
|
14
21
|
type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
|
|
15
22
|
export default class FitParser {
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -8,6 +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, _b, _c;
|
|
11
12
|
this.options = {
|
|
12
13
|
force: options.force != null ? options.force : true,
|
|
13
14
|
speedUnit: options.speedUnit || 'm/s',
|
|
@@ -16,6 +17,9 @@ class FitParser {
|
|
|
16
17
|
elapsedRecordField: options.elapsedRecordField || false,
|
|
17
18
|
pressureUnit: options.pressureUnit || 'bar',
|
|
18
19
|
mode: options.mode || 'list',
|
|
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,
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
25
|
parseAsync(content) {
|
|
@@ -31,7 +35,7 @@ class FitParser {
|
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
parse(content, callback) {
|
|
34
|
-
var _a, _b;
|
|
38
|
+
var _a, _b, _c;
|
|
35
39
|
const blob = content instanceof ArrayBuffer
|
|
36
40
|
? new Uint8Array(content)
|
|
37
41
|
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
@@ -118,6 +122,15 @@ class FitParser {
|
|
|
118
122
|
const time_in_zone = [];
|
|
119
123
|
const activity_metrics = [];
|
|
120
124
|
const user_metrics = [];
|
|
125
|
+
const rawDeveloperFields = this.options.includeRawDeveloperFields === true
|
|
126
|
+
|| Array.isArray(this.options.includeRawDeveloperFields)
|
|
127
|
+
? []
|
|
128
|
+
: undefined;
|
|
129
|
+
const rawMessages = this.options.includeRawMessages === true
|
|
130
|
+
|| Array.isArray(this.options.includeRawMessages)
|
|
131
|
+
? []
|
|
132
|
+
: undefined;
|
|
133
|
+
const messageCountsByGlobalNumber = new Map();
|
|
121
134
|
let loopIndex = headerLength;
|
|
122
135
|
const messageTypes = [];
|
|
123
136
|
const developerFields = [];
|
|
@@ -128,12 +141,41 @@ class FitParser {
|
|
|
128
141
|
let lastStopTimestamp;
|
|
129
142
|
let pausedTime = 0;
|
|
130
143
|
while (loopIndex < crcStart) {
|
|
131
|
-
const {
|
|
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);
|
|
132
145
|
loopIndex = nextIndex;
|
|
146
|
+
if (globalMessageNumber !== undefined) {
|
|
147
|
+
const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
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
|
+
}
|
|
162
|
+
recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
|
|
163
|
+
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
|
|
164
|
+
global_message_number: globalMessageNumber,
|
|
165
|
+
message_index: messageIndex,
|
|
166
|
+
developer_data_index: field.developerDataIndex,
|
|
167
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
168
|
+
raw_value: field.rawValue,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (this.options.rawMessagesOnly) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
133
175
|
if (messageType !== ''
|
|
134
176
|
&& messageType !== 'definition'
|
|
135
177
|
&& message !== undefined) {
|
|
136
|
-
((
|
|
178
|
+
((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
|
|
137
179
|
}
|
|
138
180
|
switch (messageType) {
|
|
139
181
|
case 'lap':
|
|
@@ -249,6 +291,16 @@ class FitParser {
|
|
|
249
291
|
break;
|
|
250
292
|
}
|
|
251
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
|
+
}
|
|
252
304
|
fitObj.hr_zone = hr_zone;
|
|
253
305
|
fitObj.power_zone = power_zone;
|
|
254
306
|
fitObj.dive_gases = dive_gases;
|
|
@@ -268,11 +320,17 @@ class FitParser {
|
|
|
268
320
|
fitObj.activity_metrics = activity_metrics;
|
|
269
321
|
fitObj.user_metrics = user_metrics;
|
|
270
322
|
fitObj.messages = messages;
|
|
323
|
+
if (rawDeveloperFields) {
|
|
324
|
+
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
325
|
+
}
|
|
326
|
+
if (rawMessages) {
|
|
327
|
+
fitObj.raw_messages = rawMessages;
|
|
328
|
+
}
|
|
271
329
|
if (isCascadeNeeded) {
|
|
272
330
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
|
|
273
331
|
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
|
|
274
332
|
sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
|
|
275
|
-
fitObj.activity = Object.assign(Object.assign({}, ((
|
|
333
|
+
fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
|
|
276
334
|
sessions,
|
|
277
335
|
events,
|
|
278
336
|
hrv,
|
package/dist/cjs/fit_types.d.ts
CHANGED
|
@@ -9,6 +9,31 @@ export type Unit<T extends string> = Record<T, {
|
|
|
9
9
|
multiplier: number;
|
|
10
10
|
offset: number;
|
|
11
11
|
}>;
|
|
12
|
+
export interface ParsedRawDeveloperField {
|
|
13
|
+
global_message_number: number;
|
|
14
|
+
message_index: number;
|
|
15
|
+
developer_data_index: number;
|
|
16
|
+
field_definition_number: number;
|
|
17
|
+
raw_value: number[];
|
|
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
|
+
}
|
|
12
37
|
export type Activity = 'manual' | 'auto_multi_sport' | number;
|
|
13
38
|
export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
|
|
14
39
|
export type ActivityLevel = 'low' | 'medium' | 'high' | number;
|
|
@@ -2070,6 +2095,8 @@ export interface ParsedFit {
|
|
|
2070
2095
|
protocolVersion?: number;
|
|
2071
2096
|
profileVersion?: number;
|
|
2072
2097
|
messages?: ParsedMessages;
|
|
2098
|
+
raw_developer_fields?: ParsedRawDeveloperField[];
|
|
2099
|
+
raw_messages?: ParsedRawFitMessage[];
|
|
2073
2100
|
file_creator: ParsedFileCreator;
|
|
2074
2101
|
device_settings: ParsedDeviceSettings;
|
|
2075
2102
|
dive_summary?: ParsedDiveSummary;
|
|
@@ -175,6 +175,31 @@ function generateUtilities() {
|
|
|
175
175
|
]),
|
|
176
176
|
]));
|
|
177
177
|
nodes.push(unitType);
|
|
178
|
+
nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
|
|
179
|
+
generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
180
|
+
generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
181
|
+
generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
182
|
+
generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
183
|
+
generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
|
|
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
|
+
]));
|
|
178
203
|
return nodes;
|
|
179
204
|
}
|
|
180
205
|
function generateAdditionalFields(msg) {
|
|
@@ -258,6 +283,8 @@ function generateFitType() {
|
|
|
258
283
|
generateProperty('protocolVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
|
|
259
284
|
generateProperty('profileVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
|
|
260
285
|
generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
|
|
286
|
+
generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
|
|
287
|
+
generateArrayProperty('raw_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
|
|
261
288
|
...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
|
|
262
289
|
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
|
|
263
290
|
? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
|
package/dist/fit-parser.d.ts
CHANGED
|
@@ -2,6 +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, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
|
|
5
6
|
export interface FitParserOptions {
|
|
6
7
|
force?: boolean;
|
|
7
8
|
speedUnit?: string;
|
|
@@ -10,6 +11,12 @@ export interface FitParserOptions {
|
|
|
10
11
|
elapsedRecordField?: boolean;
|
|
11
12
|
pressureUnit?: string;
|
|
12
13
|
mode?: 'list' | 'cascade' | 'both';
|
|
14
|
+
/** Retains exact developer-field bytes for all or selected global message numbers. */
|
|
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;
|
|
13
20
|
}
|
|
14
21
|
type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
|
|
15
22
|
export default class FitParser {
|
package/dist/fit-parser.js
CHANGED
|
@@ -3,6 +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, _b, _c;
|
|
6
7
|
this.options = {
|
|
7
8
|
force: options.force != null ? options.force : true,
|
|
8
9
|
speedUnit: options.speedUnit || 'm/s',
|
|
@@ -11,6 +12,9 @@ export default class FitParser {
|
|
|
11
12
|
elapsedRecordField: options.elapsedRecordField || false,
|
|
12
13
|
pressureUnit: options.pressureUnit || 'bar',
|
|
13
14
|
mode: options.mode || 'list',
|
|
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,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
16
20
|
parseAsync(content) {
|
|
@@ -26,7 +30,7 @@ export default class FitParser {
|
|
|
26
30
|
});
|
|
27
31
|
}
|
|
28
32
|
parse(content, callback) {
|
|
29
|
-
var _a, _b;
|
|
33
|
+
var _a, _b, _c;
|
|
30
34
|
const blob = content instanceof ArrayBuffer
|
|
31
35
|
? new Uint8Array(content)
|
|
32
36
|
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
@@ -113,6 +117,15 @@ export default class FitParser {
|
|
|
113
117
|
const time_in_zone = [];
|
|
114
118
|
const activity_metrics = [];
|
|
115
119
|
const user_metrics = [];
|
|
120
|
+
const rawDeveloperFields = this.options.includeRawDeveloperFields === true
|
|
121
|
+
|| Array.isArray(this.options.includeRawDeveloperFields)
|
|
122
|
+
? []
|
|
123
|
+
: undefined;
|
|
124
|
+
const rawMessages = this.options.includeRawMessages === true
|
|
125
|
+
|| Array.isArray(this.options.includeRawMessages)
|
|
126
|
+
? []
|
|
127
|
+
: undefined;
|
|
128
|
+
const messageCountsByGlobalNumber = new Map();
|
|
116
129
|
let loopIndex = headerLength;
|
|
117
130
|
const messageTypes = [];
|
|
118
131
|
const developerFields = [];
|
|
@@ -123,12 +136,41 @@ export default class FitParser {
|
|
|
123
136
|
let lastStopTimestamp;
|
|
124
137
|
let pausedTime = 0;
|
|
125
138
|
while (loopIndex < crcStart) {
|
|
126
|
-
const {
|
|
139
|
+
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
127
140
|
loopIndex = nextIndex;
|
|
141
|
+
if (globalMessageNumber !== undefined) {
|
|
142
|
+
const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
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
|
+
}
|
|
157
|
+
recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
|
|
158
|
+
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
|
|
159
|
+
global_message_number: globalMessageNumber,
|
|
160
|
+
message_index: messageIndex,
|
|
161
|
+
developer_data_index: field.developerDataIndex,
|
|
162
|
+
field_definition_number: field.fieldDefinitionNumber,
|
|
163
|
+
raw_value: field.rawValue,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (this.options.rawMessagesOnly) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
128
170
|
if (messageType !== ''
|
|
129
171
|
&& messageType !== 'definition'
|
|
130
172
|
&& message !== undefined) {
|
|
131
|
-
((
|
|
173
|
+
((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
|
|
132
174
|
}
|
|
133
175
|
switch (messageType) {
|
|
134
176
|
case 'lap':
|
|
@@ -244,6 +286,16 @@ export default class FitParser {
|
|
|
244
286
|
break;
|
|
245
287
|
}
|
|
246
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
|
+
}
|
|
247
299
|
fitObj.hr_zone = hr_zone;
|
|
248
300
|
fitObj.power_zone = power_zone;
|
|
249
301
|
fitObj.dive_gases = dive_gases;
|
|
@@ -263,11 +315,17 @@ export default class FitParser {
|
|
|
263
315
|
fitObj.activity_metrics = activity_metrics;
|
|
264
316
|
fitObj.user_metrics = user_metrics;
|
|
265
317
|
fitObj.messages = messages;
|
|
318
|
+
if (rawDeveloperFields) {
|
|
319
|
+
fitObj.raw_developer_fields = rawDeveloperFields;
|
|
320
|
+
}
|
|
321
|
+
if (rawMessages) {
|
|
322
|
+
fitObj.raw_messages = rawMessages;
|
|
323
|
+
}
|
|
266
324
|
if (isCascadeNeeded) {
|
|
267
325
|
laps = mapDataIntoLap(laps, 'records', records);
|
|
268
326
|
laps = mapDataIntoLap(laps, 'lengths', lengths);
|
|
269
327
|
sessions = mapDataIntoSession(sessions, laps);
|
|
270
|
-
fitObj.activity = Object.assign(Object.assign({}, ((
|
|
328
|
+
fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
|
|
271
329
|
sessions,
|
|
272
330
|
events,
|
|
273
331
|
hrv,
|
package/dist/fit_types.d.ts
CHANGED
|
@@ -9,6 +9,31 @@ export type Unit<T extends string> = Record<T, {
|
|
|
9
9
|
multiplier: number;
|
|
10
10
|
offset: number;
|
|
11
11
|
}>;
|
|
12
|
+
export interface ParsedRawDeveloperField {
|
|
13
|
+
global_message_number: number;
|
|
14
|
+
message_index: number;
|
|
15
|
+
developer_data_index: number;
|
|
16
|
+
field_definition_number: number;
|
|
17
|
+
raw_value: number[];
|
|
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
|
+
}
|
|
12
37
|
export type Activity = 'manual' | 'auto_multi_sport' | number;
|
|
13
38
|
export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
|
|
14
39
|
export type ActivityLevel = 'low' | 'medium' | 'high' | number;
|
|
@@ -2070,6 +2095,8 @@ export interface ParsedFit {
|
|
|
2070
2095
|
protocolVersion?: number;
|
|
2071
2096
|
profileVersion?: number;
|
|
2072
2097
|
messages?: ParsedMessages;
|
|
2098
|
+
raw_developer_fields?: ParsedRawDeveloperField[];
|
|
2099
|
+
raw_messages?: ParsedRawFitMessage[];
|
|
2073
2100
|
file_creator: ParsedFileCreator;
|
|
2074
2101
|
device_settings: ParsedDeviceSettings;
|
|
2075
2102
|
dive_summary?: ParsedDiveSummary;
|
package/dist/type_generator.js
CHANGED
|
@@ -154,6 +154,31 @@ export function generateUtilities() {
|
|
|
154
154
|
]),
|
|
155
155
|
]));
|
|
156
156
|
nodes.push(unitType);
|
|
157
|
+
nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
|
|
158
|
+
generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
159
|
+
generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
160
|
+
generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
161
|
+
generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
162
|
+
generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
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
|
+
]));
|
|
157
182
|
return nodes;
|
|
158
183
|
}
|
|
159
184
|
function generateAdditionalFields(msg) {
|
|
@@ -237,6 +262,8 @@ export function generateFitType() {
|
|
|
237
262
|
generateProperty('protocolVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
238
263
|
generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
239
264
|
generateProperty('messages', ts.factory.createTypeReferenceNode('ParsedMessages')),
|
|
265
|
+
generateArrayProperty('raw_developer_fields', ts.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
|
|
266
|
+
generateArrayProperty('raw_messages', ts.factory.createTypeReferenceNode('ParsedRawFitMessage')),
|
|
240
267
|
...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
|
|
241
268
|
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
|
|
242
269
|
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
|