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