fit-file-parser 4.0.1 → 4.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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Change Log
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 4.1.0
6
+
7
+ ### Added
8
+
9
+ - Generate all 124 standard messages, 1,406 fields, and 200 profile types from
10
+ the exactly pinned Garmin FIT SDK 21.208.0 profile.
11
+ - Retain every recognized message in file order under the typed
12
+ `ParsedFit.messages` index without changing existing list, cascade, or
13
+ singleton outputs.
14
+ - Decode Garmin strength-training `set` messages in list and cascade modes.
15
+ - Add reproducible generated-profile and privacy-safe external corpus audits,
16
+ and enforce profile freshness and coverage in CI.
17
+
18
+ ### Fixed
19
+
20
+ - Decode fields from their wire base types, including compatible developer
21
+ enum/uint8/byte definitions and correctly sized numeric arrays.
22
+ - Reconstruct compressed timestamps and keep timestamp state isolated between
23
+ parser instances.
24
+ - Accept omitted header CRCs, validate file CRCs across the complete FIT header
25
+ and data section, and report strict header and file CRC failures explicitly.
26
+ - Reject structurally unsafe FIT inputs consistently in callback and Promise
27
+ APIs while retaining force-mode recovery for CRC corruption.
28
+ - Correct Garmin profile mappings for OHR settings, monitoring HR, sleep,
29
+ time-in-zone, altitude offsets, and lap/segment flow and grit summaries.
30
+ - Correct Celsius-to-Kelvin conversion, add `celsius` as the canonical
31
+ temperature unit, and retain `°C` as a supported alias.
32
+
33
+ ### Compatibility and documentation
34
+
35
+ - Preserve compatible legacy field names, scales, value shapes, parser
36
+ signatures, output modes, and date behavior while adding canonical profile
37
+ names.
38
+ - Add regression coverage for temperature, pressure, validation, compressed
39
+ timestamps, generated profile messages, repeated messages, and MTB
40
+ flow/grit data.
41
+ - Refresh the README with current runtime, API, units, output modes, developer
42
+ fields, encoder behavior, and repository commands.
43
+
44
+ ## 4.0.2
45
+
46
+ - Preserve record alignment when developer-field descriptions are missing or
47
+ appear after their message definitions.
48
+ - Decode subsequent developer-field values once their descriptions become
49
+ available, in both strict and force modes.
50
+
3
51
  ## 4.0.0
4
52
 
5
53
  ### FIT decoder performance
package/README.md CHANGED
@@ -1,181 +1,252 @@
1
1
  # fit-file-parser
2
2
 
3
- > Parse your .FIT files easily, directly from JS.
4
- > Written in Typescript
3
+ [![CI](https://github.com/jimmykane/fit-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/jimmykane/fit-parser/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/fit-file-parser.svg)](https://www.npmjs.com/package/fit-file-parser)
5
+ [![license](https://img.shields.io/npm/l/fit-file-parser.svg)](./LICENSE)
5
6
 
6
- ## Install
7
+ Parse and encode FIT files in JavaScript and TypeScript. The parser supports
8
+ files produced by Garmin, Polar, Suunto, and other FIT-compatible devices,
9
+ including developer-defined data.
7
10
 
8
- ```
9
- $ npm install fit-file-parser --save
10
- ```
11
+ ## Features
12
+
13
+ - Parse Node.js `Buffer` and standard `ArrayBuffer` inputs.
14
+ - Choose flat lists, nested activity data, or both output shapes.
15
+ - Convert speed, length, temperature, and pressure fields to preferred units.
16
+ - Decode developer fields while preserving record alignment when descriptions
17
+ arrive after their definitions.
18
+ - Encode profile-agnostic FIT messages with validated field definitions and
19
+ CRCs.
20
+ - Use ESM or CommonJS with bundled TypeScript declarations.
21
+
22
+ ## Requirements
11
23
 
12
- ## How to use
24
+ - Node.js 20 or newer
13
25
 
14
- See in [examples](./examples) folder:
26
+ ## Installation
15
27
 
16
- ### using callbacks
28
+ ```sh
29
+ npm install fit-file-parser
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ The Promise API is the simplest way to parse a file:
17
35
 
18
36
  ```javascript
19
- import fs from 'node:fs/promises'
37
+ import { readFile } from 'node:fs/promises'
20
38
  import FitParser from 'fit-file-parser'
21
39
 
22
- fs.readFile('./example.fit', (err, content) => {
23
- // Create a FitParser instance (options argument is optional)
24
- if (err) {
25
- console.error(err)
26
- }
27
- const fitParser = new FitParser({
28
- force: true,
29
- speedUnit: 'km/h',
30
- lengthUnit: 'km',
31
- temperatureUnit: 'kelvin',
32
- pressureUnit: 'bar', // accept bar, cbar and psi (default is bar)
33
- elapsedRecordField: true,
34
- mode: 'cascade',
35
- })
40
+ const content = await readFile('./activity.fit')
41
+ const parser = new FitParser({
42
+ mode: 'list',
43
+ speedUnit: 'km/h',
44
+ lengthUnit: 'km',
45
+ })
36
46
 
37
- // Parse your file
38
- fitParser.parse(content, (error, data) => {
39
- // Handle result of parse method
40
- if (error) {
41
- console.error(error)
42
- }
43
- else {
44
- console.log(JSON.stringify(data))
45
- }
46
- })
47
+ const data = await parser.parseAsync(content)
48
+
49
+ console.log({
50
+ sessions: data.sessions?.length ?? 0,
51
+ laps: data.laps?.length ?? 0,
52
+ records: data.records?.length ?? 0,
47
53
  })
48
54
  ```
49
55
 
50
- ### using async/await
56
+ ### Callback API
51
57
 
52
58
  ```javascript
53
- import fs from 'node:fs/promises'
59
+ import { readFile } from 'node:fs'
54
60
  import FitParser from 'fit-file-parser'
55
61
 
56
- const buffer = await fs.readFile('./example.fit')
57
- const fitObject = await fitParser.parseAsync(buffer)
62
+ readFile('./activity.fit', (readError, content) => {
63
+ if (readError) {
64
+ console.error(readError)
65
+ return
66
+ }
67
+
68
+ const parser = new FitParser()
69
+ parser.parse(content, (parseError, data) => {
70
+ if (parseError) {
71
+ console.error(parseError)
72
+ return
73
+ }
74
+
75
+ console.log(data)
76
+ })
77
+ })
58
78
  ```
59
79
 
60
- ## Encoding
80
+ Parser errors are strings. `parseAsync()` rejects with the same value that the
81
+ callback API receives as its first argument.
61
82
 
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.
83
+ ## Parser options
67
84
 
68
- ```javascript
69
- import { FitBaseType, FitEncoder } from 'fit-file-parser'
85
+ All options are optional.
70
86
 
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
- ])
87
+ | Option | Values | Default | Behavior |
88
+ | -------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
90
+ | `force` | `true`, `false` | `true` | Skips header and file CRC validation and enables supported best-effort field recovery. Structural header and data bounds are always validated. |
91
+ | `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
92
+ | `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
93
+ | `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
94
+ | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
95
+ | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
76
96
 
77
- const fitBytes = encoder.close()
78
- ```
97
+ `force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
98
+ too short, have an invalid header size or signature, or declare data beyond the
99
+ available bytes are rejected in both modes.
79
100
 
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.
101
+ ## Output modes
84
102
 
85
- ## Development
103
+ The mode controls where sessions, laps, records, and related activity
104
+ collections are exposed.
86
105
 
87
- To build the project, run:
106
+ | Mode | Root lists | Nested under `activity` | Default |
107
+ | --------- | ---------- | ----------------------- | ------- |
108
+ | `list` | Yes | No | Yes |
109
+ | `cascade` | No | Yes | No |
110
+ | `both` | Yes | Yes | No |
88
111
 
89
- ```
90
- npm run build
91
- ```
112
+ In cascade output, sessions contain their laps and laps contain their records
113
+ and lengths. Other parsed FIT message collections remain available where the
114
+ parser exposes them.
92
115
 
93
- To run tests, run:
116
+ Every recognized message is also retained in file order in `data.messages`.
117
+ This additive index is useful for message kinds that historically exposed
118
+ only the last value at the root:
94
119
 
95
- ```
96
- npm test
120
+ ```javascript
121
+ const workoutSteps = data.messages?.workout_step ?? []
122
+ const diveSummaries = data.messages?.dive_summary ?? []
97
123
  ```
98
124
 
99
- To rebuild the typescript types (as they are autogenerated from the `FIT` object) run:
125
+ Existing root lists, cascade nesting, and last-message root properties remain
126
+ unchanged.
100
127
 
101
- ```npm run codegen
128
+ ## Inputs
102
129
 
103
- ```
130
+ Both parser methods accept:
104
131
 
105
- > this will update the file `src/fit_types.ts` as it is only running with node (with type stripping), regenerating them requires node>=245
132
+ - Node.js `Buffer`
133
+ - `ArrayBuffer`
106
134
 
107
- To run the codegen in watch mode during local development, run:
135
+ ```javascript
136
+ const parsed = await new FitParser().parseAsync(arrayBuffer)
137
+ ```
108
138
 
109
- ```npm run dev
139
+ ## Developer fields
110
140
 
111
- ```
141
+ FIT producers may define custom fields outside the standard profile. The
142
+ parser consumes every developer field's declared byte size so later messages
143
+ stay aligned. If a field description is not available yet, that value is
144
+ omitted. Subsequent values are decoded by name once the description appears.
112
145
 
113
- To build the local examples, run:
146
+ ## Encoding
114
147
 
115
- ```
116
- npm run examples
117
- ```
148
+ `FitEncoder` writes FIT headers, message definitions, data messages, and CRCs.
149
+ It is profile-agnostic: callers provide profile message and field numbers,
150
+ base types, sizes, and values in their raw FIT representation. Applying FIT
151
+ scales and offsets is the caller's responsibility.
118
152
 
119
- To lint and format the code, run:
153
+ ```javascript
154
+ import { FitBaseType, FitEncoder } from 'fit-file-parser'
120
155
 
121
- ```
122
- npm run lint
156
+ const encoder = new FitEncoder()
157
+ encoder.writeMessage(0, [
158
+ {
159
+ number: 0,
160
+ size: 1,
161
+ baseType: FitBaseType.Enum,
162
+ value: 6,
163
+ },
164
+ {
165
+ number: 4,
166
+ size: 4,
167
+ baseType: FitBaseType.Uint32,
168
+ value: FitEncoder.toFitTimestamp(new Date()),
169
+ },
170
+ ])
171
+
172
+ const fitBytes = encoder.close()
123
173
  ```
124
174
 
125
- To run the typechecker, run:
175
+ `writeMessage(globalMessageNumber, fields, localMessageNumber?)` accepts local
176
+ message numbers from 0 through 15. Definitions are emitted automatically and
177
+ reused until the shape assigned to that local number changes. `close()` returns
178
+ a `Uint8Array`.
126
179
 
127
- ```
128
- npm run type-check
129
- ```
180
+ The encoder also provides:
130
181
 
131
- ## API Documentation
182
+ - `FitEncoder.string(value)` for null-terminated UTF-8 field bytes.
183
+ - `FitEncoder.toFitTimestamp(value)` for FIT timestamps.
184
+ - `FitEncoder.calculateCRC(bytes)` for FIT-compatible CRC calculation.
132
185
 
133
- ### new FitParser(Object _options_)
186
+ Scalar 64-bit values use `bigint`. Strings, numeric arrays, and other
187
+ variable-length values use exact-size `Uint8Array` values. Invalid field
188
+ definitions or numeric ranges throw before a partial message is written.
134
189
 
135
- Needed to create a new instance. _options_ is optional, and is used to customize the returned object.
190
+ ## TypeScript and module formats
136
191
 
137
- Allowed properties :
192
+ The package includes TypeScript declarations and exports
193
+ `FitParserOptions`, `FitEncoderField`, and `FitEncoderOptions`.
138
194
 
139
- - `mode`: String
140
- - `cascade`: Returned object is organized as a tree, eg. each lap contains a `records` fields, that is an array of its records (**default**)
141
- - `list`: Returned object is organized as lists of sessions, laps, records, etc..., without parent-child relation
142
- - `both`: A mix of the two other modes, eg. `records` are available inside the root field as well as inside each laps
143
- - `lengthUnit`: String
144
- - `m`: Lengths are in meters (**default**)
145
- - `km`: Lengths are in kilometers
146
- - `mi`: Lengths are in miles
147
- - `temperatureUnit`: String
148
- - `celsius`:Temperatures are in °C (**default**)
149
- - `kelvin`: Temperatures are in °K
150
- - `fahrenheit`: Temperatures are in °F
151
- - `speedUnit`: String
152
- - `m/s`: Speeds are in meters per seconds (**default**)
153
- - `km/h`: Speeds are in kilometers per hour
154
- - `mph`: Speeds are in miles per hour
155
- - `force`: Boolean
156
- - `true`: Continues even if they are errors (**default for now**)
157
- - `false`: Stops if an error occurs
158
- - `elapsedRecordField`: Boolean
159
- - `true`: Includes `elapsed_time`, containing the elapsed time in seconds since the first record, and `timer_time`, containing the time shown on the device, inside each `record` field
160
- - `false` (**default**)
195
+ ESM:
161
196
 
162
- ### fitParser.parse(Buffer _file_, Function _callback_)
197
+ ```javascript
198
+ import FitParser, { FitBaseType, FitEncoder } from 'fit-file-parser'
199
+ ```
163
200
 
164
- _callback_ receives two arguments, the first as a error String, and the second as Object, result of parsing.
201
+ CommonJS:
165
202
 
166
- ### fitParser.parseAsync(Buffer _file_)
203
+ ```javascript
204
+ const {
205
+ default: FitParser,
206
+ FitBaseType,
207
+ FitEncoder,
208
+ } = require('fit-file-parser')
209
+ ```
167
210
 
168
- returns a Promise that resolves to the result of parsing.
211
+ ## Development
169
212
 
170
- ## Contributors
213
+ Run commands from the repository root.
214
+
215
+ | Command | Purpose |
216
+ | ---------------------------------- | -------------------------------------------------- |
217
+ | `npm ci` | Install locked dependencies. |
218
+ | `npm run build` | Build ESM and CommonJS output. |
219
+ | `npm test -- --run` | Run the complete test suite once. |
220
+ | `npm test -- --run test/<file>.ts` | Run a focused test file. |
221
+ | `npm run codegen` | Regenerate the Garmin profile and public types. |
222
+ | `npm run codegen:check` | Verify both generated files are current. |
223
+ | `npm run profile:audit` | Audit SDK profile coverage and private overlays. |
224
+ | `npm run corpus:check -- <path>` | Validate an external FIT corpus without file data. |
225
+ | `npm run lint` | Check lint and formatting rules. |
226
+ | `npm run fmt` | Apply the configured formatting rules. |
227
+ | `npm run type-check` | Run TypeScript without emitting files. |
228
+ | `npm run examples` | Build and regenerate checked-in example outputs. |
229
+ | `npm run check` | Run profile audit, lint, types, tests, and builds. |
230
+
231
+ Do not edit `src/garmin_profile.generated.ts` or `src/fit_types.ts` manually.
232
+ Update the pinned SDK, compatibility overrides, or a generator, then run
233
+ `npm run codegen`.
234
+
235
+ Repository-specific automation guidance is tracked in
236
+ [`.agent/README.md`](./.agent/README.md). More examples are available in the
237
+ [`examples`](./examples) directory, and release notes are in the
238
+ [`CHANGELOG`](./CHANGELOG.md).
171
239
 
172
- All started thanks to [Pierre Jacquier](https://github.com/pierremtb)
240
+ ## Contributors
173
241
 
174
- Big thanks to [Mikael Lofjärd](https://github.com/mlofjard) for [his early prototype](https://github.com/mlofjard/jsonfit).
175
- See [CONTRIBUTORS](./CONTRIBUTORS.md).
242
+ This project started from work by
243
+ [Pierre Jacquier](https://github.com/pierremtb). Thanks to
244
+ [Mikael Lofjärd](https://github.com/mlofjard) for
245
+ [his early prototype](https://github.com/mlofjard/jsonfit), and to everyone in
246
+ [`CONTRIBUTORS.md`](./CONTRIBUTORS.md).
176
247
 
177
248
  ## License
178
249
 
179
- MIT license; see [LICENSE](./LICENSE).
250
+ MIT; see [`LICENSE`](./LICENSE).
180
251
 
181
- (c) 2019 Dimitrios Kanellopoulos
252
+ Copyright 2019-present Dimitrios Kanellopoulos.
package/dist/binary.d.ts CHANGED
@@ -7,10 +7,22 @@ export interface MessageTypeDefinition {
7
7
  globalMessageNumber: number;
8
8
  numberOfFields: number;
9
9
  fieldDefs: FieldDefinition[];
10
+ developerFieldDefs?: DeveloperFieldDefinition[];
10
11
  rawData?: any[];
11
12
  }
13
+ export interface DeveloperFieldDefinition {
14
+ developerDataIndex: number;
15
+ fieldDefinitionNumber: number;
16
+ size: number;
17
+ resolvedFieldDef?: FieldDefinition;
18
+ resolvedFrom?: unknown;
19
+ }
20
+ export interface DecoderState {
21
+ lastTimestamp?: number;
22
+ monitoringTimestamp?: number;
23
+ }
12
24
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
13
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
25
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
14
26
  messageType: MesgNum | '';
15
27
  nextIndex: number;
16
28
  message?: any;