fit-file-parser 4.1.0 → 5.0.2

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
@@ -2,6 +2,65 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.0.2
6
+
7
+ ### Fixed
8
+
9
+ - Clarify the 5.0 migration for standard session field 196: use canonical
10
+ `metabolic_calories` rather than the old duplicate `resting_calories` label.
11
+ - Clarify that the removed guessed `recovery_advisor` label was standard
12
+ session field 140, which the SDK defines as `avg_depth` in meters.
13
+
14
+ ## 5.0.1
15
+
16
+ ### Fixed
17
+
18
+ - Run CI and version-driven npm publishing from the repository's `main`
19
+ branch, with a guarded manual recovery path for a missed release.
20
+
21
+ ## 5.0.0
22
+
23
+ ### Changed
24
+
25
+ - Generate the complete standard message, field, and type surface from the
26
+ pinned Garmin FIT SDK without handwritten standard-profile overrides.
27
+ - Preserve SDK alphanumeric tokens when generating public `snake_case` names,
28
+ including `n2`, `po2`, `time128`, and Garmin product identifiers.
29
+ - Restrict non-SDK support to a collision-checked allowlist of Garmin fields
30
+ and private messages observed in the external FIT corpus.
31
+ - Emit only parsed FIT values: Garmin product names are no longer inferred,
32
+ and record elapsed/timer values are generated only when
33
+ `elapsedRecordField: true` is requested.
34
+
35
+ ### Breaking
36
+
37
+ - Remove compatibility aliases such as `resting_calories`,
38
+ `recovery_advisor`, `repeat_dive_time`, and deprecated time-in-zone names.
39
+ - Remove guessed stress fields, handwritten enum/type additions, placeholder
40
+ zero-mask names, and the misspelled `hip_swing_excercise_name` type.
41
+ - Use the pinned SDK's `cadence_zone_high_bondary` spelling and generated
42
+ alphanumeric field/type spellings instead of manual corrections.
43
+ - Correct generated declarations to expose runtime `Date` values, numeric FIT
44
+ `bool` fields and unknown enum IDs, decoded mask objects, nullable array
45
+ entries, and optional profile fields.
46
+ - Keep undated cascade rows in file order and avoid duplicating them when a lap
47
+ or session boundary timestamp is absent.
48
+
49
+ ## 4.1.1
50
+
51
+ ### Fixed
52
+
53
+ - Apply Garmin FIT profile scale and offset to primitive numeric fields, while
54
+ retaining semicircle conversion only for fields declared in semicircles.
55
+ - Apply FIT developer-field offsets with the parser's equivalent signed form,
56
+ including scaled signed developer values.
57
+ - Decode native diving ascent-rate fields as meters per second instead of
58
+ geographic coordinates.
59
+ - Decode native dive depth and bottom-time fields with their Garmin FIT SDK
60
+ scale instead of exposing their encoded integer representation.
61
+ - Keep the pinned Garmin SDK authoritative for standard field types, arrays,
62
+ scales, offsets, and units while retaining compatible public field names.
63
+
5
64
  ## 4.1.0
6
65
 
7
66
  ### Added
package/README.md CHANGED
@@ -29,6 +29,57 @@ including developer-defined data.
29
29
  npm install fit-file-parser
30
30
  ```
31
31
 
32
+ ## Migrating from 4.x to 5.0
33
+
34
+ Version 5.0 is a breaking release because parsed output now follows the pinned
35
+ Garmin FIT SDK profile without compatibility aliases or guessed fields. Parser
36
+ construction, module imports, output modes, and parser options are unchanged.
37
+
38
+ Update field access, destructuring, persisted schemas, and snapshots to use the
39
+ SDK-backed names. Common migrations include:
40
+
41
+ | 4.x name | 5.0 name |
42
+ | ------------------------------ | ---------------------------- |
43
+ | `speed_1s` | `speed1s` |
44
+ | `start_n_2`, `end_n_2` | `start_n2`, `end_n2` |
45
+ | `o_2_toxicity` | `o2_toxicity` |
46
+ | `avg_spo_2`, `reading_spo_2` | `avg_spo2`, `reading_spo2` |
47
+ | `po_2` | `po2` |
48
+ | `cycle_length_16` | `cycle_length16` |
49
+ | `map_3_sample_mean` | `map3_sample_mean` |
50
+ | `time_256` | `time256` |
51
+ | `last_night_5_min_high` | `last_night5_min_high` |
52
+ | `average_7_day_deviation` | `average7_day_deviation` |
53
+ | `spo_2_data`, `hsa_spo_2_data` | `spo2_data`, `hsa_spo2_data` |
54
+ | `repeat_dive_time` | `repeat_dive_interval` |
55
+ | `cadence_zone_high_boundary` | `cadence_zone_high_bondary` |
56
+ | `HipSwingExcerciseName` | `HipSwingExerciseName` |
57
+ | `resting_calories` | `metabolic_calories` |
58
+
59
+ The same alphanumeric-token rule applies to enum strings, for example
60
+ `camera_orientation_90` becomes `camera_orientation90`, `po_2_warn` becomes
61
+ `po2_warn`, and `power_3s` becomes `power3s`. The generated TypeScript
62
+ declarations are the exhaustive name and value reference for the pinned SDK.
63
+
64
+ Parser 4 exposed standard session field 196 (`metabolic_calories`) a second
65
+ time as `resting_calories`; use the canonical `metabolic_calories` name in 5.0.
66
+ `recovery_advisor` was a guessed label for standard session field 140, whose
67
+ canonical SDK field is `avg_depth` in meters. Other behavior to account for
68
+ during migration:
69
+
70
+ - `product_name` is emitted only when it exists in the FIT input. It is no
71
+ longer inferred from `manufacturer` and `product`.
72
+ - Record `elapsed_time` and `timer_time` require
73
+ `elapsedRecordField: true`.
74
+ - FIT timestamps are typed as `Date`, FIT `bool` values remain numeric, mask
75
+ fields are `{ value, ...flags }` objects, unknown enum values remain numbers,
76
+ and array entries may be `null` when the FIT invalid sentinel is retained.
77
+ - Every profile field is optional because individual FIT message definitions
78
+ determine which fields are present.
79
+ - When upgrading from 4.1.0 or earlier, remove application-side scale or offset
80
+ corrections for parsed numeric values, including dive depth, bottom time,
81
+ ascent rate, and developer fields. The parser now applies the SDK metadata.
82
+
32
83
  ## Quick start
33
84
 
34
85
  The Promise API is the simplest way to parse a file:
@@ -125,6 +176,28 @@ const diveSummaries = data.messages?.dive_summary ?? []
125
176
  Existing root lists, cascade nesting, and last-message root properties remain
126
177
  unchanged.
127
178
 
179
+ ## Profile-backed output
180
+
181
+ Standard message names, field names, enum values, wire types, scales, offsets,
182
+ arrays, and units come from the exactly pinned Garmin FIT SDK profile. Public
183
+ names use the parser's generated `snake_case` form while preserving SDK
184
+ alphanumeric tokens such as `n2`, `po2`, and `time128`. The parser does not add
185
+ compatibility aliases for alternate field spellings.
186
+
187
+ The small vendor extension table contains only Garmin fields and private
188
+ messages observed in the external FIT corpus. Extensions cannot replace a
189
+ standard SDK field or type value; the profile audit rejects collisions and
190
+ unregistered additions.
191
+
192
+ Only values present in the FIT input are emitted. In particular,
193
+ `product_name` is not inferred from `manufacturer` and `product`, and record
194
+ `elapsed_time` and `timer_time` are added only when `elapsedRecordField: true`
195
+ is requested. Parsed FIT timestamps are `Date` objects, FIT `bool` fields keep
196
+ their numeric wire values, mask fields decode to `{ value, ...flags }` objects,
197
+ unknown enum IDs remain numbers, and invalid entries retained inside FIT arrays
198
+ are `null`. All profile fields are optional because each FIT message definition
199
+ chooses which fields are present.
200
+
128
201
  ## Inputs
129
202
 
130
203
  Both parser methods accept:
@@ -228,8 +301,24 @@ Run commands from the repository root.
228
301
  | `npm run examples` | Build and regenerate checked-in example outputs. |
229
302
  | `npm run check` | Run profile audit, lint, types, tests, and builds. |
230
303
 
304
+ ### External FIT corpus (optional)
305
+
306
+ The external corpus is not part of this repository or the npm package. For the
307
+ standard contributor layout, clone it alongside this checkout, then run the
308
+ aggregate-only validation command:
309
+
310
+ ```sh
311
+ git clone https://github.com/ThomasKuehne/FIT-test-files.git ../FIT-test-files
312
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery
313
+ ```
314
+
315
+ The command accepts any corpus path; the sibling location is only a convenient
316
+ convention. The corpus contains a known header-CRC failure that is expected to
317
+ recover only in force mode. It reports aggregate counts and never prints file
318
+ names or parsed activity data.
319
+
231
320
  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
321
+ Update the pinned SDK, audited vendor extensions, or a generator, then run
233
322
  `npm run codegen`.
234
323
 
235
324
  Repository-specific automation guidance is tracked in
package/dist/binary.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { FitParserOptions } from './fit-parser.js';
2
- import type { FieldDefinition } from './fit.js';
3
- import type { MesgNum } from './fit_types.js';
2
+ import type { FieldDefinition, MessageName } from './fit.js';
4
3
  import { Buffer } from 'buffer';
5
4
  export interface MessageTypeDefinition {
6
5
  littleEndian: boolean;
@@ -23,7 +22,7 @@ export interface DecoderState {
23
22
  }
24
23
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
25
24
  export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
26
- messageType: MesgNum | '';
25
+ messageType: MessageName | 'definition' | '';
27
26
  nextIndex: number;
28
27
  message?: any;
29
28
  };
package/dist/binary.js CHANGED
@@ -118,17 +118,25 @@ function readData(blob, dataView, fDef, startIndex) {
118
118
  }
119
119
  return isArray ? values : values[0];
120
120
  }
121
- function formatByType(data, type, scale, offset) {
121
+ function formatByType(data, type, scale, offset, units) {
122
122
  switch (type) {
123
123
  case 'date_time':
124
124
  case 'local_date_time':
125
125
  return new Date(data * 1000 + GarminTimeOffset);
126
126
  case 'sint32':
127
- return data * FIT.scConst;
127
+ return units === 'semicircles'
128
+ ? data * FIT.scConst
129
+ : scale ? data / scale + offset : data;
130
+ case 'sint8':
128
131
  case 'uint8':
132
+ case 'uint8z':
129
133
  case 'sint16':
130
- case 'uint32':
131
134
  case 'uint16':
135
+ case 'uint16z':
136
+ case 'uint32':
137
+ case 'uint32z':
138
+ case 'float32':
139
+ case 'float64':
132
140
  return scale ? data / scale + offset : data;
133
141
  case 'uint32_array':
134
142
  case 'uint16_array':
@@ -185,7 +193,7 @@ function formatByType(data, type, scale, offset) {
185
193
  dataItem.value = data & Number(key);
186
194
  }
187
195
  else {
188
- dataItem[value] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
196
+ dataItem[value] = (data & Number(key)) !== 0;
189
197
  }
190
198
  }
191
199
  return dataItem;
@@ -254,10 +262,10 @@ function formatFieldValue(data, fDef, options, fields) {
254
262
  if (isInvalidValue(item, rawType)) {
255
263
  return null;
256
264
  }
257
- return applyOptions(formatByType(item, fDef.type, scale, offset), field, options, fields);
265
+ return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
258
266
  });
259
267
  }
260
- return applyOptions(formatByType(data, fDef.type, scale, offset), field, options, fields);
268
+ return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
261
269
  }
262
270
  function isOutputFieldName(field) {
263
271
  return field !== 'unknown' && field !== '' && field !== undefined;
@@ -289,7 +297,7 @@ function applyOptions(data, field, options, fields) {
289
297
  case 'vertical_speed':
290
298
  case 'avg_speed':
291
299
  case 'max_speed':
292
- case 'speed_1s':
300
+ case 'speed1s':
293
301
  case 'ball_speed':
294
302
  case 'enhanced_avg_speed':
295
303
  case 'enhanced_max_speed':
@@ -340,22 +348,8 @@ function applyOptions(data, field, options, fields) {
340
348
  return data;
341
349
  }
342
350
  }
343
- function applyGarminProductName(fields) {
344
- if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
345
- return;
346
- }
347
- const product = fields.product;
348
- if (typeof product !== 'number') {
349
- return;
350
- }
351
- const productName = FIT.types.garmin_product[product];
352
- if (typeof productName === 'string') {
353
- // Keep the raw protocol ID and expose the SDK product name separately.
354
- fields.product_name = productName;
355
- }
356
- }
357
351
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
358
- var _a, _b, _c, _d, _e, _f;
352
+ var _a, _b, _c, _d, _e, _f, _g;
359
353
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
360
354
  if (!description) {
361
355
  developerFieldDef.resolvedFieldDef = undefined;
@@ -392,7 +386,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
392
386
  name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
393
387
  dataType: getFitMessageBaseType(baseType & 15),
394
388
  scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
395
- offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
389
+ // FIT developer-field descriptions use `raw / scale - offset`. This
390
+ // parser's formatter retains the legacy equivalent signed offset and adds
391
+ // it after scaling.
392
+ offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
393
+ units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
396
394
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
397
395
  developerDataIndex: developerFieldDef.developerDataIndex,
398
396
  isDeveloperField: true,
@@ -435,7 +433,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
435
433
  const fDefIndex = startIndex + 6 + i * 3;
436
434
  const baseType = blob[fDefIndex + 2];
437
435
  const wireType = FIT.types.fit_base_type[baseType];
438
- const { field, type, baseType: profileBaseType, array, scale, offset, aliases, } = message.getAttributes(blob[fDefIndex]);
436
+ const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
439
437
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
440
438
  const fDef = {
441
439
  type: profileCompatible ? type : wireType,
@@ -452,8 +450,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
452
450
  dataType: getFitMessageBaseType(baseType & 15),
453
451
  scale: profileCompatible ? scale : null,
454
452
  offset: profileCompatible ? offset : 0,
453
+ units: profileCompatible ? units : '',
455
454
  requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
456
- aliases: profileCompatible ? aliases : undefined,
457
455
  };
458
456
  mTypeDef.fieldDefs.push(fDef);
459
457
  }
@@ -532,13 +530,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
532
530
  if (isOutputFieldName(fDef.name)) {
533
531
  fields[fDef.name] = data;
534
532
  }
535
- if (fDef.aliases) {
536
- for (const alias of fDef.aliases) {
537
- if (isOutputFieldName(alias.field)) {
538
- fields[alias.field] = data;
539
- }
540
- }
541
- }
542
533
  }
543
534
  for (let i = 0; i < developerFieldDefs.length; i++) {
544
535
  const data = rawData[messageType.fieldDefs.length + i];
@@ -560,13 +551,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
560
551
  if (isOutputFieldName(fDef.name)) {
561
552
  fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
562
553
  }
563
- if (fDef.aliases) {
564
- for (const alias of fDef.aliases) {
565
- if (isOutputFieldName(alias.field)) {
566
- fields[alias.field] = formatFieldValue(data, Object.assign(Object.assign(Object.assign({}, fDef), alias), { name: alias.field, aliases: undefined }), options, fields);
567
- }
568
- }
569
- }
570
554
  }
571
555
  for (let i = 0; i < developerFieldDefs.length; i++) {
572
556
  const data = rawData[messageType.fieldDefs.length + i];
@@ -596,7 +580,6 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
596
580
  validFieldCount++;
597
581
  }
598
582
  }
599
- applyGarminProductName(fields);
600
583
  if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
601
584
  fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
602
585
  fields.timer_time = fields.elapsed_time - pausedTime;
@@ -1,6 +1,5 @@
1
1
  import type { FitParserOptions } from './fit-parser.js';
2
- import type { FieldDefinition } from './fit.js';
3
- import type { MesgNum } from './fit_types.js';
2
+ import type { FieldDefinition, MessageName } from './fit.js';
4
3
  import { Buffer } from 'buffer';
5
4
  export interface MessageTypeDefinition {
6
5
  littleEndian: boolean;
@@ -23,7 +22,7 @@ export interface DecoderState {
23
22
  }
24
23
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
25
24
  export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
26
- messageType: MesgNum | '';
25
+ messageType: MessageName | 'definition' | '';
27
26
  nextIndex: number;
28
27
  message?: any;
29
28
  };
@@ -124,17 +124,25 @@ function readData(blob, dataView, fDef, startIndex) {
124
124
  }
125
125
  return isArray ? values : values[0];
126
126
  }
127
- function formatByType(data, type, scale, offset) {
127
+ function formatByType(data, type, scale, offset, units) {
128
128
  switch (type) {
129
129
  case 'date_time':
130
130
  case 'local_date_time':
131
131
  return new Date(data * 1000 + GarminTimeOffset);
132
132
  case 'sint32':
133
- return data * fit_js_1.FIT.scConst;
133
+ return units === 'semicircles'
134
+ ? data * fit_js_1.FIT.scConst
135
+ : scale ? data / scale + offset : data;
136
+ case 'sint8':
134
137
  case 'uint8':
138
+ case 'uint8z':
135
139
  case 'sint16':
136
- case 'uint32':
137
140
  case 'uint16':
141
+ case 'uint16z':
142
+ case 'uint32':
143
+ case 'uint32z':
144
+ case 'float32':
145
+ case 'float64':
138
146
  return scale ? data / scale + offset : data;
139
147
  case 'uint32_array':
140
148
  case 'uint16_array':
@@ -191,7 +199,7 @@ function formatByType(data, type, scale, offset) {
191
199
  dataItem.value = data & Number(key);
192
200
  }
193
201
  else {
194
- dataItem[value] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
202
+ dataItem[value] = (data & Number(key)) !== 0;
195
203
  }
196
204
  }
197
205
  return dataItem;
@@ -260,10 +268,10 @@ function formatFieldValue(data, fDef, options, fields) {
260
268
  if (isInvalidValue(item, rawType)) {
261
269
  return null;
262
270
  }
263
- return applyOptions(formatByType(item, fDef.type, scale, offset), field, options, fields);
271
+ return applyOptions(formatByType(item, fDef.type, scale, offset, fDef.units), field, options, fields);
264
272
  });
265
273
  }
266
- return applyOptions(formatByType(data, fDef.type, scale, offset), field, options, fields);
274
+ return applyOptions(formatByType(data, fDef.type, scale, offset, fDef.units), field, options, fields);
267
275
  }
268
276
  function isOutputFieldName(field) {
269
277
  return field !== 'unknown' && field !== '' && field !== undefined;
@@ -295,7 +303,7 @@ function applyOptions(data, field, options, fields) {
295
303
  case 'vertical_speed':
296
304
  case 'avg_speed':
297
305
  case 'max_speed':
298
- case 'speed_1s':
306
+ case 'speed1s':
299
307
  case 'ball_speed':
300
308
  case 'enhanced_avg_speed':
301
309
  case 'enhanced_max_speed':
@@ -346,22 +354,8 @@ function applyOptions(data, field, options, fields) {
346
354
  return data;
347
355
  }
348
356
  }
349
- function applyGarminProductName(fields) {
350
- if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
351
- return;
352
- }
353
- const product = fields.product;
354
- if (typeof product !== 'number') {
355
- return;
356
- }
357
- const productName = fit_js_1.FIT.types.garmin_product[product];
358
- if (typeof productName === 'string') {
359
- // Keep the raw protocol ID and expose the SDK product name separately.
360
- fields.product_name = productName;
361
- }
362
- }
363
357
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
364
- var _a, _b, _c, _d, _e, _f;
358
+ var _a, _b, _c, _d, _e, _f, _g;
365
359
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
366
360
  if (!description) {
367
361
  developerFieldDef.resolvedFieldDef = undefined;
@@ -398,7 +392,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
398
392
  name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
399
393
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
400
394
  scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
401
- offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
395
+ // FIT developer-field descriptions use `raw / scale - offset`. This
396
+ // parser's formatter retains the legacy equivalent signed offset and adds
397
+ // it after scaling.
398
+ offset: -((_f = description.offset) !== null && _f !== void 0 ? _f : 0),
399
+ units: (_g = description.units) !== null && _g !== void 0 ? _g : '',
402
400
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
403
401
  developerDataIndex: developerFieldDef.developerDataIndex,
404
402
  isDeveloperField: true,
@@ -441,7 +439,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
441
439
  const fDefIndex = startIndex + 6 + i * 3;
442
440
  const baseType = blob[fDefIndex + 2];
443
441
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
444
- const { field, type, baseType: profileBaseType, array, scale, offset, aliases, } = message.getAttributes(blob[fDefIndex]);
442
+ const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
445
443
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
446
444
  const fDef = {
447
445
  type: profileCompatible ? type : wireType,
@@ -458,8 +456,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
458
456
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
459
457
  scale: profileCompatible ? scale : null,
460
458
  offset: profileCompatible ? offset : 0,
459
+ units: profileCompatible ? units : '',
461
460
  requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
462
- aliases: profileCompatible ? aliases : undefined,
463
461
  };
464
462
  mTypeDef.fieldDefs.push(fDef);
465
463
  }
@@ -538,13 +536,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
538
536
  if (isOutputFieldName(fDef.name)) {
539
537
  fields[fDef.name] = data;
540
538
  }
541
- if (fDef.aliases) {
542
- for (const alias of fDef.aliases) {
543
- if (isOutputFieldName(alias.field)) {
544
- fields[alias.field] = data;
545
- }
546
- }
547
- }
548
539
  }
549
540
  for (let i = 0; i < developerFieldDefs.length; i++) {
550
541
  const data = rawData[messageType.fieldDefs.length + i];
@@ -566,13 +557,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
566
557
  if (isOutputFieldName(fDef.name)) {
567
558
  fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
568
559
  }
569
- if (fDef.aliases) {
570
- for (const alias of fDef.aliases) {
571
- if (isOutputFieldName(alias.field)) {
572
- fields[alias.field] = formatFieldValue(data, Object.assign(Object.assign(Object.assign({}, fDef), alias), { name: alias.field, aliases: undefined }), options, fields);
573
- }
574
- }
575
- }
576
560
  }
577
561
  for (let i = 0; i < developerFieldDefs.length; i++) {
578
562
  const data = rawData[messageType.fieldDefs.length + i];
@@ -602,7 +586,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
602
586
  validFieldCount++;
603
587
  }
604
588
  }
605
- applyGarminProductName(fields);
606
589
  if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
607
590
  fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
608
591
  fields.timer_time = fields.elapsed_time - pausedTime;
@@ -168,8 +168,10 @@ class FitParser {
168
168
  case 'record':
169
169
  if (!startDate) {
170
170
  startDate = message.timestamp;
171
- message.elapsed_time = 0;
172
- message.timer_time = 0;
171
+ if (this.options.elapsedRecordField) {
172
+ message.elapsed_time = 0;
173
+ message.timer_time = 0;
174
+ }
173
175
  }
174
176
  records.push(message);
175
177
  break;
package/dist/cjs/fit.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { FitOptions, MesgNum } from './fit_types.js';
2
+ export type MessageName = Exclude<MesgNum, number | 'definition'>;
2
3
  export interface FieldDefinition {
3
4
  type: string | number;
4
5
  rawType?: string | number;
@@ -12,10 +13,10 @@ export interface FieldDefinition {
12
13
  dataType: string;
13
14
  scale?: number | null;
14
15
  offset?: number;
16
+ units?: string;
15
17
  requiresBoundedDataView?: boolean;
16
18
  developerDataIndex?: number;
17
19
  isDeveloperField?: boolean;
18
- aliases?: MessageObject[];
19
20
  }
20
21
  export interface MessageObject {
21
22
  field: string;
@@ -25,20 +26,21 @@ export interface MessageObject {
25
26
  scale: number | null;
26
27
  offset: number;
27
28
  units: string;
28
- aliases?: MessageObject[];
29
29
  }
30
30
  export interface Message {
31
- name: MesgNum;
31
+ name: MessageName;
32
32
  [fieldId: number]: MessageObject;
33
33
  }
34
34
  export interface FitType {
35
35
  scConst: number;
36
36
  options: FitOptions;
37
- messages: {
38
- [messageId: number]: Message;
39
- };
40
- types: {
41
- [typeName: string]: Record<number, string | number>;
42
- };
37
+ messages: Record<number, Message>;
38
+ types: Record<string, Record<number, string | number>>;
43
39
  }
40
+ /**
41
+ * Garmin fields observed in the external FIT corpus but absent from the pinned
42
+ * public SDK profile. These additions may not replace standard SDK fields.
43
+ */
44
+ export declare const FIT_VENDOR_MESSAGE_EXTENSIONS: Record<number, Message>;
45
+ export declare const FIT_VENDOR_TYPE_EXTENSIONS: Record<string, Record<number, string>>;
44
46
  export declare const FIT: FitType;