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/cjs/binary.js
CHANGED
|
@@ -9,31 +9,48 @@ const fit_js_1 = require("./fit.js");
|
|
|
9
9
|
const messages_js_1 = require("./messages.js");
|
|
10
10
|
const CompressedLocalMsgNumMask = 0x60;
|
|
11
11
|
const CompressedHeaderMask = 0x80;
|
|
12
|
+
const CompressedTimestampMask = 0x1F;
|
|
12
13
|
const GarminTimeOffset = 631065600000;
|
|
13
|
-
let monitoring_timestamp = 0;
|
|
14
14
|
const InvalidFieldData = Symbol('invalid FIT field data');
|
|
15
15
|
const formatTypeMetadata = new Map();
|
|
16
|
-
|
|
16
|
+
const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
|
|
17
|
+
function baseTypeSize(type) {
|
|
17
18
|
switch (type) {
|
|
19
|
+
case 'enum':
|
|
20
|
+
case 'sint8':
|
|
21
|
+
case 'uint8':
|
|
22
|
+
case 'uint8z':
|
|
23
|
+
case 'byte':
|
|
24
|
+
return 1;
|
|
18
25
|
case 'sint16':
|
|
19
26
|
case 'uint16':
|
|
20
27
|
case 'uint16z':
|
|
21
|
-
return
|
|
28
|
+
return 2;
|
|
22
29
|
case 'sint32':
|
|
23
30
|
case 'uint32':
|
|
24
31
|
case 'uint32z':
|
|
25
32
|
case 'float32':
|
|
26
|
-
return
|
|
33
|
+
return 4;
|
|
27
34
|
case 'float64':
|
|
28
|
-
return
|
|
29
|
-
case 'uint16_array':
|
|
30
|
-
return size % 2 !== 0;
|
|
31
|
-
case 'uint32_array':
|
|
32
|
-
return size % 4 !== 0;
|
|
35
|
+
return 8;
|
|
33
36
|
default:
|
|
34
|
-
return
|
|
37
|
+
return undefined;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
function requiresBoundedEndianDataView(type, size) {
|
|
41
|
+
const elementSize = baseTypeSize(type);
|
|
42
|
+
return elementSize !== undefined && size % elementSize !== 0;
|
|
43
|
+
}
|
|
44
|
+
function areProfileBaseTypesCompatible(profileType, wireType) {
|
|
45
|
+
if (profileType === undefined || profileType === wireType) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// FIT producers commonly interchange enum, uint8, and byte definitions.
|
|
49
|
+
// They have the same width and value representation; keeping the semantic
|
|
50
|
+
// profile type is safe while still rejecting width and sign conflicts.
|
|
51
|
+
return uint8CompatibleTypes.has(String(profileType))
|
|
52
|
+
&& uint8CompatibleTypes.has(String(wireType));
|
|
53
|
+
}
|
|
37
54
|
function addEndian(littleEndian, bytes) {
|
|
38
55
|
let result = 0;
|
|
39
56
|
if (!littleEndian)
|
|
@@ -43,77 +60,10 @@ function addEndian(littleEndian, bytes) {
|
|
|
43
60
|
}
|
|
44
61
|
return result;
|
|
45
62
|
}
|
|
46
|
-
function readData(blob, dataView, fDef, startIndex
|
|
47
|
-
var _a;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < fDef.size; i++) {
|
|
51
|
-
array8.push(blob[startIndex + i]);
|
|
52
|
-
}
|
|
53
|
-
return array8;
|
|
54
|
-
}
|
|
55
|
-
if (fDef.endianAbility) {
|
|
56
|
-
const requiresBoundedDataView = (_a = fDef.requiresBoundedDataView) !== null && _a !== void 0 ? _a : (fDef.requiresBoundedDataView = requiresBoundedEndianDataView(fDef.type, fDef.size));
|
|
57
|
-
let fieldDataView = dataView;
|
|
58
|
-
let fieldStartIndex = startIndex;
|
|
59
|
-
if (startIndex < 0
|
|
60
|
-
|| startIndex + fDef.size > blob.length
|
|
61
|
-
|| startIndex + fDef.size > dataView.byteLength) {
|
|
62
|
-
const paddedField = new Uint8Array(fDef.size);
|
|
63
|
-
for (let index = 0; index < fDef.size; index++) {
|
|
64
|
-
paddedField[index] = blob[startIndex + index];
|
|
65
|
-
}
|
|
66
|
-
fieldDataView = new DataView(paddedField.buffer);
|
|
67
|
-
fieldStartIndex = 0;
|
|
68
|
-
}
|
|
69
|
-
else if (requiresBoundedDataView) {
|
|
70
|
-
fieldDataView = new DataView(dataView.buffer, dataView.byteOffset + startIndex, fDef.size);
|
|
71
|
-
fieldStartIndex = 0;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
switch (fDef.type) {
|
|
75
|
-
case 'sint16':
|
|
76
|
-
return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
|
|
77
|
-
case 'uint16':
|
|
78
|
-
case 'uint16z':
|
|
79
|
-
return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
|
|
80
|
-
case 'sint32':
|
|
81
|
-
return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
|
|
82
|
-
case 'uint32':
|
|
83
|
-
case 'uint32z':
|
|
84
|
-
return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
|
|
85
|
-
case 'float32':
|
|
86
|
-
return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
|
|
87
|
-
case 'float64':
|
|
88
|
-
return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
|
|
89
|
-
case 'uint32_array': {
|
|
90
|
-
const array32 = [];
|
|
91
|
-
for (let i = 0; i < fDef.size; i += 4) {
|
|
92
|
-
array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
|
|
93
|
-
}
|
|
94
|
-
return array32;
|
|
95
|
-
}
|
|
96
|
-
case 'uint16_array': {
|
|
97
|
-
const array16 = [];
|
|
98
|
-
for (let i = 0; i < fDef.size; i += 2) {
|
|
99
|
-
array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
|
|
100
|
-
}
|
|
101
|
-
return array16;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
if (!options.force) {
|
|
107
|
-
throw e;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const temp = [];
|
|
111
|
-
for (let i = 0; i < fDef.size; i++) {
|
|
112
|
-
temp.push(blob[startIndex + i]);
|
|
113
|
-
}
|
|
114
|
-
return addEndian(fDef.littleEndian, temp);
|
|
115
|
-
}
|
|
116
|
-
if (fDef.type === 'string') {
|
|
63
|
+
function readData(blob, dataView, fDef, startIndex) {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
const rawType = (_b = (_a = fDef.rawType) !== null && _a !== void 0 ? _a : fit_js_1.FIT.types.fit_base_type[fDef.baseTypeNo]) !== null && _b !== void 0 ? _b : fDef.type;
|
|
66
|
+
if (rawType === 'string') {
|
|
117
67
|
const temp = [];
|
|
118
68
|
for (let i = 0; i < fDef.size; i++) {
|
|
119
69
|
if (blob[startIndex + i]) {
|
|
@@ -122,30 +72,77 @@ function readData(blob, dataView, fDef, startIndex, options) {
|
|
|
122
72
|
}
|
|
123
73
|
return buffer_1.Buffer.from(temp).toString('utf-8');
|
|
124
74
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
75
|
+
const elementSize = baseTypeSize(rawType);
|
|
76
|
+
if (elementSize === undefined) {
|
|
77
|
+
return InvalidFieldData;
|
|
78
|
+
}
|
|
79
|
+
if (fDef.size < elementSize
|
|
80
|
+
|| fDef.size % elementSize !== 0
|
|
81
|
+
|| startIndex < 0
|
|
82
|
+
|| startIndex + fDef.size > blob.length
|
|
83
|
+
|| startIndex + fDef.size > dataView.byteLength) {
|
|
84
|
+
return InvalidFieldData;
|
|
131
85
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
86
|
+
const isArray = fDef.array === true
|
|
87
|
+
|| String(fDef.type).endsWith('_array')
|
|
88
|
+
|| fDef.type === 'exercise_category_array'
|
|
89
|
+
|| (fDef.isDeveloperField === true && fDef.size > elementSize);
|
|
90
|
+
const elementCount = isArray ? fDef.size / elementSize : 1;
|
|
91
|
+
const values = [];
|
|
92
|
+
for (let elementIndex = 0; elementIndex < elementCount; elementIndex++) {
|
|
93
|
+
const offset = startIndex + elementIndex * elementSize;
|
|
94
|
+
switch (rawType) {
|
|
95
|
+
case 'sint8': {
|
|
96
|
+
const value = blob[offset];
|
|
97
|
+
values.push(value > 127 ? value - 256 : value);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'sint16':
|
|
101
|
+
values.push(dataView.getInt16(offset, fDef.littleEndian));
|
|
102
|
+
break;
|
|
103
|
+
case 'uint16':
|
|
104
|
+
case 'uint16z':
|
|
105
|
+
values.push(dataView.getUint16(offset, fDef.littleEndian));
|
|
106
|
+
break;
|
|
107
|
+
case 'sint32':
|
|
108
|
+
values.push(dataView.getInt32(offset, fDef.littleEndian));
|
|
109
|
+
break;
|
|
110
|
+
case 'uint32':
|
|
111
|
+
case 'uint32z':
|
|
112
|
+
values.push(dataView.getUint32(offset, fDef.littleEndian));
|
|
113
|
+
break;
|
|
114
|
+
case 'float32':
|
|
115
|
+
values.push(dataView.getFloat32(offset, fDef.littleEndian));
|
|
116
|
+
break;
|
|
117
|
+
case 'float64':
|
|
118
|
+
values.push(dataView.getFloat64(offset, fDef.littleEndian));
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
values.push(blob[offset]);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
135
124
|
}
|
|
136
|
-
return
|
|
125
|
+
return isArray ? values : values[0];
|
|
137
126
|
}
|
|
138
|
-
function formatByType(data, type, scale, offset) {
|
|
127
|
+
function formatByType(data, type, scale, offset, units) {
|
|
139
128
|
switch (type) {
|
|
140
129
|
case 'date_time':
|
|
141
130
|
case 'local_date_time':
|
|
142
131
|
return new Date(data * 1000 + GarminTimeOffset);
|
|
143
132
|
case 'sint32':
|
|
144
|
-
return
|
|
133
|
+
return units === 'semicircles'
|
|
134
|
+
? data * fit_js_1.FIT.scConst
|
|
135
|
+
: scale ? data / scale + offset : data;
|
|
136
|
+
case 'sint8':
|
|
145
137
|
case 'uint8':
|
|
138
|
+
case 'uint8z':
|
|
146
139
|
case 'sint16':
|
|
147
|
-
case 'uint32':
|
|
148
140
|
case 'uint16':
|
|
141
|
+
case 'uint16z':
|
|
142
|
+
case 'uint32':
|
|
143
|
+
case 'uint32z':
|
|
144
|
+
case 'float32':
|
|
145
|
+
case 'float64':
|
|
149
146
|
return scale ? data / scale + offset : data;
|
|
150
147
|
case 'uint32_array':
|
|
151
148
|
case 'uint16_array':
|
|
@@ -160,6 +157,16 @@ function formatByType(data, type, scale, offset) {
|
|
|
160
157
|
});
|
|
161
158
|
}
|
|
162
159
|
return scale ? data / scale + offset : data;
|
|
160
|
+
case 'exercise_category_array':
|
|
161
|
+
if (Array.isArray(data)) {
|
|
162
|
+
return data.map((dataItem) => {
|
|
163
|
+
if (isInvalidValue(dataItem, 'uint16')) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return formatByType(dataItem, 'exercise_category', scale, offset);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return formatByType(data, 'exercise_category', scale, offset);
|
|
163
170
|
default:
|
|
164
171
|
{
|
|
165
172
|
const typeMap = fit_js_1.FIT.types[type];
|
|
@@ -192,7 +199,7 @@ function formatByType(data, type, scale, offset) {
|
|
|
192
199
|
dataItem.value = data & Number(key);
|
|
193
200
|
}
|
|
194
201
|
else {
|
|
195
|
-
dataItem[value] =
|
|
202
|
+
dataItem[value] = (data & Number(key)) !== 0;
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
return dataItem;
|
|
@@ -218,10 +225,9 @@ function isInvalidValue(data, type) {
|
|
|
218
225
|
case 'string':
|
|
219
226
|
return data === 0x00;
|
|
220
227
|
case 'float32':
|
|
221
|
-
return data
|
|
228
|
+
return Number.isNaN(data);
|
|
222
229
|
case 'float64':
|
|
223
|
-
|
|
224
|
-
return data === 0xFFFFFFFFFFFFFFFF;
|
|
230
|
+
return Number.isNaN(data);
|
|
225
231
|
case 'uint8z':
|
|
226
232
|
return data === 0x00;
|
|
227
233
|
case 'uint16z':
|
|
@@ -249,6 +255,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
|
|
|
249
255
|
const baseType = fit_js_1.FIT.types.fit_base_type[baseTypeNo];
|
|
250
256
|
return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
|
|
251
257
|
}
|
|
258
|
+
function formatFieldValue(data, fDef, options, fields) {
|
|
259
|
+
var _a, _b, _c;
|
|
260
|
+
const field = fDef.name;
|
|
261
|
+
const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
|
|
262
|
+
const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
|
|
263
|
+
if (Array.isArray(data)
|
|
264
|
+
&& !String(fDef.type).endsWith('_array')
|
|
265
|
+
&& fDef.type !== 'exercise_category_array') {
|
|
266
|
+
const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : fit_js_1.FIT.types.fit_base_type[fDef.baseTypeNo];
|
|
267
|
+
return data.map((item) => {
|
|
268
|
+
if (isInvalidValue(item, rawType)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
|
|
275
|
+
}
|
|
276
|
+
function isOutputFieldName(field) {
|
|
277
|
+
return field !== 'unknown' && field !== '' && field !== undefined;
|
|
278
|
+
}
|
|
252
279
|
function convertTo(data, unitsList, unitName) {
|
|
253
280
|
const options = fit_js_1.FIT.options[unitsList];
|
|
254
281
|
const unit = options[unitName];
|
|
@@ -276,7 +303,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
276
303
|
case 'vertical_speed':
|
|
277
304
|
case 'avg_speed':
|
|
278
305
|
case 'max_speed':
|
|
279
|
-
case '
|
|
306
|
+
case 'speed1s':
|
|
280
307
|
case 'ball_speed':
|
|
281
308
|
case 'enhanced_avg_speed':
|
|
282
309
|
case 'enhanced_max_speed':
|
|
@@ -307,8 +334,10 @@ function applyOptions(data, field, options, fields) {
|
|
|
307
334
|
return convertTo(data, 'lengthUnits', options.lengthUnit);
|
|
308
335
|
case 'temperature':
|
|
309
336
|
case 'min_temperature':
|
|
337
|
+
case 'temperature_min':
|
|
310
338
|
case 'avg_temperature':
|
|
311
339
|
case 'max_temperature':
|
|
340
|
+
case 'temperature_max':
|
|
312
341
|
return convertTo(data, 'temperatureUnits', options.temperatureUnit);
|
|
313
342
|
case 'pressure':
|
|
314
343
|
case 'start_pressure':
|
|
@@ -325,22 +354,8 @@ function applyOptions(data, field, options, fields) {
|
|
|
325
354
|
return data;
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
|
-
function applyGarminProductName(fields) {
|
|
329
|
-
if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
const product = fields.product;
|
|
333
|
-
if (typeof product !== 'number') {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const productName = fit_js_1.FIT.types.garmin_product[product];
|
|
337
|
-
if (typeof productName === 'string') {
|
|
338
|
-
// Keep the raw protocol ID and expose the SDK product name separately.
|
|
339
|
-
fields.product_name = productName;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
357
|
function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
|
|
343
|
-
var _a, _b, _c, _d;
|
|
358
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
344
359
|
const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
|
|
345
360
|
if (!description) {
|
|
346
361
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
@@ -351,9 +366,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
351
366
|
&& developerFieldDef.resolvedFieldDef) {
|
|
352
367
|
return developerFieldDef.resolvedFieldDef;
|
|
353
368
|
}
|
|
354
|
-
const
|
|
369
|
+
const describedBaseType = description.fit_base_type_id;
|
|
370
|
+
const baseType = typeof describedBaseType === 'number'
|
|
371
|
+
? describedBaseType
|
|
372
|
+
: Number((_b = Object.entries(fit_js_1.FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
|
|
355
373
|
const type = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
356
|
-
if (
|
|
374
|
+
if (!Number.isInteger(baseType) || type === undefined) {
|
|
357
375
|
developerFieldDef.resolvedFieldDef = undefined;
|
|
358
376
|
developerFieldDef.resolvedFrom = undefined;
|
|
359
377
|
if (options.force) {
|
|
@@ -363,15 +381,22 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
363
381
|
}
|
|
364
382
|
const resolvedFieldDef = {
|
|
365
383
|
type,
|
|
384
|
+
rawType: type,
|
|
366
385
|
fDefNo: developerFieldDef.fieldDefinitionNumber,
|
|
367
386
|
size: developerFieldDef.size,
|
|
387
|
+
array: type !== 'string'
|
|
388
|
+
&& developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
|
|
368
389
|
endianAbility: (baseType & 128) === 128,
|
|
369
390
|
littleEndian,
|
|
370
391
|
baseTypeNo: baseType,
|
|
371
|
-
name: (
|
|
392
|
+
name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
|
|
372
393
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
373
|
-
scale: (
|
|
374
|
-
|
|
394
|
+
scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
|
|
395
|
+
// FIT developer-field descriptions use `raw / scale - offset`. This
|
|
396
|
+
// parser's formatter retains the legacy equivalent signed offset and adds
|
|
397
|
+
// it after scaling.
|
|
398
|
+
offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
|
|
399
|
+
units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
|
|
375
400
|
requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
|
|
376
401
|
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
377
402
|
isDeveloperField: true,
|
|
@@ -380,11 +405,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
|
|
|
380
405
|
developerFieldDef.resolvedFrom = description;
|
|
381
406
|
return resolvedFieldDef;
|
|
382
407
|
}
|
|
383
|
-
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
|
|
384
|
-
var _a, _b, _c
|
|
408
|
+
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
|
|
409
|
+
var _a, _b, _c;
|
|
385
410
|
const recordHeader = blob[startIndex];
|
|
386
411
|
let localMessageType = recordHeader & 15;
|
|
387
|
-
|
|
412
|
+
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
413
|
+
if (isCompressedTimestamp) {
|
|
388
414
|
// compressed timestamp
|
|
389
415
|
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
390
416
|
}
|
|
@@ -412,19 +438,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
412
438
|
for (let i = 0; i < numberOfFields; i++) {
|
|
413
439
|
const fDefIndex = startIndex + 6 + i * 3;
|
|
414
440
|
const baseType = blob[fDefIndex + 2];
|
|
415
|
-
const
|
|
441
|
+
const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
442
|
+
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
443
|
+
const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
416
444
|
const fDef = {
|
|
417
|
-
type,
|
|
445
|
+
type: profileCompatible ? type : wireType,
|
|
446
|
+
rawType: wireType,
|
|
418
447
|
fDefNo: blob[fDefIndex],
|
|
419
448
|
size: blob[fDefIndex + 1],
|
|
449
|
+
array: profileCompatible
|
|
450
|
+
? array === true || String(type).endsWith('_array')
|
|
451
|
+
: false,
|
|
420
452
|
endianAbility: (baseType & 128) === 128,
|
|
421
453
|
littleEndian: lEnd,
|
|
422
454
|
baseTypeNo: baseType,
|
|
423
|
-
name: field,
|
|
455
|
+
name: profileCompatible ? field : '',
|
|
424
456
|
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
425
|
-
scale,
|
|
426
|
-
offset,
|
|
427
|
-
|
|
457
|
+
scale: profileCompatible ? scale : null,
|
|
458
|
+
offset: profileCompatible ? offset : 0,
|
|
459
|
+
units: profileCompatible ? units : '',
|
|
460
|
+
requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
|
|
428
461
|
};
|
|
429
462
|
mTypeDef.fieldDefs.push(fDef);
|
|
430
463
|
}
|
|
@@ -446,8 +479,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
446
479
|
};
|
|
447
480
|
}
|
|
448
481
|
const messageType = messageTypes[localMessageType] || messageTypes[0];
|
|
449
|
-
// TODO: handle compressed header ((recordHeader & 128) == 128)
|
|
450
|
-
// uncompressed header
|
|
451
482
|
let messageSize = 0;
|
|
452
483
|
let readDataFromIndex = startIndex + 1;
|
|
453
484
|
const fields = {};
|
|
@@ -458,10 +489,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
458
489
|
let validFieldCount = 0;
|
|
459
490
|
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
460
491
|
const fDef = messageType.fieldDefs[i];
|
|
461
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
462
|
-
if (
|
|
492
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
493
|
+
if (data !== InvalidFieldData
|
|
494
|
+
&& !isInvalidValue(data, fDef.type)
|
|
495
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
463
496
|
rawData[i] = data;
|
|
464
497
|
validFieldCount++;
|
|
498
|
+
if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
|
|
499
|
+
decoderState.lastTimestamp = data;
|
|
500
|
+
}
|
|
465
501
|
}
|
|
466
502
|
else {
|
|
467
503
|
rawData[i] = InvalidFieldData;
|
|
@@ -474,8 +510,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
474
510
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
475
511
|
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
476
512
|
if (fDef) {
|
|
477
|
-
const data = readData(blob, dataView, fDef, readDataFromIndex
|
|
478
|
-
if (
|
|
513
|
+
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
514
|
+
if (data !== InvalidFieldData
|
|
515
|
+
&& !isInvalidValue(data, fDef.type)
|
|
516
|
+
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
479
517
|
rawData[rawDataIndex] = data;
|
|
480
518
|
validFieldCount++;
|
|
481
519
|
}
|
|
@@ -495,9 +533,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
495
533
|
continue;
|
|
496
534
|
}
|
|
497
535
|
const fDef = messageType.fieldDefs[i];
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
fields[field] = data;
|
|
536
|
+
if (isOutputFieldName(fDef.name)) {
|
|
537
|
+
fields[fDef.name] = data;
|
|
501
538
|
}
|
|
502
539
|
}
|
|
503
540
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
@@ -517,20 +554,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
517
554
|
continue;
|
|
518
555
|
}
|
|
519
556
|
const fDef = messageType.fieldDefs[i];
|
|
520
|
-
if (fDef.
|
|
521
|
-
|
|
522
|
-
const { type } = fDef;
|
|
523
|
-
const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
|
|
524
|
-
const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
|
|
525
|
-
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
const { name: field, type } = fDef;
|
|
529
|
-
const scale = (_f = fDef.scale) !== null && _f !== void 0 ? _f : null;
|
|
530
|
-
const offset = (_g = fDef.offset) !== null && _g !== void 0 ? _g : 0;
|
|
531
|
-
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
532
|
-
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
|
|
533
|
-
}
|
|
557
|
+
if (isOutputFieldName(fDef.name)) {
|
|
558
|
+
fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
|
|
534
559
|
}
|
|
535
560
|
}
|
|
536
561
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
@@ -541,10 +566,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
541
566
|
}
|
|
542
567
|
const field = fDef.name;
|
|
543
568
|
if (field !== 'unknown' && field !== '') {
|
|
544
|
-
fields[field] =
|
|
569
|
+
fields[field] = formatFieldValue(data, fDef, options, fields);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (isCompressedTimestamp) {
|
|
573
|
+
const previousTimestamp = decoderState.lastTimestamp;
|
|
574
|
+
if (previousTimestamp === undefined) {
|
|
575
|
+
if (!options.force) {
|
|
576
|
+
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const timeOffset = recordHeader & CompressedTimestampMask;
|
|
581
|
+
const previousOffset = previousTimestamp & CompressedTimestampMask;
|
|
582
|
+
const rollover = timeOffset < previousOffset ? 0x20 : 0;
|
|
583
|
+
const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
|
|
584
|
+
decoderState.lastTimestamp = timestamp;
|
|
585
|
+
fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
|
|
586
|
+
validFieldCount++;
|
|
545
587
|
}
|
|
546
588
|
}
|
|
547
|
-
applyGarminProductName(fields);
|
|
548
589
|
if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
|
|
549
590
|
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
550
591
|
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
@@ -555,17 +596,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
555
596
|
developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
|
|
556
597
|
}
|
|
557
598
|
if (message.name === 'monitoring') {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
599
|
+
const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
|
|
600
|
+
const rawTimestamp = timestampFieldIndex >= 0
|
|
601
|
+
? rawData[timestampFieldIndex]
|
|
602
|
+
: InvalidFieldData;
|
|
603
|
+
if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
|
|
604
|
+
decoderState.monitoringTimestamp = rawTimestamp;
|
|
605
|
+
fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
|
|
563
606
|
}
|
|
564
|
-
if (fields.timestamp16
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
607
|
+
else if (fields.timestamp16
|
|
608
|
+
&& decoderState.monitoringTimestamp !== undefined
|
|
609
|
+
&& !fields.timestamp) {
|
|
610
|
+
decoderState.monitoringTimestamp
|
|
611
|
+
+= (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
|
|
612
|
+
fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
|
|
569
613
|
}
|
|
570
614
|
}
|
|
571
615
|
return {
|