fit-file-parser 4.0.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +273 -117
- package/dist/binary.d.ts +7 -4
- package/dist/binary.js +207 -163
- package/dist/cjs/binary.d.ts +7 -4
- package/dist/cjs/binary.js +207 -163
- package/dist/cjs/fit-parser.js +41 -21
- package/dist/cjs/fit.d.ts +15 -7
- package/dist/cjs/fit.js +137 -9078
- package/dist/cjs/fit_types.d.ts +1384 -298
- package/dist/cjs/garmin_profile.generated.d.ts +9 -0
- package/dist/cjs/garmin_profile.generated.js +17838 -0
- package/dist/cjs/helper.js +22 -12
- package/dist/cjs/messages.d.ts +2 -3
- package/dist/cjs/type_generator.d.ts +3 -0
- package/dist/cjs/type_generator.js +56 -27
- package/dist/fit-parser.js +41 -21
- package/dist/fit.d.ts +15 -7
- package/dist/fit.js +136 -9077
- package/dist/fit_types.d.ts +1384 -298
- package/dist/garmin_profile.generated.d.ts +9 -0
- package/dist/garmin_profile.generated.js +17835 -0
- package/dist/helper.js +22 -12
- package/dist/messages.d.ts +2 -3
- package/dist/type_generator.d.ts +3 -0
- package/dist/type_generator.js +55 -27
- package/package.json +9 -6
package/dist/binary.js
CHANGED
|
@@ -3,31 +3,48 @@ import { FIT } from './fit.js';
|
|
|
3
3
|
import { getFitMessage, getFitMessageBaseType } from './messages.js';
|
|
4
4
|
const CompressedLocalMsgNumMask = 0x60;
|
|
5
5
|
const CompressedHeaderMask = 0x80;
|
|
6
|
+
const CompressedTimestampMask = 0x1F;
|
|
6
7
|
const GarminTimeOffset = 631065600000;
|
|
7
|
-
let monitoring_timestamp = 0;
|
|
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 baseTypeSize(type) {
|
|
11
12
|
switch (type) {
|
|
13
|
+
case 'enum':
|
|
14
|
+
case 'sint8':
|
|
15
|
+
case 'uint8':
|
|
16
|
+
case 'uint8z':
|
|
17
|
+
case 'byte':
|
|
18
|
+
return 1;
|
|
12
19
|
case 'sint16':
|
|
13
20
|
case 'uint16':
|
|
14
21
|
case 'uint16z':
|
|
15
|
-
return
|
|
22
|
+
return 2;
|
|
16
23
|
case 'sint32':
|
|
17
24
|
case 'uint32':
|
|
18
25
|
case 'uint32z':
|
|
19
26
|
case 'float32':
|
|
20
|
-
return
|
|
27
|
+
return 4;
|
|
21
28
|
case 'float64':
|
|
22
|
-
return
|
|
23
|
-
case 'uint16_array':
|
|
24
|
-
return size % 2 !== 0;
|
|
25
|
-
case 'uint32_array':
|
|
26
|
-
return size % 4 !== 0;
|
|
29
|
+
return 8;
|
|
27
30
|
default:
|
|
28
|
-
return
|
|
31
|
+
return undefined;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
34
|
+
function requiresBoundedEndianDataView(type, size) {
|
|
35
|
+
const elementSize = baseTypeSize(type);
|
|
36
|
+
return elementSize !== undefined && size % elementSize !== 0;
|
|
37
|
+
}
|
|
38
|
+
function areProfileBaseTypesCompatible(profileType, wireType) {
|
|
39
|
+
if (profileType === undefined || profileType === wireType) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// FIT producers commonly interchange enum, uint8, and byte definitions.
|
|
43
|
+
// They have the same width and value representation; keeping the semantic
|
|
44
|
+
// profile type is safe while still rejecting width and sign conflicts.
|
|
45
|
+
return uint8CompatibleTypes.has(String(profileType))
|
|
46
|
+
&& uint8CompatibleTypes.has(String(wireType));
|
|
47
|
+
}
|
|
31
48
|
export function addEndian(littleEndian, bytes) {
|
|
32
49
|
let result = 0;
|
|
33
50
|
if (!littleEndian)
|
|
@@ -37,77 +54,10 @@ export function addEndian(littleEndian, bytes) {
|
|
|
37
54
|
}
|
|
38
55
|
return result;
|
|
39
56
|
}
|
|
40
|
-
function readData(blob, dataView, fDef, startIndex
|
|
41
|
-
var _a;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < fDef.size; i++) {
|
|
45
|
-
array8.push(blob[startIndex + i]);
|
|
46
|
-
}
|
|
47
|
-
return array8;
|
|
48
|
-
}
|
|
49
|
-
if (fDef.endianAbility) {
|
|
50
|
-
const requiresBoundedDataView = (_a = fDef.requiresBoundedDataView) !== null && _a !== void 0 ? _a : (fDef.requiresBoundedDataView = requiresBoundedEndianDataView(fDef.type, fDef.size));
|
|
51
|
-
let fieldDataView = dataView;
|
|
52
|
-
let fieldStartIndex = startIndex;
|
|
53
|
-
if (startIndex < 0
|
|
54
|
-
|| startIndex + fDef.size > blob.length
|
|
55
|
-
|| startIndex + fDef.size > dataView.byteLength) {
|
|
56
|
-
const paddedField = new Uint8Array(fDef.size);
|
|
57
|
-
for (let index = 0; index < fDef.size; index++) {
|
|
58
|
-
paddedField[index] = blob[startIndex + index];
|
|
59
|
-
}
|
|
60
|
-
fieldDataView = new DataView(paddedField.buffer);
|
|
61
|
-
fieldStartIndex = 0;
|
|
62
|
-
}
|
|
63
|
-
else if (requiresBoundedDataView) {
|
|
64
|
-
fieldDataView = new DataView(dataView.buffer, dataView.byteOffset + startIndex, fDef.size);
|
|
65
|
-
fieldStartIndex = 0;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
switch (fDef.type) {
|
|
69
|
-
case 'sint16':
|
|
70
|
-
return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
|
|
71
|
-
case 'uint16':
|
|
72
|
-
case 'uint16z':
|
|
73
|
-
return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
|
|
74
|
-
case 'sint32':
|
|
75
|
-
return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
|
|
76
|
-
case 'uint32':
|
|
77
|
-
case 'uint32z':
|
|
78
|
-
return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
|
|
79
|
-
case 'float32':
|
|
80
|
-
return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
|
|
81
|
-
case 'float64':
|
|
82
|
-
return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
|
|
83
|
-
case 'uint32_array': {
|
|
84
|
-
const array32 = [];
|
|
85
|
-
for (let i = 0; i < fDef.size; i += 4) {
|
|
86
|
-
array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
|
|
87
|
-
}
|
|
88
|
-
return array32;
|
|
89
|
-
}
|
|
90
|
-
case 'uint16_array': {
|
|
91
|
-
const array16 = [];
|
|
92
|
-
for (let i = 0; i < fDef.size; i += 2) {
|
|
93
|
-
array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
|
|
94
|
-
}
|
|
95
|
-
return array16;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
if (!options.force) {
|
|
101
|
-
throw e;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const temp = [];
|
|
105
|
-
for (let i = 0; i < fDef.size; i++) {
|
|
106
|
-
temp.push(blob[startIndex + i]);
|
|
107
|
-
}
|
|
108
|
-
return addEndian(fDef.littleEndian, temp);
|
|
109
|
-
}
|
|
110
|
-
if (fDef.type === 'string') {
|
|
57
|
+
function readData(blob, dataView, fDef, startIndex) {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
const rawType = (_b = (_a = fDef.rawType) !== null && _a !== void 0 ? _a : FIT.types.fit_base_type[fDef.baseTypeNo]) !== null && _b !== void 0 ? _b : fDef.type;
|
|
60
|
+
if (rawType === 'string') {
|
|
111
61
|
const temp = [];
|
|
112
62
|
for (let i = 0; i < fDef.size; i++) {
|
|
113
63
|
if (blob[startIndex + i]) {
|
|
@@ -116,30 +66,77 @@ function readData(blob, dataView, fDef, startIndex, options) {
|
|
|
116
66
|
}
|
|
117
67
|
return Buffer.from(temp).toString('utf-8');
|
|
118
68
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
69
|
+
const elementSize = baseTypeSize(rawType);
|
|
70
|
+
if (elementSize === undefined) {
|
|
71
|
+
return InvalidFieldData;
|
|
72
|
+
}
|
|
73
|
+
if (fDef.size < elementSize
|
|
74
|
+
|| fDef.size % elementSize !== 0
|
|
75
|
+
|| startIndex < 0
|
|
76
|
+
|| startIndex + fDef.size > blob.length
|
|
77
|
+
|| startIndex + fDef.size > dataView.byteLength) {
|
|
78
|
+
return InvalidFieldData;
|
|
125
79
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
80
|
+
const isArray = fDef.array === true
|
|
81
|
+
|| String(fDef.type).endsWith('_array')
|
|
82
|
+
|| fDef.type === 'exercise_category_array'
|
|
83
|
+
|| (fDef.isDeveloperField === true && fDef.size > elementSize);
|
|
84
|
+
const elementCount = isArray ? fDef.size / elementSize : 1;
|
|
85
|
+
const values = [];
|
|
86
|
+
for (let elementIndex = 0; elementIndex < elementCount; elementIndex++) {
|
|
87
|
+
const offset = startIndex + elementIndex * elementSize;
|
|
88
|
+
switch (rawType) {
|
|
89
|
+
case 'sint8': {
|
|
90
|
+
const value = blob[offset];
|
|
91
|
+
values.push(value > 127 ? value - 256 : value);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'sint16':
|
|
95
|
+
values.push(dataView.getInt16(offset, fDef.littleEndian));
|
|
96
|
+
break;
|
|
97
|
+
case 'uint16':
|
|
98
|
+
case 'uint16z':
|
|
99
|
+
values.push(dataView.getUint16(offset, fDef.littleEndian));
|
|
100
|
+
break;
|
|
101
|
+
case 'sint32':
|
|
102
|
+
values.push(dataView.getInt32(offset, fDef.littleEndian));
|
|
103
|
+
break;
|
|
104
|
+
case 'uint32':
|
|
105
|
+
case 'uint32z':
|
|
106
|
+
values.push(dataView.getUint32(offset, fDef.littleEndian));
|
|
107
|
+
break;
|
|
108
|
+
case 'float32':
|
|
109
|
+
values.push(dataView.getFloat32(offset, fDef.littleEndian));
|
|
110
|
+
break;
|
|
111
|
+
case 'float64':
|
|
112
|
+
values.push(dataView.getFloat64(offset, fDef.littleEndian));
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
values.push(blob[offset]);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
129
118
|
}
|
|
130
|
-
return
|
|
119
|
+
return isArray ? values : values[0];
|
|
131
120
|
}
|
|
132
|
-
function formatByType(data, type, scale, offset) {
|
|
121
|
+
function formatByType(data, type, scale, offset, units) {
|
|
133
122
|
switch (type) {
|
|
134
123
|
case 'date_time':
|
|
135
124
|
case 'local_date_time':
|
|
136
125
|
return new Date(data * 1000 + GarminTimeOffset);
|
|
137
126
|
case 'sint32':
|
|
138
|
-
return
|
|
127
|
+
return units === 'semicircles'
|
|
128
|
+
? data * FIT.scConst
|
|
129
|
+
: scale ? data / scale + offset : data;
|
|
130
|
+
case 'sint8':
|
|
139
131
|
case 'uint8':
|
|
132
|
+
case 'uint8z':
|
|
140
133
|
case 'sint16':
|
|
141
|
-
case 'uint32':
|
|
142
134
|
case 'uint16':
|
|
135
|
+
case 'uint16z':
|
|
136
|
+
case 'uint32':
|
|
137
|
+
case 'uint32z':
|
|
138
|
+
case 'float32':
|
|
139
|
+
case 'float64':
|
|
143
140
|
return scale ? data / scale + offset : data;
|
|
144
141
|
case 'uint32_array':
|
|
145
142
|
case 'uint16_array':
|
|
@@ -154,6 +151,16 @@ function formatByType(data, type, scale, offset) {
|
|
|
154
151
|
});
|
|
155
152
|
}
|
|
156
153
|
return scale ? data / scale + offset : data;
|
|
154
|
+
case 'exercise_category_array':
|
|
155
|
+
if (Array.isArray(data)) {
|
|
156
|
+
return data.map((dataItem) => {
|
|
157
|
+
if (isInvalidValue(dataItem, 'uint16')) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return formatByType(dataItem, 'exercise_category', scale, offset);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return formatByType(data, 'exercise_category', scale, offset);
|
|
157
164
|
default:
|
|
158
165
|
{
|
|
159
166
|
const typeMap = FIT.types[type];
|
|
@@ -186,7 +193,7 @@ function formatByType(data, type, scale, offset) {
|
|
|
186
193
|
dataItem.value = data & Number(key);
|
|
187
194
|
}
|
|
188
195
|
else {
|
|
189
|
-
dataItem[value] =
|
|
196
|
+
dataItem[value] = (data & Number(key)) !== 0;
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
return dataItem;
|
|
@@ -212,10 +219,9 @@ function isInvalidValue(data, type) {
|
|
|
212
219
|
case 'string':
|
|
213
220
|
return data === 0x00;
|
|
214
221
|
case 'float32':
|
|
215
|
-
return data
|
|
222
|
+
return Number.isNaN(data);
|
|
216
223
|
case 'float64':
|
|
217
|
-
|
|
218
|
-
return data === 0xFFFFFFFFFFFFFFFF;
|
|
224
|
+
return Number.isNaN(data);
|
|
219
225
|
case 'uint8z':
|
|
220
226
|
return data === 0x00;
|
|
221
227
|
case 'uint16z':
|
|
@@ -243,6 +249,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
|
|
|
243
249
|
const baseType = FIT.types.fit_base_type[baseTypeNo];
|
|
244
250
|
return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
|
|
245
251
|
}
|
|
252
|
+
function formatFieldValue(data, fDef, options, fields) {
|
|
253
|
+
var _a, _b, _c;
|
|
254
|
+
const field = fDef.name;
|
|
255
|
+
const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
|
|
256
|
+
const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
|
|
257
|
+
if (Array.isArray(data)
|
|
258
|
+
&& !String(fDef.type).endsWith('_array')
|
|
259
|
+
&& fDef.type !== 'exercise_category_array') {
|
|
260
|
+
const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : FIT.types.fit_base_type[fDef.baseTypeNo];
|
|
261
|
+
return data.map((item) => {
|
|
262
|
+
if (isInvalidValue(item, rawType)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
|
|
269
|
+
}
|
|
270
|
+
function isOutputFieldName(field) {
|
|
271
|
+
return field !== 'unknown' && field !== '' && field !== undefined;
|
|
272
|
+
}
|
|
246
273
|
function convertTo(data, unitsList, unitName) {
|
|
247
274
|
const options = FIT.options[unitsList];
|
|
248
275
|
const unit = options[unitName];
|
|
@@ -270,7 +297,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
270
297
|
case 'vertical_speed':
|
|
271
298
|
case 'avg_speed':
|
|
272
299
|
case 'max_speed':
|
|
273
|
-
case '
|
|
300
|
+
case 'speed1s':
|
|
274
301
|
case 'ball_speed':
|
|
275
302
|
case 'enhanced_avg_speed':
|
|
276
303
|
case 'enhanced_max_speed':
|
|
@@ -301,8 +328,10 @@ function applyOptions(data, field, options, fields) {
|
|
|
301
328
|
return convertTo(data, 'lengthUnits', options.lengthUnit);
|
|
302
329
|
case 'temperature':
|
|
303
330
|
case 'min_temperature':
|
|
331
|
+
case 'temperature_min':
|
|
304
332
|
case 'avg_temperature':
|
|
305
333
|
case 'max_temperature':
|
|
334
|
+
case 'temperature_max':
|
|
306
335
|
return convertTo(data, 'temperatureUnits', options.temperatureUnit);
|
|
307
336
|
case 'pressure':
|
|
308
337
|
case 'start_pressure':
|
|
@@ -319,22 +348,8 @@ function applyOptions(data, field, options, fields) {
|
|
|
319
348
|
return data;
|
|
320
349
|
}
|
|
321
350
|
}
|
|
322
|
-
function applyGarminProductName(fields) {
|
|
323
|
-
if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const product = fields.product;
|
|
327
|
-
if (typeof product !== 'number') {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const productName = FIT.types.garmin_product[product];
|
|
331
|
-
if (typeof productName === 'string') {
|
|
332
|
-
// Keep the raw protocol ID and expose the SDK product name separately.
|
|
333
|
-
fields.product_name = productName;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
351
|
function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
|
|
337
|
-
var _a, _b, _c, _d;
|
|
352
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
338
353
|
const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
|
|
339
354
|
if (!description) {
|
|
340
355
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
@@ -345,9 +360,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
345
360
|
&& developerFieldDef.resolvedFieldDef) {
|
|
346
361
|
return developerFieldDef.resolvedFieldDef;
|
|
347
362
|
}
|
|
348
|
-
const
|
|
363
|
+
const describedBaseType = description.fit_base_type_id;
|
|
364
|
+
const baseType = typeof describedBaseType === 'number'
|
|
365
|
+
? describedBaseType
|
|
366
|
+
: Number((_b = Object.entries(FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
|
|
349
367
|
const type = FIT.types.fit_base_type[baseType];
|
|
350
|
-
if (
|
|
368
|
+
if (!Number.isInteger(baseType) || type === undefined) {
|
|
351
369
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
352
370
|
developerFieldDef.resolvedFrom = undefined;
|
|
353
371
|
if (options.force) {
|
|
@@ -357,15 +375,22 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
357
375
|
}
|
|
358
376
|
const resolvedFieldDef = {
|
|
359
377
|
type,
|
|
378
|
+
rawType: type,
|
|
360
379
|
fDefNo: developerFieldDef.fieldDefinitionNumber,
|
|
361
380
|
size: developerFieldDef.size,
|
|
381
|
+
array: type !== 'string'
|
|
382
|
+
&& developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
|
|
362
383
|
endianAbility: (baseType & 128) === 128,
|
|
363
384
|
littleEndian,
|
|
364
385
|
baseTypeNo: baseType,
|
|
365
|
-
name: (
|
|
386
|
+
name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
|
|
366
387
|
dataType: getFitMessageBaseType(baseType & 15),
|
|
367
|
-
scale: (
|
|
368
|
-
|
|
388
|
+
scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
|
|
389
|
+
// FIT developer-field descriptions use `raw / scale - offset`. This
|
|
390
|
+
// parser's formatter retains the legacy equivalent signed offset and adds
|
|
391
|
+
// it after scaling.
|
|
392
|
+
offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
|
|
393
|
+
units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
|
|
369
394
|
requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
|
|
370
395
|
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
371
396
|
isDeveloperField: true,
|
|
@@ -374,11 +399,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
374
399
|
developerFieldDef.resolvedFrom = description;
|
|
375
400
|
return resolvedFieldDef;
|
|
376
401
|
}
|
|
377
|
-
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
378
|
-
var _a, _b, _c
|
|
402
|
+
export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
|
|
403
|
+
var _a, _b, _c;
|
|
379
404
|
const recordHeader = blob[startIndex];
|
|
380
405
|
let localMessageType = recordHeader & 15;
|
|
381
|
-
|
|
406
|
+
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
407
|
+
if (isCompressedTimestamp) {
|
|
382
408
|
// compressed timestamp
|
|
383
409
|
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
384
410
|
}
|
|
@@ -406,19 +432,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
406
432
|
for (let i = 0; i < numberOfFields; i++) {
|
|
407
433
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
408
434
|
const baseType = blob[fDefIndex + 2];
|
|
409
|
-
const
|
|
435
|
+
const wireType = FIT.types.fit_base_type[baseType];
|
|
436
|
+
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
437
|
+
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
410
438
|
const fDef = {
|
|
411
|
-
type,
|
|
439
|
+
type: profileCompatible ? type : wireType,
|
|
440
|
+
rawType: wireType,
|
|
412
441
|
fDefNo: blob[fDefIndex],
|
|
413
442
|
size: blob[fDefIndex + 1],
|
|
443
|
+
array: profileCompatible
|
|
444
|
+
? array === true || String(type).endsWith('_array')
|
|
445
|
+
: false,
|
|
414
446
|
endianAbility: (baseType & 128) === 128,
|
|
415
447
|
littleEndian: lEnd,
|
|
416
448
|
baseTypeNo: baseType,
|
|
417
|
-
name: field,
|
|
449
|
+
name: profileCompatible ? field : '',
|
|
418
450
|
dataType: getFitMessageBaseType(baseType & 15),
|
|
419
|
-
scale,
|
|
420
|
-
offset,
|
|
421
|
-
|
|
451
|
+
scale: profileCompatible ? scale : null,
|
|
452
|
+
offset: profileCompatible ? offset : 0,
|
|
453
|
+
units: profileCompatible ? units : '',
|
|
454
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
|
|
422
455
|
};
|
|
423
456
|
mTypeDef.fieldDefs.push(fDef);
|
|
424
457
|
}
|
|
@@ -440,8 +473,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
440
473
|
};
|
|
441
474
|
}
|
|
442
475
|
const messageType = messageTypes[localMessageType] || messageTypes[0];
|
|
443
|
-
// TODO: handle compressed header ((recordHeader & 128) == 128)
|
|
444
|
-
// uncompressed header
|
|
445
476
|
let messageSize = 0;
|
|
446
477
|
let readDataFromIndex = startIndex + 1;
|
|
447
478
|
const fields = {};
|
|
@@ -452,10 +483,15 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
452
483
|
let validFieldCount = 0;
|
|
453
484
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
454
485
|
const fDef = messageType.fieldDefs[i];
|
|
455
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
456
|
-
if (
|
|
486
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
487
|
+
if (data !== InvalidFieldData
|
|
488
|
+
&& !isInvalidValue(data, fDef.type)
|
|
489
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
457
490
|
rawData[i] = data;
|
|
458
491
|
validFieldCount++;
|
|
492
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
|
|
493
|
+
decoderState.lastTimestamp = data;
|
|
494
|
+
}
|
|
459
495
|
}
|
|
460
496
|
else {
|
|
461
497
|
rawData[i] = InvalidFieldData;
|
|
@@ -468,8 +504,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
468
504
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
469
505
|
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
470
506
|
if (fDef) {
|
|
471
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
472
|
-
if (
|
|
507
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
508
|
+
if (data !== InvalidFieldData
|
|
509
|
+
&& !isInvalidValue(data, fDef.type)
|
|
510
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
473
511
|
rawData[rawDataIndex] = data;
|
|
474
512
|
validFieldCount++;
|
|
475
513
|
}
|
|
@@ -489,9 +527,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
489
527
|
continue;
|
|
490
528
|
}
|
|
491
529
|
const fDef = messageType.fieldDefs[i];
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
fields[field] = data;
|
|
530
|
+
if (isOutputFieldName(fDef.name)) {
|
|
531
|
+
fields[fDef.name] = data;
|
|
495
532
|
}
|
|
496
533
|
}
|
|
497
534
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
@@ -511,20 +548,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
511
548
|
continue;
|
|
512
549
|
}
|
|
513
550
|
const fDef = messageType.fieldDefs[i];
|
|
514
|
-
if (fDef.
|
|
515
|
-
|
|
516
|
-
const { type } = fDef;
|
|
517
|
-
const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
|
|
518
|
-
const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
|
|
519
|
-
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
const { name: field, type } = fDef;
|
|
523
|
-
const scale = (_f = fDef.scale) !== null && _f !== void 0 ? _f : null;
|
|
524
|
-
const offset = (_g = fDef.offset) !== null && _g !== void 0 ? _g : 0;
|
|
525
|
-
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
526
|
-
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
527
|
-
}
|
|
551
|
+
if (isOutputFieldName(fDef.name)) {
|
|
552
|
+
fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
|
|
528
553
|
}
|
|
529
554
|
}
|
|
530
555
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
@@ -535,10 +560,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
535
560
|
}
|
|
536
561
|
const field = fDef.name;
|
|
537
562
|
if (field !== 'unknown' && field !== '') {
|
|
538
|
-
fields[field] =
|
|
563
|
+
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (isCompressedTimestamp) {
|
|
567
|
+
const previousTimestamp = decoderState.lastTimestamp;
|
|
568
|
+
if (previousTimestamp === undefined) {
|
|
569
|
+
if (!options.force) {
|
|
570
|
+
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
575
|
+
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
576
|
+
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
577
|
+
const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
|
|
578
|
+
decoderState.lastTimestamp = timestamp;
|
|
579
|
+
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
580
|
+
validFieldCount++;
|
|
539
581
|
}
|
|
540
582
|
}
|
|
541
|
-
applyGarminProductName(fields);
|
|
542
583
|
if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
|
|
543
584
|
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
544
585
|
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
@@ -549,17 +590,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
549
590
|
developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
|
|
550
591
|
}
|
|
551
592
|
if (message.name === 'monitoring') {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
593
|
+
const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
|
|
594
|
+
const rawTimestamp = timestampFieldIndex >= 0
|
|
595
|
+
? rawData[timestampFieldIndex]
|
|
596
|
+
: InvalidFieldData;
|
|
597
|
+
if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
|
|
598
|
+
decoderState.monitoringTimestamp = rawTimestamp;
|
|
599
|
+
fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
|
|
557
600
|
}
|
|
558
|
-
if (fields.timestamp16
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
601
|
+
else if (fields.timestamp16
|
|
602
|
+
&& decoderState.monitoringTimestamp !== undefined
|
|
603
|
+
&& !fields.timestamp) {
|
|
604
|
+
decoderState.monitoringTimestamp
|
|
605
|
+
+= (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
|
|
606
|
+
fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
|
|
563
607
|
}
|
|
564
608
|
}
|
|
565
609
|
return {
|
package/dist/cjs/binary.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FitParserOptions } from './fit-parser.js';
|
|
2
|
-
import type { FieldDefinition } from './fit.js';
|
|
3
|
-
import type { MesgNum } from './fit_types.js';
|
|
2
|
+
import type { FieldDefinition, MessageName } from './fit.js';
|
|
4
3
|
import { Buffer } from 'buffer';
|
|
5
4
|
export interface MessageTypeDefinition {
|
|
6
5
|
littleEndian: boolean;
|
|
@@ -17,9 +16,13 @@ export interface DeveloperFieldDefinition {
|
|
|
17
16
|
resolvedFieldDef?: FieldDefinition;
|
|
18
17
|
resolvedFrom?: unknown;
|
|
19
18
|
}
|
|
19
|
+
export interface DecoderState {
|
|
20
|
+
lastTimestamp?: number;
|
|
21
|
+
monitoringTimestamp?: number;
|
|
22
|
+
}
|
|
20
23
|
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
21
|
-
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
|
|
22
|
-
messageType:
|
|
24
|
+
export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
|
|
25
|
+
messageType: MessageName | 'definition' | '';
|
|
23
26
|
nextIndex: number;
|
|
24
27
|
message?: any;
|
|
25
28
|
};
|