@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/encoder.js
CHANGED
|
@@ -1,323 +1,323 @@
|
|
|
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 CrcCalculator from "./crc-calculator.js";
|
|
14
|
-
import FIT from "./fit.js";
|
|
15
|
-
import MesgDefinition from "./mesg-definition.js";
|
|
16
|
-
import OutputStream from "./output-stream.js";
|
|
17
|
-
import Profile from "./profile.js";
|
|
18
|
-
import Utils from "./utils.js";
|
|
19
|
-
|
|
20
|
-
const HEADER_WITH_CRC_SIZE = 14;
|
|
21
|
-
const HEADER_WITHOUT_CRC_SIZE = 12;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* A class for encoding FIT files.
|
|
25
|
-
* @class
|
|
26
|
-
*/
|
|
27
|
-
class Encoder {
|
|
28
|
-
/**
|
|
29
|
-
Creates a FIT File Encoder
|
|
30
|
-
* @param {Object} [options] - Encoder options (optional)
|
|
31
|
-
* @param {Object.<number,{object, object }} [options.fieldDescriptions=null] - (optional, default null) fieldDescriptions
|
|
32
|
-
* @constructor
|
|
33
|
-
*/
|
|
34
|
-
constructor({ fieldDescriptions = null, } = {}) {
|
|
35
|
-
this.#fieldDescriptions = {};
|
|
36
|
-
|
|
37
|
-
for (const [key, {developerDataIdMesg, fieldDescriptionMesg}] of Object.entries(fieldDescriptions ?? {})) {
|
|
38
|
-
this.addDeveloperField(key, developerDataIdMesg, fieldDescriptionMesg);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this.#writeEmptyFileHeader();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Closes the encoder and returns the file data
|
|
46
|
-
* @returns {Uint8Array} A Uint8Array containing the file data
|
|
47
|
-
*/
|
|
48
|
-
close() {
|
|
49
|
-
this.#updateFileHeader();
|
|
50
|
-
this.#writeFileCrc();
|
|
51
|
-
|
|
52
|
-
return this.#outputStream.uint8Array;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Encodes a mesg into the file.
|
|
57
|
-
* @param {Object} mesg - The message data
|
|
58
|
-
* @param {Number} mesg.mesgNum - The mesg number for this message
|
|
59
|
-
* @return {this}
|
|
60
|
-
*/
|
|
61
|
-
writeMesg(mesg) {
|
|
62
|
-
return this.onMesg(mesg.mesgNum, mesg);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Encodes a mesg into the file.
|
|
67
|
-
* This method can be used as a Decoder~mesgListener callback.
|
|
68
|
-
* @param {Number} mesgNum - The message number for this message
|
|
69
|
-
* @param {Object} mesg - The message data
|
|
70
|
-
* @return {this}
|
|
71
|
-
*/
|
|
72
|
-
onMesg(mesgNum, mesg) {
|
|
73
|
-
try {
|
|
74
|
-
const mesgDefinition = this.#createMesgDefinition(mesgNum, mesg);
|
|
75
|
-
this.#writeMesgDefinitionIfNotActive(mesgDefinition);
|
|
76
|
-
|
|
77
|
-
// Write Message Header
|
|
78
|
-
this.#outputStream.writeUInt8(mesgDefinition.localMesgNum);
|
|
79
|
-
|
|
80
|
-
// Write Field Values
|
|
81
|
-
mesgDefinition.fieldDefinitions.forEach((fieldDefinition) => {
|
|
82
|
-
const values = this.#transformValues(mesg[fieldDefinition.name], fieldDefinition);
|
|
83
|
-
const baseTypeDef = FIT.BaseTypeDefinitions[fieldDefinition.baseType];
|
|
84
|
-
|
|
85
|
-
this.#outputStream.write(values, baseTypeDef.type);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Write Developer Field Values
|
|
89
|
-
mesgDefinition.developerFieldDefinitions.forEach((developerFieldDefinition) => {
|
|
90
|
-
const values = this.#transformValues(
|
|
91
|
-
mesg.developerFields[developerFieldDefinition.key],
|
|
92
|
-
developerFieldDefinition);
|
|
93
|
-
|
|
94
|
-
const baseTypeDef = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType];
|
|
95
|
-
|
|
96
|
-
this.#outputStream.write(values, baseTypeDef.type);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
"Could not write Message", {
|
|
102
|
-
cause: {
|
|
103
|
-
mesg,
|
|
104
|
-
cause: {
|
|
105
|
-
message: error.message,
|
|
106
|
-
cause: error.cause,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return this;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Adds a Developer Data Field Description and associated Developer Data Id Message to the Endoder
|
|
118
|
-
* This provides the Encoder with the context required to write Developer Fields to the output-stream.
|
|
119
|
-
* *** This method does not write the messages to the output-stream ***
|
|
120
|
-
* This method can be used as a Decoder~fieldDescriptionListener callback.
|
|
121
|
-
* @param {Number} key - The message number for this message
|
|
122
|
-
* @param {Object} developerDataIdMesg - The Developer Data Id mesg
|
|
123
|
-
* @param {Object} fieldDescriptionMesg - The Field Description mesg
|
|
124
|
-
* @return {this}
|
|
125
|
-
*/
|
|
126
|
-
addDeveloperField(key, developerDataIdMesg, fieldDescriptionMesg) {
|
|
127
|
-
if(developerDataIdMesg.developerDataIndex == null || fieldDescriptionMesg.developerDataIndex == null) {
|
|
128
|
-
throw new Error("addDeveloperField() - one or more developerDataIndex values are null.", {
|
|
129
|
-
cause: {
|
|
130
|
-
key,
|
|
131
|
-
developerDataIdMesg,
|
|
132
|
-
fieldDescriptionMesg
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if(developerDataIdMesg.developerDataIndex !== fieldDescriptionMesg.developerDataIndex) {
|
|
138
|
-
throw new Error("addDeveloperField() - developerDataIndex values do not match.", {
|
|
139
|
-
cause: {
|
|
140
|
-
key,
|
|
141
|
-
developerDataIdMesg,
|
|
142
|
-
fieldDescriptionMesg
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.#fieldDescriptions[key] = {
|
|
148
|
-
developerDataIdMesg,
|
|
149
|
-
fieldDescriptionMesg
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return this;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
#writeEmptyFileHeader() {
|
|
156
|
-
Array(HEADER_WITH_CRC_SIZE).fill(0).forEach((zero) => {
|
|
157
|
-
this.#outputStream.writeUInt8(zero);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
#updateFileHeader() {
|
|
162
|
-
const arrayBuffer = new ArrayBuffer(HEADER_WITH_CRC_SIZE);
|
|
163
|
-
const dataView = new DataView(arrayBuffer);
|
|
164
|
-
|
|
165
|
-
// Header Size
|
|
166
|
-
dataView.setUint8(0, HEADER_WITH_CRC_SIZE);
|
|
167
|
-
|
|
168
|
-
// Protocol Version
|
|
169
|
-
dataView.setUint8(1, 2);
|
|
170
|
-
|
|
171
|
-
// Profile Version
|
|
172
|
-
dataView.setUint16(2, Profile.version.major * 1000 + Profile.version.minor, true);
|
|
173
|
-
|
|
174
|
-
// Data Size
|
|
175
|
-
dataView.setUint32(4, this.#outputStream.length - HEADER_WITH_CRC_SIZE, true);
|
|
176
|
-
|
|
177
|
-
// Data Type ".FIT"
|
|
178
|
-
dataView.setUint8(8, 0x2E);
|
|
179
|
-
dataView.setUint8(9, 0x46);
|
|
180
|
-
dataView.setUint8(10, 0x49);
|
|
181
|
-
dataView.setUint8(11, 0x54);
|
|
182
|
-
|
|
183
|
-
// Header CRC
|
|
184
|
-
const crc = CrcCalculator.calculateCRC(new Uint8Array(arrayBuffer), 0, HEADER_WITHOUT_CRC_SIZE);
|
|
185
|
-
dataView.setUint16(12, crc, true);
|
|
186
|
-
|
|
187
|
-
this.#outputStream.set(new Uint8Array(arrayBuffer));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
#writeFileCrc() {
|
|
191
|
-
const crc = CrcCalculator.calculateCRC(this.#outputStream.uint8Array, 0, this.#outputStream.length);
|
|
192
|
-
this.#outputStream.writeUInt16(crc);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
#transformValues(value, fieldDefinition) {
|
|
196
|
-
const values = Array.isArray(value) ? value : [value,];
|
|
197
|
-
|
|
198
|
-
return values.map((value) => {
|
|
199
|
-
return this.#transformValue(value, fieldDefinition);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
#transformValue(value, fieldDefinition) {
|
|
204
|
-
try {
|
|
205
|
-
if (FIT.isNotNumberStringDateOrBoolean(value)) {
|
|
206
|
-
return FIT.BaseTypeDefinitions[fieldDefinition.baseType].invalid;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Is this a numeric field?
|
|
210
|
-
if (FIT.NumericFieldTypes.includes(fieldDefinition.type)) {
|
|
211
|
-
if (!FIT.isNumeric(value)) {
|
|
212
|
-
throw new Error();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const scale = fieldDefinition.components.length > 1 ? 1 : fieldDefinition.scale;
|
|
216
|
-
const offset = fieldDefinition.components.length > 1 ? 0 : fieldDefinition.offset;
|
|
217
|
-
|
|
218
|
-
return (value + offset) * scale;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Is this a date_time field?
|
|
222
|
-
if (fieldDefinition.type === "dateTime") {
|
|
223
|
-
if (FIT.isDate(value)) {
|
|
224
|
-
return Utils.convertDateToDateTime(value);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!FIT.isNumeric(value)) {
|
|
228
|
-
throw new Error();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return value;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Is this a string field
|
|
235
|
-
if (fieldDefinition.type === "string") {
|
|
236
|
-
if (!FIT.isString(value)) {
|
|
237
|
-
throw new Error();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return value;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Must be a FIT type field
|
|
244
|
-
if (FIT.isNumeric(value)) {
|
|
245
|
-
return value;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const profileType = Profile.types[fieldDefinition.type];
|
|
249
|
-
|
|
250
|
-
const [typeValue,] = Object.entries(profileType).find(([, typeValue,]) => {
|
|
251
|
-
return typeValue === value;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return typeValue;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Could not convert "${value}" to "${fieldDefinition.type}"`,
|
|
259
|
-
{ cause: { value, fieldDefinition, }, });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Creates a MesgDefinition from the mesgNum and mesg.
|
|
265
|
-
* @param {Number} mesgNum - The mesg number for this message
|
|
266
|
-
* @param {Object} [mesg] - The message data
|
|
267
|
-
* @return {MesgDefinition}
|
|
268
|
-
*/
|
|
269
|
-
#createMesgDefinition = (mesgNum, mesg) => {
|
|
270
|
-
const mesgDefinition = new MesgDefinition(mesgNum, mesg, { fieldDescriptions: this.#fieldDescriptions, });
|
|
271
|
-
mesgDefinition.localMesgNum = this.#lookupLocalMesgNum(mesgDefinition);
|
|
272
|
-
|
|
273
|
-
return mesgDefinition;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Searches the #localMesgDefinitions for a matching mesgDefinition
|
|
278
|
-
* @param {Object} mesgDefinition - the mesg definition to match
|
|
279
|
-
* @return The localMesgNum to be used with mesgDefinition
|
|
280
|
-
*/
|
|
281
|
-
#lookupLocalMesgNum = (mesgDefinition) => {
|
|
282
|
-
const localMesgNum = this.#localMesgDefinitions.findIndex((localMesgDefinition) => {
|
|
283
|
-
return localMesgDefinition?.equals(mesgDefinition) ?? false;
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return (localMesgNum !== -1 ? localMesgNum : this.#nextLocalMesgNum++) & FIT.LOCAL_MESG_NUM_MASK;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Writes the mesgDefinition to the output stream, if it is not one of the currently active 16
|
|
291
|
-
* @param {Object} mesgDefinition - the mesg definition to match
|
|
292
|
-
* @return The localMesgNum to be used with mesgDefinition
|
|
293
|
-
*/
|
|
294
|
-
#writeMesgDefinitionIfNotActive = (mesgDefinition) => {
|
|
295
|
-
const localMesgNum = mesgDefinition.localMesgNum;
|
|
296
|
-
|
|
297
|
-
if (this.#localMesgDefinitions[localMesgNum] == null
|
|
298
|
-
|| !this.#localMesgDefinitions[localMesgNum].equals(mesgDefinition)) {
|
|
299
|
-
this.#writeMesgDefinition(mesgDefinition);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return localMesgNum;
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Writes the mesgDefinition to the output stream
|
|
307
|
-
* @param {Object} mesgDefinition - the mesg definition to write
|
|
308
|
-
* @return {this}
|
|
309
|
-
*/
|
|
310
|
-
#writeMesgDefinition(mesgDefinition) {
|
|
311
|
-
mesgDefinition.write(this.#outputStream);
|
|
312
|
-
this.#localMesgDefinitions[mesgDefinition.localMesgNum] = mesgDefinition;
|
|
313
|
-
|
|
314
|
-
return this;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
#localMesgDefinitions = Array(16).fill(null);
|
|
318
|
-
#nextLocalMesgNum = 0;
|
|
319
|
-
#outputStream = new OutputStream();
|
|
320
|
-
#fieldDescriptions = null;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export default Encoder;
|
|
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 CrcCalculator from "./crc-calculator.js";
|
|
14
|
+
import FIT from "./fit.js";
|
|
15
|
+
import MesgDefinition from "./mesg-definition.js";
|
|
16
|
+
import OutputStream from "./output-stream.js";
|
|
17
|
+
import Profile from "./profile.js";
|
|
18
|
+
import Utils from "./utils.js";
|
|
19
|
+
|
|
20
|
+
const HEADER_WITH_CRC_SIZE = 14;
|
|
21
|
+
const HEADER_WITHOUT_CRC_SIZE = 12;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A class for encoding FIT files.
|
|
25
|
+
* @class
|
|
26
|
+
*/
|
|
27
|
+
class Encoder {
|
|
28
|
+
/**
|
|
29
|
+
Creates a FIT File Encoder
|
|
30
|
+
* @param {Object} [options] - Encoder options (optional)
|
|
31
|
+
* @param {Object.<number,{object, object }} [options.fieldDescriptions=null] - (optional, default null) fieldDescriptions
|
|
32
|
+
* @constructor
|
|
33
|
+
*/
|
|
34
|
+
constructor({ fieldDescriptions = null, } = {}) {
|
|
35
|
+
this.#fieldDescriptions = {};
|
|
36
|
+
|
|
37
|
+
for (const [key, {developerDataIdMesg, fieldDescriptionMesg}] of Object.entries(fieldDescriptions ?? {})) {
|
|
38
|
+
this.addDeveloperField(key, developerDataIdMesg, fieldDescriptionMesg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.#writeEmptyFileHeader();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Closes the encoder and returns the file data
|
|
46
|
+
* @returns {Uint8Array} A Uint8Array containing the file data
|
|
47
|
+
*/
|
|
48
|
+
close() {
|
|
49
|
+
this.#updateFileHeader();
|
|
50
|
+
this.#writeFileCrc();
|
|
51
|
+
|
|
52
|
+
return this.#outputStream.uint8Array;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Encodes a mesg into the file.
|
|
57
|
+
* @param {Object} mesg - The message data
|
|
58
|
+
* @param {Number} mesg.mesgNum - The mesg number for this message
|
|
59
|
+
* @return {this}
|
|
60
|
+
*/
|
|
61
|
+
writeMesg(mesg) {
|
|
62
|
+
return this.onMesg(mesg.mesgNum, mesg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Encodes a mesg into the file.
|
|
67
|
+
* This method can be used as a Decoder~mesgListener callback.
|
|
68
|
+
* @param {Number} mesgNum - The message number for this message
|
|
69
|
+
* @param {Object} mesg - The message data
|
|
70
|
+
* @return {this}
|
|
71
|
+
*/
|
|
72
|
+
onMesg(mesgNum, mesg) {
|
|
73
|
+
try {
|
|
74
|
+
const mesgDefinition = this.#createMesgDefinition(mesgNum, mesg);
|
|
75
|
+
this.#writeMesgDefinitionIfNotActive(mesgDefinition);
|
|
76
|
+
|
|
77
|
+
// Write Message Header
|
|
78
|
+
this.#outputStream.writeUInt8(mesgDefinition.localMesgNum);
|
|
79
|
+
|
|
80
|
+
// Write Field Values
|
|
81
|
+
mesgDefinition.fieldDefinitions.forEach((fieldDefinition) => {
|
|
82
|
+
const values = this.#transformValues(mesg[fieldDefinition.name], fieldDefinition);
|
|
83
|
+
const baseTypeDef = FIT.BaseTypeDefinitions[fieldDefinition.baseType];
|
|
84
|
+
|
|
85
|
+
this.#outputStream.write(values, baseTypeDef.type);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Write Developer Field Values
|
|
89
|
+
mesgDefinition.developerFieldDefinitions.forEach((developerFieldDefinition) => {
|
|
90
|
+
const values = this.#transformValues(
|
|
91
|
+
mesg.developerFields[developerFieldDefinition.key],
|
|
92
|
+
developerFieldDefinition);
|
|
93
|
+
|
|
94
|
+
const baseTypeDef = FIT.BaseTypeDefinitions[developerFieldDefinition.baseType];
|
|
95
|
+
|
|
96
|
+
this.#outputStream.write(values, baseTypeDef.type);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Could not write Message", {
|
|
102
|
+
cause: {
|
|
103
|
+
mesg,
|
|
104
|
+
cause: {
|
|
105
|
+
message: error.message,
|
|
106
|
+
cause: error.cause,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Adds a Developer Data Field Description and associated Developer Data Id Message to the Endoder
|
|
118
|
+
* This provides the Encoder with the context required to write Developer Fields to the output-stream.
|
|
119
|
+
* *** This method does not write the messages to the output-stream ***
|
|
120
|
+
* This method can be used as a Decoder~fieldDescriptionListener callback.
|
|
121
|
+
* @param {Number} key - The message number for this message
|
|
122
|
+
* @param {Object} developerDataIdMesg - The Developer Data Id mesg
|
|
123
|
+
* @param {Object} fieldDescriptionMesg - The Field Description mesg
|
|
124
|
+
* @return {this}
|
|
125
|
+
*/
|
|
126
|
+
addDeveloperField(key, developerDataIdMesg, fieldDescriptionMesg) {
|
|
127
|
+
if(developerDataIdMesg.developerDataIndex == null || fieldDescriptionMesg.developerDataIndex == null) {
|
|
128
|
+
throw new Error("addDeveloperField() - one or more developerDataIndex values are null.", {
|
|
129
|
+
cause: {
|
|
130
|
+
key,
|
|
131
|
+
developerDataIdMesg,
|
|
132
|
+
fieldDescriptionMesg
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if(developerDataIdMesg.developerDataIndex !== fieldDescriptionMesg.developerDataIndex) {
|
|
138
|
+
throw new Error("addDeveloperField() - developerDataIndex values do not match.", {
|
|
139
|
+
cause: {
|
|
140
|
+
key,
|
|
141
|
+
developerDataIdMesg,
|
|
142
|
+
fieldDescriptionMesg
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.#fieldDescriptions[key] = {
|
|
148
|
+
developerDataIdMesg,
|
|
149
|
+
fieldDescriptionMesg
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#writeEmptyFileHeader() {
|
|
156
|
+
Array(HEADER_WITH_CRC_SIZE).fill(0).forEach((zero) => {
|
|
157
|
+
this.#outputStream.writeUInt8(zero);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#updateFileHeader() {
|
|
162
|
+
const arrayBuffer = new ArrayBuffer(HEADER_WITH_CRC_SIZE);
|
|
163
|
+
const dataView = new DataView(arrayBuffer);
|
|
164
|
+
|
|
165
|
+
// Header Size
|
|
166
|
+
dataView.setUint8(0, HEADER_WITH_CRC_SIZE);
|
|
167
|
+
|
|
168
|
+
// Protocol Version
|
|
169
|
+
dataView.setUint8(1, 2);
|
|
170
|
+
|
|
171
|
+
// Profile Version
|
|
172
|
+
dataView.setUint16(2, Profile.version.major * 1000 + Profile.version.minor, true);
|
|
173
|
+
|
|
174
|
+
// Data Size
|
|
175
|
+
dataView.setUint32(4, this.#outputStream.length - HEADER_WITH_CRC_SIZE, true);
|
|
176
|
+
|
|
177
|
+
// Data Type ".FIT"
|
|
178
|
+
dataView.setUint8(8, 0x2E);
|
|
179
|
+
dataView.setUint8(9, 0x46);
|
|
180
|
+
dataView.setUint8(10, 0x49);
|
|
181
|
+
dataView.setUint8(11, 0x54);
|
|
182
|
+
|
|
183
|
+
// Header CRC
|
|
184
|
+
const crc = CrcCalculator.calculateCRC(new Uint8Array(arrayBuffer), 0, HEADER_WITHOUT_CRC_SIZE);
|
|
185
|
+
dataView.setUint16(12, crc, true);
|
|
186
|
+
|
|
187
|
+
this.#outputStream.set(new Uint8Array(arrayBuffer));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#writeFileCrc() {
|
|
191
|
+
const crc = CrcCalculator.calculateCRC(this.#outputStream.uint8Array, 0, this.#outputStream.length);
|
|
192
|
+
this.#outputStream.writeUInt16(crc);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#transformValues(value, fieldDefinition) {
|
|
196
|
+
const values = Array.isArray(value) ? value : [value,];
|
|
197
|
+
|
|
198
|
+
return values.map((value) => {
|
|
199
|
+
return this.#transformValue(value, fieldDefinition);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#transformValue(value, fieldDefinition) {
|
|
204
|
+
try {
|
|
205
|
+
if (FIT.isNotNumberStringDateOrBoolean(value)) {
|
|
206
|
+
return FIT.BaseTypeDefinitions[fieldDefinition.baseType].invalid;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Is this a numeric field?
|
|
210
|
+
if (FIT.NumericFieldTypes.includes(fieldDefinition.type)) {
|
|
211
|
+
if (!FIT.isNumeric(value)) {
|
|
212
|
+
throw new Error();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const scale = fieldDefinition.components.length > 1 ? 1 : fieldDefinition.scale;
|
|
216
|
+
const offset = fieldDefinition.components.length > 1 ? 0 : fieldDefinition.offset;
|
|
217
|
+
|
|
218
|
+
return (value + offset) * scale;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Is this a date_time field?
|
|
222
|
+
if (fieldDefinition.type === "dateTime") {
|
|
223
|
+
if (FIT.isDate(value)) {
|
|
224
|
+
return Utils.convertDateToDateTime(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!FIT.isNumeric(value)) {
|
|
228
|
+
throw new Error();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Is this a string field
|
|
235
|
+
if (fieldDefinition.type === "string") {
|
|
236
|
+
if (!FIT.isString(value)) {
|
|
237
|
+
throw new Error();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Must be a FIT type field
|
|
244
|
+
if (FIT.isNumeric(value)) {
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const profileType = Profile.types[fieldDefinition.type];
|
|
249
|
+
|
|
250
|
+
const [typeValue,] = Object.entries(profileType).find(([, typeValue,]) => {
|
|
251
|
+
return typeValue === value;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return typeValue;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Could not convert "${value}" to "${fieldDefinition.type}"`,
|
|
259
|
+
{ cause: { value, fieldDefinition, }, });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Creates a MesgDefinition from the mesgNum and mesg.
|
|
265
|
+
* @param {Number} mesgNum - The mesg number for this message
|
|
266
|
+
* @param {Object} [mesg] - The message data
|
|
267
|
+
* @return {MesgDefinition}
|
|
268
|
+
*/
|
|
269
|
+
#createMesgDefinition = (mesgNum, mesg) => {
|
|
270
|
+
const mesgDefinition = new MesgDefinition(mesgNum, mesg, { fieldDescriptions: this.#fieldDescriptions, });
|
|
271
|
+
mesgDefinition.localMesgNum = this.#lookupLocalMesgNum(mesgDefinition);
|
|
272
|
+
|
|
273
|
+
return mesgDefinition;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Searches the #localMesgDefinitions for a matching mesgDefinition
|
|
278
|
+
* @param {Object} mesgDefinition - the mesg definition to match
|
|
279
|
+
* @return The localMesgNum to be used with mesgDefinition
|
|
280
|
+
*/
|
|
281
|
+
#lookupLocalMesgNum = (mesgDefinition) => {
|
|
282
|
+
const localMesgNum = this.#localMesgDefinitions.findIndex((localMesgDefinition) => {
|
|
283
|
+
return localMesgDefinition?.equals(mesgDefinition) ?? false;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return (localMesgNum !== -1 ? localMesgNum : this.#nextLocalMesgNum++) & FIT.LOCAL_MESG_NUM_MASK;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Writes the mesgDefinition to the output stream, if it is not one of the currently active 16
|
|
291
|
+
* @param {Object} mesgDefinition - the mesg definition to match
|
|
292
|
+
* @return The localMesgNum to be used with mesgDefinition
|
|
293
|
+
*/
|
|
294
|
+
#writeMesgDefinitionIfNotActive = (mesgDefinition) => {
|
|
295
|
+
const localMesgNum = mesgDefinition.localMesgNum;
|
|
296
|
+
|
|
297
|
+
if (this.#localMesgDefinitions[localMesgNum] == null
|
|
298
|
+
|| !this.#localMesgDefinitions[localMesgNum].equals(mesgDefinition)) {
|
|
299
|
+
this.#writeMesgDefinition(mesgDefinition);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return localMesgNum;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Writes the mesgDefinition to the output stream
|
|
307
|
+
* @param {Object} mesgDefinition - the mesg definition to write
|
|
308
|
+
* @return {this}
|
|
309
|
+
*/
|
|
310
|
+
#writeMesgDefinition(mesgDefinition) {
|
|
311
|
+
mesgDefinition.write(this.#outputStream);
|
|
312
|
+
this.#localMesgDefinitions[mesgDefinition.localMesgNum] = mesgDefinition;
|
|
313
|
+
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#localMesgDefinitions = Array(16).fill(null);
|
|
318
|
+
#nextLocalMesgNum = 0;
|
|
319
|
+
#outputStream = new OutputStream();
|
|
320
|
+
#fieldDescriptions = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default Encoder;
|