fit-file-parser 1.9.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Pierre Jacquier
4
+ http://pierrejacquier.com | @pierremtb
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # fit-file-parser
2
+
3
+ > Parse your .FIT files easily, directly from JS.
4
+ > Written in ES6. (Hope to change)
5
+
6
+
7
+ ## Install
8
+
9
+ ```
10
+ $ npm install fit-file-parser --save
11
+ ```
12
+
13
+ ## How to use
14
+
15
+ See in [examples](./examples) folder:
16
+
17
+ ```javascript
18
+ // Require the module
19
+ var FitParser = require('./../dist/fit-file-parser.js').default;
20
+
21
+ // Read a .FIT file
22
+ var fs = require('fs');
23
+ fs.readFile('./example.fit', function (err, content) {
24
+
25
+ // Create a FitParser instance (options argument is optional)
26
+ var fitParser = new FitParser({
27
+ force: true,
28
+ speedUnit: 'km/h',
29
+ lengthUnit: 'km',
30
+ temperatureUnit: 'kelvin',
31
+ elapsedRecordField: true,
32
+ mode: 'cascade',
33
+ });
34
+
35
+ // Parse your file
36
+ fitParser.parse(content, function (error, data) {
37
+
38
+ // Handle result of parse method
39
+ if (error) {
40
+ console.log(error);
41
+ } else {
42
+ console.log(JSON.stringify(data));
43
+ }
44
+
45
+ });
46
+
47
+ });
48
+ ```
49
+
50
+ ## API Documentation
51
+ ### new FitParser(Object _options_)
52
+ Needed to create a new instance. _options_ is optional, and is used to customize the returned object.
53
+
54
+ Allowed properties :
55
+ - `mode`: String
56
+ - `cascade`: Returned object is organized as a tree, eg. each lap contains a `records` fields, that is an array of its records (**default**)
57
+ - `list`: Returned object is organized as lists of sessions, laps, records, etc..., without parent-child relation
58
+ - `both`: A mix of the two other modes, eg. `records` are available inside the root field as well as inside each laps
59
+ - `lengthUnit`: String
60
+ - `m`: Lengths are in meters (**default**)
61
+ - `km`: Lengths are in kilometers
62
+ - `mi`: Lengths are in miles
63
+ - `temperatureUnit`: String
64
+ - `celsius`:Temperatures are in °C (**default**)
65
+ - `kelvin`: Temperatures are in °K
66
+ - `fahrenheit`: Temperatures are in °F
67
+ - `speedUnit`: String
68
+ - `m/s`: Speeds are in meters per seconds (**default**)
69
+ - `km/h`: Speeds are in kilometers per hour
70
+ - `mph`: Speeds are in miles per hour
71
+ - `force`: Boolean
72
+ - `true`: Continues even if they are errors (**default for now**)
73
+ - `false`: Stops if an error occurs
74
+ - `elapsedRecordField`: Boolean
75
+ - `true`: Includes `elapsed_time`, containing the elapsed time in seconds since the first record, and `timer_time`, containing the time shown on the device, inside each `record` field
76
+ - `false` (**default**)
77
+
78
+ ### fitParser.parse(Buffer _file_, Function _callback_)
79
+ _callback_ receives two arguments, the first as a error String, and the second as Object, result of parsing.
80
+
81
+ ## Contributors
82
+ All started thanks to [Pierre Jacquier](https://github.com/pierremtb)
83
+
84
+ Big thanks to [Mikael Lofjärd](https://github.com/mlofjard) for [his early prototype](https://github.com/mlofjard/jsonfit).
85
+ See [CONTRIBUTORS](./CONTRIBUTORS.md).
86
+
87
+ ## License
88
+
89
+ MIT license; see [LICENSE](./LICENSE).
90
+
91
+ (c) 2019 Dimitrios Kanellopoulos
package/dist/binary.js ADDED
@@ -0,0 +1,440 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.addEndian = addEndian;
7
+ exports.readRecord = readRecord;
8
+ exports.getArrayBuffer = getArrayBuffer;
9
+ exports.calculateCRC = calculateCRC;
10
+
11
+ var _fit = require('./fit');
12
+
13
+ var _messages = require('./messages');
14
+
15
+ var _buffer = require('buffer');
16
+
17
+ function addEndian(littleEndian, bytes) {
18
+ var result = 0;
19
+ if (!littleEndian) bytes.reverse();
20
+ for (var i = 0; i < bytes.length; i++) {
21
+ result += bytes[i] << (i << 3) >>> 0;
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ var timestamp = 0;
28
+ var lastTimeOffset = 0;
29
+ var CompressedTimeMask = 31;
30
+ var CompressedLocalMesgNumMask = 0x60;
31
+ var CompressedHeaderMask = 0x80;
32
+ var GarminTimeOffset = 631065600000;
33
+ var monitoring_timestamp = 0;
34
+
35
+ function readData(blob, fDef, startIndex, options) {
36
+ if (fDef.endianAbility === true) {
37
+ var temp = [];
38
+ for (var i = 0; i < fDef.size; i++) {
39
+ temp.push(blob[startIndex + i]);
40
+ }
41
+
42
+ var buffer = new Uint8Array(temp).buffer;
43
+ var dataView = new DataView(buffer);
44
+
45
+ try {
46
+ switch (fDef.type) {
47
+ case 'sint16':
48
+ return dataView.getInt16(0, fDef.littleEndian);
49
+ case 'uint16':
50
+ case 'uint16z':
51
+ return dataView.getUint16(0, fDef.littleEndian);
52
+ case 'sint32':
53
+ return dataView.getInt32(0, fDef.littleEndian);
54
+ case 'uint32':
55
+ case 'uint32z':
56
+ return dataView.getUint32(0, fDef.littleEndian);
57
+ case 'float32':
58
+ return dataView.getFloat32(0, fDef.littleEndian);
59
+ case 'float64':
60
+ return dataView.getFloat64(0, fDef.littleEndian);
61
+ case 'uint32_array':
62
+ var array32 = [];
63
+ for (var _i = 0; _i < fDef.size; _i += 4) {
64
+ array32.push(dataView.getUint32(_i, fDef.littleEndian));
65
+ }
66
+ return array32;
67
+ case 'uint16_array':
68
+ var array = [];
69
+ for (var _i2 = 0; _i2 < fDef.size; _i2 += 2) {
70
+ array.push(dataView.getUint16(_i2, fDef.littleEndian));
71
+ }
72
+ return array;
73
+ }
74
+ } catch (e) {
75
+ if (!options.force) {
76
+ throw e;
77
+ }
78
+ }
79
+
80
+ return addEndian(fDef.littleEndian, temp);
81
+ }
82
+
83
+ if (fDef.type === 'string') {
84
+ var _temp = [];
85
+ for (var _i3 = 0; _i3 < fDef.size; _i3++) {
86
+ if (blob[startIndex + _i3]) {
87
+ _temp.push(blob[startIndex + _i3]);
88
+ }
89
+ }
90
+ return new _buffer.Buffer.from(_temp).toString('utf-8');
91
+ }
92
+
93
+ if (fDef.type === 'byte_array') {
94
+ var _temp2 = [];
95
+ for (var _i4 = 0; _i4 < fDef.size; _i4++) {
96
+ _temp2.push(blob[startIndex + _i4]);
97
+ }
98
+ return _temp2;
99
+ }
100
+
101
+ return blob[startIndex];
102
+ }
103
+
104
+ function formatByType(data, type, scale, offset) {
105
+ switch (type) {
106
+ case 'date_time':
107
+ case 'local_date_time':
108
+ return new Date(data * 1000 + GarminTimeOffset);
109
+ case 'sint32':
110
+ return data * _fit.FIT.scConst;
111
+ case 'uint8':
112
+ case 'sint16':
113
+ case 'uint32':
114
+ case 'uint16':
115
+ return scale ? data / scale + offset : data;
116
+ case 'uint32_array':
117
+ case 'uint16_array':
118
+ return data.map(function (dataItem) {
119
+ return scale ? dataItem / scale + offset : dataItem;
120
+ });
121
+ default:
122
+ if (!_fit.FIT.types[type]) {
123
+ return data;
124
+ }
125
+ // Quick check for a mask
126
+ var values = [];
127
+ for (var key in _fit.FIT.types[type]) {
128
+ if (_fit.FIT.types[type].hasOwnProperty(key)) {
129
+ values.push(_fit.FIT.types[type][key]);
130
+ }
131
+ }
132
+ if (values.indexOf('mask') === -1) {
133
+ return _fit.FIT.types[type][data];
134
+ }
135
+ var dataItem = {};
136
+ for (var key in _fit.FIT.types[type]) {
137
+ if (_fit.FIT.types[type].hasOwnProperty(key)) {
138
+ if (_fit.FIT.types[type][key] === 'mask') {
139
+ dataItem.value = data & key;
140
+ } else {
141
+ dataItem[_fit.FIT.types[type][key]] = !!((data & 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
142
+ }
143
+ }
144
+ }
145
+ return dataItem;
146
+ }
147
+ }
148
+
149
+ function isInvalidValue(data, type) {
150
+ switch (type) {
151
+ case 'enum':
152
+ return data === 0xFF;
153
+ case 'sint8':
154
+ return data === 0x7F;
155
+ case 'uint8':
156
+ return data === 0xFF;
157
+ case 'sint16':
158
+ return data === 0x7FFF;
159
+ case 'uint16':
160
+ return data === 0xFFFF;
161
+ case 'sint32':
162
+ return data === 0x7FFFFFFF;
163
+ case 'uint32':
164
+ return data === 0xFFFFFFFF;
165
+ case 'string':
166
+ return data === 0x00;
167
+ case 'float32':
168
+ return data === 0xFFFFFFFF;
169
+ case 'float64':
170
+ return data === 0xFFFFFFFFFFFFFFFF;
171
+ case 'uint8z':
172
+ return data === 0x00;
173
+ case 'uint16z':
174
+ return data === 0x0000;
175
+ case 'uint32z':
176
+ return data === 0x000000;
177
+ case 'byte':
178
+ return data === 0xFF;
179
+ case 'sint64':
180
+ return data === 0x7FFFFFFFFFFFFFFF;
181
+ case 'uint64':
182
+ return data === 0xFFFFFFFFFFFFFFFF;
183
+ case 'uint64z':
184
+ return data === 0x0000000000000000;
185
+ default:
186
+ return false;
187
+ }
188
+ }
189
+
190
+ function convertTo(data, unitsList, speedUnit) {
191
+ var unitObj = _fit.FIT.options[unitsList][speedUnit];
192
+ return unitObj ? data * unitObj.multiplier + unitObj.offset : data;
193
+ }
194
+
195
+ function applyOptions(data, field, options) {
196
+ switch (field) {
197
+ case 'speed':
198
+ case 'enhanced_speed':
199
+ case 'vertical_speed':
200
+ case 'avg_speed':
201
+ case 'max_speed':
202
+ case 'speed_1s':
203
+ case 'ball_speed':
204
+ case 'enhanced_avg_speed':
205
+ case 'enhanced_max_speed':
206
+ case 'avg_pos_vertical_speed':
207
+ case 'max_pos_vertical_speed':
208
+ case 'avg_neg_vertical_speed':
209
+ case 'max_neg_vertical_speed':
210
+ return convertTo(data, 'speedUnits', options.speedUnit);
211
+ case 'distance':
212
+ case 'total_distance':
213
+ case 'enhanced_avg_altitude':
214
+ case 'enhanced_min_altitude':
215
+ case 'enhanced_max_altitude':
216
+ case 'enhanced_altitude':
217
+ case 'height':
218
+ case 'odometer':
219
+ case 'avg_stroke_distance':
220
+ case 'min_altitude':
221
+ case 'avg_altitude':
222
+ case 'max_altitude':
223
+ case 'total_ascent':
224
+ case 'total_descent':
225
+ case 'altitude':
226
+ case 'cycle_length':
227
+ case 'auto_wheelsize':
228
+ case 'custom_wheelsize':
229
+ case 'gps_accuracy':
230
+ return convertTo(data, 'lengthUnits', options.lengthUnit);
231
+ case 'temperature':
232
+ case 'avg_temperature':
233
+ case 'max_temperature':
234
+ return convertTo(data, 'temperatureUnits', options.temperatureUnit);
235
+ default:
236
+ return data;
237
+ }
238
+ }
239
+
240
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
241
+ var recordHeader = blob[startIndex];
242
+ var localMessageType = recordHeader & 15;
243
+
244
+ if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
245
+ //compressed timestamp
246
+
247
+ var timeoffset = recordHeader & CompressedTimeMask;
248
+ timestamp += timeoffset - lastTimeOffset & CompressedTimeMask;
249
+ lastTimeOffset = timeoffset;
250
+
251
+ localMessageType = (recordHeader & CompressedLocalMesgNumMask) >> 5;
252
+ } else if ((recordHeader & 64) === 64) {
253
+ // is definition message
254
+ // startIndex + 1 is reserved
255
+
256
+ var hasDeveloperData = (recordHeader & 32) === 32;
257
+ var lEnd = blob[startIndex + 2] === 0;
258
+ var numberOfFields = blob[startIndex + 5];
259
+ var numberOfDeveloperDataFields = hasDeveloperData ? blob[startIndex + 5 + numberOfFields * 3 + 1] : 0;
260
+
261
+ var mTypeDef = {
262
+ littleEndian: lEnd,
263
+ globalMessageNumber: addEndian(lEnd, [blob[startIndex + 3], blob[startIndex + 4]]),
264
+ numberOfFields: numberOfFields + numberOfDeveloperDataFields,
265
+ fieldDefs: []
266
+ };
267
+
268
+ var _message = (0, _messages.getFitMessage)(mTypeDef.globalMessageNumber);
269
+
270
+ for (var i = 0; i < numberOfFields; i++) {
271
+ var fDefIndex = startIndex + 6 + i * 3;
272
+ var baseType = blob[fDefIndex + 2];
273
+
274
+ var _message$getAttribute = _message.getAttributes(blob[fDefIndex]),
275
+ field = _message$getAttribute.field,
276
+ type = _message$getAttribute.type;
277
+
278
+ var fDef = {
279
+ type: type,
280
+ fDefNo: blob[fDefIndex],
281
+ size: blob[fDefIndex + 1],
282
+ endianAbility: (baseType & 128) === 128,
283
+ littleEndian: lEnd,
284
+ baseTypeNo: baseType & 15,
285
+ name: field,
286
+ dataType: (0, _messages.getFitMessageBaseType)(baseType & 15)
287
+ };
288
+
289
+ mTypeDef.fieldDefs.push(fDef);
290
+ }
291
+
292
+ // numberOfDeveloperDataFields = 0 so it wont crash here and wont loop
293
+ for (var _i5 = 0; _i5 < numberOfDeveloperDataFields; _i5++) {
294
+ // If we fail to parse then try catch
295
+ try {
296
+ var _fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + _i5 * 3;
297
+
298
+ var fieldNum = blob[_fDefIndex];
299
+ var size = blob[_fDefIndex + 1];
300
+ var devDataIndex = blob[_fDefIndex + 2];
301
+
302
+ var devDef = developerFields[devDataIndex][fieldNum];
303
+
304
+ var _baseType = devDef.fit_base_type_id;
305
+
306
+ var _fDef = {
307
+ type: _fit.FIT.types.fit_base_type[_baseType],
308
+ fDefNo: fieldNum,
309
+ size: size,
310
+ endianAbility: (_baseType & 128) === 128,
311
+ littleEndian: lEnd,
312
+ baseTypeNo: _baseType & 15,
313
+ name: devDef.field_name,
314
+ dataType: (0, _messages.getFitMessageBaseType)(_baseType & 15),
315
+ scale: devDef.scale || 1,
316
+ offset: devDef.offset || 0,
317
+ developerDataIndex: devDataIndex,
318
+ isDeveloperField: true
319
+ };
320
+
321
+ mTypeDef.fieldDefs.push(_fDef);
322
+ } catch (e) {
323
+ if (options.force) {
324
+ continue;
325
+ }
326
+ throw e;
327
+ }
328
+ }
329
+
330
+ messageTypes[localMessageType] = mTypeDef;
331
+
332
+ var nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
333
+ var nextIndexWithDeveloperData = nextIndex + 1;
334
+
335
+ return {
336
+ messageType: 'definition',
337
+ nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex
338
+ };
339
+ }
340
+
341
+ var messageType = messageTypes[localMessageType] || messageTypes[0];
342
+
343
+ // TODO: handle compressed header ((recordHeader & 128) == 128)
344
+
345
+ // uncompressed header
346
+ var messageSize = 0;
347
+ var readDataFromIndex = startIndex + 1;
348
+ var fields = {};
349
+ var message = (0, _messages.getFitMessage)(messageType.globalMessageNumber);
350
+
351
+ for (var _i6 = 0; _i6 < messageType.fieldDefs.length; _i6++) {
352
+ var _fDef2 = messageType.fieldDefs[_i6];
353
+ var data = readData(blob, _fDef2, readDataFromIndex, options);
354
+
355
+ if (!isInvalidValue(data, _fDef2.type)) {
356
+ if (_fDef2.isDeveloperField) {
357
+
358
+ var field = _fDef2.name;
359
+ var type = _fDef2.type;
360
+ var scale = _fDef2.scale;
361
+ var offset = _fDef2.offset;
362
+
363
+ fields[_fDef2.name] = applyOptions(formatByType(data, type, scale, offset), field, options);
364
+ } else {
365
+ var _message$getAttribute2 = message.getAttributes(_fDef2.fDefNo),
366
+ _field = _message$getAttribute2.field,
367
+ _type = _message$getAttribute2.type,
368
+ _scale = _message$getAttribute2.scale,
369
+ _offset = _message$getAttribute2.offset;
370
+
371
+ if (_field !== 'unknown' && _field !== '' && _field !== undefined) {
372
+ fields[_field] = applyOptions(formatByType(data, _type, _scale, _offset), _field, options);
373
+ }
374
+ }
375
+
376
+ if (message.name === 'record' && options.elapsedRecordField) {
377
+ fields.elapsed_time = (fields.timestamp - startDate) / 1000;
378
+ fields.timer_time = fields.elapsed_time - pausedTime;
379
+ }
380
+ }
381
+
382
+ readDataFromIndex += _fDef2.size;
383
+ messageSize += _fDef2.size;
384
+ }
385
+
386
+ if (message.name === 'field_description') {
387
+ developerFields[fields.developer_data_index] = developerFields[fields.developer_data_index] || [];
388
+ developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
389
+ }
390
+
391
+ if (message.name === 'monitoring') {
392
+ //we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
393
+ if (fields.timestamp) {
394
+ monitoring_timestamp = fields.timestamp;
395
+ fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
396
+ }
397
+ if (fields.timestamp16 && !fields.timestamp) {
398
+ monitoring_timestamp += fields.timestamp16 - (monitoring_timestamp & 0xFFFF) & 0xFFFF;
399
+ //fields.timestamp = monitoring_timestamp;
400
+ fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
401
+ }
402
+ }
403
+
404
+ var result = {
405
+ messageType: message.name,
406
+ nextIndex: startIndex + messageSize + 1,
407
+ message: fields
408
+ };
409
+
410
+ return result;
411
+ }
412
+
413
+ function getArrayBuffer(buffer) {
414
+ if (buffer instanceof ArrayBuffer) {
415
+ return buffer;
416
+ }
417
+ var ab = new ArrayBuffer(buffer.length);
418
+ var view = new Uint8Array(ab);
419
+ for (var i = 0; i < buffer.length; ++i) {
420
+ view[i] = buffer[i];
421
+ }
422
+ return ab;
423
+ }
424
+
425
+ function calculateCRC(blob, start, end) {
426
+ var crcTable = [0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400];
427
+
428
+ var crc = 0;
429
+ for (var i = start; i < end; i++) {
430
+ var byteVal = blob[i];
431
+ var tmp = crcTable[crc & 0xF];
432
+ crc = crc >> 4 & 0x0FFF;
433
+ crc = crc ^ tmp ^ crcTable[byteVal & 0xF];
434
+ tmp = crcTable[crc & 0xF];
435
+ crc = crc >> 4 & 0x0FFF;
436
+ crc = crc ^ tmp ^ crcTable[byteVal >> 4 & 0xF];
437
+ }
438
+
439
+ return crc;
440
+ }