fit-file-parser 3.0.1 → 3.1.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/CHANGELOG.md +5 -0
- package/README.md +25 -0
- package/dist/cjs/fit-encoder.d.ts +68 -0
- package/dist/cjs/fit-encoder.js +354 -0
- package/dist/cjs/fit-parser.d.ts +2 -1
- package/dist/cjs/fit-parser.js +4 -0
- package/dist/cjs/fit.js +28 -0
- package/dist/cjs/fit_types.d.ts +1 -1
- package/dist/fit-encoder.d.ts +68 -0
- package/dist/fit-encoder.js +350 -0
- package/dist/fit-parser.d.ts +2 -1
- package/dist/fit-parser.js +1 -0
- package/dist/fit.js +28 -0
- package/dist/fit_types.d.ts +1 -1
- package/package.json +9 -2
- package/.agent/rules/fit-parser-dev.md +0 -35
- package/.agent/skills/add-fit-message/SKILL.md +0 -29
- package/.agent/workflows/fit-workflows.md +0 -28
- package/.editorconfig +0 -3
- package/.github/workflows/ci.yml +0 -40
- package/.vscode/extensions.json +0 -6
- package/.vscode/launch.json +0 -27
- package/.vscode/tasks.json +0 -34
- package/INVESTIGATING.md +0 -50
- package/check_ids.js +0 -24
- package/check_jump_fields.js +0 -46
- package/codegen/codegen.ts +0 -9
- package/eslint.config.js +0 -20
- package/find_field.js +0 -23
- package/find_field_esm.js +0 -28
- package/inspect_parser.ts +0 -34
- package/output.txt +0 -154
- package/scan_jumps.js +0 -29
- package/scripts/deep_probe.js +0 -74
- package/scripts/inspect_fit.js +0 -69
- package/tsconfig.cjs.json +0 -8
- package/tsconfig.json +0 -16
- package/vitest.config.js +0 -8
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const FIT_HEADER_SIZE = 14;
|
|
2
|
+
const FIT_EPOCH_MS = 631065600000;
|
|
3
|
+
const UINT8_MAX = 0xFF;
|
|
4
|
+
const UINT16_MAX = 0xFFFF;
|
|
5
|
+
const UINT32_MAX = 0xFFFFFFFF;
|
|
6
|
+
const BIGINT_ZERO = BigInt(0);
|
|
7
|
+
const BIGINT_EIGHT = BigInt(8);
|
|
8
|
+
const BIGINT_BYTE_MASK = BigInt(0xFF);
|
|
9
|
+
const UINT64_MAX = BigInt('18446744073709551615');
|
|
10
|
+
const UINT64_MODULUS = BigInt('18446744073709551616');
|
|
11
|
+
const SINT8_MIN = -0x80;
|
|
12
|
+
const SINT8_MAX = 0x7F;
|
|
13
|
+
const SINT16_MIN = -0x8000;
|
|
14
|
+
const SINT16_MAX = 0x7FFF;
|
|
15
|
+
const SINT32_MIN = -0x80000000;
|
|
16
|
+
const SINT32_MAX = 0x7FFFFFFF;
|
|
17
|
+
const SINT64_MIN = BigInt('-9223372036854775808');
|
|
18
|
+
const SINT64_MAX = BigInt('9223372036854775807');
|
|
19
|
+
/** FIT definition base-type bytes, including the endian flag where required. */
|
|
20
|
+
export var FitBaseType;
|
|
21
|
+
(function (FitBaseType) {
|
|
22
|
+
FitBaseType[FitBaseType["Enum"] = 0] = "Enum";
|
|
23
|
+
FitBaseType[FitBaseType["Sint8"] = 1] = "Sint8";
|
|
24
|
+
FitBaseType[FitBaseType["Uint8"] = 2] = "Uint8";
|
|
25
|
+
FitBaseType[FitBaseType["String"] = 7] = "String";
|
|
26
|
+
FitBaseType[FitBaseType["Uint8z"] = 10] = "Uint8z";
|
|
27
|
+
FitBaseType[FitBaseType["Byte"] = 13] = "Byte";
|
|
28
|
+
FitBaseType[FitBaseType["Sint16"] = 131] = "Sint16";
|
|
29
|
+
FitBaseType[FitBaseType["Uint16"] = 132] = "Uint16";
|
|
30
|
+
FitBaseType[FitBaseType["Sint32"] = 133] = "Sint32";
|
|
31
|
+
FitBaseType[FitBaseType["Uint32"] = 134] = "Uint32";
|
|
32
|
+
FitBaseType[FitBaseType["Float32"] = 136] = "Float32";
|
|
33
|
+
FitBaseType[FitBaseType["Float64"] = 137] = "Float64";
|
|
34
|
+
FitBaseType[FitBaseType["Uint16z"] = 139] = "Uint16z";
|
|
35
|
+
FitBaseType[FitBaseType["Uint32z"] = 140] = "Uint32z";
|
|
36
|
+
FitBaseType[FitBaseType["Sint64"] = 142] = "Sint64";
|
|
37
|
+
FitBaseType[FitBaseType["Uint64"] = 143] = "Uint64";
|
|
38
|
+
FitBaseType[FitBaseType["Uint64z"] = 144] = "Uint64z";
|
|
39
|
+
})(FitBaseType || (FitBaseType = {}));
|
|
40
|
+
/**
|
|
41
|
+
* A generic FIT binary encoder. Callers supply profile-specific field
|
|
42
|
+
* definitions and already-scaled field values. Numeric arrays, strings, and
|
|
43
|
+
* variable-length field values are supplied as raw `Uint8Array` values.
|
|
44
|
+
*/
|
|
45
|
+
export class FitEncoder {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
this.data = [];
|
|
49
|
+
this.activeDefinitions = new Map();
|
|
50
|
+
this.protocolVersion = FitEncoder.assertIntegerInRange((_a = options.protocolVersion) !== null && _a !== void 0 ? _a : 2, 0, UINT8_MAX, 'FIT protocol version');
|
|
51
|
+
this.profileVersion = FitEncoder.assertIntegerInRange((_b = options.profileVersion) !== null && _b !== void 0 ? _b : 21188, 0, UINT16_MAX, 'FIT profile version');
|
|
52
|
+
}
|
|
53
|
+
writeMessage(globalMessageNumber, fields, localMessageNumber = 0) {
|
|
54
|
+
this.validateMessage(globalMessageNumber, fields, localMessageNumber);
|
|
55
|
+
const definitionSignature = JSON.stringify({
|
|
56
|
+
globalMessageNumber,
|
|
57
|
+
fields: fields.map(field => this.getFieldDefinition(field)),
|
|
58
|
+
});
|
|
59
|
+
if (this.activeDefinitions.get(localMessageNumber) !== definitionSignature) {
|
|
60
|
+
this.writeDefinition(localMessageNumber, globalMessageNumber, fields);
|
|
61
|
+
this.activeDefinitions.set(localMessageNumber, definitionSignature);
|
|
62
|
+
}
|
|
63
|
+
this.writeUInt8(localMessageNumber);
|
|
64
|
+
fields.forEach(field => this.writeFieldValue(field));
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
if (this.data.length > UINT32_MAX) {
|
|
69
|
+
throw new RangeError('FIT data section cannot exceed 4294967295 bytes');
|
|
70
|
+
}
|
|
71
|
+
const header = [
|
|
72
|
+
FIT_HEADER_SIZE,
|
|
73
|
+
this.protocolVersion,
|
|
74
|
+
this.profileVersion & UINT8_MAX,
|
|
75
|
+
(this.profileVersion >>> 8) & UINT8_MAX,
|
|
76
|
+
this.data.length & UINT8_MAX,
|
|
77
|
+
(this.data.length >>> 8) & UINT8_MAX,
|
|
78
|
+
(this.data.length >>> 16) & UINT8_MAX,
|
|
79
|
+
(this.data.length >>> 24) & UINT8_MAX,
|
|
80
|
+
0x2E,
|
|
81
|
+
0x46,
|
|
82
|
+
0x49,
|
|
83
|
+
0x54,
|
|
84
|
+
];
|
|
85
|
+
const headerCRC = FitEncoder.calculateCRC(header);
|
|
86
|
+
const output = header.concat([headerCRC & UINT8_MAX, (headerCRC >>> 8) & UINT8_MAX], this.data);
|
|
87
|
+
const fileCRC = FitEncoder.calculateCRC(output);
|
|
88
|
+
output.push(fileCRC & UINT8_MAX, (fileCRC >>> 8) & UINT8_MAX);
|
|
89
|
+
return new Uint8Array(output);
|
|
90
|
+
}
|
|
91
|
+
static string(value) {
|
|
92
|
+
const bytes = new TextEncoder().encode(value);
|
|
93
|
+
if (bytes.length > UINT8_MAX - 1) {
|
|
94
|
+
throw new RangeError('FIT string fields can contain at most 254 UTF-8 bytes plus the null terminator');
|
|
95
|
+
}
|
|
96
|
+
const output = new Uint8Array(bytes.length + 1);
|
|
97
|
+
output.set(bytes);
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
static toFitTimestamp(date) {
|
|
101
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
102
|
+
throw new TypeError('FIT timestamp requires a valid Date');
|
|
103
|
+
}
|
|
104
|
+
const timestamp = Math.floor((date.getTime() - FIT_EPOCH_MS) / 1000);
|
|
105
|
+
return FitEncoder.assertIntegerInRange(timestamp, 0, UINT32_MAX, 'FIT timestamp');
|
|
106
|
+
}
|
|
107
|
+
static calculateCRC(bytes) {
|
|
108
|
+
let crc = 0;
|
|
109
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
110
|
+
let value = crc ^ bytes[index];
|
|
111
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
112
|
+
value = value & 1 ? (value >>> 1) ^ 0xA001 : value >>> 1;
|
|
113
|
+
}
|
|
114
|
+
crc = value;
|
|
115
|
+
}
|
|
116
|
+
return crc;
|
|
117
|
+
}
|
|
118
|
+
validateMessage(globalMessageNumber, fields, localMessageNumber) {
|
|
119
|
+
FitEncoder.assertIntegerInRange(globalMessageNumber, 0, UINT16_MAX, 'FIT global message number');
|
|
120
|
+
FitEncoder.assertIntegerInRange(localMessageNumber, 0, 0x0F, 'FIT local message number');
|
|
121
|
+
if (!Array.isArray(fields) || fields.length > UINT8_MAX) {
|
|
122
|
+
throw new RangeError('FIT message definitions support between 0 and 255 fields');
|
|
123
|
+
}
|
|
124
|
+
fields.forEach(field => this.validateField(field));
|
|
125
|
+
}
|
|
126
|
+
validateField(field) {
|
|
127
|
+
if (!field || typeof field !== 'object') {
|
|
128
|
+
throw new TypeError('FIT field definitions must be objects');
|
|
129
|
+
}
|
|
130
|
+
FitEncoder.assertIntegerInRange(field.number, 0, UINT8_MAX, 'FIT field number');
|
|
131
|
+
FitEncoder.assertIntegerInRange(field.size, 1, UINT8_MAX, 'FIT field size');
|
|
132
|
+
FitEncoder.assertIntegerInRange(field.baseType, 0, UINT8_MAX, 'FIT base type');
|
|
133
|
+
if (!this.isSupportedBaseType(field.baseType)) {
|
|
134
|
+
throw new RangeError(`Unsupported FIT base type ${field.baseType}`);
|
|
135
|
+
}
|
|
136
|
+
if (field.value instanceof Uint8Array) {
|
|
137
|
+
if (field.value.length !== field.size) {
|
|
138
|
+
throw new RangeError(`FIT field ${field.number} expected ${field.size} bytes, received ${field.value.length}`);
|
|
139
|
+
}
|
|
140
|
+
const baseTypeSize = this.getBaseTypeSize(field.baseType);
|
|
141
|
+
if (baseTypeSize && field.size % baseTypeSize !== 0) {
|
|
142
|
+
throw new RangeError(`FIT field ${field.number} size must be a multiple of ${baseTypeSize}`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const baseTypeSize = this.getBaseTypeSize(field.baseType);
|
|
147
|
+
if (baseTypeSize === undefined || field.size !== baseTypeSize) {
|
|
148
|
+
throw new RangeError(`FIT field ${field.number} has an invalid size for base type ${field.baseType}`);
|
|
149
|
+
}
|
|
150
|
+
switch (field.baseType) {
|
|
151
|
+
case FitBaseType.Sint64:
|
|
152
|
+
this.assertBigIntInRange(field.value, SINT64_MIN, SINT64_MAX, field.number);
|
|
153
|
+
return;
|
|
154
|
+
case FitBaseType.Uint64:
|
|
155
|
+
case FitBaseType.Uint64z:
|
|
156
|
+
this.assertBigIntInRange(field.value, BIGINT_ZERO, UINT64_MAX, field.number);
|
|
157
|
+
return;
|
|
158
|
+
case FitBaseType.Sint8:
|
|
159
|
+
this.assertNumberInRange(field.value, SINT8_MIN, SINT8_MAX, field.number);
|
|
160
|
+
return;
|
|
161
|
+
case FitBaseType.Sint16:
|
|
162
|
+
this.assertNumberInRange(field.value, SINT16_MIN, SINT16_MAX, field.number);
|
|
163
|
+
return;
|
|
164
|
+
case FitBaseType.Sint32:
|
|
165
|
+
this.assertNumberInRange(field.value, SINT32_MIN, SINT32_MAX, field.number);
|
|
166
|
+
return;
|
|
167
|
+
case FitBaseType.Float32:
|
|
168
|
+
if (typeof field.value !== 'number' || !Number.isFinite(field.value)) {
|
|
169
|
+
throw new RangeError(`FIT field ${field.number} requires a finite numeric value`);
|
|
170
|
+
}
|
|
171
|
+
if (!Number.isFinite(Math.fround(field.value))) {
|
|
172
|
+
throw new RangeError(`FIT field ${field.number} must be representable as a finite float32`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
case FitBaseType.Float64:
|
|
176
|
+
if (typeof field.value !== 'number' || !Number.isFinite(field.value)) {
|
|
177
|
+
throw new RangeError(`FIT field ${field.number} requires a finite numeric value`);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
default:
|
|
181
|
+
this.assertNumberInRange(field.value, 0, this.getUnsignedMaximum(field.baseType), field.number);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
assertNumberInRange(value, minimum, maximum, fieldNumber) {
|
|
185
|
+
if (typeof value !== 'number'
|
|
186
|
+
|| !Number.isInteger(value)
|
|
187
|
+
|| value < minimum
|
|
188
|
+
|| value > maximum) {
|
|
189
|
+
throw new RangeError(`FIT field ${fieldNumber} must be an integer between ${minimum} and ${maximum}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
assertBigIntInRange(value, minimum, maximum, fieldNumber) {
|
|
193
|
+
if (typeof value !== 'bigint' || value < minimum || value > maximum) {
|
|
194
|
+
throw new RangeError(`FIT field ${fieldNumber} must be a bigint between ${minimum} and ${maximum}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
getUnsignedMaximum(baseType) {
|
|
198
|
+
switch (baseType) {
|
|
199
|
+
case FitBaseType.Enum:
|
|
200
|
+
case FitBaseType.Uint8:
|
|
201
|
+
case FitBaseType.Uint8z:
|
|
202
|
+
case FitBaseType.Byte:
|
|
203
|
+
return UINT8_MAX;
|
|
204
|
+
case FitBaseType.Uint16:
|
|
205
|
+
case FitBaseType.Uint16z:
|
|
206
|
+
return UINT16_MAX;
|
|
207
|
+
case FitBaseType.Uint32:
|
|
208
|
+
case FitBaseType.Uint32z:
|
|
209
|
+
return UINT32_MAX;
|
|
210
|
+
default:
|
|
211
|
+
throw new RangeError(`Unsupported FIT base type ${baseType}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
getFieldDefinition(field) {
|
|
215
|
+
return { number: field.number, size: field.size, baseType: field.baseType };
|
|
216
|
+
}
|
|
217
|
+
writeDefinition(localMessageNumber, globalMessageNumber, fields) {
|
|
218
|
+
this.writeUInt8(0x40 | localMessageNumber);
|
|
219
|
+
this.writeUInt8(0);
|
|
220
|
+
this.writeUInt8(0);
|
|
221
|
+
this.writeUInt16(globalMessageNumber);
|
|
222
|
+
this.writeUInt8(fields.length);
|
|
223
|
+
fields.forEach((field) => {
|
|
224
|
+
this.writeUInt8(field.number);
|
|
225
|
+
this.writeUInt8(field.size);
|
|
226
|
+
this.writeUInt8(field.baseType);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
writeFieldValue(field) {
|
|
230
|
+
if (field.value instanceof Uint8Array) {
|
|
231
|
+
field.value.forEach(value => this.writeUInt8(value));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
switch (field.baseType) {
|
|
235
|
+
case FitBaseType.Enum:
|
|
236
|
+
case FitBaseType.Uint8:
|
|
237
|
+
case FitBaseType.Uint8z:
|
|
238
|
+
case FitBaseType.Byte:
|
|
239
|
+
this.writeUInt8(field.value);
|
|
240
|
+
return;
|
|
241
|
+
case FitBaseType.Sint8:
|
|
242
|
+
this.writeInt8(field.value);
|
|
243
|
+
return;
|
|
244
|
+
case FitBaseType.Uint16:
|
|
245
|
+
case FitBaseType.Uint16z:
|
|
246
|
+
this.writeUInt16(field.value);
|
|
247
|
+
return;
|
|
248
|
+
case FitBaseType.Sint16:
|
|
249
|
+
this.writeInt16(field.value);
|
|
250
|
+
return;
|
|
251
|
+
case FitBaseType.Uint32:
|
|
252
|
+
case FitBaseType.Uint32z:
|
|
253
|
+
this.writeUInt32(field.value);
|
|
254
|
+
return;
|
|
255
|
+
case FitBaseType.Sint32:
|
|
256
|
+
this.writeInt32(field.value);
|
|
257
|
+
return;
|
|
258
|
+
case FitBaseType.Float32:
|
|
259
|
+
this.writeFloat32(field.value);
|
|
260
|
+
return;
|
|
261
|
+
case FitBaseType.Float64:
|
|
262
|
+
this.writeFloat64(field.value);
|
|
263
|
+
return;
|
|
264
|
+
case FitBaseType.Sint64:
|
|
265
|
+
this.writeInt64(field.value);
|
|
266
|
+
return;
|
|
267
|
+
case FitBaseType.Uint64:
|
|
268
|
+
case FitBaseType.Uint64z:
|
|
269
|
+
this.writeUInt64(field.value);
|
|
270
|
+
return;
|
|
271
|
+
default:
|
|
272
|
+
throw new Error(`Unsupported FIT base type ${field.baseType}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
getBaseTypeSize(baseType) {
|
|
276
|
+
switch (baseType) {
|
|
277
|
+
case FitBaseType.Enum:
|
|
278
|
+
case FitBaseType.Sint8:
|
|
279
|
+
case FitBaseType.Uint8:
|
|
280
|
+
case FitBaseType.Uint8z:
|
|
281
|
+
case FitBaseType.Byte:
|
|
282
|
+
return 1;
|
|
283
|
+
case FitBaseType.Sint16:
|
|
284
|
+
case FitBaseType.Uint16:
|
|
285
|
+
case FitBaseType.Uint16z:
|
|
286
|
+
return 2;
|
|
287
|
+
case FitBaseType.Sint32:
|
|
288
|
+
case FitBaseType.Uint32:
|
|
289
|
+
case FitBaseType.Uint32z:
|
|
290
|
+
case FitBaseType.Float32:
|
|
291
|
+
return 4;
|
|
292
|
+
case FitBaseType.Float64:
|
|
293
|
+
case FitBaseType.Sint64:
|
|
294
|
+
case FitBaseType.Uint64:
|
|
295
|
+
case FitBaseType.Uint64z:
|
|
296
|
+
return 8;
|
|
297
|
+
default:
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
isSupportedBaseType(baseType) {
|
|
302
|
+
return (baseType === FitBaseType.String
|
|
303
|
+
|| this.getBaseTypeSize(baseType) !== undefined);
|
|
304
|
+
}
|
|
305
|
+
writeUInt8(value) {
|
|
306
|
+
this.data.push(value);
|
|
307
|
+
}
|
|
308
|
+
writeUInt16(value) {
|
|
309
|
+
this.data.push(value & UINT8_MAX, (value >>> 8) & UINT8_MAX);
|
|
310
|
+
}
|
|
311
|
+
writeInt8(value) {
|
|
312
|
+
this.writeUInt8(value < 0 ? 0x100 + value : value);
|
|
313
|
+
}
|
|
314
|
+
writeInt16(value) {
|
|
315
|
+
this.writeUInt16(value < 0 ? 0x10000 + value : value);
|
|
316
|
+
}
|
|
317
|
+
writeUInt32(value) {
|
|
318
|
+
this.data.push(value & UINT8_MAX, Math.floor(value / 0x100) & UINT8_MAX, Math.floor(value / 0x10000) & UINT8_MAX, Math.floor(value / 0x1000000) & UINT8_MAX);
|
|
319
|
+
}
|
|
320
|
+
writeInt32(value) {
|
|
321
|
+
this.writeUInt32(value < 0 ? 0x100000000 + value : value);
|
|
322
|
+
}
|
|
323
|
+
writeUInt64(value) {
|
|
324
|
+
for (let byteIndex = BIGINT_ZERO; byteIndex < BIGINT_EIGHT; byteIndex++) {
|
|
325
|
+
this.writeUInt8(Number((value >> (byteIndex * BIGINT_EIGHT)) & BIGINT_BYTE_MASK));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
writeInt64(value) {
|
|
329
|
+
this.writeUInt64(value < BIGINT_ZERO ? UINT64_MODULUS + value : value);
|
|
330
|
+
}
|
|
331
|
+
writeFloat32(value) {
|
|
332
|
+
const bytes = new Uint8Array(4);
|
|
333
|
+
new DataView(bytes.buffer).setFloat32(0, value, true);
|
|
334
|
+
bytes.forEach(byte => this.writeUInt8(byte));
|
|
335
|
+
}
|
|
336
|
+
writeFloat64(value) {
|
|
337
|
+
const bytes = new Uint8Array(8);
|
|
338
|
+
new DataView(bytes.buffer).setFloat64(0, value, true);
|
|
339
|
+
bytes.forEach(byte => this.writeUInt8(byte));
|
|
340
|
+
}
|
|
341
|
+
static assertIntegerInRange(value, minimum, maximum, label) {
|
|
342
|
+
if (typeof value !== 'number'
|
|
343
|
+
|| !Number.isInteger(value)
|
|
344
|
+
|| value < minimum
|
|
345
|
+
|| value > maximum) {
|
|
346
|
+
throw new RangeError(`${label} must be an integer between ${minimum} and ${maximum}`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
}
|
package/dist/fit-parser.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Buffer } from 'buffer';
|
|
2
2
|
import type { ParsedFit } from './fit_types.js';
|
|
3
|
+
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
4
|
+
export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
|
|
3
5
|
export interface FitParserOptions {
|
|
4
6
|
force?: boolean;
|
|
5
7
|
speedUnit?: string;
|
|
@@ -16,4 +18,3 @@ export default class FitParser {
|
|
|
16
18
|
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
17
19
|
parse(content: ArrayBuffer | Buffer<ArrayBuffer>, callback: FitParserCallback): void;
|
|
18
20
|
}
|
|
19
|
-
export {};
|
package/dist/fit-parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { calculateCRC, getArrayBuffer, readRecord } from './binary.js';
|
|
2
2
|
import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
|
|
3
|
+
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
3
4
|
export default class FitParser {
|
|
4
5
|
constructor(options = {}) {
|
|
5
6
|
this.options = {
|
package/dist/fit.js
CHANGED
|
@@ -3731,6 +3731,7 @@ export const FIT = {
|
|
|
3731
3731
|
offset: 0,
|
|
3732
3732
|
units: '',
|
|
3733
3733
|
},
|
|
3734
|
+
7: { field: 'sub_sport', type: 'sub_sport', scale: null, offset: 0, units: '' },
|
|
3734
3735
|
},
|
|
3735
3736
|
32: {
|
|
3736
3737
|
name: 'course_point',
|
|
@@ -6146,6 +6147,33 @@ export const FIT = {
|
|
|
6146
6147
|
23: 'u_turn',
|
|
6147
6148
|
24: 'segment_start',
|
|
6148
6149
|
25: 'segment_end',
|
|
6150
|
+
27: 'campsite',
|
|
6151
|
+
28: 'aid_station',
|
|
6152
|
+
29: 'rest_area',
|
|
6153
|
+
30: 'general_distance',
|
|
6154
|
+
31: 'service',
|
|
6155
|
+
32: 'energy_gel',
|
|
6156
|
+
33: 'sports_drink',
|
|
6157
|
+
34: 'mile_marker',
|
|
6158
|
+
35: 'checkpoint',
|
|
6159
|
+
36: 'shelter',
|
|
6160
|
+
37: 'meeting_spot',
|
|
6161
|
+
38: 'overlook',
|
|
6162
|
+
39: 'toilet',
|
|
6163
|
+
40: 'shower',
|
|
6164
|
+
41: 'gear',
|
|
6165
|
+
42: 'sharp_curve',
|
|
6166
|
+
43: 'steep_incline',
|
|
6167
|
+
44: 'tunnel',
|
|
6168
|
+
45: 'bridge',
|
|
6169
|
+
46: 'obstacle',
|
|
6170
|
+
47: 'crossing',
|
|
6171
|
+
48: 'store',
|
|
6172
|
+
49: 'transition',
|
|
6173
|
+
50: 'navaid',
|
|
6174
|
+
51: 'transport',
|
|
6175
|
+
52: 'alert',
|
|
6176
|
+
53: 'info',
|
|
6149
6177
|
},
|
|
6150
6178
|
manufacturer: {
|
|
6151
6179
|
0: 0,
|
package/dist/fit_types.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export type Goal = 'time' | 'distance' | 'calories' | 'frequency' | 'steps' | 'a
|
|
|
69
69
|
export type GoalRecurrence = 'off' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
|
|
70
70
|
export type GoalSource = 'auto' | 'community' | 'user';
|
|
71
71
|
export type Schedule = 'workout' | 'course';
|
|
72
|
-
export type CoursePoint = 'generic' | 'summit' | 'valley' | 'water' | 'food' | 'danger' | 'left' | 'right' | 'straight' | 'first_aid' | 'fourth_category' | 'third_category' | 'second_category' | 'first_category' | 'hors_category' | 'sprint' | 'left_fork' | 'right_fork' | 'middle_fork' | 'slight_left' | 'sharp_left' | 'slight_right' | 'sharp_right' | 'u_turn' | 'segment_start' | 'segment_end';
|
|
72
|
+
export type CoursePoint = 'generic' | 'summit' | 'valley' | 'water' | 'food' | 'danger' | 'left' | 'right' | 'straight' | 'first_aid' | 'fourth_category' | 'third_category' | 'second_category' | 'first_category' | 'hors_category' | 'sprint' | 'left_fork' | 'right_fork' | 'middle_fork' | 'slight_left' | 'sharp_left' | 'slight_right' | 'sharp_right' | 'u_turn' | 'segment_start' | 'segment_end' | 'campsite' | 'aid_station' | 'rest_area' | 'general_distance' | 'service' | 'energy_gel' | 'sports_drink' | 'mile_marker' | 'checkpoint' | 'shelter' | 'meeting_spot' | 'overlook' | 'toilet' | 'shower' | 'gear' | 'sharp_curve' | 'steep_incline' | 'tunnel' | 'bridge' | 'obstacle' | 'crossing' | 'store' | 'transition' | 'navaid' | 'transport' | 'alert' | 'info';
|
|
73
73
|
export type Manufacturer = '0' | 'garmin' | 'garmin_fr405_antfs' | 'zephyr' | 'dayton' | 'idt' | 'srm' | 'quarq' | 'ibike' | 'saris' | 'spark_hk' | 'tanita' | 'echowell' | 'dynastream_oem' | 'nautilus' | 'dynastream' | 'timex' | 'metrigear' | 'xelic' | 'beurer' | 'cardiosport' | 'a_and_d' | 'hmm' | 'suunto' | 'thita_elektronik' | 'gpulse' | 'clean_mobile' | 'pedal_brain' | 'peaksware' | 'saxonar' | 'lemond_fitness' | 'dexcom' | 'wahoo_fitness' | 'octane_fitness' | 'archinoetics' | 'the_hurt_box' | 'citizen_systems' | 'magellan' | 'osynce' | 'holux' | 'concept2' | 'one_giant_leap' | 'ace_sensor' | 'brim_brothers' | 'xplova' | 'perception_digital' | 'bf1systems' | 'pioneer' | 'spantec' | 'metalogics' | '4iiiis' | 'seiko_epson' | 'seiko_epson_oem' | 'ifor_powell' | 'maxwell_guider' | 'star_trac' | 'breakaway' | 'alatech_technology_ltd' | 'mio_technology_europe' | 'rotor' | 'geonaute' | 'id_bike' | 'specialized' | 'wtek' | 'physical_enterprises' | 'north_pole_engineering' | 'bkool' | 'cateye' | 'stages_cycling' | 'sigmasport' | 'tomtom' | 'peripedal' | 'wattbike' | 'moxy' | 'ciclosport' | 'powerbahn' | 'acorn_projects_aps' | 'lifebeam' | 'bontrager' | 'wellgo' | 'scosche' | 'magura' | 'woodway' | 'elite' | 'nielsen_kellerman' | 'dk_city' | 'tacx' | 'direction_technology' | 'magtonic' | '1partcarbon' | 'inside_ride_technologies' | 'sound_of_motion' | 'stryd' | 'icg' | 'mipulse' | 'bsx_athletics' | 'look' | 'campagnolo_srl' | 'body_bike_smart' | 'praxisworks' | 'limits_technology' | 'topaction_technology' | 'cosinuss' | 'fitcare' | 'magene' | 'giant_manufacturing_co' | 'tigrasport' | 'salutron' | 'technogym' | 'bryton_sensors' | 'latitude_limited' | 'soaring_technology' | 'igpsport' | 'thinkrider' | 'gopher_sport' | 'waterrower' | 'orangetheory' | 'inpeak' | 'kinetic' | 'johnson_health_tech' | 'polar_electro' | 'seesense' | 'nci_technology' | 'development' | 'healthandlife' | 'lezyne' | 'scribe_labs' | 'zwift' | 'watteam' | 'recon' | 'favero_electronics' | 'dynovelo' | 'strava' | 'precor' | 'bryton' | 'sram' | 'navman' | 'cobi' | 'spivi' | 'mio_magellan' | 'evesports' | 'sensitivus_gauge' | 'podoon' | 'life_time_fitness' | 'falco_e_motors' | 'minoura' | 'cycliq' | 'luxottica' | 'trainer_road' | 'the_sufferfest' | 'fullspeedahead' | 'virtualtraining' | 'feedbacksports' | 'omata' | 'vdo' | 'magneticdays' | 'hammerhead' | 'kinetic_by_kurt' | 'shapelog' | 'dabuziduo' | 'jetblack' | 'coros' | 'virtugo' | 'velosense' | 'actigraphcorp';
|
|
74
74
|
export type GarminProduct = 'hrm_bike' | 'hrm1' | 'axh01' | 'axb01' | 'axb02' | 'hrm2ss' | 'dsi_alf02' | 'hrm3ss' | 'hrm_run_single_byte_product_id' | 'bsm' | 'bcm' | 'axs01' | 'hrm_tri_single_byte_product_id' | 'fr225_single_byte_product_id' | 'fr301_china' | 'fr301_japan' | 'fr301_korea' | 'fr301_taiwan' | 'fr405' | 'fr50' | 'fr405_japan' | 'fr60' | 'dsi_alf01' | 'fr310xt' | 'edge500' | 'fr110' | 'edge800' | 'edge500_taiwan' | 'edge500_japan' | 'chirp' | 'fr110_japan' | 'edge200' | 'fr910xt' | 'edge800_taiwan' | 'edge800_japan' | 'alf04' | 'fr610' | 'fr210_japan' | 'vector_ss' | 'vector_cp' | 'edge800_china' | 'edge500_china' | 'fr610_japan' | 'edge500_korea' | 'fr70' | 'fr310xt_4t' | 'amx' | 'fr10' | 'edge800_korea' | 'swim' | 'fr910xt_china' | 'fenix' | 'edge200_taiwan' | 'edge510' | 'edge810' | 'tempe' | 'fr910xt_japan' | 'fr620' | 'fr220' | 'fr910xt_korea' | 'fr10_japan' | 'edge810_japan' | 'virb_elite' | 'edge_touring' | 'edge510_japan' | 'hrm_tri' | 'hrm_run' | 'fr920xt' | 'edge510_asia' | 'edge810_china' | 'edge810_taiwan' | 'edge1000' | 'vivo_fit' | 'virb_remote' | 'vivo_ki' | 'fr15' | 'vivo_active' | 'edge510_korea' | 'fr620_japan' | 'fr620_china' | 'fr220_japan' | 'fr220_china' | 'approach_s6' | 'vivo_smart' | 'fenix2' | 'epix' | 'fenix3' | 'edge1000_taiwan' | 'edge1000_japan' | 'fr15_japan' | 'edge520' | 'edge1000_china' | 'fr620_russia' | 'fr220_russia' | 'vector_s' | 'edge1000_korea' | 'fr920xt_taiwan' | 'fr920xt_china' | 'fr920xt_japan' | 'virbx' | 'vivo_smart_apac' | 'etrex_touch' | 'edge25' | 'fr25' | 'vivo_fit2' | 'fr225' | 'fr630' | 'fr230' | 'fr735xt' | 'vivo_active_apac' | 'vector_2' | 'vector_2s' | 'virbxe' | 'fr620_taiwan' | 'fr220_taiwan' | 'truswing' | 'fenix3_china' | 'fenix3_twn' | 'varia_headlight' | 'varia_taillight_old' | 'edge_explore_1000' | 'fr225_asia' | 'varia_radar_taillight' | 'varia_radar_display' | 'edge20' | 'd2_bravo' | 'approach_s20' | 'varia_remote' | 'hrm4_run' | 'vivo_active_hr' | 'vivo_smart_hr' | 'vivo_move' | 'varia_vision' | 'vivo_fit3' | 'fenix3_hr' | 'virb_ultra_30' | 'index_smart_scale' | 'fr235' | 'fenix3_chronos' | 'oregon7xx' | 'rino7xx' | 'nautix' | 'edge_820' | 'edge_explore_820' | 'fenix5s' | 'd2_bravo_titanium' | 'varia_ut800' | 'running_dynamics_pod' | 'fenix5x' | 'vivo_fit_jr' | 'fr935' | 'fenix5' | 'descent' | 'sdm4' | 'edge_remote' | 'training_center' | 'connectiq_simulator' | 'android_antplus_plugin' | 'connect';
|
|
75
75
|
export type AntplusDeviceType = 'antfs' | 'bike_power' | 'environment_sensor_legacy' | 'multi_sport_speed_distance' | 'control' | 'fitness_equipment' | 'blood_pressure' | 'geocache_node' | 'light_electric_vehicle' | 'env_sensor' | 'racquet' | 'control_hub' | 'muscle_oxygen' | 'shifting' | 'bike_light_main' | 'bike_light_shared' | 'exd' | 'bike_radar' | 'bike_aero' | 'weight_scale' | 'heart_rate' | 'bike_speed_cadence' | 'bike_cadence' | 'bike_speed' | 'stride_speed_distance';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fit-file-parser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
|
|
7
7
|
"author": {
|
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
"garmin",
|
|
33
33
|
"parse"
|
|
34
34
|
],
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md",
|
|
40
|
+
"CONTRIBUTORS.md"
|
|
41
|
+
],
|
|
35
42
|
"exports": {
|
|
36
43
|
".": {
|
|
37
44
|
"types": "./dist/fit-parser.d.ts",
|
|
@@ -75,4 +82,4 @@
|
|
|
75
82
|
"typescript": "^5.9.3",
|
|
76
83
|
"vitest": "^4.0.13"
|
|
77
84
|
}
|
|
78
|
-
}
|
|
85
|
+
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# FIT Parser Development Rules
|
|
2
|
-
|
|
3
|
-
You are working on the `fit-parser` library. Follow these rules strictly to ensure correctness and stability.
|
|
4
|
-
|
|
5
|
-
## Core Principles
|
|
6
|
-
|
|
7
|
-
1. **Trust the Official SDK**:
|
|
8
|
-
* Always verify Message IDs and Field IDs against the Official Garmin FIT SDK (`@garmin/fitsdk`).
|
|
9
|
-
* **NEVER** guess or reuse existing IDs if they seem wrong (e.g., Message 140 vs 285).
|
|
10
|
-
* If `@garmin/fitsdk` is available in `node_modules`, use it as the source of truth.
|
|
11
|
-
|
|
12
|
-
2. **Investigation First**:
|
|
13
|
-
* If a field is missing, use `npm run inspect <file>` or `npm run probe <file> <value>` BEFORE modifying code.
|
|
14
|
-
* Confirm the field exists in the file and identify its raw value.
|
|
15
|
-
|
|
16
|
-
3. **Testing is Mandatory**:
|
|
17
|
-
* Every new message or field definition MUST have a corresponding test case in `test/`.
|
|
18
|
-
* Test against real FIT files whenever possible (use `examples/` directory).
|
|
19
|
-
* Use `npm run test` to verify changes.
|
|
20
|
-
|
|
21
|
-
4. **Code Generation**:
|
|
22
|
-
* `src/fit_types.ts` is AUTO-GENERATED.
|
|
23
|
-
* If you modify `src/fit.ts`, you **MUST** run `npm run codegen` to update types.
|
|
24
|
-
* Do not edit `src/fit_types.ts` manually.
|
|
25
|
-
|
|
26
|
-
## Workflow checklist
|
|
27
|
-
|
|
28
|
-
When adding a new message or fixing a field:
|
|
29
|
-
|
|
30
|
-
- [ ] Locate official Message ID in Garmin SDK
|
|
31
|
-
- [ ] Locate Field IDs and Types in Garmin SDK
|
|
32
|
-
- [ ] Update `src/fit.ts` with new definition
|
|
33
|
-
- [ ] Run `npm run codegen`
|
|
34
|
-
- [ ] Add/Update test in `test/`
|
|
35
|
-
- [ ] Run `npm run build && npm run test`
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: add-fit-message
|
|
3
|
-
description: Add a new FIT message or update an existing one
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Add/Update FIT Message
|
|
7
|
-
|
|
8
|
-
## Prerequisites
|
|
9
|
-
|
|
10
|
-
- Identify Message ID (e.g., 285 for Jump)
|
|
11
|
-
- Identify Field IDs and Types from Garmin SDK
|
|
12
|
-
|
|
13
|
-
## Steps
|
|
14
|
-
|
|
15
|
-
1. **Modify `src/fit.ts`** - Add message definition
|
|
16
|
-
2. **Run `npm run codegen`** - Updates `src/fit_types.ts`
|
|
17
|
-
3. **Add Test** - Create test in `test/`
|
|
18
|
-
4. **Verify** - Run `npm run build && npm run test`
|
|
19
|
-
|
|
20
|
-
## Example Entry
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
285: {
|
|
24
|
-
name: 'jump',
|
|
25
|
-
253: { field: 'timestamp', type: 'date_time', scale: null, offset: 0, units: 's' },
|
|
26
|
-
0: { field: 'distance', type: 'float32', scale: null, offset: 0, units: 'm' },
|
|
27
|
-
5: { field: 'position_lat', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
|
|
28
|
-
}
|
|
29
|
-
```
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: common fit-parser development workflows
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Inspect FIT File
|
|
6
|
-
|
|
7
|
-
// turbo
|
|
8
|
-
1. node scripts/inspect_fit.js <path_to_fit_file> [message_key]
|
|
9
|
-
|
|
10
|
-
# Probe FIT File for Value
|
|
11
|
-
|
|
12
|
-
// turbo
|
|
13
|
-
1. node scripts/deep_probe.js <path_to_fit_file> <value> [tolerance]
|
|
14
|
-
|
|
15
|
-
# Run Tests
|
|
16
|
-
|
|
17
|
-
// turbo
|
|
18
|
-
1. npm run build && npm run test
|
|
19
|
-
|
|
20
|
-
# Add New Message
|
|
21
|
-
|
|
22
|
-
1. Locate Message ID in `@garmin/fitsdk/src/profile.js`
|
|
23
|
-
2. Update `src/fit.ts` with message definition
|
|
24
|
-
// turbo
|
|
25
|
-
3. npm run codegen
|
|
26
|
-
4. Add test in `test/`
|
|
27
|
-
// turbo
|
|
28
|
-
5. npm run build && npm run test
|
package/.editorconfig
DELETED
package/.github/workflows/ci.yml
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
node-version: [20.x, 22.x, 24.x, 25.x]
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v6
|
|
19
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
-
uses: actions/setup-node@v6
|
|
21
|
-
with:
|
|
22
|
-
node-version: ${{ matrix.node-version }}
|
|
23
|
-
cache: npm
|
|
24
|
-
cache-dependency-path: package-lock.json
|
|
25
|
-
- run: npm ci
|
|
26
|
-
- run: npm run build
|
|
27
|
-
- run: npm run test -- --coverage
|
|
28
|
-
- run: npm run lint
|
|
29
|
-
if: matrix.node-version == '25.x'
|
|
30
|
-
- run: npm run type-check
|
|
31
|
-
if: matrix.node-version == '25.x'
|
|
32
|
-
- run: |
|
|
33
|
-
npm run codegen
|
|
34
|
-
if git diff --exit-code src/fit_types.ts; then
|
|
35
|
-
echo "✓ src/fit_types.ts is unchanged after codegen"
|
|
36
|
-
else
|
|
37
|
-
echo "✗ src/fit_types.ts has been modified - please commit the changes"
|
|
38
|
-
exit 1
|
|
39
|
-
fi
|
|
40
|
-
if: matrix.node-version == '25.x'
|
package/.vscode/extensions.json
DELETED
package/.vscode/launch.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "0.2.0",
|
|
3
|
-
"configurations": [
|
|
4
|
-
{
|
|
5
|
-
"name": "Run Dev (Codegen Watcher)",
|
|
6
|
-
"type": "node",
|
|
7
|
-
"request": "launch",
|
|
8
|
-
"runtimeExecutable": "npm",
|
|
9
|
-
"runtimeArgs": [
|
|
10
|
-
"run",
|
|
11
|
-
"dev"
|
|
12
|
-
],
|
|
13
|
-
"console": "integratedTerminal"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"type": "node",
|
|
17
|
-
"request": "launch",
|
|
18
|
-
"name": "Debug Vitest",
|
|
19
|
-
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
|
20
|
-
"args": [
|
|
21
|
-
"run"
|
|
22
|
-
],
|
|
23
|
-
"console": "integratedTerminal",
|
|
24
|
-
"internalConsoleOptions": "neverOpen"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|