fit-file-parser 6.0.2 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +55 -1
- package/dist/cjs/fit-parser.d.ts +3 -0
- package/dist/cjs/fit-parser.js +17 -1
- package/dist/cjs/fit_types.d.ts +1 -1
- package/dist/cjs/profile-lookup.d.ts +19 -0
- package/dist/cjs/profile-lookup.js +150 -0
- package/dist/cjs/profile.js +2 -0
- package/dist/cjs/raw-message-reader.d.ts +60 -0
- package/dist/cjs/raw-message-reader.js +377 -0
- package/dist/fit-parser.d.ts +3 -0
- package/dist/fit-parser.js +2 -0
- package/dist/fit_types.d.ts +1 -1
- package/dist/profile-lookup.d.ts +19 -0
- package/dist/profile-lookup.js +140 -0
- package/dist/profile.js +2 -0
- package/dist/raw-message-reader.d.ts +60 -0
- package/dist/raw-message-reader.js +368 -0
- package/package.json +29 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FitMessageReaderError = void 0;
|
|
4
|
+
exports.getFitBaseTypeId = getFitBaseTypeId;
|
|
5
|
+
exports.readFitUnsignedField = readFitUnsignedField;
|
|
6
|
+
exports.readFitStringField = readFitStringField;
|
|
7
|
+
exports.readFitMessages = readFitMessages;
|
|
8
|
+
exports.fitTimestampToUnixMilliseconds = fitTimestampToUnixMilliseconds;
|
|
9
|
+
const FIT_EPOCH_MS = 631065600000;
|
|
10
|
+
const FIT_BASE_TYPE_WIDTHS = new Map([
|
|
11
|
+
[0, 1],
|
|
12
|
+
[1, 1],
|
|
13
|
+
[2, 1],
|
|
14
|
+
[3, 2],
|
|
15
|
+
[4, 2],
|
|
16
|
+
[5, 4],
|
|
17
|
+
[6, 4],
|
|
18
|
+
[7, 1],
|
|
19
|
+
[8, 4],
|
|
20
|
+
[9, 8],
|
|
21
|
+
[10, 1],
|
|
22
|
+
[11, 2],
|
|
23
|
+
[12, 4],
|
|
24
|
+
[13, 1],
|
|
25
|
+
[14, 8],
|
|
26
|
+
[15, 8],
|
|
27
|
+
[16, 8],
|
|
28
|
+
]);
|
|
29
|
+
const FIT_UNSIGNED_SCALAR_BASE_TYPES = new Set([0, 2, 4, 6, 10, 11, 12, 13]);
|
|
30
|
+
const CRC_TABLE = [
|
|
31
|
+
0,
|
|
32
|
+
0xCC01,
|
|
33
|
+
0xD801,
|
|
34
|
+
0x1400,
|
|
35
|
+
0xF001,
|
|
36
|
+
0x3C00,
|
|
37
|
+
0x2800,
|
|
38
|
+
0xE401,
|
|
39
|
+
0xA001,
|
|
40
|
+
0x6C00,
|
|
41
|
+
0x7800,
|
|
42
|
+
0xB401,
|
|
43
|
+
0x5000,
|
|
44
|
+
0x9C01,
|
|
45
|
+
0x8801,
|
|
46
|
+
0x4400,
|
|
47
|
+
];
|
|
48
|
+
/** Error thrown when the strict raw-message reader rejects a FIT input. */
|
|
49
|
+
class FitMessageReaderError extends Error {
|
|
50
|
+
constructor(code) {
|
|
51
|
+
super(code);
|
|
52
|
+
this.name = 'FitMessageReaderError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.FitMessageReaderError = FitMessageReaderError;
|
|
57
|
+
function calculateCRC(bytes) {
|
|
58
|
+
let value = 0;
|
|
59
|
+
for (const byte of bytes) {
|
|
60
|
+
value = (value >>> 4) ^ CRC_TABLE[value & 15] ^ CRC_TABLE[byte & 15];
|
|
61
|
+
value = (value >>> 4) ^ CRC_TABLE[value & 15] ^ CRC_TABLE[byte >>> 4];
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
/** Returns the five-bit FIT base-type identifier, or null for reserved/unknown values. */
|
|
66
|
+
function getFitBaseTypeId(baseType) {
|
|
67
|
+
if (!Number.isInteger(baseType) || baseType < 0 || baseType > 0xFF) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (baseType & 0x60) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const id = baseType & 0x1F;
|
|
74
|
+
return FIT_BASE_TYPE_WIDTHS.has(id) ? id : null;
|
|
75
|
+
}
|
|
76
|
+
function isValidFieldDefinition(field) {
|
|
77
|
+
const baseTypeId = getFitBaseTypeId(field.baseType);
|
|
78
|
+
const width = baseTypeId === null
|
|
79
|
+
? undefined
|
|
80
|
+
: FIT_BASE_TYPE_WIDTHS.get(baseTypeId);
|
|
81
|
+
return width !== undefined
|
|
82
|
+
&& field.size > 0
|
|
83
|
+
&& (baseTypeId === 7 || field.size % width === 0);
|
|
84
|
+
}
|
|
85
|
+
/** Reads an unsigned one-, two-, or four-byte raw FIT field. */
|
|
86
|
+
function readFitUnsignedField(field, expectedBaseType, size, littleEndian) {
|
|
87
|
+
if (!field) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const expectedType = getFitBaseTypeId(expectedBaseType);
|
|
91
|
+
const actualType = getFitBaseTypeId(field.baseType);
|
|
92
|
+
const compatibleByte = size === 1
|
|
93
|
+
&& expectedType !== null
|
|
94
|
+
&& actualType !== null
|
|
95
|
+
&& [0, 2, 13].includes(expectedType)
|
|
96
|
+
&& [0, 2, 13].includes(actualType);
|
|
97
|
+
if (expectedType === null
|
|
98
|
+
|| actualType === null
|
|
99
|
+
|| !FIT_UNSIGNED_SCALAR_BASE_TYPES.has(expectedType)
|
|
100
|
+
|| FIT_BASE_TYPE_WIDTHS.get(expectedType) !== size
|
|
101
|
+
|| field.size !== size
|
|
102
|
+
|| !(field.bytes instanceof Uint8Array)
|
|
103
|
+
|| field.bytes.byteLength !== field.size
|
|
104
|
+
|| (actualType !== expectedType && !compatibleByte)) {
|
|
105
|
+
throw new TypeError('FIT field does not match the expected unsigned type');
|
|
106
|
+
}
|
|
107
|
+
const view = new DataView(field.bytes.buffer, field.bytes.byteOffset, field.bytes.byteLength);
|
|
108
|
+
const value = size === 1
|
|
109
|
+
? view.getUint8(0)
|
|
110
|
+
: size === 2
|
|
111
|
+
? view.getUint16(0, littleEndian)
|
|
112
|
+
: view.getUint32(0, littleEndian);
|
|
113
|
+
const invalid = [10, 11, 12].includes(expectedType)
|
|
114
|
+
? 0
|
|
115
|
+
: size === 1
|
|
116
|
+
? 0xFF
|
|
117
|
+
: size === 2
|
|
118
|
+
? 0xFFFF
|
|
119
|
+
: 0xFFFFFFFF;
|
|
120
|
+
return value === invalid ? undefined : value;
|
|
121
|
+
}
|
|
122
|
+
/** Reads UTF-8 bytes, removes trailing NUL padding, and preserves interior NUL separators. */
|
|
123
|
+
function readFitStringField(field) {
|
|
124
|
+
if (!field) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (getFitBaseTypeId(field.baseType) !== 7
|
|
128
|
+
|| field.size <= 0
|
|
129
|
+
|| !(field.bytes instanceof Uint8Array)
|
|
130
|
+
|| field.bytes.byteLength !== field.size) {
|
|
131
|
+
throw new TypeError('FIT field is not a string');
|
|
132
|
+
}
|
|
133
|
+
const value = new TextDecoder('utf-8', {
|
|
134
|
+
fatal: true,
|
|
135
|
+
ignoreBOM: true,
|
|
136
|
+
}).decode(field.bytes).replace(/\0+$/, '');
|
|
137
|
+
return value || undefined;
|
|
138
|
+
}
|
|
139
|
+
function readTimestamp(field, bytes, littleEndian) {
|
|
140
|
+
if (field.size !== 4 || (field.baseType & 0x1F) !== 6) {
|
|
141
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
142
|
+
}
|
|
143
|
+
const value = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, littleEndian);
|
|
144
|
+
return value === 0xFFFFFFFF ? undefined : value;
|
|
145
|
+
}
|
|
146
|
+
function validateOptions(options) {
|
|
147
|
+
var _a;
|
|
148
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
149
|
+
throw new FitMessageReaderError('invalid_input');
|
|
150
|
+
}
|
|
151
|
+
const maxInputBytes = (_a = options.maxInputBytes) !== null && _a !== void 0 ? _a : Number.POSITIVE_INFINITY;
|
|
152
|
+
if (maxInputBytes !== Number.POSITIVE_INFINITY
|
|
153
|
+
&& (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 0)) {
|
|
154
|
+
throw new FitMessageReaderError('invalid_input');
|
|
155
|
+
}
|
|
156
|
+
if (options.messageNumbers === undefined) {
|
|
157
|
+
return { maxInputBytes };
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(options.messageNumbers)) {
|
|
160
|
+
throw new FitMessageReaderError('invalid_input');
|
|
161
|
+
}
|
|
162
|
+
const selectedMessages = new Set();
|
|
163
|
+
for (const messageNumber of options.messageNumbers) {
|
|
164
|
+
if (!Number.isSafeInteger(messageNumber)
|
|
165
|
+
|| messageNumber < 0
|
|
166
|
+
|| messageNumber > 0xFFFF) {
|
|
167
|
+
throw new FitMessageReaderError('invalid_input');
|
|
168
|
+
}
|
|
169
|
+
selectedMessages.add(messageNumber);
|
|
170
|
+
}
|
|
171
|
+
return { maxInputBytes, selectedMessages };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Strictly validates a FIT file and retains raw fields for selected messages.
|
|
175
|
+
* This entry point does not load the semantic FIT profile or format field values.
|
|
176
|
+
*/
|
|
177
|
+
function readFitMessages(input, options = {}) {
|
|
178
|
+
var _a;
|
|
179
|
+
const { maxInputBytes, selectedMessages } = validateOptions(options);
|
|
180
|
+
if (!(input instanceof ArrayBuffer) && !(input instanceof Uint8Array)) {
|
|
181
|
+
throw new FitMessageReaderError('invalid_input');
|
|
182
|
+
}
|
|
183
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
184
|
+
if (bytes.length > maxInputBytes) {
|
|
185
|
+
throw new FitMessageReaderError('input_limit');
|
|
186
|
+
}
|
|
187
|
+
if (bytes.length < 14) {
|
|
188
|
+
throw new FitMessageReaderError('invalid_header');
|
|
189
|
+
}
|
|
190
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
191
|
+
const headerSize = bytes[0];
|
|
192
|
+
if (headerSize !== 12 && headerSize !== 14) {
|
|
193
|
+
throw new FitMessageReaderError('invalid_header');
|
|
194
|
+
}
|
|
195
|
+
const dataEnd = headerSize + view.getUint32(4, true);
|
|
196
|
+
const protocolMajorVersion = bytes[1] >>> 4;
|
|
197
|
+
if ((protocolMajorVersion !== 1 && protocolMajorVersion !== 2)
|
|
198
|
+
|| bytes[8] !== 0x2E
|
|
199
|
+
|| bytes[9] !== 0x46
|
|
200
|
+
|| bytes[10] !== 0x49
|
|
201
|
+
|| bytes[11] !== 0x54
|
|
202
|
+
|| dataEnd + 2 !== bytes.length) {
|
|
203
|
+
throw new FitMessageReaderError('invalid_header');
|
|
204
|
+
}
|
|
205
|
+
if (headerSize === 14
|
|
206
|
+
&& view.getUint16(12, true) !== 0
|
|
207
|
+
&& calculateCRC(bytes.subarray(0, 12)) !== view.getUint16(12, true)) {
|
|
208
|
+
throw new FitMessageReaderError('invalid_crc');
|
|
209
|
+
}
|
|
210
|
+
if (calculateCRC(bytes.subarray(0, dataEnd)) !== view.getUint16(dataEnd, true)) {
|
|
211
|
+
throw new FitMessageReaderError('invalid_crc');
|
|
212
|
+
}
|
|
213
|
+
const definitions = new Map();
|
|
214
|
+
const messageIndexes = new Map();
|
|
215
|
+
const messages = [];
|
|
216
|
+
const issues = [];
|
|
217
|
+
let cursor = headerSize;
|
|
218
|
+
let lastTimestamp;
|
|
219
|
+
const take = (size) => {
|
|
220
|
+
if (cursor + size > dataEnd) {
|
|
221
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
222
|
+
}
|
|
223
|
+
const value = bytes.subarray(cursor, cursor + size);
|
|
224
|
+
cursor += size;
|
|
225
|
+
return value;
|
|
226
|
+
};
|
|
227
|
+
// `take` owns bounds checking and advances the shared cursor for every record component.
|
|
228
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
229
|
+
while (cursor < dataEnd) {
|
|
230
|
+
const recordHeader = take(1)[0];
|
|
231
|
+
const compressed = (recordHeader & 0x80) !== 0;
|
|
232
|
+
const localMessageNumber = compressed
|
|
233
|
+
? (recordHeader >> 5) & 3
|
|
234
|
+
: recordHeader & 15;
|
|
235
|
+
if (!compressed && (recordHeader & 0x10) !== 0) {
|
|
236
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
237
|
+
}
|
|
238
|
+
if (!compressed && (recordHeader & 0x40) !== 0) {
|
|
239
|
+
const definitionHeader = take(5);
|
|
240
|
+
if (definitionHeader[0] !== 0 || definitionHeader[1] > 1) {
|
|
241
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
242
|
+
}
|
|
243
|
+
const littleEndian = definitionHeader[1] === 0;
|
|
244
|
+
const globalMessageNumber = littleEndian
|
|
245
|
+
? definitionHeader[2] | (definitionHeader[3] << 8)
|
|
246
|
+
: (definitionHeader[2] << 8) | definitionHeader[3];
|
|
247
|
+
const selected = selectedMessages === undefined
|
|
248
|
+
|| selectedMessages.has(globalMessageNumber);
|
|
249
|
+
const readFields = (count) => {
|
|
250
|
+
const fields = [];
|
|
251
|
+
const fieldNumbers = new Set();
|
|
252
|
+
for (let index = 0; index < count; index++) {
|
|
253
|
+
const raw = take(3);
|
|
254
|
+
const field = {
|
|
255
|
+
fieldNumber: raw[0],
|
|
256
|
+
size: raw[1],
|
|
257
|
+
baseType: raw[2],
|
|
258
|
+
};
|
|
259
|
+
if (fieldNumbers.has(field.fieldNumber)
|
|
260
|
+
|| field.size === 0
|
|
261
|
+
|| (selected && !isValidFieldDefinition(field))) {
|
|
262
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
263
|
+
}
|
|
264
|
+
fieldNumbers.add(field.fieldNumber);
|
|
265
|
+
fields.push(field);
|
|
266
|
+
}
|
|
267
|
+
return fields;
|
|
268
|
+
};
|
|
269
|
+
const fields = readFields(definitionHeader[4]);
|
|
270
|
+
const developerFields = [];
|
|
271
|
+
if ((recordHeader & 0x20) !== 0) {
|
|
272
|
+
const developerFieldKeys = new Set();
|
|
273
|
+
const count = take(1)[0];
|
|
274
|
+
for (let index = 0; index < count; index++) {
|
|
275
|
+
const raw = take(3);
|
|
276
|
+
const key = `${raw[2]}:${raw[0]}`;
|
|
277
|
+
if (raw[1] === 0 || developerFieldKeys.has(key)) {
|
|
278
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
279
|
+
}
|
|
280
|
+
developerFieldKeys.add(key);
|
|
281
|
+
developerFields.push({
|
|
282
|
+
fieldNumber: raw[0],
|
|
283
|
+
size: raw[1],
|
|
284
|
+
developerDataIndex: raw[2],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
definitions.set(localMessageNumber, {
|
|
289
|
+
globalMessageNumber,
|
|
290
|
+
littleEndian,
|
|
291
|
+
fields,
|
|
292
|
+
developerFields,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!compressed && (recordHeader & 0x20) !== 0) {
|
|
297
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
298
|
+
}
|
|
299
|
+
const definition = definitions.get(localMessageNumber);
|
|
300
|
+
if (!definition) {
|
|
301
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
302
|
+
}
|
|
303
|
+
const messageIndex = (_a = messageIndexes.get(definition.globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
304
|
+
messageIndexes.set(definition.globalMessageNumber, messageIndex + 1);
|
|
305
|
+
const selected = selectedMessages === undefined
|
|
306
|
+
|| selectedMessages.has(definition.globalMessageNumber);
|
|
307
|
+
let timestamp;
|
|
308
|
+
let compressedTimestamp;
|
|
309
|
+
if (compressed) {
|
|
310
|
+
const timestampField = definition.fields[0];
|
|
311
|
+
if (!timestampField
|
|
312
|
+
|| timestampField.fieldNumber !== 253
|
|
313
|
+
|| timestampField.baseType !== 0x86
|
|
314
|
+
|| timestampField.size !== 4
|
|
315
|
+
|| lastTimestamp === undefined) {
|
|
316
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
317
|
+
}
|
|
318
|
+
timestamp = Math.floor(lastTimestamp / 32) * 32 + (recordHeader & 31);
|
|
319
|
+
if (timestamp < lastTimestamp) {
|
|
320
|
+
timestamp += 32;
|
|
321
|
+
}
|
|
322
|
+
if (timestamp >= 0xFFFFFFFF) {
|
|
323
|
+
throw new FitMessageReaderError('invalid_structure');
|
|
324
|
+
}
|
|
325
|
+
lastTimestamp = timestamp;
|
|
326
|
+
compressedTimestamp = timestamp;
|
|
327
|
+
}
|
|
328
|
+
const fields = [];
|
|
329
|
+
for (const field of definition.fields) {
|
|
330
|
+
if (compressed && field.fieldNumber === 253) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const raw = take(field.size);
|
|
334
|
+
if (field.fieldNumber === 253) {
|
|
335
|
+
try {
|
|
336
|
+
timestamp = readTimestamp(field, raw, definition.littleEndian);
|
|
337
|
+
lastTimestamp = timestamp;
|
|
338
|
+
}
|
|
339
|
+
catch (_b) {
|
|
340
|
+
lastTimestamp = undefined;
|
|
341
|
+
issues.push({
|
|
342
|
+
code: 'invalid_timestamp',
|
|
343
|
+
globalMessageNumber: definition.globalMessageNumber,
|
|
344
|
+
messageIndex,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (selected) {
|
|
349
|
+
fields.push(Object.assign(Object.assign({}, field), { bytes: raw.slice() }));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const developerFields = [];
|
|
353
|
+
for (const field of definition.developerFields) {
|
|
354
|
+
const raw = take(field.size);
|
|
355
|
+
if (selected) {
|
|
356
|
+
developerFields.push(Object.assign(Object.assign({}, field), { bytes: raw.slice() }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (selected) {
|
|
360
|
+
messages.push(Object.assign(Object.assign(Object.assign({ globalMessageNumber: definition.globalMessageNumber, messageIndex, littleEndian: definition.littleEndian }, (timestamp === undefined ? {} : { timestamp })), (compressedTimestamp === undefined ? {} : { compressedTimestamp })), { fields,
|
|
361
|
+
developerFields }));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
protocolVersion: bytes[1],
|
|
366
|
+
profileVersion: view.getUint16(2, true),
|
|
367
|
+
messages,
|
|
368
|
+
issues,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Converts a native FIT timestamp to a JavaScript epoch millisecond value. */
|
|
372
|
+
function fitTimestampToUnixMilliseconds(timestamp) {
|
|
373
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 0xFFFFFFFF) {
|
|
374
|
+
throw new RangeError('FIT timestamp must be an integer between 0 and 4294967294');
|
|
375
|
+
}
|
|
376
|
+
return FIT_EPOCH_MS + timestamp * 1000;
|
|
377
|
+
}
|
package/dist/fit-parser.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { ParsedFit } from './fit_types.js';
|
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
4
|
export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
|
|
5
5
|
export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
|
|
6
|
+
export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
|
|
7
|
+
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
8
|
+
export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
|
|
6
9
|
export interface FitParserOptions {
|
|
7
10
|
force?: boolean;
|
|
8
11
|
speedUnit?: string;
|
package/dist/fit-parser.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { calculateCRC, readRecord } from './binary.js';
|
|
2
2
|
import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
|
|
3
3
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
|
+
export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
|
|
5
|
+
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
4
6
|
export default class FitParser {
|
|
5
7
|
constructor(options = {}) {
|
|
6
8
|
var _a, _b, _c, _d;
|
package/dist/fit_types.d.ts
CHANGED
|
@@ -213,7 +213,7 @@ export type SportEvent = 'uncategorized' | 'geocaching' | 'fitness' | 'recreatio
|
|
|
213
213
|
export type SquatExerciseName = 'leg_press' | 'back_squat_with_body_bar' | 'back_squats' | 'weighted_back_squats' | 'balancing_squat' | 'weighted_balancing_squat' | 'barbell_back_squat' | 'barbell_box_squat' | 'barbell_front_squat' | 'barbell_hack_squat' | 'barbell_hang_squat_snatch' | 'barbell_lateral_step_up' | 'barbell_quarter_squat' | 'barbell_siff_squat' | 'barbell_squat_snatch' | 'barbell_squat_with_heels_raised' | 'barbell_stepover' | 'barbell_step_up' | 'bench_squat_with_rotational_chop' | 'weighted_bench_squat_with_rotational_chop' | 'body_weight_wall_squat' | 'weighted_wall_squat' | 'box_step_squat' | 'weighted_box_step_squat' | 'braced_squat' | 'crossed_arm_barbell_front_squat' | 'crossover_dumbbell_step_up' | 'dumbbell_front_squat' | 'dumbbell_split_squat' | 'dumbbell_squat' | 'dumbbell_squat_clean' | 'dumbbell_stepover' | 'dumbbell_step_up' | 'elevated_single_leg_squat' | 'weighted_elevated_single_leg_squat' | 'figure_four_squats' | 'weighted_figure_four_squats' | 'goblet_squat' | 'kettlebell_squat' | 'kettlebell_swing_overhead' | 'kettlebell_swing_with_flip_to_squat' | 'lateral_dumbbell_step_up' | 'one_legged_squat' | 'overhead_dumbbell_squat' | 'overhead_squat' | 'partial_single_leg_squat' | 'weighted_partial_single_leg_squat' | 'pistol_squat' | 'weighted_pistol_squat' | 'plie_slides' | 'weighted_plie_slides' | 'plie_squat' | 'weighted_plie_squat' | 'prisoner_squat' | 'weighted_prisoner_squat' | 'single_leg_bench_get_up' | 'weighted_single_leg_bench_get_up' | 'single_leg_bench_squat' | 'weighted_single_leg_bench_squat' | 'single_leg_squat_on_swiss_ball' | 'weighted_single_leg_squat_on_swiss_ball' | 'squat' | 'weighted_squat' | 'squats_with_band' | 'staggered_squat' | 'weighted_staggered_squat' | 'step_up' | 'weighted_step_up' | 'suitcase_squats' | 'sumo_squat' | 'sumo_squat_slide_in' | 'weighted_sumo_squat_slide_in' | 'sumo_squat_to_high_pull' | 'sumo_squat_to_stand' | 'weighted_sumo_squat_to_stand' | 'sumo_squat_with_rotation' | 'weighted_sumo_squat_with_rotation' | 'swiss_ball_body_weight_wall_squat' | 'weighted_swiss_ball_wall_squat' | 'thrusters' | 'uneven_squat' | 'weighted_uneven_squat' | 'waist_slimming_squat' | 'wall_ball' | 'wide_stance_barbell_squat' | 'wide_stance_goblet_squat' | 'zercher_squat' | 'kbs_overhead' | 'squat_and_side_kick' | 'squat_jumps_in_n_out' | 'pilates_plie_squats_parallel_turned_out_flat_and_heels' | 'releve_straight_leg_and_knee_bent_with_one_leg_variation' | 'alternating_box_dumbbell_step_ups' | 'dumbbell_overhead_squat_single_arm' | 'dumbbell_squat_snatch' | 'medicine_ball_squat' | 'wall_ball_squat_and_press' | 'squat_american_swing' | 'air_squat' | 'dumbbell_thrusters' | 'overhead_barbell_squat' | number;
|
|
214
214
|
export type StairStepperExerciseName = 'stair_stepper' | number;
|
|
215
215
|
export type StrokeType = 'no_event' | 'other' | 'serve' | 'forehand' | 'backhand' | 'smash' | number;
|
|
216
|
-
export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'all' | number;
|
|
216
|
+
export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'mountain_enduro' | 'mountain_downhill' | 'all' | number;
|
|
217
217
|
export type SupportedExdScreenLayouts = 'full_screen' | 'half_vertical' | 'half_horizontal' | 'half_vertical_right_split' | 'half_horizontal_bottom_split' | 'full_quarter_split' | 'half_vertical_left_split' | 'half_horizontal_top_split' | number;
|
|
218
218
|
export type SuspensionExerciseName = 'chest_fly' | 'chest_press' | 'crunch' | 'curl' | 'dip' | 'face_pull' | 'glute_bridge' | 'hamstring_curl' | 'hip_drop' | 'inverted_row' | 'knee_drive_jump' | 'knee_to_chest' | 'lat_pullover' | 'lunge' | 'mountain_climber' | 'pendulum' | 'pike' | 'plank' | 'power_pull' | 'pull_up' | 'push_up' | 'reverse_mountain_climber' | 'reverse_plank' | 'rollout' | 'row' | 'side_lunge' | 'side_plank' | 'single_leg_deadlift' | 'single_leg_squat' | 'sit_up' | 'split' | 'squat' | 'squat_jump' | 'tricep_press' | 'y_fly' | number;
|
|
219
219
|
export type SwimStroke = 'freestyle' | 'backstroke' | 'breaststroke' | 'butterfly' | 'drill' | 'mixed' | 'im' | 'im_by_round' | 'rimo' | number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Resolves a FIT manufacturer identifier to its canonical profile name. */
|
|
2
|
+
export declare function getFitManufacturerName(value: number | string | null | undefined): string | null;
|
|
3
|
+
/** Resolves a Garmin product identifier to its canonical profile name. */
|
|
4
|
+
export declare function getFitGarminProductName(value: number | string | null | undefined): string | null;
|
|
5
|
+
/** Resolves a FIT sport identifier to its canonical profile name. */
|
|
6
|
+
export declare function getFitSportName(value: number | string | null | undefined): string | null;
|
|
7
|
+
/** Resolves a FIT sub-sport identifier to its canonical profile name. */
|
|
8
|
+
export declare function getFitSubSportName(value: number | string | null | undefined): string | null;
|
|
9
|
+
/** Resolves a FIT sport name to its numeric profile identifier. */
|
|
10
|
+
export declare function getFitSportId(value: string | null | undefined): number | null;
|
|
11
|
+
/** Resolves a FIT sub-sport name to its numeric profile identifier. */
|
|
12
|
+
export declare function getFitSubSportId(value: string | null | undefined): number | null;
|
|
13
|
+
/** Resolves a FIT course-point name to its numeric profile identifier. */
|
|
14
|
+
export declare function getFitCoursePointId(value: string | null | undefined): number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a Garmin product identifier to a human-readable device name.
|
|
17
|
+
* This does not alter parsed `product` or `product_name` fields.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getFitGarminProductDisplayName(value: number | string | null | undefined): string | null;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { PROFILE_TYPES } from './profile.js';
|
|
2
|
+
function normalizeProfileName(value) {
|
|
3
|
+
return value.trim().toLowerCase().replace(/[\s_-]/g, '');
|
|
4
|
+
}
|
|
5
|
+
const FIT_PROFILE_MANUFACTURERS = PROFILE_TYPES.manufacturer;
|
|
6
|
+
const FIT_PROFILE_GARMIN_PRODUCTS = PROFILE_TYPES.garmin_product;
|
|
7
|
+
const FIT_PROFILE_SPORTS = PROFILE_TYPES.sport;
|
|
8
|
+
const FIT_PROFILE_SUB_SPORTS = PROFILE_TYPES.sub_sport;
|
|
9
|
+
const FIT_PROFILE_COURSE_POINTS = PROFILE_TYPES.course_point;
|
|
10
|
+
function createProfileIdMap(mapping) {
|
|
11
|
+
return new Map(Object.entries(mapping)
|
|
12
|
+
.filter((entry) => typeof entry[1] === 'string')
|
|
13
|
+
.map(([id, name]) => [normalizeProfileName(name), Number(id)]));
|
|
14
|
+
}
|
|
15
|
+
const FIT_PROFILE_SPORT_IDS = createProfileIdMap(FIT_PROFILE_SPORTS);
|
|
16
|
+
const FIT_PROFILE_SUB_SPORT_IDS = createProfileIdMap(FIT_PROFILE_SUB_SPORTS);
|
|
17
|
+
const FIT_PROFILE_COURSE_POINT_IDS = createProfileIdMap(FIT_PROFILE_COURSE_POINTS);
|
|
18
|
+
function getProfileName(mapping, value) {
|
|
19
|
+
if (value === null || value === undefined) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const normalizedValue = typeof value === 'string' ? value.trim() : value;
|
|
26
|
+
if (normalizedValue === '' || (typeof normalizedValue === 'string' && !/^\d+$/.test(normalizedValue))) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const id = Number(normalizedValue);
|
|
30
|
+
if (!Number.isSafeInteger(id) || id < 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const name = mapping[id];
|
|
34
|
+
return typeof name === 'string' ? name : null;
|
|
35
|
+
}
|
|
36
|
+
function getProfileId(mapping, value) {
|
|
37
|
+
var _a;
|
|
38
|
+
if (typeof value !== 'string') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const name = normalizeProfileName(value);
|
|
42
|
+
if (name === '') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return (_a = mapping.get(name)) !== null && _a !== void 0 ? _a : null;
|
|
46
|
+
}
|
|
47
|
+
/** Resolves a FIT manufacturer identifier to its canonical profile name. */
|
|
48
|
+
export function getFitManufacturerName(value) {
|
|
49
|
+
return getProfileName(FIT_PROFILE_MANUFACTURERS, value);
|
|
50
|
+
}
|
|
51
|
+
/** Resolves a Garmin product identifier to its canonical profile name. */
|
|
52
|
+
export function getFitGarminProductName(value) {
|
|
53
|
+
return getProfileName(FIT_PROFILE_GARMIN_PRODUCTS, value);
|
|
54
|
+
}
|
|
55
|
+
/** Resolves a FIT sport identifier to its canonical profile name. */
|
|
56
|
+
export function getFitSportName(value) {
|
|
57
|
+
return getProfileName(FIT_PROFILE_SPORTS, value);
|
|
58
|
+
}
|
|
59
|
+
/** Resolves a FIT sub-sport identifier to its canonical profile name. */
|
|
60
|
+
export function getFitSubSportName(value) {
|
|
61
|
+
return getProfileName(FIT_PROFILE_SUB_SPORTS, value);
|
|
62
|
+
}
|
|
63
|
+
/** Resolves a FIT sport name to its numeric profile identifier. */
|
|
64
|
+
export function getFitSportId(value) {
|
|
65
|
+
return getProfileId(FIT_PROFILE_SPORT_IDS, value);
|
|
66
|
+
}
|
|
67
|
+
/** Resolves a FIT sub-sport name to its numeric profile identifier. */
|
|
68
|
+
export function getFitSubSportId(value) {
|
|
69
|
+
return getProfileId(FIT_PROFILE_SUB_SPORT_IDS, value);
|
|
70
|
+
}
|
|
71
|
+
/** Resolves a FIT course-point name to its numeric profile identifier. */
|
|
72
|
+
export function getFitCoursePointId(value) {
|
|
73
|
+
return getProfileId(FIT_PROFILE_COURSE_POINT_IDS, value);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolves a Garmin product identifier to a human-readable device name.
|
|
77
|
+
* This does not alter parsed `product` or `product_name` fields.
|
|
78
|
+
*/
|
|
79
|
+
export function getFitGarminProductDisplayName(value) {
|
|
80
|
+
const name = getFitGarminProductName(value);
|
|
81
|
+
if (!name) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
// Preserve the established display spelling for the optical heart-rate product.
|
|
85
|
+
const displaySource = name === 'o_hr' ? 'o_h_r' : name;
|
|
86
|
+
const formatted = displaySource
|
|
87
|
+
.replace(/^fr(\d+)/i, 'Forerunner $1')
|
|
88
|
+
.replace(/^fenix(\d+)/i, 'Fenix $1')
|
|
89
|
+
.replace(/^edge(\d+)/i, 'Edge $1')
|
|
90
|
+
.replace(/^vivoactive/i, 'VivoActive')
|
|
91
|
+
.replace(/^vivosmart/i, 'VivoSmart')
|
|
92
|
+
.replace(/^vivofit/i, 'VivoFit')
|
|
93
|
+
.replace(/^vivomove/i, 'VivoMove')
|
|
94
|
+
.replace(/^vivosport/i, 'VivoSport')
|
|
95
|
+
.replace(/^approach([A-Z\d])/i, 'Approach $1')
|
|
96
|
+
.replace(/^marq([A-Z])/i, 'Marq $1')
|
|
97
|
+
.replace(/^hrm/i, 'HRM ')
|
|
98
|
+
.replace(/_/g, ' ')
|
|
99
|
+
.replace(/([a-z])([A-Z0-9])/g, '$1 $2')
|
|
100
|
+
.replace(/(\d)([a-z])/gi, '$1 $2');
|
|
101
|
+
return formatted
|
|
102
|
+
.split(' ')
|
|
103
|
+
.map((word) => {
|
|
104
|
+
const lower = word.toLowerCase();
|
|
105
|
+
if (lower === 'apac')
|
|
106
|
+
return 'APAC';
|
|
107
|
+
if (lower === 'xt')
|
|
108
|
+
return 'XT';
|
|
109
|
+
if (lower === 'lte')
|
|
110
|
+
return 'LTE';
|
|
111
|
+
if (lower === 'hr')
|
|
112
|
+
return 'HR';
|
|
113
|
+
if (lower === 'gps')
|
|
114
|
+
return 'GPS';
|
|
115
|
+
if (lower === 'mtb')
|
|
116
|
+
return 'MTB';
|
|
117
|
+
if (lower === 'ii')
|
|
118
|
+
return 'II';
|
|
119
|
+
if (lower === 'iii')
|
|
120
|
+
return 'III';
|
|
121
|
+
if (lower === 'm' && name.toLowerCase().includes('645m'))
|
|
122
|
+
return 'Music';
|
|
123
|
+
if (lower === 'jpn')
|
|
124
|
+
return 'Japan';
|
|
125
|
+
if (lower === 'chn')
|
|
126
|
+
return 'China';
|
|
127
|
+
if (lower === 'twn')
|
|
128
|
+
return 'Taiwan';
|
|
129
|
+
if (lower === 'kor')
|
|
130
|
+
return 'Korea';
|
|
131
|
+
if (lower === 'rus')
|
|
132
|
+
return 'Russia';
|
|
133
|
+
if (lower === 'sea')
|
|
134
|
+
return 'SEA';
|
|
135
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
136
|
+
})
|
|
137
|
+
.join(' ')
|
|
138
|
+
.replace(/Vivo Active/g, 'VivoActive')
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
package/dist/profile.js
CHANGED
|
@@ -17476,6 +17476,8 @@ export const PROFILE_TYPES = {
|
|
|
17476
17476
|
125: 'rally',
|
|
17477
17477
|
126: 'pool_triathlon',
|
|
17478
17478
|
127: 'e_bike_enduro',
|
|
17479
|
+
153: 'mountain_enduro',
|
|
17480
|
+
154: 'mountain_downhill',
|
|
17479
17481
|
254: 'all',
|
|
17480
17482
|
},
|
|
17481
17483
|
supported_exd_screen_layouts: {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type FitMessageReaderErrorCode = 'invalid_input' | 'input_limit' | 'invalid_header' | 'invalid_crc' | 'invalid_structure';
|
|
2
|
+
export type FitMessageReaderIssueCode = 'invalid_timestamp';
|
|
3
|
+
export interface FitRawField {
|
|
4
|
+
fieldNumber: number;
|
|
5
|
+
size: number;
|
|
6
|
+
baseType: number;
|
|
7
|
+
bytes: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
export interface FitRawDeveloperField {
|
|
10
|
+
fieldNumber: number;
|
|
11
|
+
size: number;
|
|
12
|
+
developerDataIndex: number;
|
|
13
|
+
bytes: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
export interface FitRawMessage {
|
|
16
|
+
globalMessageNumber: number;
|
|
17
|
+
messageIndex: number;
|
|
18
|
+
littleEndian: boolean;
|
|
19
|
+
/** Native FIT timestamp in seconds since the FIT epoch, when available. */
|
|
20
|
+
timestamp?: number;
|
|
21
|
+
/** Reconstructed native timestamp when the record used a compressed header. */
|
|
22
|
+
compressedTimestamp?: number;
|
|
23
|
+
fields: FitRawField[];
|
|
24
|
+
developerFields: FitRawDeveloperField[];
|
|
25
|
+
}
|
|
26
|
+
export interface FitMessageReaderIssue {
|
|
27
|
+
code: FitMessageReaderIssueCode;
|
|
28
|
+
globalMessageNumber: number;
|
|
29
|
+
messageIndex: number;
|
|
30
|
+
}
|
|
31
|
+
export interface FitMessageReaderResult {
|
|
32
|
+
protocolVersion: number;
|
|
33
|
+
profileVersion: number;
|
|
34
|
+
messages: FitRawMessage[];
|
|
35
|
+
issues: FitMessageReaderIssue[];
|
|
36
|
+
}
|
|
37
|
+
export interface FitMessageReaderOptions {
|
|
38
|
+
/** Retain only these global message numbers. Omit to retain every message. */
|
|
39
|
+
messageNumbers?: readonly number[];
|
|
40
|
+
/** Reject larger inputs before allocating retained message data. */
|
|
41
|
+
maxInputBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Error thrown when the strict raw-message reader rejects a FIT input. */
|
|
44
|
+
export declare class FitMessageReaderError extends Error {
|
|
45
|
+
readonly code: FitMessageReaderErrorCode;
|
|
46
|
+
constructor(code: FitMessageReaderErrorCode);
|
|
47
|
+
}
|
|
48
|
+
/** Returns the five-bit FIT base-type identifier, or null for reserved/unknown values. */
|
|
49
|
+
export declare function getFitBaseTypeId(baseType: number): number | null;
|
|
50
|
+
/** Reads an unsigned one-, two-, or four-byte raw FIT field. */
|
|
51
|
+
export declare function readFitUnsignedField(field: FitRawField | undefined, expectedBaseType: number, size: 1 | 2 | 4, littleEndian: boolean): number | undefined;
|
|
52
|
+
/** Reads UTF-8 bytes, removes trailing NUL padding, and preserves interior NUL separators. */
|
|
53
|
+
export declare function readFitStringField(field: FitRawField | undefined): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Strictly validates a FIT file and retains raw fields for selected messages.
|
|
56
|
+
* This entry point does not load the semantic FIT profile or format field values.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readFitMessages(input: ArrayBuffer | Uint8Array, options?: FitMessageReaderOptions): FitMessageReaderResult;
|
|
59
|
+
/** Converts a native FIT timestamp to a JavaScript epoch millisecond value. */
|
|
60
|
+
export declare function fitTimestampToUnixMilliseconds(timestamp: number): number;
|