fit-file-parser 2.0.5 → 2.1.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/dist/cjs/binary.d.ts +11 -0
- package/dist/cjs/binary.js +410 -0
- package/dist/cjs/fit-parser.d.ts +19 -0
- package/dist/cjs/fit-parser.js +242 -0
- package/dist/cjs/fit.d.ts +37 -0
- package/dist/cjs/fit.js +7899 -0
- package/dist/cjs/fit_types.d.ts +974 -0
- package/dist/cjs/fit_types.js +9 -0
- package/dist/cjs/helper.d.ts +3 -0
- package/dist/cjs/helper.js +66 -0
- package/dist/cjs/messages.d.ts +7 -0
- package/dist/cjs/messages.js +26 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/type_generator.d.ts +26 -0
- package/dist/cjs/type_generator.js +263 -0
- package/dist/cjs/types.d.ts +55 -0
- package/dist/cjs/types.js +2 -0
- package/dist/fit.js +7 -0
- package/dist/helper.js +3 -8
- package/package.json +4 -5
- package/tsconfig.cjs.json +8 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FitParserOptions } from './fit-parser.js';
|
|
2
|
+
import type { MesgNum } from './fit_types.js';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
|
|
5
|
+
export declare function readRecord(blob: Uint8Array, messageTypes: any[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number): {
|
|
6
|
+
messageType: MesgNum | '';
|
|
7
|
+
nextIndex: number;
|
|
8
|
+
message?: any;
|
|
9
|
+
};
|
|
10
|
+
export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
|
|
11
|
+
export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addEndian = addEndian;
|
|
4
|
+
exports.readRecord = readRecord;
|
|
5
|
+
exports.getArrayBuffer = getArrayBuffer;
|
|
6
|
+
exports.calculateCRC = calculateCRC;
|
|
7
|
+
const buffer_1 = require("buffer");
|
|
8
|
+
const fit_js_1 = require("./fit.js");
|
|
9
|
+
const messages_js_1 = require("./messages.js");
|
|
10
|
+
const CompressedLocalMsgNumMask = 0x60;
|
|
11
|
+
const CompressedHeaderMask = 0x80;
|
|
12
|
+
const GarminTimeOffset = 631065600000;
|
|
13
|
+
let monitoring_timestamp = 0;
|
|
14
|
+
function addEndian(littleEndian, bytes) {
|
|
15
|
+
let result = 0;
|
|
16
|
+
if (!littleEndian)
|
|
17
|
+
bytes.reverse();
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
result += (bytes[i] << (i << 3)) >>> 0;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function readData(blob, fDef, startIndex, options) {
|
|
24
|
+
if (fDef.endianAbility) {
|
|
25
|
+
const temp = [];
|
|
26
|
+
for (let i = 0; i < fDef.size; i++) {
|
|
27
|
+
temp.push(blob[startIndex + i]);
|
|
28
|
+
}
|
|
29
|
+
const { buffer } = new Uint8Array(temp);
|
|
30
|
+
const dataView = new DataView(buffer);
|
|
31
|
+
try {
|
|
32
|
+
switch (fDef.type) {
|
|
33
|
+
case 'sint16':
|
|
34
|
+
return dataView.getInt16(0, fDef.littleEndian);
|
|
35
|
+
case 'uint16':
|
|
36
|
+
case 'uint16z':
|
|
37
|
+
return dataView.getUint16(0, fDef.littleEndian);
|
|
38
|
+
case 'sint32':
|
|
39
|
+
return dataView.getInt32(0, fDef.littleEndian);
|
|
40
|
+
case 'uint32':
|
|
41
|
+
case 'uint32z':
|
|
42
|
+
return dataView.getUint32(0, fDef.littleEndian);
|
|
43
|
+
case 'float32':
|
|
44
|
+
return dataView.getFloat32(0, fDef.littleEndian);
|
|
45
|
+
case 'float64':
|
|
46
|
+
return dataView.getFloat64(0, fDef.littleEndian);
|
|
47
|
+
case 'uint32_array': {
|
|
48
|
+
const array32 = [];
|
|
49
|
+
for (let i = 0; i < fDef.size; i += 4) {
|
|
50
|
+
array32.push(dataView.getUint32(i, fDef.littleEndian));
|
|
51
|
+
}
|
|
52
|
+
return array32;
|
|
53
|
+
}
|
|
54
|
+
case 'uint16_array': {
|
|
55
|
+
const array = [];
|
|
56
|
+
for (let i = 0; i < fDef.size; i += 2) {
|
|
57
|
+
array.push(dataView.getUint16(i, fDef.littleEndian));
|
|
58
|
+
}
|
|
59
|
+
return array;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (!options.force) {
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return addEndian(fDef.littleEndian, temp);
|
|
69
|
+
}
|
|
70
|
+
if (fDef.type === 'string') {
|
|
71
|
+
const temp = [];
|
|
72
|
+
for (let i = 0; i < fDef.size; i++) {
|
|
73
|
+
if (blob[startIndex + i]) {
|
|
74
|
+
temp.push(blob[startIndex + i]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return buffer_1.Buffer.from(temp).toString('utf-8');
|
|
78
|
+
}
|
|
79
|
+
if (fDef.type === 'byte_array') {
|
|
80
|
+
const temp = [];
|
|
81
|
+
for (let i = 0; i < fDef.size; i++) {
|
|
82
|
+
temp.push(blob[startIndex + i]);
|
|
83
|
+
}
|
|
84
|
+
return temp;
|
|
85
|
+
}
|
|
86
|
+
return blob[startIndex];
|
|
87
|
+
}
|
|
88
|
+
function formatByType(data, type, scale, offset) {
|
|
89
|
+
switch (type) {
|
|
90
|
+
case 'date_time':
|
|
91
|
+
case 'local_date_time':
|
|
92
|
+
return new Date(data * 1000 + GarminTimeOffset);
|
|
93
|
+
case 'sint32':
|
|
94
|
+
return data * fit_js_1.FIT.scConst;
|
|
95
|
+
case 'uint8':
|
|
96
|
+
case 'sint16':
|
|
97
|
+
case 'uint32':
|
|
98
|
+
case 'uint16':
|
|
99
|
+
return scale ? data / scale + offset : data;
|
|
100
|
+
case 'uint32_array':
|
|
101
|
+
case 'uint16_array':
|
|
102
|
+
return data.map((dataItem) => scale ? dataItem / scale + offset : dataItem);
|
|
103
|
+
default:
|
|
104
|
+
{
|
|
105
|
+
if (!fit_js_1.FIT.types[type]) {
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
// Quick check for a mask
|
|
109
|
+
const values = [];
|
|
110
|
+
for (const key in fit_js_1.FIT.types[type]) {
|
|
111
|
+
if (key in fit_js_1.FIT.types[type]) {
|
|
112
|
+
values.push(String(fit_js_1.FIT.types[type][key]));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!values.includes('mask')) {
|
|
116
|
+
return fit_js_1.FIT.types[type][data];
|
|
117
|
+
}
|
|
118
|
+
const dataItem = {};
|
|
119
|
+
for (const key in fit_js_1.FIT.types[type]) {
|
|
120
|
+
if (key in fit_js_1.FIT.types[type]) {
|
|
121
|
+
if (fit_js_1.FIT.types[type][key] === 'mask') {
|
|
122
|
+
dataItem.value = data & Number(key);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
dataItem[fit_js_1.FIT.types[type][key]] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return dataItem;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function isInvalidValue(data, type) {
|
|
134
|
+
switch (type) {
|
|
135
|
+
case 'enum':
|
|
136
|
+
return data === 0xFF;
|
|
137
|
+
case 'sint8':
|
|
138
|
+
return data === 0x7F;
|
|
139
|
+
case 'uint8':
|
|
140
|
+
return data === 0xFF;
|
|
141
|
+
case 'sint16':
|
|
142
|
+
return data === 0x7FFF;
|
|
143
|
+
case 'uint16':
|
|
144
|
+
return data === 0xFFFF;
|
|
145
|
+
case 'sint32':
|
|
146
|
+
return data === 0x7FFFFFFF;
|
|
147
|
+
case 'uint32':
|
|
148
|
+
return data === 0xFFFFFFFF;
|
|
149
|
+
case 'string':
|
|
150
|
+
return data === 0x00;
|
|
151
|
+
case 'float32':
|
|
152
|
+
return data === 0xFFFFFFFF;
|
|
153
|
+
case 'float64':
|
|
154
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
155
|
+
return data === 0xFFFFFFFFFFFFFFFF;
|
|
156
|
+
case 'uint8z':
|
|
157
|
+
return data === 0x00;
|
|
158
|
+
case 'uint16z':
|
|
159
|
+
return data === 0x0000;
|
|
160
|
+
case 'uint32z':
|
|
161
|
+
return data === 0x000000;
|
|
162
|
+
case 'byte':
|
|
163
|
+
return data === 0xFF;
|
|
164
|
+
case 'sint64':
|
|
165
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
166
|
+
return data === 0x7FFFFFFFFFFFFFFF;
|
|
167
|
+
case 'uint64':
|
|
168
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
169
|
+
return data === 0xFFFFFFFFFFFFFFFF;
|
|
170
|
+
case 'uint64z':
|
|
171
|
+
return data === 0x0000000000000000;
|
|
172
|
+
default:
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function convertTo(data, unitsList, unitName) {
|
|
177
|
+
const options = fit_js_1.FIT.options[unitsList];
|
|
178
|
+
const unit = options[unitName];
|
|
179
|
+
return unit ? data * unit.multiplier + unit.offset : data;
|
|
180
|
+
}
|
|
181
|
+
function applyOptions(data, field, options) {
|
|
182
|
+
switch (field) {
|
|
183
|
+
case 'speed':
|
|
184
|
+
case 'enhanced_speed':
|
|
185
|
+
case 'vertical_speed':
|
|
186
|
+
case 'avg_speed':
|
|
187
|
+
case 'max_speed':
|
|
188
|
+
case 'speed_1s':
|
|
189
|
+
case 'ball_speed':
|
|
190
|
+
case 'enhanced_avg_speed':
|
|
191
|
+
case 'enhanced_max_speed':
|
|
192
|
+
case 'avg_pos_vertical_speed':
|
|
193
|
+
case 'max_pos_vertical_speed':
|
|
194
|
+
case 'avg_neg_vertical_speed':
|
|
195
|
+
case 'max_neg_vertical_speed':
|
|
196
|
+
return convertTo(data, 'speedUnits', options.speedUnit);
|
|
197
|
+
case 'distance':
|
|
198
|
+
case 'total_distance':
|
|
199
|
+
case 'enhanced_avg_altitude':
|
|
200
|
+
case 'enhanced_min_altitude':
|
|
201
|
+
case 'enhanced_max_altitude':
|
|
202
|
+
case 'enhanced_altitude':
|
|
203
|
+
case 'height':
|
|
204
|
+
case 'odometer':
|
|
205
|
+
case 'avg_stroke_distance':
|
|
206
|
+
case 'min_altitude':
|
|
207
|
+
case 'avg_altitude':
|
|
208
|
+
case 'max_altitude':
|
|
209
|
+
case 'total_ascent':
|
|
210
|
+
case 'total_descent':
|
|
211
|
+
case 'altitude':
|
|
212
|
+
case 'cycle_length':
|
|
213
|
+
case 'auto_wheelsize':
|
|
214
|
+
case 'custom_wheelsize':
|
|
215
|
+
case 'gps_accuracy':
|
|
216
|
+
return convertTo(data, 'lengthUnits', options.lengthUnit);
|
|
217
|
+
case 'temperature':
|
|
218
|
+
case 'avg_temperature':
|
|
219
|
+
case 'max_temperature':
|
|
220
|
+
return convertTo(data, 'temperatureUnits', options.temperatureUnit);
|
|
221
|
+
case 'pressure':
|
|
222
|
+
case 'start_pressure':
|
|
223
|
+
case 'end_pressure':
|
|
224
|
+
return convertTo(data, 'pressureUnits', options.pressureUnit);
|
|
225
|
+
default:
|
|
226
|
+
return data;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
|
|
230
|
+
const recordHeader = blob[startIndex];
|
|
231
|
+
let localMessageType = recordHeader & 15;
|
|
232
|
+
if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
|
|
233
|
+
// compressed timestamp
|
|
234
|
+
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
235
|
+
}
|
|
236
|
+
else if ((recordHeader & 64) === 64) {
|
|
237
|
+
// is definition message
|
|
238
|
+
// startIndex + 1 is reserved
|
|
239
|
+
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
240
|
+
const lEnd = blob[startIndex + 2] === 0;
|
|
241
|
+
const numberOfFields = blob[startIndex + 5];
|
|
242
|
+
const numberOfDeveloperDataFields = hasDeveloperData
|
|
243
|
+
? blob[startIndex + 5 + numberOfFields * 3 + 1]
|
|
244
|
+
: 0;
|
|
245
|
+
const mTypeDef = {
|
|
246
|
+
littleEndian: lEnd,
|
|
247
|
+
globalMessageNumber: addEndian(lEnd, [
|
|
248
|
+
blob[startIndex + 3],
|
|
249
|
+
blob[startIndex + 4],
|
|
250
|
+
]),
|
|
251
|
+
numberOfFields: numberOfFields + numberOfDeveloperDataFields,
|
|
252
|
+
fieldDefs: [],
|
|
253
|
+
};
|
|
254
|
+
const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
|
|
255
|
+
for (let i = 0; i < numberOfFields; i++) {
|
|
256
|
+
const fDefIndex = startIndex + 6 + i * 3;
|
|
257
|
+
const baseType = blob[fDefIndex + 2];
|
|
258
|
+
const { field, type } = message.getAttributes(blob[fDefIndex]);
|
|
259
|
+
const fDef = {
|
|
260
|
+
type,
|
|
261
|
+
fDefNo: blob[fDefIndex],
|
|
262
|
+
size: blob[fDefIndex + 1],
|
|
263
|
+
endianAbility: (baseType & 128) === 128,
|
|
264
|
+
littleEndian: lEnd,
|
|
265
|
+
baseTypeNo: baseType & 15,
|
|
266
|
+
name: field,
|
|
267
|
+
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
268
|
+
};
|
|
269
|
+
mTypeDef.fieldDefs.push(fDef);
|
|
270
|
+
}
|
|
271
|
+
// numberOfDeveloperDataFields = 0 so it wont crash here and wont loop
|
|
272
|
+
for (let i = 0; i < numberOfDeveloperDataFields; i++) {
|
|
273
|
+
// If we fail to parse then try catch
|
|
274
|
+
try {
|
|
275
|
+
const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
|
|
276
|
+
const fieldNum = blob[fDefIndex];
|
|
277
|
+
const size = blob[fDefIndex + 1];
|
|
278
|
+
const devDataIndex = blob[fDefIndex + 2];
|
|
279
|
+
const devDef = developerFields[devDataIndex][fieldNum];
|
|
280
|
+
const baseType = devDef.fit_base_type_id;
|
|
281
|
+
const fDef = {
|
|
282
|
+
type: fit_js_1.FIT.types.fit_base_type[baseType],
|
|
283
|
+
fDefNo: fieldNum,
|
|
284
|
+
size,
|
|
285
|
+
endianAbility: (baseType & 128) === 128,
|
|
286
|
+
littleEndian: lEnd,
|
|
287
|
+
baseTypeNo: baseType & 15,
|
|
288
|
+
name: devDef.field_name,
|
|
289
|
+
dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
|
|
290
|
+
scale: devDef.scale || 1,
|
|
291
|
+
offset: devDef.offset || 0,
|
|
292
|
+
developerDataIndex: devDataIndex,
|
|
293
|
+
isDeveloperField: true,
|
|
294
|
+
};
|
|
295
|
+
mTypeDef.fieldDefs.push(fDef);
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
if (options.force) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
messageTypes[localMessageType] = mTypeDef;
|
|
305
|
+
const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
|
|
306
|
+
const nextIndexWithDeveloperData = nextIndex + 1;
|
|
307
|
+
return {
|
|
308
|
+
messageType: 'definition',
|
|
309
|
+
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const messageType = messageTypes[localMessageType] || messageTypes[0];
|
|
313
|
+
// TODO: handle compressed header ((recordHeader & 128) == 128)
|
|
314
|
+
// uncompressed header
|
|
315
|
+
let messageSize = 0;
|
|
316
|
+
let readDataFromIndex = startIndex + 1;
|
|
317
|
+
const fields = {};
|
|
318
|
+
const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
|
|
319
|
+
for (let i = 0; i < messageType.fieldDefs.length; i++) {
|
|
320
|
+
const fDef = messageType.fieldDefs[i];
|
|
321
|
+
const data = readData(blob, fDef, readDataFromIndex, options);
|
|
322
|
+
if (!isInvalidValue(data, fDef.type)) {
|
|
323
|
+
if (fDef.isDeveloperField) {
|
|
324
|
+
const field = fDef.name;
|
|
325
|
+
const { type } = fDef;
|
|
326
|
+
const { scale } = fDef;
|
|
327
|
+
const { offset } = fDef;
|
|
328
|
+
fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
|
|
332
|
+
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
|
333
|
+
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (message.name === 'record' && options.elapsedRecordField) {
|
|
337
|
+
fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
|
|
338
|
+
fields.timer_time = fields.elapsed_time - pausedTime;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
readDataFromIndex += fDef.size;
|
|
342
|
+
messageSize += fDef.size;
|
|
343
|
+
}
|
|
344
|
+
if (message.name === 'field_description') {
|
|
345
|
+
developerFields[fields.developer_data_index]
|
|
346
|
+
= developerFields[fields.developer_data_index] || [];
|
|
347
|
+
developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
|
|
348
|
+
}
|
|
349
|
+
if (message.name === 'monitoring') {
|
|
350
|
+
// TODO weirdly uses global variables?
|
|
351
|
+
// we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
|
|
352
|
+
if (fields.timestamp) {
|
|
353
|
+
monitoring_timestamp = fields.timestamp;
|
|
354
|
+
fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
|
|
355
|
+
}
|
|
356
|
+
if (fields.timestamp16 && !fields.timestamp) {
|
|
357
|
+
monitoring_timestamp
|
|
358
|
+
+= (fields.timestamp16 - (monitoring_timestamp & 0xFFFF)) & 0xFFFF;
|
|
359
|
+
// fields.timestamp = monitoring_timestamp;
|
|
360
|
+
fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
messageType: message.name,
|
|
365
|
+
nextIndex: startIndex + messageSize + 1,
|
|
366
|
+
message: fields,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function getArrayBuffer(buffer) {
|
|
370
|
+
if (buffer instanceof ArrayBuffer) {
|
|
371
|
+
return buffer;
|
|
372
|
+
}
|
|
373
|
+
const ab = new ArrayBuffer(buffer.length);
|
|
374
|
+
const view = new Uint8Array(ab);
|
|
375
|
+
for (let i = 0; i < buffer.length; ++i) {
|
|
376
|
+
view[i] = buffer[i];
|
|
377
|
+
}
|
|
378
|
+
return ab;
|
|
379
|
+
}
|
|
380
|
+
function calculateCRC(blob, start, end) {
|
|
381
|
+
const crcTable = [
|
|
382
|
+
0x0000,
|
|
383
|
+
0xCC01,
|
|
384
|
+
0xD801,
|
|
385
|
+
0x1400,
|
|
386
|
+
0xF001,
|
|
387
|
+
0x3C00,
|
|
388
|
+
0x2800,
|
|
389
|
+
0xE401,
|
|
390
|
+
0xA001,
|
|
391
|
+
0x6C00,
|
|
392
|
+
0x7800,
|
|
393
|
+
0xB401,
|
|
394
|
+
0x5000,
|
|
395
|
+
0x9C01,
|
|
396
|
+
0x8801,
|
|
397
|
+
0x4400,
|
|
398
|
+
];
|
|
399
|
+
let crc = 0;
|
|
400
|
+
for (let i = start; i < end; i++) {
|
|
401
|
+
const byteVal = blob[i];
|
|
402
|
+
let tmp = crcTable[crc & 0xF];
|
|
403
|
+
crc = (crc >> 4) & 0x0FFF;
|
|
404
|
+
crc = crc ^ tmp ^ crcTable[byteVal & 0xF];
|
|
405
|
+
tmp = crcTable[crc & 0xF];
|
|
406
|
+
crc = (crc >> 4) & 0x0FFF;
|
|
407
|
+
crc = crc ^ tmp ^ crcTable[(byteVal >> 4) & 0xF];
|
|
408
|
+
}
|
|
409
|
+
return crc;
|
|
410
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Buffer } from 'buffer';
|
|
2
|
+
import type { ParsedFit } from './fit_types.js';
|
|
3
|
+
export interface FitParserOptions {
|
|
4
|
+
force?: boolean;
|
|
5
|
+
speedUnit?: string;
|
|
6
|
+
lengthUnit?: string;
|
|
7
|
+
temperatureUnit?: string;
|
|
8
|
+
elapsedRecordField?: boolean;
|
|
9
|
+
pressureUnit?: string;
|
|
10
|
+
mode?: 'list' | 'cascade' | 'both';
|
|
11
|
+
}
|
|
12
|
+
type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
|
|
13
|
+
export default class FitParser {
|
|
14
|
+
private readonly options;
|
|
15
|
+
constructor(options?: FitParserOptions);
|
|
16
|
+
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
17
|
+
parse(content: ArrayBuffer | Buffer<ArrayBuffer>, callback: FitParserCallback): void;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const binary_js_1 = require("./binary.js");
|
|
4
|
+
const helper_js_1 = require("./helper.js");
|
|
5
|
+
class FitParser {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = {
|
|
8
|
+
force: options.force != null ? options.force : true,
|
|
9
|
+
speedUnit: options.speedUnit || 'm/s',
|
|
10
|
+
lengthUnit: options.lengthUnit || 'm',
|
|
11
|
+
temperatureUnit: options.temperatureUnit || 'celsius',
|
|
12
|
+
elapsedRecordField: options.elapsedRecordField || false,
|
|
13
|
+
pressureUnit: options.pressureUnit || 'bar',
|
|
14
|
+
mode: options.mode || 'list',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
parseAsync(content) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
this.parse(content, (error, data) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
reject(error);
|
|
22
|
+
}
|
|
23
|
+
else if (data) {
|
|
24
|
+
resolve(data);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
parse(content, callback) {
|
|
30
|
+
var _a;
|
|
31
|
+
const blob = new Uint8Array((0, binary_js_1.getArrayBuffer)(content));
|
|
32
|
+
if (blob.length < 12) {
|
|
33
|
+
callback('File to small to be a FIT file', undefined);
|
|
34
|
+
if (!this.options.force) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const headerLength = blob[0];
|
|
39
|
+
if (headerLength !== 14 && headerLength !== 12) {
|
|
40
|
+
callback('Incorrect header size', undefined);
|
|
41
|
+
if (!this.options.force) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let fileTypeString = '';
|
|
46
|
+
for (let i = 8; i < 12; i++) {
|
|
47
|
+
fileTypeString += String.fromCharCode(blob[i]);
|
|
48
|
+
}
|
|
49
|
+
if (fileTypeString !== '.FIT') {
|
|
50
|
+
callback('Missing \'.FIT\' in header', undefined);
|
|
51
|
+
if (!this.options.force) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (headerLength === 14) {
|
|
56
|
+
const crcHeader = blob[12] + (blob[13] << 8);
|
|
57
|
+
const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
|
|
58
|
+
if (crcHeader !== crcHeaderCalc) {
|
|
59
|
+
// callback('Header CRC mismatch', {});
|
|
60
|
+
// TODO: fix Header CRC check
|
|
61
|
+
if (!this.options.force) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const protocolVersion = blob[1];
|
|
67
|
+
const profileVersion = blob[2] + (blob[3] << 8);
|
|
68
|
+
const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
|
|
69
|
+
const crcStart = dataLength + headerLength;
|
|
70
|
+
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
71
|
+
const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
|
|
72
|
+
if (crcFile !== crcFileCalc) {
|
|
73
|
+
// callback('File CRC mismatch', {});
|
|
74
|
+
// TODO: fix File CRC check
|
|
75
|
+
if (!this.options.force) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const fitObj = {
|
|
80
|
+
profileVersion,
|
|
81
|
+
protocolVersion,
|
|
82
|
+
};
|
|
83
|
+
let sessions = [];
|
|
84
|
+
let laps = [];
|
|
85
|
+
const records = [];
|
|
86
|
+
const events = [];
|
|
87
|
+
const hr_zone = [];
|
|
88
|
+
const power_zone = [];
|
|
89
|
+
const hrv = [];
|
|
90
|
+
const device_infos = [];
|
|
91
|
+
const applications = [];
|
|
92
|
+
const fieldDescriptions = [];
|
|
93
|
+
const dive_gases = [];
|
|
94
|
+
const course_points = [];
|
|
95
|
+
const sports = [];
|
|
96
|
+
const monitors = [];
|
|
97
|
+
const stress = [];
|
|
98
|
+
const definitions = [];
|
|
99
|
+
const file_ids = [];
|
|
100
|
+
const monitor_info = [];
|
|
101
|
+
const lengths = [];
|
|
102
|
+
const tank_updates = [];
|
|
103
|
+
const tank_summaries = [];
|
|
104
|
+
let loopIndex = headerLength;
|
|
105
|
+
const messageTypes = [];
|
|
106
|
+
const developerFields = [];
|
|
107
|
+
const isModeCascade = this.options.mode === 'cascade';
|
|
108
|
+
const isCascadeNeeded = isModeCascade || this.options.mode === 'both';
|
|
109
|
+
let startDate;
|
|
110
|
+
let lastStopTimestamp;
|
|
111
|
+
let pausedTime = 0;
|
|
112
|
+
while (loopIndex < crcStart) {
|
|
113
|
+
const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime);
|
|
114
|
+
loopIndex = nextIndex;
|
|
115
|
+
switch (messageType) {
|
|
116
|
+
case 'lap':
|
|
117
|
+
laps.push(message);
|
|
118
|
+
break;
|
|
119
|
+
case 'session':
|
|
120
|
+
sessions.push(message);
|
|
121
|
+
break;
|
|
122
|
+
case 'event':
|
|
123
|
+
if (message.event === 'timer') {
|
|
124
|
+
if (message.event_type === 'stop_all') {
|
|
125
|
+
lastStopTimestamp = message.timestamp;
|
|
126
|
+
}
|
|
127
|
+
else if (message.event_type === 'start' && lastStopTimestamp) {
|
|
128
|
+
pausedTime += (message.timestamp - lastStopTimestamp) / 1000;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
events.push(message);
|
|
132
|
+
break;
|
|
133
|
+
case 'length':
|
|
134
|
+
lengths.push(message);
|
|
135
|
+
break;
|
|
136
|
+
case 'hrv':
|
|
137
|
+
hrv.push(message);
|
|
138
|
+
break;
|
|
139
|
+
case 'hr_zone':
|
|
140
|
+
hr_zone.push(message);
|
|
141
|
+
break;
|
|
142
|
+
case 'power_zone':
|
|
143
|
+
power_zone.push(message);
|
|
144
|
+
break;
|
|
145
|
+
case 'record':
|
|
146
|
+
if (!startDate) {
|
|
147
|
+
startDate = message.timestamp;
|
|
148
|
+
message.elapsed_time = 0;
|
|
149
|
+
message.timer_time = 0;
|
|
150
|
+
}
|
|
151
|
+
records.push(message);
|
|
152
|
+
break;
|
|
153
|
+
case 'field_description':
|
|
154
|
+
fieldDescriptions.push(message);
|
|
155
|
+
break;
|
|
156
|
+
case 'device_info':
|
|
157
|
+
device_infos.push(message);
|
|
158
|
+
break;
|
|
159
|
+
case 'developer_data_id':
|
|
160
|
+
applications.push(message);
|
|
161
|
+
break;
|
|
162
|
+
case 'dive_gas':
|
|
163
|
+
dive_gases.push(message);
|
|
164
|
+
break;
|
|
165
|
+
case 'course_point':
|
|
166
|
+
course_points.push(message);
|
|
167
|
+
break;
|
|
168
|
+
case 'sport':
|
|
169
|
+
sports.push(message);
|
|
170
|
+
break;
|
|
171
|
+
case 'file_id':
|
|
172
|
+
if (message) {
|
|
173
|
+
file_ids.push(message);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'definition':
|
|
177
|
+
if (message) {
|
|
178
|
+
definitions.push(message);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case 'monitoring':
|
|
182
|
+
monitors.push(message);
|
|
183
|
+
break;
|
|
184
|
+
case 'monitoring_info':
|
|
185
|
+
monitor_info.push(message);
|
|
186
|
+
break;
|
|
187
|
+
case 'stress_level':
|
|
188
|
+
stress.push(message);
|
|
189
|
+
break;
|
|
190
|
+
case 'software':
|
|
191
|
+
fitObj.software = message;
|
|
192
|
+
break;
|
|
193
|
+
case 'tank_update':
|
|
194
|
+
tank_updates.push(message);
|
|
195
|
+
break;
|
|
196
|
+
case 'tank_summary':
|
|
197
|
+
tank_summaries.push(message);
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
if (messageType !== '') {
|
|
201
|
+
fitObj[messageType] = message;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
fitObj.hr_zone = hr_zone;
|
|
207
|
+
fitObj.power_zone = power_zone;
|
|
208
|
+
fitObj.dive_gases = dive_gases;
|
|
209
|
+
fitObj.course_points = course_points;
|
|
210
|
+
fitObj.sports = sports;
|
|
211
|
+
fitObj.monitors = monitors;
|
|
212
|
+
fitObj.stress = stress;
|
|
213
|
+
fitObj.file_ids = file_ids;
|
|
214
|
+
fitObj.monitor_info = monitor_info;
|
|
215
|
+
fitObj.definitions = definitions;
|
|
216
|
+
fitObj.tank_updates = tank_updates;
|
|
217
|
+
fitObj.tank_summaries = tank_summaries;
|
|
218
|
+
if (isCascadeNeeded) {
|
|
219
|
+
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
|
|
220
|
+
laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
|
|
221
|
+
sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
|
|
222
|
+
fitObj.activity = Object.assign(Object.assign({}, ((_a = fitObj.activity) !== null && _a !== void 0 ? _a : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
|
|
223
|
+
sessions,
|
|
224
|
+
events,
|
|
225
|
+
hrv,
|
|
226
|
+
device_infos, developer_data_ids: applications, field_descriptions: fieldDescriptions, sports });
|
|
227
|
+
}
|
|
228
|
+
if (!isModeCascade) {
|
|
229
|
+
fitObj.sessions = sessions;
|
|
230
|
+
fitObj.laps = laps;
|
|
231
|
+
fitObj.lengths = lengths;
|
|
232
|
+
fitObj.records = records;
|
|
233
|
+
fitObj.events = events;
|
|
234
|
+
fitObj.device_infos = device_infos;
|
|
235
|
+
fitObj.developer_data_ids = applications;
|
|
236
|
+
fitObj.field_descriptions = fieldDescriptions;
|
|
237
|
+
fitObj.hrv = hrv;
|
|
238
|
+
}
|
|
239
|
+
callback(undefined, fitObj);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.default = FitParser;
|