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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
- Add the public `FitEncoder` API for writing FIT headers, definitions, data messages, and CRCs.
|
|
6
|
+
- Preserve the `course.sub_sport` field while parsing FIT course files.
|
|
7
|
+
|
|
3
8
|
<a name="1.5.4"></a>
|
|
4
9
|
|
|
5
10
|
## [1.5.4](https://github.com/jimmykane/fit-parser/compare/v1.0.0...v1.5.3) (2019-03-01)
|
package/README.md
CHANGED
|
@@ -57,6 +57,31 @@ const buffer = await fs.readFile('./example.fit')
|
|
|
57
57
|
const fitObject = await fitParser.parseAsync(buffer)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Encoding
|
|
61
|
+
|
|
62
|
+
`FitEncoder` writes FIT headers, message definitions, data messages, and CRCs.
|
|
63
|
+
It is profile-agnostic: callers provide profile field identifiers and values in
|
|
64
|
+
their raw FIT representation (including any scale or offset). Scalar 64-bit
|
|
65
|
+
values use `bigint`; strings and numeric arrays use exact-size raw
|
|
66
|
+
`Uint8Array` values.
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
import { FitBaseType, FitEncoder } from 'fit-file-parser'
|
|
70
|
+
|
|
71
|
+
const encoder = new FitEncoder()
|
|
72
|
+
encoder.writeMessage(0, [
|
|
73
|
+
{ number: 0, size: 1, baseType: FitBaseType.Enum, value: 6 }, // FileId.type = course
|
|
74
|
+
{ number: 4, size: 4, baseType: FitBaseType.Uint32, value: FitEncoder.toFitTimestamp(new Date()) },
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
const fitBytes = encoder.close()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use a distinct local message number (the optional third `writeMessage`
|
|
81
|
+
argument) for each recurring message shape to avoid redundant definitions.
|
|
82
|
+
The encoder validates field definitions and numeric ranges before writing, so
|
|
83
|
+
an exception never leaves a partial message in the output.
|
|
84
|
+
|
|
60
85
|
## Development
|
|
61
86
|
|
|
62
87
|
To build the project, run:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** FIT definition base-type bytes, including the endian flag where required. */
|
|
2
|
+
export declare enum FitBaseType {
|
|
3
|
+
Enum = 0,
|
|
4
|
+
Sint8 = 1,
|
|
5
|
+
Uint8 = 2,
|
|
6
|
+
String = 7,
|
|
7
|
+
Uint8z = 10,
|
|
8
|
+
Byte = 13,
|
|
9
|
+
Sint16 = 131,
|
|
10
|
+
Uint16 = 132,
|
|
11
|
+
Sint32 = 133,
|
|
12
|
+
Uint32 = 134,
|
|
13
|
+
Float32 = 136,
|
|
14
|
+
Float64 = 137,
|
|
15
|
+
Uint16z = 139,
|
|
16
|
+
Uint32z = 140,
|
|
17
|
+
Sint64 = 142,
|
|
18
|
+
Uint64 = 143,
|
|
19
|
+
Uint64z = 144
|
|
20
|
+
}
|
|
21
|
+
export interface FitEncoderField {
|
|
22
|
+
number: number;
|
|
23
|
+
size: number;
|
|
24
|
+
baseType: FitBaseType | number;
|
|
25
|
+
value: number | bigint | Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
export interface FitEncoderOptions {
|
|
28
|
+
protocolVersion?: number;
|
|
29
|
+
profileVersion?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A generic FIT binary encoder. Callers supply profile-specific field
|
|
33
|
+
* definitions and already-scaled field values. Numeric arrays, strings, and
|
|
34
|
+
* variable-length field values are supplied as raw `Uint8Array` values.
|
|
35
|
+
*/
|
|
36
|
+
export declare class FitEncoder {
|
|
37
|
+
private readonly data;
|
|
38
|
+
private readonly activeDefinitions;
|
|
39
|
+
private readonly protocolVersion;
|
|
40
|
+
private readonly profileVersion;
|
|
41
|
+
constructor(options?: FitEncoderOptions);
|
|
42
|
+
writeMessage(globalMessageNumber: number, fields: FitEncoderField[], localMessageNumber?: number): this;
|
|
43
|
+
close(): Uint8Array;
|
|
44
|
+
static string(value: string): Uint8Array;
|
|
45
|
+
static toFitTimestamp(date: Date): number;
|
|
46
|
+
static calculateCRC(bytes: ArrayLike<number>): number;
|
|
47
|
+
private validateMessage;
|
|
48
|
+
private validateField;
|
|
49
|
+
private assertNumberInRange;
|
|
50
|
+
private assertBigIntInRange;
|
|
51
|
+
private getUnsignedMaximum;
|
|
52
|
+
private getFieldDefinition;
|
|
53
|
+
private writeDefinition;
|
|
54
|
+
private writeFieldValue;
|
|
55
|
+
private getBaseTypeSize;
|
|
56
|
+
private isSupportedBaseType;
|
|
57
|
+
private writeUInt8;
|
|
58
|
+
private writeUInt16;
|
|
59
|
+
private writeInt8;
|
|
60
|
+
private writeInt16;
|
|
61
|
+
private writeUInt32;
|
|
62
|
+
private writeInt32;
|
|
63
|
+
private writeUInt64;
|
|
64
|
+
private writeInt64;
|
|
65
|
+
private writeFloat32;
|
|
66
|
+
private writeFloat64;
|
|
67
|
+
private static assertIntegerInRange;
|
|
68
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FitEncoder = exports.FitBaseType = void 0;
|
|
4
|
+
const FIT_HEADER_SIZE = 14;
|
|
5
|
+
const FIT_EPOCH_MS = 631065600000;
|
|
6
|
+
const UINT8_MAX = 0xFF;
|
|
7
|
+
const UINT16_MAX = 0xFFFF;
|
|
8
|
+
const UINT32_MAX = 0xFFFFFFFF;
|
|
9
|
+
const BIGINT_ZERO = BigInt(0);
|
|
10
|
+
const BIGINT_EIGHT = BigInt(8);
|
|
11
|
+
const BIGINT_BYTE_MASK = BigInt(0xFF);
|
|
12
|
+
const UINT64_MAX = BigInt('18446744073709551615');
|
|
13
|
+
const UINT64_MODULUS = BigInt('18446744073709551616');
|
|
14
|
+
const SINT8_MIN = -0x80;
|
|
15
|
+
const SINT8_MAX = 0x7F;
|
|
16
|
+
const SINT16_MIN = -0x8000;
|
|
17
|
+
const SINT16_MAX = 0x7FFF;
|
|
18
|
+
const SINT32_MIN = -0x80000000;
|
|
19
|
+
const SINT32_MAX = 0x7FFFFFFF;
|
|
20
|
+
const SINT64_MIN = BigInt('-9223372036854775808');
|
|
21
|
+
const SINT64_MAX = BigInt('9223372036854775807');
|
|
22
|
+
/** FIT definition base-type bytes, including the endian flag where required. */
|
|
23
|
+
var FitBaseType;
|
|
24
|
+
(function (FitBaseType) {
|
|
25
|
+
FitBaseType[FitBaseType["Enum"] = 0] = "Enum";
|
|
26
|
+
FitBaseType[FitBaseType["Sint8"] = 1] = "Sint8";
|
|
27
|
+
FitBaseType[FitBaseType["Uint8"] = 2] = "Uint8";
|
|
28
|
+
FitBaseType[FitBaseType["String"] = 7] = "String";
|
|
29
|
+
FitBaseType[FitBaseType["Uint8z"] = 10] = "Uint8z";
|
|
30
|
+
FitBaseType[FitBaseType["Byte"] = 13] = "Byte";
|
|
31
|
+
FitBaseType[FitBaseType["Sint16"] = 131] = "Sint16";
|
|
32
|
+
FitBaseType[FitBaseType["Uint16"] = 132] = "Uint16";
|
|
33
|
+
FitBaseType[FitBaseType["Sint32"] = 133] = "Sint32";
|
|
34
|
+
FitBaseType[FitBaseType["Uint32"] = 134] = "Uint32";
|
|
35
|
+
FitBaseType[FitBaseType["Float32"] = 136] = "Float32";
|
|
36
|
+
FitBaseType[FitBaseType["Float64"] = 137] = "Float64";
|
|
37
|
+
FitBaseType[FitBaseType["Uint16z"] = 139] = "Uint16z";
|
|
38
|
+
FitBaseType[FitBaseType["Uint32z"] = 140] = "Uint32z";
|
|
39
|
+
FitBaseType[FitBaseType["Sint64"] = 142] = "Sint64";
|
|
40
|
+
FitBaseType[FitBaseType["Uint64"] = 143] = "Uint64";
|
|
41
|
+
FitBaseType[FitBaseType["Uint64z"] = 144] = "Uint64z";
|
|
42
|
+
})(FitBaseType || (exports.FitBaseType = FitBaseType = {}));
|
|
43
|
+
/**
|
|
44
|
+
* A generic FIT binary encoder. Callers supply profile-specific field
|
|
45
|
+
* definitions and already-scaled field values. Numeric arrays, strings, and
|
|
46
|
+
* variable-length field values are supplied as raw `Uint8Array` values.
|
|
47
|
+
*/
|
|
48
|
+
class FitEncoder {
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
var _a, _b;
|
|
51
|
+
this.data = [];
|
|
52
|
+
this.activeDefinitions = new Map();
|
|
53
|
+
this.protocolVersion = FitEncoder.assertIntegerInRange((_a = options.protocolVersion) !== null && _a !== void 0 ? _a : 2, 0, UINT8_MAX, 'FIT protocol version');
|
|
54
|
+
this.profileVersion = FitEncoder.assertIntegerInRange((_b = options.profileVersion) !== null && _b !== void 0 ? _b : 21188, 0, UINT16_MAX, 'FIT profile version');
|
|
55
|
+
}
|
|
56
|
+
writeMessage(globalMessageNumber, fields, localMessageNumber = 0) {
|
|
57
|
+
this.validateMessage(globalMessageNumber, fields, localMessageNumber);
|
|
58
|
+
const definitionSignature = JSON.stringify({
|
|
59
|
+
globalMessageNumber,
|
|
60
|
+
fields: fields.map(field => this.getFieldDefinition(field)),
|
|
61
|
+
});
|
|
62
|
+
if (this.activeDefinitions.get(localMessageNumber) !== definitionSignature) {
|
|
63
|
+
this.writeDefinition(localMessageNumber, globalMessageNumber, fields);
|
|
64
|
+
this.activeDefinitions.set(localMessageNumber, definitionSignature);
|
|
65
|
+
}
|
|
66
|
+
this.writeUInt8(localMessageNumber);
|
|
67
|
+
fields.forEach(field => this.writeFieldValue(field));
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
if (this.data.length > UINT32_MAX) {
|
|
72
|
+
throw new RangeError('FIT data section cannot exceed 4294967295 bytes');
|
|
73
|
+
}
|
|
74
|
+
const header = [
|
|
75
|
+
FIT_HEADER_SIZE,
|
|
76
|
+
this.protocolVersion,
|
|
77
|
+
this.profileVersion & UINT8_MAX,
|
|
78
|
+
(this.profileVersion >>> 8) & UINT8_MAX,
|
|
79
|
+
this.data.length & UINT8_MAX,
|
|
80
|
+
(this.data.length >>> 8) & UINT8_MAX,
|
|
81
|
+
(this.data.length >>> 16) & UINT8_MAX,
|
|
82
|
+
(this.data.length >>> 24) & UINT8_MAX,
|
|
83
|
+
0x2E,
|
|
84
|
+
0x46,
|
|
85
|
+
0x49,
|
|
86
|
+
0x54,
|
|
87
|
+
];
|
|
88
|
+
const headerCRC = FitEncoder.calculateCRC(header);
|
|
89
|
+
const output = header.concat([headerCRC & UINT8_MAX, (headerCRC >>> 8) & UINT8_MAX], this.data);
|
|
90
|
+
const fileCRC = FitEncoder.calculateCRC(output);
|
|
91
|
+
output.push(fileCRC & UINT8_MAX, (fileCRC >>> 8) & UINT8_MAX);
|
|
92
|
+
return new Uint8Array(output);
|
|
93
|
+
}
|
|
94
|
+
static string(value) {
|
|
95
|
+
const bytes = new TextEncoder().encode(value);
|
|
96
|
+
if (bytes.length > UINT8_MAX - 1) {
|
|
97
|
+
throw new RangeError('FIT string fields can contain at most 254 UTF-8 bytes plus the null terminator');
|
|
98
|
+
}
|
|
99
|
+
const output = new Uint8Array(bytes.length + 1);
|
|
100
|
+
output.set(bytes);
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
static toFitTimestamp(date) {
|
|
104
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
105
|
+
throw new TypeError('FIT timestamp requires a valid Date');
|
|
106
|
+
}
|
|
107
|
+
const timestamp = Math.floor((date.getTime() - FIT_EPOCH_MS) / 1000);
|
|
108
|
+
return FitEncoder.assertIntegerInRange(timestamp, 0, UINT32_MAX, 'FIT timestamp');
|
|
109
|
+
}
|
|
110
|
+
static calculateCRC(bytes) {
|
|
111
|
+
let crc = 0;
|
|
112
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
113
|
+
let value = crc ^ bytes[index];
|
|
114
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
115
|
+
value = value & 1 ? (value >>> 1) ^ 0xA001 : value >>> 1;
|
|
116
|
+
}
|
|
117
|
+
crc = value;
|
|
118
|
+
}
|
|
119
|
+
return crc;
|
|
120
|
+
}
|
|
121
|
+
validateMessage(globalMessageNumber, fields, localMessageNumber) {
|
|
122
|
+
FitEncoder.assertIntegerInRange(globalMessageNumber, 0, UINT16_MAX, 'FIT global message number');
|
|
123
|
+
FitEncoder.assertIntegerInRange(localMessageNumber, 0, 0x0F, 'FIT local message number');
|
|
124
|
+
if (!Array.isArray(fields) || fields.length > UINT8_MAX) {
|
|
125
|
+
throw new RangeError('FIT message definitions support between 0 and 255 fields');
|
|
126
|
+
}
|
|
127
|
+
fields.forEach(field => this.validateField(field));
|
|
128
|
+
}
|
|
129
|
+
validateField(field) {
|
|
130
|
+
if (!field || typeof field !== 'object') {
|
|
131
|
+
throw new TypeError('FIT field definitions must be objects');
|
|
132
|
+
}
|
|
133
|
+
FitEncoder.assertIntegerInRange(field.number, 0, UINT8_MAX, 'FIT field number');
|
|
134
|
+
FitEncoder.assertIntegerInRange(field.size, 1, UINT8_MAX, 'FIT field size');
|
|
135
|
+
FitEncoder.assertIntegerInRange(field.baseType, 0, UINT8_MAX, 'FIT base type');
|
|
136
|
+
if (!this.isSupportedBaseType(field.baseType)) {
|
|
137
|
+
throw new RangeError(`Unsupported FIT base type ${field.baseType}`);
|
|
138
|
+
}
|
|
139
|
+
if (field.value instanceof Uint8Array) {
|
|
140
|
+
if (field.value.length !== field.size) {
|
|
141
|
+
throw new RangeError(`FIT field ${field.number} expected ${field.size} bytes, received ${field.value.length}`);
|
|
142
|
+
}
|
|
143
|
+
const baseTypeSize = this.getBaseTypeSize(field.baseType);
|
|
144
|
+
if (baseTypeSize && field.size % baseTypeSize !== 0) {
|
|
145
|
+
throw new RangeError(`FIT field ${field.number} size must be a multiple of ${baseTypeSize}`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const baseTypeSize = this.getBaseTypeSize(field.baseType);
|
|
150
|
+
if (baseTypeSize === undefined || field.size !== baseTypeSize) {
|
|
151
|
+
throw new RangeError(`FIT field ${field.number} has an invalid size for base type ${field.baseType}`);
|
|
152
|
+
}
|
|
153
|
+
switch (field.baseType) {
|
|
154
|
+
case FitBaseType.Sint64:
|
|
155
|
+
this.assertBigIntInRange(field.value, SINT64_MIN, SINT64_MAX, field.number);
|
|
156
|
+
return;
|
|
157
|
+
case FitBaseType.Uint64:
|
|
158
|
+
case FitBaseType.Uint64z:
|
|
159
|
+
this.assertBigIntInRange(field.value, BIGINT_ZERO, UINT64_MAX, field.number);
|
|
160
|
+
return;
|
|
161
|
+
case FitBaseType.Sint8:
|
|
162
|
+
this.assertNumberInRange(field.value, SINT8_MIN, SINT8_MAX, field.number);
|
|
163
|
+
return;
|
|
164
|
+
case FitBaseType.Sint16:
|
|
165
|
+
this.assertNumberInRange(field.value, SINT16_MIN, SINT16_MAX, field.number);
|
|
166
|
+
return;
|
|
167
|
+
case FitBaseType.Sint32:
|
|
168
|
+
this.assertNumberInRange(field.value, SINT32_MIN, SINT32_MAX, field.number);
|
|
169
|
+
return;
|
|
170
|
+
case FitBaseType.Float32:
|
|
171
|
+
if (typeof field.value !== 'number' || !Number.isFinite(field.value)) {
|
|
172
|
+
throw new RangeError(`FIT field ${field.number} requires a finite numeric value`);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isFinite(Math.fround(field.value))) {
|
|
175
|
+
throw new RangeError(`FIT field ${field.number} must be representable as a finite float32`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
case FitBaseType.Float64:
|
|
179
|
+
if (typeof field.value !== 'number' || !Number.isFinite(field.value)) {
|
|
180
|
+
throw new RangeError(`FIT field ${field.number} requires a finite numeric value`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
default:
|
|
184
|
+
this.assertNumberInRange(field.value, 0, this.getUnsignedMaximum(field.baseType), field.number);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
assertNumberInRange(value, minimum, maximum, fieldNumber) {
|
|
188
|
+
if (typeof value !== 'number'
|
|
189
|
+
|| !Number.isInteger(value)
|
|
190
|
+
|| value < minimum
|
|
191
|
+
|| value > maximum) {
|
|
192
|
+
throw new RangeError(`FIT field ${fieldNumber} must be an integer between ${minimum} and ${maximum}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
assertBigIntInRange(value, minimum, maximum, fieldNumber) {
|
|
196
|
+
if (typeof value !== 'bigint' || value < minimum || value > maximum) {
|
|
197
|
+
throw new RangeError(`FIT field ${fieldNumber} must be a bigint between ${minimum} and ${maximum}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
getUnsignedMaximum(baseType) {
|
|
201
|
+
switch (baseType) {
|
|
202
|
+
case FitBaseType.Enum:
|
|
203
|
+
case FitBaseType.Uint8:
|
|
204
|
+
case FitBaseType.Uint8z:
|
|
205
|
+
case FitBaseType.Byte:
|
|
206
|
+
return UINT8_MAX;
|
|
207
|
+
case FitBaseType.Uint16:
|
|
208
|
+
case FitBaseType.Uint16z:
|
|
209
|
+
return UINT16_MAX;
|
|
210
|
+
case FitBaseType.Uint32:
|
|
211
|
+
case FitBaseType.Uint32z:
|
|
212
|
+
return UINT32_MAX;
|
|
213
|
+
default:
|
|
214
|
+
throw new RangeError(`Unsupported FIT base type ${baseType}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
getFieldDefinition(field) {
|
|
218
|
+
return { number: field.number, size: field.size, baseType: field.baseType };
|
|
219
|
+
}
|
|
220
|
+
writeDefinition(localMessageNumber, globalMessageNumber, fields) {
|
|
221
|
+
this.writeUInt8(0x40 | localMessageNumber);
|
|
222
|
+
this.writeUInt8(0);
|
|
223
|
+
this.writeUInt8(0);
|
|
224
|
+
this.writeUInt16(globalMessageNumber);
|
|
225
|
+
this.writeUInt8(fields.length);
|
|
226
|
+
fields.forEach((field) => {
|
|
227
|
+
this.writeUInt8(field.number);
|
|
228
|
+
this.writeUInt8(field.size);
|
|
229
|
+
this.writeUInt8(field.baseType);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
writeFieldValue(field) {
|
|
233
|
+
if (field.value instanceof Uint8Array) {
|
|
234
|
+
field.value.forEach(value => this.writeUInt8(value));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
switch (field.baseType) {
|
|
238
|
+
case FitBaseType.Enum:
|
|
239
|
+
case FitBaseType.Uint8:
|
|
240
|
+
case FitBaseType.Uint8z:
|
|
241
|
+
case FitBaseType.Byte:
|
|
242
|
+
this.writeUInt8(field.value);
|
|
243
|
+
return;
|
|
244
|
+
case FitBaseType.Sint8:
|
|
245
|
+
this.writeInt8(field.value);
|
|
246
|
+
return;
|
|
247
|
+
case FitBaseType.Uint16:
|
|
248
|
+
case FitBaseType.Uint16z:
|
|
249
|
+
this.writeUInt16(field.value);
|
|
250
|
+
return;
|
|
251
|
+
case FitBaseType.Sint16:
|
|
252
|
+
this.writeInt16(field.value);
|
|
253
|
+
return;
|
|
254
|
+
case FitBaseType.Uint32:
|
|
255
|
+
case FitBaseType.Uint32z:
|
|
256
|
+
this.writeUInt32(field.value);
|
|
257
|
+
return;
|
|
258
|
+
case FitBaseType.Sint32:
|
|
259
|
+
this.writeInt32(field.value);
|
|
260
|
+
return;
|
|
261
|
+
case FitBaseType.Float32:
|
|
262
|
+
this.writeFloat32(field.value);
|
|
263
|
+
return;
|
|
264
|
+
case FitBaseType.Float64:
|
|
265
|
+
this.writeFloat64(field.value);
|
|
266
|
+
return;
|
|
267
|
+
case FitBaseType.Sint64:
|
|
268
|
+
this.writeInt64(field.value);
|
|
269
|
+
return;
|
|
270
|
+
case FitBaseType.Uint64:
|
|
271
|
+
case FitBaseType.Uint64z:
|
|
272
|
+
this.writeUInt64(field.value);
|
|
273
|
+
return;
|
|
274
|
+
default:
|
|
275
|
+
throw new Error(`Unsupported FIT base type ${field.baseType}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
getBaseTypeSize(baseType) {
|
|
279
|
+
switch (baseType) {
|
|
280
|
+
case FitBaseType.Enum:
|
|
281
|
+
case FitBaseType.Sint8:
|
|
282
|
+
case FitBaseType.Uint8:
|
|
283
|
+
case FitBaseType.Uint8z:
|
|
284
|
+
case FitBaseType.Byte:
|
|
285
|
+
return 1;
|
|
286
|
+
case FitBaseType.Sint16:
|
|
287
|
+
case FitBaseType.Uint16:
|
|
288
|
+
case FitBaseType.Uint16z:
|
|
289
|
+
return 2;
|
|
290
|
+
case FitBaseType.Sint32:
|
|
291
|
+
case FitBaseType.Uint32:
|
|
292
|
+
case FitBaseType.Uint32z:
|
|
293
|
+
case FitBaseType.Float32:
|
|
294
|
+
return 4;
|
|
295
|
+
case FitBaseType.Float64:
|
|
296
|
+
case FitBaseType.Sint64:
|
|
297
|
+
case FitBaseType.Uint64:
|
|
298
|
+
case FitBaseType.Uint64z:
|
|
299
|
+
return 8;
|
|
300
|
+
default:
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
isSupportedBaseType(baseType) {
|
|
305
|
+
return (baseType === FitBaseType.String
|
|
306
|
+
|| this.getBaseTypeSize(baseType) !== undefined);
|
|
307
|
+
}
|
|
308
|
+
writeUInt8(value) {
|
|
309
|
+
this.data.push(value);
|
|
310
|
+
}
|
|
311
|
+
writeUInt16(value) {
|
|
312
|
+
this.data.push(value & UINT8_MAX, (value >>> 8) & UINT8_MAX);
|
|
313
|
+
}
|
|
314
|
+
writeInt8(value) {
|
|
315
|
+
this.writeUInt8(value < 0 ? 0x100 + value : value);
|
|
316
|
+
}
|
|
317
|
+
writeInt16(value) {
|
|
318
|
+
this.writeUInt16(value < 0 ? 0x10000 + value : value);
|
|
319
|
+
}
|
|
320
|
+
writeUInt32(value) {
|
|
321
|
+
this.data.push(value & UINT8_MAX, Math.floor(value / 0x100) & UINT8_MAX, Math.floor(value / 0x10000) & UINT8_MAX, Math.floor(value / 0x1000000) & UINT8_MAX);
|
|
322
|
+
}
|
|
323
|
+
writeInt32(value) {
|
|
324
|
+
this.writeUInt32(value < 0 ? 0x100000000 + value : value);
|
|
325
|
+
}
|
|
326
|
+
writeUInt64(value) {
|
|
327
|
+
for (let byteIndex = BIGINT_ZERO; byteIndex < BIGINT_EIGHT; byteIndex++) {
|
|
328
|
+
this.writeUInt8(Number((value >> (byteIndex * BIGINT_EIGHT)) & BIGINT_BYTE_MASK));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
writeInt64(value) {
|
|
332
|
+
this.writeUInt64(value < BIGINT_ZERO ? UINT64_MODULUS + value : value);
|
|
333
|
+
}
|
|
334
|
+
writeFloat32(value) {
|
|
335
|
+
const bytes = new Uint8Array(4);
|
|
336
|
+
new DataView(bytes.buffer).setFloat32(0, value, true);
|
|
337
|
+
bytes.forEach(byte => this.writeUInt8(byte));
|
|
338
|
+
}
|
|
339
|
+
writeFloat64(value) {
|
|
340
|
+
const bytes = new Uint8Array(8);
|
|
341
|
+
new DataView(bytes.buffer).setFloat64(0, value, true);
|
|
342
|
+
bytes.forEach(byte => this.writeUInt8(byte));
|
|
343
|
+
}
|
|
344
|
+
static assertIntegerInRange(value, minimum, maximum, label) {
|
|
345
|
+
if (typeof value !== 'number'
|
|
346
|
+
|| !Number.isInteger(value)
|
|
347
|
+
|| value < minimum
|
|
348
|
+
|| value > maximum) {
|
|
349
|
+
throw new RangeError(`${label} must be an integer between ${minimum} and ${maximum}`);
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
exports.FitEncoder = FitEncoder;
|
package/dist/cjs/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/cjs/fit-parser.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FitEncoder = exports.FitBaseType = void 0;
|
|
3
4
|
const binary_js_1 = require("./binary.js");
|
|
4
5
|
const helper_js_1 = require("./helper.js");
|
|
6
|
+
var fit_encoder_js_1 = require("./fit-encoder.js");
|
|
7
|
+
Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function () { return fit_encoder_js_1.FitBaseType; } });
|
|
8
|
+
Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
|
|
5
9
|
class FitParser {
|
|
6
10
|
constructor(options = {}) {
|
|
7
11
|
this.options = {
|
package/dist/cjs/fit.js
CHANGED
|
@@ -3734,6 +3734,7 @@ exports.FIT = {
|
|
|
3734
3734
|
offset: 0,
|
|
3735
3735
|
units: '',
|
|
3736
3736
|
},
|
|
3737
|
+
7: { field: 'sub_sport', type: 'sub_sport', scale: null, offset: 0, units: '' },
|
|
3737
3738
|
},
|
|
3738
3739
|
32: {
|
|
3739
3740
|
name: 'course_point',
|
|
@@ -6149,6 +6150,33 @@ exports.FIT = {
|
|
|
6149
6150
|
23: 'u_turn',
|
|
6150
6151
|
24: 'segment_start',
|
|
6151
6152
|
25: 'segment_end',
|
|
6153
|
+
27: 'campsite',
|
|
6154
|
+
28: 'aid_station',
|
|
6155
|
+
29: 'rest_area',
|
|
6156
|
+
30: 'general_distance',
|
|
6157
|
+
31: 'service',
|
|
6158
|
+
32: 'energy_gel',
|
|
6159
|
+
33: 'sports_drink',
|
|
6160
|
+
34: 'mile_marker',
|
|
6161
|
+
35: 'checkpoint',
|
|
6162
|
+
36: 'shelter',
|
|
6163
|
+
37: 'meeting_spot',
|
|
6164
|
+
38: 'overlook',
|
|
6165
|
+
39: 'toilet',
|
|
6166
|
+
40: 'shower',
|
|
6167
|
+
41: 'gear',
|
|
6168
|
+
42: 'sharp_curve',
|
|
6169
|
+
43: 'steep_incline',
|
|
6170
|
+
44: 'tunnel',
|
|
6171
|
+
45: 'bridge',
|
|
6172
|
+
46: 'obstacle',
|
|
6173
|
+
47: 'crossing',
|
|
6174
|
+
48: 'store',
|
|
6175
|
+
49: 'transition',
|
|
6176
|
+
50: 'navaid',
|
|
6177
|
+
51: 'transport',
|
|
6178
|
+
52: 'alert',
|
|
6179
|
+
53: 'info',
|
|
6152
6180
|
},
|
|
6153
6181
|
manufacturer: {
|
|
6154
6182
|
0: 0,
|
package/dist/cjs/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';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** FIT definition base-type bytes, including the endian flag where required. */
|
|
2
|
+
export declare enum FitBaseType {
|
|
3
|
+
Enum = 0,
|
|
4
|
+
Sint8 = 1,
|
|
5
|
+
Uint8 = 2,
|
|
6
|
+
String = 7,
|
|
7
|
+
Uint8z = 10,
|
|
8
|
+
Byte = 13,
|
|
9
|
+
Sint16 = 131,
|
|
10
|
+
Uint16 = 132,
|
|
11
|
+
Sint32 = 133,
|
|
12
|
+
Uint32 = 134,
|
|
13
|
+
Float32 = 136,
|
|
14
|
+
Float64 = 137,
|
|
15
|
+
Uint16z = 139,
|
|
16
|
+
Uint32z = 140,
|
|
17
|
+
Sint64 = 142,
|
|
18
|
+
Uint64 = 143,
|
|
19
|
+
Uint64z = 144
|
|
20
|
+
}
|
|
21
|
+
export interface FitEncoderField {
|
|
22
|
+
number: number;
|
|
23
|
+
size: number;
|
|
24
|
+
baseType: FitBaseType | number;
|
|
25
|
+
value: number | bigint | Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
export interface FitEncoderOptions {
|
|
28
|
+
protocolVersion?: number;
|
|
29
|
+
profileVersion?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A generic FIT binary encoder. Callers supply profile-specific field
|
|
33
|
+
* definitions and already-scaled field values. Numeric arrays, strings, and
|
|
34
|
+
* variable-length field values are supplied as raw `Uint8Array` values.
|
|
35
|
+
*/
|
|
36
|
+
export declare class FitEncoder {
|
|
37
|
+
private readonly data;
|
|
38
|
+
private readonly activeDefinitions;
|
|
39
|
+
private readonly protocolVersion;
|
|
40
|
+
private readonly profileVersion;
|
|
41
|
+
constructor(options?: FitEncoderOptions);
|
|
42
|
+
writeMessage(globalMessageNumber: number, fields: FitEncoderField[], localMessageNumber?: number): this;
|
|
43
|
+
close(): Uint8Array;
|
|
44
|
+
static string(value: string): Uint8Array;
|
|
45
|
+
static toFitTimestamp(date: Date): number;
|
|
46
|
+
static calculateCRC(bytes: ArrayLike<number>): number;
|
|
47
|
+
private validateMessage;
|
|
48
|
+
private validateField;
|
|
49
|
+
private assertNumberInRange;
|
|
50
|
+
private assertBigIntInRange;
|
|
51
|
+
private getUnsignedMaximum;
|
|
52
|
+
private getFieldDefinition;
|
|
53
|
+
private writeDefinition;
|
|
54
|
+
private writeFieldValue;
|
|
55
|
+
private getBaseTypeSize;
|
|
56
|
+
private isSupportedBaseType;
|
|
57
|
+
private writeUInt8;
|
|
58
|
+
private writeUInt16;
|
|
59
|
+
private writeInt8;
|
|
60
|
+
private writeInt16;
|
|
61
|
+
private writeUInt32;
|
|
62
|
+
private writeInt32;
|
|
63
|
+
private writeUInt64;
|
|
64
|
+
private writeInt64;
|
|
65
|
+
private writeFloat32;
|
|
66
|
+
private writeFloat64;
|
|
67
|
+
private static assertIntegerInRange;
|
|
68
|
+
}
|