fit_js 0.94.5 → 0.94.7
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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +37 -0
- data/Rakefile +4 -0
- data/app/.DS_Store +0 -0
- data/app/assets/.DS_Store +0 -0
- data/app/assets/javascripts/accumulator.js +48 -0
- data/app/assets/javascripts/bit-stream.js +85 -0
- data/app/assets/javascripts/crc-calculator.js +56 -0
- data/app/assets/javascripts/decoder.js +737 -0
- data/app/assets/javascripts/fit.js +95 -0
- data/app/assets/javascripts/fitjs.js +11 -0
- data/app/assets/javascripts/index.js +18 -0
- data/app/assets/javascripts/profile.js +21386 -0
- data/app/assets/javascripts/stream.js +250 -0
- data/app/assets/javascripts/utils-hr-mesg.js +175 -0
- data/app/assets/javascripts/utils-internal.js +35 -0
- data/app/assets/javascripts/utils.js +31 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/fit_js.gemspec +27 -0
- data/lib/fitjs/rails/version.rb +5 -0
- data/sig/fitjs.rbs +4 -0
- metadata +46 -11
@@ -0,0 +1,737 @@
|
|
1
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
2
|
+
// Copyright 2022 Garmin International, Inc.
|
3
|
+
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
4
|
+
// may not use this file except in compliance with the Flexible and Interoperable Data
|
5
|
+
// Transfer (FIT) Protocol License.
|
6
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
7
|
+
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
8
|
+
// Profile Version = 21.94Release
|
9
|
+
// Tag = production/akw/21.94.00-0-g0f668193
|
10
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
11
|
+
|
12
|
+
|
13
|
+
import Accumulator from "../src/accumulator.js";
|
14
|
+
import BitStream from "../src/bit-stream.js";
|
15
|
+
import CrcCalculator from "./crc-calculator.js";
|
16
|
+
import FIT from "./fit.js";
|
17
|
+
import HrMesgUtils from "./utils-hr-mesg.js";
|
18
|
+
import Profile from "./profile.js";
|
19
|
+
import Stream from "./stream.js";
|
20
|
+
import Utils from "./utils.js";
|
21
|
+
import UtilsInternal from "./utils-internal.js";
|
22
|
+
|
23
|
+
const COMPRESSED_HEADER_MASK = 0x80;
|
24
|
+
const MESG_DEFINITION_MASK = 0x40;
|
25
|
+
const DEV_DATA_MASK = 0x20;
|
26
|
+
const MESG_HEADER_MASK = 0x00;
|
27
|
+
const LOCAL_MESG_NUM_MASK = 0x0F;
|
28
|
+
const CRC_SIZE = 2;
|
29
|
+
|
30
|
+
class Decoder {
|
31
|
+
#localMessageDefinitions = [];
|
32
|
+
#developerDataDefinitions = {};
|
33
|
+
#stream = null;
|
34
|
+
#accumulator = new Accumulator();
|
35
|
+
#messages = {};
|
36
|
+
#fieldsWithSubFields = [];
|
37
|
+
#fieldsToExpand = [];
|
38
|
+
|
39
|
+
#mesgListener = null;
|
40
|
+
#optExpandSubFields = true;
|
41
|
+
#optExpandComponents = true;
|
42
|
+
#optApplyScaleAndOffset = true;
|
43
|
+
#optConvertTypesToStrings = true;
|
44
|
+
#optConvertDateTimesToDates = true;
|
45
|
+
#optIncludeUnknownData = false;
|
46
|
+
#optMergeHeartRates = true;
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Creates a FIT File Decoder
|
50
|
+
* @constructor
|
51
|
+
* @param {Stream} stream - representing the FIT file to decode
|
52
|
+
*/
|
53
|
+
constructor(stream) {
|
54
|
+
if (stream == null) {
|
55
|
+
throw Error("FIT Runtime Error stream parameter is null or undefined");
|
56
|
+
}
|
57
|
+
|
58
|
+
this.#stream = stream;
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Inspects the file header to determine if the input stream is a FIT file
|
63
|
+
* @param {Stream} stream
|
64
|
+
* @returns {Boolean} True if the stream is a FIT file
|
65
|
+
* @static
|
66
|
+
*/
|
67
|
+
static isFIT(stream) {
|
68
|
+
try {
|
69
|
+
const fileHeaderSize = stream.peekByte();
|
70
|
+
if ([14, 12].includes(fileHeaderSize) != true) {
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
|
74
|
+
if (stream.length < fileHeaderSize + CRC_SIZE) {
|
75
|
+
return false;
|
76
|
+
}
|
77
|
+
|
78
|
+
const fileHeader = Decoder.#readFileHeader(stream, true);
|
79
|
+
if (fileHeader.dataType !== ".FIT") {
|
80
|
+
return false;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
catch (error) {
|
84
|
+
return false;
|
85
|
+
}
|
86
|
+
|
87
|
+
return true;
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Inspects the file header to determine if the input stream is a FIT file
|
92
|
+
* @returns {Boolean} True if the stream is a FIT file
|
93
|
+
*/
|
94
|
+
isFIT() {
|
95
|
+
return Decoder.isFIT(this.#stream);
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Checks that the input stream is a FIT file and verifies both the header and file CRC values
|
100
|
+
* @returns {Boolean} True if the stream passes the isFit() and CRC checks
|
101
|
+
*/
|
102
|
+
checkIntegrity() {
|
103
|
+
try {
|
104
|
+
if (!this.isFIT()) {
|
105
|
+
return false;
|
106
|
+
}
|
107
|
+
|
108
|
+
const fileHeader = Decoder.#readFileHeader(this.#stream, true);
|
109
|
+
|
110
|
+
if (this.#stream.length < fileHeader.headerSize + fileHeader.dataSize + CRC_SIZE) {
|
111
|
+
return false;
|
112
|
+
}
|
113
|
+
|
114
|
+
const buf = new Uint8Array(this.#stream.slice(0, this.#stream.length))
|
115
|
+
|
116
|
+
if (fileHeader.headerSize === 14 && fileHeader.headerCRC !== 0x0000
|
117
|
+
&& fileHeader.headerCRC != CrcCalculator.calculateCRC(buf, 0, 12)) {
|
118
|
+
return false;
|
119
|
+
}
|
120
|
+
|
121
|
+
const fileCRC = (buf[fileHeader.headerSize + fileHeader.dataSize + 1] << 8) + buf[fileHeader.headerSize + fileHeader.dataSize]
|
122
|
+
if (fileCRC != CrcCalculator.calculateCRC(buf, 0, fileHeader.headerSize + fileHeader.dataSize)) {
|
123
|
+
return false;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
catch (error) {
|
127
|
+
return false;
|
128
|
+
}
|
129
|
+
|
130
|
+
return true;
|
131
|
+
}
|
132
|
+
|
133
|
+
/**
|
134
|
+
* Message Listener Callback
|
135
|
+
*
|
136
|
+
* @callback Decoder~mesgListener
|
137
|
+
* @param {Number} mesgNum - Profile.MesgNum
|
138
|
+
* @param {Object} message - The message
|
139
|
+
*/
|
140
|
+
|
141
|
+
|
142
|
+
/**
|
143
|
+
* Read the messages from the stream.
|
144
|
+
* @param {Object=} [options] - Read options (optional)
|
145
|
+
* @param {Decoder~mesgListener} [options.mesgListener=null] - (optional, default null) mesgListener(mesgNum, message)
|
146
|
+
* @param {Boolean} [options.expandSubFields=true] - (optional, default true)
|
147
|
+
* @param {Boolean} [options.expandComponents=true] - (optional, default true)
|
148
|
+
* @param {Boolean} [options.applyScaleAndOffset=true] - (optional, default true)
|
149
|
+
* @param {Boolean} [options.convertTypesToStrings=true] - (optional, default true)
|
150
|
+
* @param {boolean} [options.convertDateTimesToDates=true] - (optional, default true)
|
151
|
+
* @param {Boolean} [options.includeUnknownData=false] - (optional, default false)
|
152
|
+
* @param {boolean} [options.mergeHeartRates=true] - (optional, default false)
|
153
|
+
* @return {Object} result - {messages:Array, errors:Array}
|
154
|
+
*/
|
155
|
+
read({
|
156
|
+
mesgListener = null,
|
157
|
+
expandSubFields = true,
|
158
|
+
expandComponents = true,
|
159
|
+
applyScaleAndOffset = true,
|
160
|
+
convertTypesToStrings = true,
|
161
|
+
convertDateTimesToDates = true,
|
162
|
+
includeUnknownData = false,
|
163
|
+
mergeHeartRates = true } = {}) {
|
164
|
+
|
165
|
+
this.#mesgListener = mesgListener;
|
166
|
+
this.#optExpandSubFields = expandSubFields
|
167
|
+
this.#optExpandComponents = expandComponents;
|
168
|
+
this.#optApplyScaleAndOffset = applyScaleAndOffset;
|
169
|
+
this.#optConvertTypesToStrings = convertTypesToStrings;
|
170
|
+
this.#optConvertDateTimesToDates = convertDateTimesToDates;
|
171
|
+
this.#optIncludeUnknownData = includeUnknownData;
|
172
|
+
this.#optMergeHeartRates = mergeHeartRates;
|
173
|
+
|
174
|
+
this.#localMessageDefinitions = [];
|
175
|
+
this.#developerDataDefinitions = {};
|
176
|
+
this.#messages = {};
|
177
|
+
|
178
|
+
const errors = [];
|
179
|
+
|
180
|
+
try {
|
181
|
+
if (this.#optMergeHeartRates && (!this.#optApplyScaleAndOffset || !this.#optExpandComponents)) {
|
182
|
+
this.#throwError("mergeHeartRates requires applyScaleAndOffset and expandComponents to be enabled");
|
183
|
+
}
|
184
|
+
|
185
|
+
this.#stream.reset();
|
186
|
+
|
187
|
+
while (this.#stream.position < this.#stream.length) {
|
188
|
+
this.#decodeNextFile();
|
189
|
+
}
|
190
|
+
|
191
|
+
if (this.#optMergeHeartRates) {
|
192
|
+
HrMesgUtils.mergeHeartRates(this.#messages.hrMesgs, this.#messages.recordMesgs);
|
193
|
+
}
|
194
|
+
}
|
195
|
+
catch (error) {
|
196
|
+
errors.push(error);
|
197
|
+
}
|
198
|
+
finally {
|
199
|
+
return { messages: this.#messages, errors: errors };
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
#decodeNextFile() {
|
204
|
+
const position = this.#stream.position;
|
205
|
+
|
206
|
+
if (!this.isFIT()) {
|
207
|
+
this.#throwError("input is not a FIT file");
|
208
|
+
}
|
209
|
+
|
210
|
+
this.#stream.crcCalculator = new CrcCalculator();
|
211
|
+
|
212
|
+
const fileHeader = Decoder.#readFileHeader(this.#stream);
|
213
|
+
|
214
|
+
// Read data messages and definitions
|
215
|
+
while (this.#stream.position < (position + fileHeader.headerSize + fileHeader.dataSize)) {
|
216
|
+
this.#decodeNextRecord();
|
217
|
+
}
|
218
|
+
|
219
|
+
// Check the CRC
|
220
|
+
const calculatedCrc = this.#stream.crcCalculator.crc;
|
221
|
+
const crc = this.#stream.readUInt16();
|
222
|
+
if (crc !== calculatedCrc) {
|
223
|
+
this.#throwError("CRC error");
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
#decodeNextRecord() {
|
228
|
+
const recordHeader = this.#stream.peekByte();
|
229
|
+
|
230
|
+
if ((recordHeader & MESG_DEFINITION_MASK) === MESG_HEADER_MASK) {
|
231
|
+
return this.#decodeMessage();
|
232
|
+
}
|
233
|
+
|
234
|
+
if ((recordHeader & COMPRESSED_HEADER_MASK) === COMPRESSED_HEADER_MASK) {
|
235
|
+
return this.#decodeCompressedTimestampDataMessage();
|
236
|
+
}
|
237
|
+
|
238
|
+
if ((recordHeader & MESG_DEFINITION_MASK) === MESG_DEFINITION_MASK) {
|
239
|
+
return this.#decodeMessageDefinition();
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
#decodeMessageDefinition() {
|
244
|
+
const recordHeader = this.#stream.readByte();
|
245
|
+
|
246
|
+
const messageDefinition = {};
|
247
|
+
messageDefinition["recordHeader"] = recordHeader;
|
248
|
+
messageDefinition["localMesgNum"] = recordHeader & LOCAL_MESG_NUM_MASK;
|
249
|
+
messageDefinition["reserved"] = this.#stream.readByte();
|
250
|
+
|
251
|
+
messageDefinition["architecture"] = this.#stream.readByte();
|
252
|
+
messageDefinition["endianness"] = messageDefinition.architecture === 0 ? Stream.LITTLE_ENDIAN : Stream.BIG_ENDIAN;
|
253
|
+
|
254
|
+
messageDefinition["globalMessageNumber"] = this.#stream.readUInt16({ endianness: messageDefinition["endianness"] });
|
255
|
+
messageDefinition["numFields"] = this.#stream.readByte();
|
256
|
+
messageDefinition["fieldDefinitions"] = [];
|
257
|
+
messageDefinition["developerFieldDefinitions"] = [];
|
258
|
+
messageDefinition["messageSize"] = 0;
|
259
|
+
messageDefinition["developerDataSize"] = 0;
|
260
|
+
|
261
|
+
for (let i = 0; i < messageDefinition.numFields; i++) {
|
262
|
+
const fieldDefinition = {
|
263
|
+
fieldDefinitionNumber: this.#stream.readByte(),
|
264
|
+
size: this.#stream.readByte(),
|
265
|
+
baseType: this.#stream.readByte()
|
266
|
+
};
|
267
|
+
|
268
|
+
if (!(fieldDefinition.baseType in FIT.BaseTypeDefinitions)) {
|
269
|
+
this.#throwError();
|
270
|
+
}
|
271
|
+
|
272
|
+
fieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].invalid;
|
273
|
+
fieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[fieldDefinition.baseType].size;
|
274
|
+
|
275
|
+
messageDefinition.fieldDefinitions.push(fieldDefinition);
|
276
|
+
messageDefinition.messageSize += fieldDefinition.size;
|
277
|
+
}
|
278
|
+
|
279
|
+
if ((recordHeader & DEV_DATA_MASK) === DEV_DATA_MASK) {
|
280
|
+
const numDevFields = this.#stream.readByte();
|
281
|
+
|
282
|
+
for (let i = 0; i < numDevFields; i++) {
|
283
|
+
const developerFieldDefinition = {
|
284
|
+
fieldDefinitionNumber: this.#stream.readByte(),
|
285
|
+
size: this.#stream.readByte(),
|
286
|
+
developerDataIndex: this.#stream.readByte()
|
287
|
+
};
|
288
|
+
|
289
|
+
messageDefinition.developerFieldDefinitions.push(developerFieldDefinition);
|
290
|
+
messageDefinition.developerDataSize += developerFieldDefinition.size;
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
let messageProfile = Profile.messages[messageDefinition.globalMessageNumber];
|
295
|
+
|
296
|
+
if (messageProfile == null && this.#optIncludeUnknownData) {
|
297
|
+
messageProfile = {
|
298
|
+
name: messageDefinition["globalMessageNumber"].toString(),
|
299
|
+
messagesKey: messageDefinition["globalMessageNumber"].toString(),
|
300
|
+
num: messageDefinition["globalMessageNumber"],
|
301
|
+
fields: {}
|
302
|
+
};
|
303
|
+
}
|
304
|
+
|
305
|
+
this.#localMessageDefinitions[messageDefinition.localMesgNum] = { ...messageDefinition, ...messageProfile };
|
306
|
+
|
307
|
+
if (messageProfile && !this.#messages.hasOwnProperty(messageProfile.messagesKey)) {
|
308
|
+
this.#messages[messageProfile.messagesKey] = [];
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
#decodeMessage() {
|
313
|
+
const recordHeader = this.#stream.readByte();
|
314
|
+
|
315
|
+
const localMesgNum = recordHeader & LOCAL_MESG_NUM_MASK;
|
316
|
+
const messageDefinition = this.#localMessageDefinitions[localMesgNum];
|
317
|
+
|
318
|
+
if (messageDefinition == null) {
|
319
|
+
this.#throwError();
|
320
|
+
}
|
321
|
+
|
322
|
+
const fields = messageDefinition.fields ?? {};
|
323
|
+
const mesgNum = messageDefinition.num;
|
324
|
+
const message = {};
|
325
|
+
this.#fieldsWithSubFields = [];
|
326
|
+
this.#fieldsToExpand = [];
|
327
|
+
|
328
|
+
messageDefinition.fieldDefinitions.forEach(fieldDefinition => {
|
329
|
+
const field = fields[fieldDefinition.fieldDefinitionNumber];
|
330
|
+
const { fieldName, rawFieldValue } = this.#readFieldValue(messageDefinition, fieldDefinition, field);
|
331
|
+
|
332
|
+
if (fieldName != null && (field != null || this.#optIncludeUnknownData)) {
|
333
|
+
message[fieldName] = { rawFieldValue, fieldDefinitionNumber: fieldDefinition.fieldDefinitionNumber };
|
334
|
+
|
335
|
+
if (field.subFields.length > 0) {
|
336
|
+
this.#fieldsWithSubFields.push(fieldName);
|
337
|
+
}
|
338
|
+
|
339
|
+
if (field.hasComponents) {
|
340
|
+
this.#fieldsToExpand.push(fieldName);
|
341
|
+
}
|
342
|
+
|
343
|
+
if (field.isAccumulated) {
|
344
|
+
this.#accumulator.add(mesgNum, fieldDefinition.fieldDefinitionNumber, rawFieldValue);
|
345
|
+
}
|
346
|
+
}
|
347
|
+
});
|
348
|
+
|
349
|
+
const developerFields = {};
|
350
|
+
|
351
|
+
messageDefinition.developerFieldDefinitions.forEach(developerFieldDefinition => {
|
352
|
+
const field = this.#lookupDeveloperDataField(developerFieldDefinition)
|
353
|
+
if (field == null) {
|
354
|
+
// If there is not a field definition, then read past the field data.
|
355
|
+
this.#stream.readBytes(developerFieldDefinition.size);
|
356
|
+
return;
|
357
|
+
}
|
358
|
+
|
359
|
+
developerFieldDefinition["baseType"] = field.fitBaseTypeId;
|
360
|
+
developerFieldDefinition["invalidValue"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].invalid;
|
361
|
+
developerFieldDefinition["baseTypeSize"] = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType].size;
|
362
|
+
|
363
|
+
const { rawFieldValue: fieldValue } = this.#readFieldValue(messageDefinition, developerFieldDefinition, field);
|
364
|
+
|
365
|
+
if (fieldValue != null) {
|
366
|
+
developerFields[field.key] = fieldValue;
|
367
|
+
}
|
368
|
+
});
|
369
|
+
|
370
|
+
if (mesgNum === Profile.MesgNum.DEVELOPER_DATA_ID) {
|
371
|
+
this.#addDeveloperDataIdToProfile(message);
|
372
|
+
}
|
373
|
+
else if (mesgNum === Profile.MesgNum.FIELD_DESCRIPTION) {
|
374
|
+
const key = Object.keys(this.#developerDataDefinitions)
|
375
|
+
.reduce((count, key) => count + this.#developerDataDefinitions[key].fields.length, 0);
|
376
|
+
message["key"] = { fieldValue: key, rawFieldValue: key };
|
377
|
+
|
378
|
+
this.#addFieldDescriptionToProfile(message);
|
379
|
+
}
|
380
|
+
else {
|
381
|
+
this.#expandSubFields(mesgNum, message);
|
382
|
+
this.#expandComponents(mesgNum, message, fields);
|
383
|
+
this.#transformValues(message, messageDefinition);
|
384
|
+
}
|
385
|
+
|
386
|
+
if (messageDefinition.name != null) {
|
387
|
+
Object.keys(message).forEach((key) => {
|
388
|
+
message[key] = message[key].fieldValue;
|
389
|
+
});
|
390
|
+
|
391
|
+
if (Object.keys(developerFields).length > 0) {
|
392
|
+
message.developerFields = developerFields;
|
393
|
+
}
|
394
|
+
|
395
|
+
this.#messages[messageDefinition.messagesKey].push(message);
|
396
|
+
this.#mesgListener?.(messageDefinition.globalMessageNumber, message);
|
397
|
+
}
|
398
|
+
}
|
399
|
+
|
400
|
+
#decodeCompressedTimestampDataMessage() {
|
401
|
+
this.#throwError("compressed timestamp messages are not currently supported");
|
402
|
+
}
|
403
|
+
|
404
|
+
#readFieldValue(messageDefinition, fieldDefinition, field) {
|
405
|
+
const rawFieldValue = this.#readRawFieldValue(messageDefinition, fieldDefinition, field);
|
406
|
+
|
407
|
+
if (rawFieldValue == null) {
|
408
|
+
return {};
|
409
|
+
}
|
410
|
+
|
411
|
+
return {
|
412
|
+
fieldName: (field?.name ?? ~~fieldDefinition.fieldDefinitionNumber),
|
413
|
+
rawFieldValue
|
414
|
+
};
|
415
|
+
}
|
416
|
+
|
417
|
+
#readRawFieldValue(messageDefinition, fieldDefinition, field) {
|
418
|
+
const rawFieldValue = this.#stream.readValue(
|
419
|
+
fieldDefinition.baseType,
|
420
|
+
fieldDefinition.size,
|
421
|
+
{
|
422
|
+
endianness: messageDefinition["endianness"],
|
423
|
+
convertInvalidToNull: !field?.hasComponents ?? false
|
424
|
+
}
|
425
|
+
);
|
426
|
+
return rawFieldValue;
|
427
|
+
}
|
428
|
+
|
429
|
+
#addDeveloperDataIdToProfile(message) {
|
430
|
+
if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
|
431
|
+
return;
|
432
|
+
}
|
433
|
+
|
434
|
+
this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] = {
|
435
|
+
developerDataIndex: message.developerDataIndex?.rawFieldValue,
|
436
|
+
developerId: message.developerId?.rawFieldValue ?? null,
|
437
|
+
applicationId: message.applicationId?.rawFieldValue ?? null,
|
438
|
+
manufacturerId: message.manufacturerId?.rawFieldValue ?? null,
|
439
|
+
applicationVersion: message.applicationVersion?.rawFieldValue ?? null,
|
440
|
+
fields: []
|
441
|
+
};
|
442
|
+
}
|
443
|
+
|
444
|
+
#addFieldDescriptionToProfile(message) {
|
445
|
+
if (message == null || message.developerDataIndex.rawFieldValue == null || message.developerDataIndex.rawFieldValue === 0xFF) {
|
446
|
+
return;
|
447
|
+
}
|
448
|
+
|
449
|
+
if (this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue] == null) {
|
450
|
+
return;
|
451
|
+
}
|
452
|
+
|
453
|
+
this.#developerDataDefinitions[message.developerDataIndex.rawFieldValue].fields.push({
|
454
|
+
developerDataIndex: message.developerDataIndex?.rawFieldValue,
|
455
|
+
fieldDefinitionNumber: message.fieldDefinitionNumber?.rawFieldValue,
|
456
|
+
fitBaseTypeId: message.fitBaseTypeId?.rawFieldValue ?? null,
|
457
|
+
fieldName: message.fieldName?.rawFieldValue ?? null,
|
458
|
+
array: message.array?.rawFieldValue ?? null,
|
459
|
+
components: message.components?.rawFieldValue ?? null,
|
460
|
+
scale: message.scale?.rawFieldValue ?? null,
|
461
|
+
offset: message.offset?.rawFieldValue ?? null,
|
462
|
+
units: message.units?.rawFieldValue ?? null,
|
463
|
+
bits: message.bits?.rawFieldValue ?? null,
|
464
|
+
accumulate: message.accumulate?.rawFieldValue ?? null,
|
465
|
+
refFieldName: message.refFieldName?.rawFieldValue ?? null,
|
466
|
+
refFieldValue: message.refFieldValue?.rawFieldValue ?? null,
|
467
|
+
fitBaseUnitId: message.fitBaseUnitId?.rawFieldValue ?? null,
|
468
|
+
nativeMesgNum: message.nativeMesgNum?.rawFieldValue ?? null,
|
469
|
+
nativeFieldNum: message.nativeFieldNum?.rawFieldValue ?? null,
|
470
|
+
key: message.key.rawFieldValue
|
471
|
+
});
|
472
|
+
}
|
473
|
+
|
474
|
+
#lookupDeveloperDataField(developerFieldDefinition) {
|
475
|
+
try {
|
476
|
+
return this.#developerDataDefinitions[developerFieldDefinition.developerDataIndex]
|
477
|
+
?.fields
|
478
|
+
?.find(def => def.fieldDefinitionNumber == developerFieldDefinition.fieldDefinitionNumber)
|
479
|
+
?? null;
|
480
|
+
}
|
481
|
+
catch {
|
482
|
+
return null;
|
483
|
+
}
|
484
|
+
}
|
485
|
+
|
486
|
+
#expandSubFields(mesgNum, message) {
|
487
|
+
if (!this.#optExpandSubFields || this.#fieldsWithSubFields.length == 0) {
|
488
|
+
return;
|
489
|
+
}
|
490
|
+
|
491
|
+
this.#fieldsWithSubFields.forEach((name) => {
|
492
|
+
const field = Profile.messages[mesgNum].fields[message[name].fieldDefinitionNumber];
|
493
|
+
this.#expandSubField(message, field);
|
494
|
+
});
|
495
|
+
}
|
496
|
+
|
497
|
+
#expandSubField(message, field) {
|
498
|
+
for (let i = 0; i < field.subFields.length; i++) {
|
499
|
+
const subField = field.subFields[i];
|
500
|
+
for (let j = 0; j < subField.map.length; j++) {
|
501
|
+
const map = subField.map[j];
|
502
|
+
const referenceField = message[map.name];
|
503
|
+
if (referenceField == null) {
|
504
|
+
continue;
|
505
|
+
}
|
506
|
+
if (referenceField.rawFieldValue === map.value) {
|
507
|
+
message[subField.name] = JSON.parse(JSON.stringify(message[field.name]));
|
508
|
+
message[subField.name].isSubField = true;
|
509
|
+
|
510
|
+
if (subField.hasComponents) {
|
511
|
+
this.#fieldsToExpand.push(subField.name);
|
512
|
+
}
|
513
|
+
break;
|
514
|
+
}
|
515
|
+
}
|
516
|
+
}
|
517
|
+
}
|
518
|
+
|
519
|
+
#expandComponents(mesgNum, message, fields) {
|
520
|
+
// TODO - What do do when the target field is not in the Profile?
|
521
|
+
// TODO - This can happen in theory, but can it happen in practice?
|
522
|
+
|
523
|
+
if (!this.#optExpandComponents || this.#fieldsToExpand.length == 0) {
|
524
|
+
return;
|
525
|
+
}
|
526
|
+
|
527
|
+
const mesg = {};
|
528
|
+
|
529
|
+
while (this.#fieldsToExpand.length > 0) {
|
530
|
+
const name = this.#fieldsToExpand.shift();
|
531
|
+
|
532
|
+
const { rawFieldValue, fieldDefinitionNumber, isSubField } = message[name];
|
533
|
+
let field = Profile.messages[mesgNum].fields[fieldDefinitionNumber];
|
534
|
+
field = isSubField ? this.#lookupSubfield(field, name) : field;
|
535
|
+
const baseType = FIT.FieldTypeToBaseType[field.type];
|
536
|
+
|
537
|
+
if (field.hasComponents === false || baseType == null) {
|
538
|
+
continue;
|
539
|
+
}
|
540
|
+
|
541
|
+
if (UtilsInternal.onlyInvalidValues(rawFieldValue, FIT.BaseTypeDefinitions[baseType].invalid)) {
|
542
|
+
continue;
|
543
|
+
}
|
544
|
+
|
545
|
+
const bitStream = new BitStream(rawFieldValue, baseType);
|
546
|
+
|
547
|
+
for (let j = 0; j < field.components.length; j++) {
|
548
|
+
const targetField = fields[field.components[j]];
|
549
|
+
if (mesg[targetField.name] == null) {
|
550
|
+
const baseType = FIT.FieldTypeToBaseType[targetField.type];
|
551
|
+
const invalidValue = FIT.BaseTypeDefinitions[baseType].invalid;
|
552
|
+
|
553
|
+
mesg[targetField.name] = {
|
554
|
+
fieldValue: [],
|
555
|
+
rawFieldValue: [],
|
556
|
+
fieldDefinitionNumber: targetField.num,
|
557
|
+
isExpandedField: true,
|
558
|
+
invalidValue,
|
559
|
+
};
|
560
|
+
}
|
561
|
+
|
562
|
+
if (bitStream.bitsAvailable < field.bits[j]) {
|
563
|
+
break;
|
564
|
+
}
|
565
|
+
|
566
|
+
let value = bitStream.readBits(field.bits[j]);
|
567
|
+
|
568
|
+
value = this.#accumulator.accumulate(mesgNum, targetField.num, value, field.bits[j]) ?? value;
|
569
|
+
|
570
|
+
mesg[targetField.name].rawFieldValue.push(value);
|
571
|
+
|
572
|
+
if (value === mesg[targetField.name].invalidValue) {
|
573
|
+
mesg[targetField.name].fieldValue.push(null);
|
574
|
+
}
|
575
|
+
else {
|
576
|
+
|
577
|
+
value = value / field.scale[j] - field.offset[j];
|
578
|
+
mesg[targetField.name].fieldValue.push(value);
|
579
|
+
}
|
580
|
+
|
581
|
+
if (targetField.hasComponents) {
|
582
|
+
this.#fieldsToExpand.push(targetField.name);
|
583
|
+
}
|
584
|
+
|
585
|
+
if (!bitStream.hasBitsAvailable) {
|
586
|
+
break;
|
587
|
+
}
|
588
|
+
}
|
589
|
+
}
|
590
|
+
|
591
|
+
Object.keys(mesg).forEach((key) => {
|
592
|
+
mesg[key].fieldValue = UtilsInternal.sanitizeValues(mesg[key].fieldValue);
|
593
|
+
mesg[key].rawFieldValue = UtilsInternal.sanitizeValues(mesg[key].rawFieldValue);
|
594
|
+
|
595
|
+
message[key] = mesg[key];
|
596
|
+
});
|
597
|
+
}
|
598
|
+
|
599
|
+
#transformValues(message, messageDefinition) {
|
600
|
+
const fields = messageDefinition?.fields ?? {};
|
601
|
+
|
602
|
+
for (const name in message) {
|
603
|
+
|
604
|
+
const { rawFieldValue, fieldDefinitionNumber, isExpandedField, isSubField } = message[name];
|
605
|
+
|
606
|
+
let field = fields[fieldDefinitionNumber];
|
607
|
+
field = isSubField ? this.#lookupSubfield(field, name) : field;
|
608
|
+
|
609
|
+
if (!isExpandedField) {
|
610
|
+
const fieldValue = this.#transformValue(messageDefinition, field, rawFieldValue);
|
611
|
+
message[name].fieldValue = fieldValue;
|
612
|
+
}
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
#transformValue(messageDefinition, field, rawFieldValue) {
|
617
|
+
let fieldValue = rawFieldValue;
|
618
|
+
|
619
|
+
if (field == null) {
|
620
|
+
fieldValue = rawFieldValue;
|
621
|
+
}
|
622
|
+
else if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
|
623
|
+
fieldValue = this.#applyScaleAndOffset(messageDefinition, field, rawFieldValue);
|
624
|
+
}
|
625
|
+
else if (field.type === "string") {
|
626
|
+
fieldValue = rawFieldValue;
|
627
|
+
}
|
628
|
+
else if (field.type === "dateTime" && this.#optConvertDateTimesToDates) {
|
629
|
+
fieldValue = Utils.convertDateTimeToDate(rawFieldValue);
|
630
|
+
}
|
631
|
+
else if (this.#optConvertTypesToStrings) {
|
632
|
+
fieldValue = this.#convertTypeToString(messageDefinition, field, rawFieldValue);
|
633
|
+
}
|
634
|
+
else {
|
635
|
+
fieldValue = rawFieldValue;
|
636
|
+
}
|
637
|
+
|
638
|
+
return fieldValue;
|
639
|
+
}
|
640
|
+
|
641
|
+
#applyScaleAndOffset(messageDefinition, field, rawFieldValue) {
|
642
|
+
if (!this.#optApplyScaleAndOffset) {
|
643
|
+
return rawFieldValue;
|
644
|
+
}
|
645
|
+
|
646
|
+
if (FIT.NumericFieldTypes.includes(field?.type ?? -1) === false) {
|
647
|
+
return rawFieldValue;
|
648
|
+
}
|
649
|
+
|
650
|
+
if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
|
651
|
+
return rawFieldValue;
|
652
|
+
}
|
653
|
+
|
654
|
+
if (rawFieldValue == null) {
|
655
|
+
return rawFieldValue;
|
656
|
+
}
|
657
|
+
|
658
|
+
if (Array.isArray(field?.scale ?? 1) && field.scale.length > 1) {
|
659
|
+
return rawFieldValue;
|
660
|
+
}
|
661
|
+
|
662
|
+
const scale = Array.isArray(field?.scale ?? 1) ? field?.scale[0] : field?.scale ?? 1;
|
663
|
+
const offset = Array.isArray(field?.offset ?? 1) ? field?.offset[0] : field?.offset ?? 0;
|
664
|
+
|
665
|
+
try {
|
666
|
+
if (Array.isArray(rawFieldValue)) {
|
667
|
+
return rawFieldValue.map((value) => {
|
668
|
+
return value == null ? value : (value / scale) - offset;
|
669
|
+
});
|
670
|
+
}
|
671
|
+
|
672
|
+
return (rawFieldValue / scale) - offset;
|
673
|
+
}
|
674
|
+
catch {
|
675
|
+
return rawFieldValue;
|
676
|
+
}
|
677
|
+
}
|
678
|
+
|
679
|
+
#convertTypeToString(messageDefinition, field, rawFieldValue) {
|
680
|
+
if ([Profile.MesgNum.DEVELOPER_DATA_ID, Profile.MesgNum.FIELD_DESCRIPTION].includes(messageDefinition.globalMessageNumber)) {
|
681
|
+
return rawFieldValue;
|
682
|
+
}
|
683
|
+
|
684
|
+
if (FIT.NumericFieldTypes.includes(field?.type ?? -1)) {
|
685
|
+
return rawFieldValue;
|
686
|
+
}
|
687
|
+
|
688
|
+
try {
|
689
|
+
const type = Profile.types[field?.type ?? -1];
|
690
|
+
|
691
|
+
if (Array.isArray(rawFieldValue)) {
|
692
|
+
return rawFieldValue.map(value => {
|
693
|
+
return value == null ? value : type?.[value] ?? value
|
694
|
+
});
|
695
|
+
}
|
696
|
+
|
697
|
+
return type?.[rawFieldValue] ?? rawFieldValue;
|
698
|
+
}
|
699
|
+
catch {
|
700
|
+
return rawFieldValue;
|
701
|
+
}
|
702
|
+
}
|
703
|
+
|
704
|
+
#lookupSubfield(field, name) {
|
705
|
+
const subField = field.subFields.find(subField => subField.name === name);
|
706
|
+
return subField != null ? subField : {};
|
707
|
+
}
|
708
|
+
|
709
|
+
static #readFileHeader(stream, resetPosition = false) {
|
710
|
+
const position = stream.position;
|
711
|
+
|
712
|
+
const fileHeader = {
|
713
|
+
headerSize: stream.readByte(),
|
714
|
+
protocolVersion: stream.readByte(),
|
715
|
+
profileVersion: stream.readUInt16() / 100,
|
716
|
+
dataSize: stream.readUInt32(),
|
717
|
+
dataType: stream.readString(4),
|
718
|
+
headerCRC: 0
|
719
|
+
};
|
720
|
+
|
721
|
+
if (fileHeader.headerSize === 14) {
|
722
|
+
fileHeader.headerCRC = stream.readUInt16()
|
723
|
+
}
|
724
|
+
|
725
|
+
if (resetPosition) {
|
726
|
+
stream.seek(position);
|
727
|
+
}
|
728
|
+
|
729
|
+
return fileHeader;
|
730
|
+
}
|
731
|
+
|
732
|
+
#throwError(error = "") {
|
733
|
+
throw Error(`FIT Runtime Error at byte ${this.#stream.position} ${error}`.trimEnd());
|
734
|
+
}
|
735
|
+
}
|
736
|
+
|
737
|
+
export default Decoder;
|