fit-file-parser 3.0.2 → 3.1.3
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/binary.js +1 -0
- package/dist/cjs/binary.js +1 -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 +8 -0
- package/dist/cjs/fit_types.d.ts +2 -0
- 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 +8 -0
- package/dist/fit_types.d.ts +2 -0
- package/package.json +10 -9
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:
|
package/dist/binary.js
CHANGED
|
@@ -254,6 +254,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
254
254
|
case 'gps_accuracy':
|
|
255
255
|
return convertTo(data, 'lengthUnits', options.lengthUnit);
|
|
256
256
|
case 'temperature':
|
|
257
|
+
case 'min_temperature':
|
|
257
258
|
case 'avg_temperature':
|
|
258
259
|
case 'max_temperature':
|
|
259
260
|
return convertTo(data, 'temperatureUnits', options.temperatureUnit);
|
package/dist/cjs/binary.js
CHANGED
|
@@ -260,6 +260,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
260
260
|
case 'gps_accuracy':
|
|
261
261
|
return convertTo(data, 'lengthUnits', options.lengthUnit);
|
|
262
262
|
case 'temperature':
|
|
263
|
+
case 'min_temperature':
|
|
263
264
|
case 'avg_temperature':
|
|
264
265
|
case 'max_temperature':
|
|
265
266
|
return convertTo(data, 'temperatureUnits', options.temperatureUnit);
|
|
@@ -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
|
@@ -2554,6 +2554,13 @@ exports.FIT = {
|
|
|
2554
2554
|
},
|
|
2555
2555
|
20: {
|
|
2556
2556
|
name: 'record',
|
|
2557
|
+
90: {
|
|
2558
|
+
field: 'garmin_performance_condition',
|
|
2559
|
+
type: 'sint8',
|
|
2560
|
+
scale: null,
|
|
2561
|
+
offset: 0,
|
|
2562
|
+
units: '',
|
|
2563
|
+
},
|
|
2557
2564
|
253: {
|
|
2558
2565
|
field: 'timestamp',
|
|
2559
2566
|
type: 'date_time',
|
|
@@ -3734,6 +3741,7 @@ exports.FIT = {
|
|
|
3734
3741
|
offset: 0,
|
|
3735
3742
|
units: '',
|
|
3736
3743
|
},
|
|
3744
|
+
7: { field: 'sub_sport', type: 'sub_sport', scale: null, offset: 0, units: '' },
|
|
3737
3745
|
},
|
|
3738
3746
|
32: {
|
|
3739
3747
|
name: 'course_point',
|
package/dist/cjs/fit_types.d.ts
CHANGED
|
@@ -642,6 +642,7 @@ export interface ParsedRecord {
|
|
|
642
642
|
vertical_ratio?: number;
|
|
643
643
|
stance_time_balance?: number;
|
|
644
644
|
step_length?: number;
|
|
645
|
+
garmin_performance_condition?: number;
|
|
645
646
|
absolute_pressure?: number;
|
|
646
647
|
depth?: number;
|
|
647
648
|
next_stop_depth?: number;
|
|
@@ -729,6 +730,7 @@ export interface ParsedCourse {
|
|
|
729
730
|
sport?: Sport;
|
|
730
731
|
name?: string;
|
|
731
732
|
capabilities?: CourseCapabilities;
|
|
733
|
+
sub_sport?: SubSport;
|
|
732
734
|
}
|
|
733
735
|
export interface ParsedCoursePoint {
|
|
734
736
|
timestamp: string;
|
|
@@ -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,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
|
@@ -2551,6 +2551,13 @@ export const FIT = {
|
|
|
2551
2551
|
},
|
|
2552
2552
|
20: {
|
|
2553
2553
|
name: 'record',
|
|
2554
|
+
90: {
|
|
2555
|
+
field: 'garmin_performance_condition',
|
|
2556
|
+
type: 'sint8',
|
|
2557
|
+
scale: null,
|
|
2558
|
+
offset: 0,
|
|
2559
|
+
units: '',
|
|
2560
|
+
},
|
|
2554
2561
|
253: {
|
|
2555
2562
|
field: 'timestamp',
|
|
2556
2563
|
type: 'date_time',
|
|
@@ -3731,6 +3738,7 @@ export const FIT = {
|
|
|
3731
3738
|
offset: 0,
|
|
3732
3739
|
units: '',
|
|
3733
3740
|
},
|
|
3741
|
+
7: { field: 'sub_sport', type: 'sub_sport', scale: null, offset: 0, units: '' },
|
|
3734
3742
|
},
|
|
3735
3743
|
32: {
|
|
3736
3744
|
name: 'course_point',
|
package/dist/fit_types.d.ts
CHANGED
|
@@ -642,6 +642,7 @@ export interface ParsedRecord {
|
|
|
642
642
|
vertical_ratio?: number;
|
|
643
643
|
stance_time_balance?: number;
|
|
644
644
|
step_length?: number;
|
|
645
|
+
garmin_performance_condition?: number;
|
|
645
646
|
absolute_pressure?: number;
|
|
646
647
|
depth?: number;
|
|
647
648
|
next_stop_depth?: number;
|
|
@@ -729,6 +730,7 @@ export interface ParsedCourse {
|
|
|
729
730
|
sport?: Sport;
|
|
730
731
|
name?: string;
|
|
731
732
|
capabilities?: CourseCapabilities;
|
|
733
|
+
sub_sport?: SubSport;
|
|
732
734
|
}
|
|
733
735
|
export interface ParsedCoursePoint {
|
|
734
736
|
timestamp: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fit-file-parser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.3",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
|
|
7
7
|
"author": {
|
|
@@ -32,13 +32,6 @@
|
|
|
32
32
|
"garmin",
|
|
33
33
|
"parse"
|
|
34
34
|
],
|
|
35
|
-
"files": [
|
|
36
|
-
"dist/",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE",
|
|
39
|
-
"CHANGELOG.md",
|
|
40
|
-
"CONTRIBUTORS.md"
|
|
41
|
-
],
|
|
42
35
|
"exports": {
|
|
43
36
|
".": {
|
|
44
37
|
"types": "./dist/fit-parser.d.ts",
|
|
@@ -47,6 +40,13 @@
|
|
|
47
40
|
}
|
|
48
41
|
},
|
|
49
42
|
"main": "dist/cjs/fit-parser.js",
|
|
43
|
+
"files": [
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"CONTRIBUTORS.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"README.md",
|
|
48
|
+
"dist/"
|
|
49
|
+
],
|
|
50
50
|
"maintainers": [
|
|
51
51
|
{
|
|
52
52
|
"email": "jimmykane9@gmail.com",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"lint": "eslint .",
|
|
67
67
|
"fmt": "eslint --fix .",
|
|
68
68
|
"type-check": "tsc --noEmit",
|
|
69
|
-
"build": "tsc --noCheck && tsc --project tsconfig.cjs.json --noCheck && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json"
|
|
69
|
+
"build": "tsc --noCheck && tsc --project tsconfig.cjs.json --noCheck && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
|
70
|
+
"prepack": "npm run build"
|
|
70
71
|
},
|
|
71
72
|
"test": "vitest",
|
|
72
73
|
"dependencies": {
|