fit-file-parser 5.0.1 → 5.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
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.1.0
6
+
7
+ ### Added
8
+
9
+ - Add opt-in, message-filterable `raw_developer_fields` output with exact
10
+ developer-field bytes, global-message occurrence, developer-data index, and
11
+ field-definition number. Existing decoded output remains unchanged when the
12
+ option is disabled.
13
+
14
+ ## 5.0.2
15
+
16
+ ### Fixed
17
+
18
+ - Clarify the 5.0 migration for standard session field 196: use canonical
19
+ `metabolic_calories` rather than the old duplicate `resting_calories` label.
20
+ - Clarify that the removed guessed `recovery_advisor` label was standard
21
+ session field 140, which the SDK defines as `avg_depth` in meters.
22
+
5
23
  ## 5.0.1
6
24
 
7
25
  ### Fixed
package/README.md CHANGED
@@ -54,14 +54,18 @@ SDK-backed names. Common migrations include:
54
54
  | `repeat_dive_time` | `repeat_dive_interval` |
55
55
  | `cadence_zone_high_boundary` | `cadence_zone_high_bondary` |
56
56
  | `HipSwingExcerciseName` | `HipSwingExerciseName` |
57
+ | `resting_calories` | `metabolic_calories` |
57
58
 
58
59
  The same alphanumeric-token rule applies to enum strings, for example
59
60
  `camera_orientation_90` becomes `camera_orientation90`, `po_2_warn` becomes
60
61
  `po2_warn`, and `power_3s` becomes `power3s`. The generated TypeScript
61
62
  declarations are the exhaustive name and value reference for the pinned SDK.
62
63
 
63
- The handwritten `resting_calories` and guessed `recovery_advisor` fields have
64
- no 5.0 replacement. Other behavior to account for during migration:
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:
65
69
 
66
70
  - `product_name` is emitted only when it exists in the FIT input. It is no
67
71
  longer inferred from `manufacturer` and `product`.
@@ -131,20 +135,55 @@ callback API receives as its first argument.
131
135
 
132
136
  All options are optional.
133
137
 
134
- | Option | Values | Default | Behavior |
135
- | -------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
136
- | `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
137
- | `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. |
138
- | `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
139
- | `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
140
- | `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
141
- | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
142
- | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
138
+ | Option | Values | Default | Behavior |
139
+ | --------------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
141
+ | `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. |
142
+ | `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
143
+ | `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
144
+ | `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
145
+ | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
146
+ | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
147
+ | `includeRawDeveloperFields` | `true`, `false`, message-number array | `false` | Adds a lossless, message-associated view of developer-field bytes at `raw_developer_fields`. |
143
148
 
144
149
  `force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
145
150
  too short, have an invalid header size or signature, or declare data beyond the
146
151
  available bytes are rejected in both modes.
147
152
 
153
+ ### Lossless developer fields
154
+
155
+ By default, developer fields retain the existing decoded, name-keyed output.
156
+ Set `includeRawDeveloperFields: true` when a consumer also needs exact field
157
+ bytes or must distinguish same-named fields from different developer-data
158
+ indexes:
159
+
160
+ ```javascript
161
+ const data = await new FitParser({
162
+ force: false,
163
+ includeRawDeveloperFields: [18],
164
+ }).parseAsync(content)
165
+
166
+ for (const field of data.raw_developer_fields ?? []) {
167
+ console.log({
168
+ globalMessageNumber: field.global_message_number,
169
+ messageIndex: field.message_index,
170
+ developerDataIndex: field.developer_data_index,
171
+ fieldDefinitionNumber: field.field_definition_number,
172
+ rawBytes: field.raw_value,
173
+ })
174
+ }
175
+ ```
176
+
177
+ `message_index` is the zero-based occurrence of that global message number in
178
+ file order. `raw_value` is a defensive plain-number copy of the exact FIT field
179
+ bytes, including interior NUL bytes and invalid sentinels. Join
180
+ `developer_data_index` and `field_definition_number` to the existing
181
+ `developer_data_ids` and `field_descriptions` collections to interpret an
182
+ application and field. The parser deliberately does not infer application- or
183
+ provider-specific semantics. Pass `true` to retain developer fields from every
184
+ global message, or an array such as `[18]` to bound collection to specific FIT
185
+ message numbers.
186
+
148
187
  ## Output modes
149
188
 
150
189
  The mode controls where sessions, laps, records, and related activity
package/dist/binary.d.ts CHANGED
@@ -20,11 +20,18 @@ export interface DecoderState {
20
20
  lastTimestamp?: number;
21
21
  monitoringTimestamp?: number;
22
22
  }
23
+ export interface RawDeveloperFieldValue {
24
+ developerDataIndex: number;
25
+ fieldDefinitionNumber: number;
26
+ rawValue: number[];
27
+ }
23
28
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
24
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
29
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
25
30
  messageType: MessageName | 'definition' | '';
26
31
  nextIndex: number;
27
32
  message?: any;
33
+ globalMessageNumber?: number;
34
+ rawDeveloperFields?: RawDeveloperFieldValue[];
28
35
  };
29
36
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
30
37
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
package/dist/binary.js CHANGED
@@ -399,7 +399,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
399
399
  developerFieldDef.resolvedFrom = description;
400
400
  return resolvedFieldDef;
401
401
  }
402
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
402
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
403
403
  var _a, _b, _c;
404
404
  const recordHeader = blob[startIndex];
405
405
  let localMessageType = recordHeader & 15;
@@ -479,6 +479,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
479
479
  const message = getFitMessage(messageType.globalMessageNumber);
480
480
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
481
481
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
482
+ const includeRawDeveloperFields = options.includeRawDeveloperFields === true
483
+ || (Array.isArray(options.includeRawDeveloperFields)
484
+ && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
485
+ const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
482
486
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
483
487
  let validFieldCount = 0;
484
488
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
@@ -502,6 +506,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
502
506
  for (let i = 0; i < developerFieldDefs.length; i++) {
503
507
  const developerFieldDef = developerFieldDefs[i];
504
508
  const rawDataIndex = messageType.fieldDefs.length + i;
509
+ if (rawDeveloperFields
510
+ && readDataFromIndex + developerFieldDef.size <= dataEnd) {
511
+ rawDeveloperFields.push({
512
+ developerDataIndex: developerFieldDef.developerDataIndex,
513
+ fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
514
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
515
+ });
516
+ }
505
517
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
506
518
  if (fDef) {
507
519
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -607,9 +619,11 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
607
619
  }
608
620
  }
609
621
  return {
622
+ globalMessageNumber: messageType.globalMessageNumber,
610
623
  messageType: message.name,
611
624
  nextIndex: startIndex + messageSize + 1,
612
625
  message: fields,
626
+ rawDeveloperFields,
613
627
  };
614
628
  }
615
629
  export function getArrayBuffer(buffer) {
@@ -20,11 +20,18 @@ export interface DecoderState {
20
20
  lastTimestamp?: number;
21
21
  monitoringTimestamp?: number;
22
22
  }
23
+ export interface RawDeveloperFieldValue {
24
+ developerDataIndex: number;
25
+ fieldDefinitionNumber: number;
26
+ rawValue: number[];
27
+ }
23
28
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
24
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
29
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
25
30
  messageType: MessageName | 'definition' | '';
26
31
  nextIndex: number;
27
32
  message?: any;
33
+ globalMessageNumber?: number;
34
+ rawDeveloperFields?: RawDeveloperFieldValue[];
28
35
  };
29
36
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
30
37
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
@@ -405,7 +405,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
405
405
  developerFieldDef.resolvedFrom = description;
406
406
  return resolvedFieldDef;
407
407
  }
408
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
408
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
409
409
  var _a, _b, _c;
410
410
  const recordHeader = blob[startIndex];
411
411
  let localMessageType = recordHeader & 15;
@@ -485,6 +485,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
485
485
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
486
486
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
487
487
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
488
+ const includeRawDeveloperFields = options.includeRawDeveloperFields === true
489
+ || (Array.isArray(options.includeRawDeveloperFields)
490
+ && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
491
+ const rawDeveloperFields = includeRawDeveloperFields ? [] : undefined;
488
492
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
489
493
  let validFieldCount = 0;
490
494
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
@@ -508,6 +512,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
508
512
  for (let i = 0; i < developerFieldDefs.length; i++) {
509
513
  const developerFieldDef = developerFieldDefs[i];
510
514
  const rawDataIndex = messageType.fieldDefs.length + i;
515
+ if (rawDeveloperFields
516
+ && readDataFromIndex + developerFieldDef.size <= dataEnd) {
517
+ rawDeveloperFields.push({
518
+ developerDataIndex: developerFieldDef.developerDataIndex,
519
+ fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
520
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
521
+ });
522
+ }
511
523
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
512
524
  if (fDef) {
513
525
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -613,9 +625,11 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
613
625
  }
614
626
  }
615
627
  return {
628
+ globalMessageNumber: messageType.globalMessageNumber,
616
629
  messageType: message.name,
617
630
  nextIndex: startIndex + messageSize + 1,
618
631
  message: fields,
632
+ rawDeveloperFields,
619
633
  };
620
634
  }
621
635
  function getArrayBuffer(buffer) {
@@ -2,6 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
5
6
  export interface FitParserOptions {
6
7
  force?: boolean;
7
8
  speedUnit?: string;
@@ -10,6 +11,8 @@ export interface FitParserOptions {
10
11
  elapsedRecordField?: boolean;
11
12
  pressureUnit?: string;
12
13
  mode?: 'list' | 'cascade' | 'both';
14
+ /** Retains exact developer-field bytes for all or selected global message numbers. */
15
+ includeRawDeveloperFields?: boolean | readonly number[];
13
16
  }
14
17
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
15
18
  export default class FitParser {
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function
8
8
  Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
9
9
  class FitParser {
10
10
  constructor(options = {}) {
11
+ var _a;
11
12
  this.options = {
12
13
  force: options.force != null ? options.force : true,
13
14
  speedUnit: options.speedUnit || 'm/s',
@@ -16,6 +17,7 @@ class FitParser {
16
17
  elapsedRecordField: options.elapsedRecordField || false,
17
18
  pressureUnit: options.pressureUnit || 'bar',
18
19
  mode: options.mode || 'list',
20
+ includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
19
21
  };
20
22
  }
21
23
  parseAsync(content) {
@@ -31,7 +33,7 @@ class FitParser {
31
33
  });
32
34
  }
33
35
  parse(content, callback) {
34
- var _a, _b;
36
+ var _a, _b, _c;
35
37
  const blob = content instanceof ArrayBuffer
36
38
  ? new Uint8Array(content)
37
39
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
@@ -118,6 +120,11 @@ class FitParser {
118
120
  const time_in_zone = [];
119
121
  const activity_metrics = [];
120
122
  const user_metrics = [];
123
+ const rawDeveloperFields = this.options.includeRawDeveloperFields === true
124
+ || Array.isArray(this.options.includeRawDeveloperFields)
125
+ ? []
126
+ : undefined;
127
+ const messageCountsByGlobalNumber = new Map();
121
128
  let loopIndex = headerLength;
122
129
  const messageTypes = [];
123
130
  const developerFields = [];
@@ -128,12 +135,25 @@ class FitParser {
128
135
  let lastStopTimestamp;
129
136
  let pausedTime = 0;
130
137
  while (loopIndex < crcStart) {
131
- const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
138
+ const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
132
139
  loopIndex = nextIndex;
140
+ if (globalMessageNumber !== undefined) {
141
+ const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
142
+ messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
143
+ recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
144
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
145
+ global_message_number: globalMessageNumber,
146
+ message_index: messageIndex,
147
+ developer_data_index: field.developerDataIndex,
148
+ field_definition_number: field.fieldDefinitionNumber,
149
+ raw_value: field.rawValue,
150
+ });
151
+ });
152
+ }
133
153
  if (messageType !== ''
134
154
  && messageType !== 'definition'
135
155
  && message !== undefined) {
136
- ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
156
+ ((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
137
157
  }
138
158
  switch (messageType) {
139
159
  case 'lap':
@@ -268,11 +288,14 @@ class FitParser {
268
288
  fitObj.activity_metrics = activity_metrics;
269
289
  fitObj.user_metrics = user_metrics;
270
290
  fitObj.messages = messages;
291
+ if (rawDeveloperFields) {
292
+ fitObj.raw_developer_fields = rawDeveloperFields;
293
+ }
271
294
  if (isCascadeNeeded) {
272
295
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
273
296
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
274
297
  sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
275
- fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
298
+ fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
276
299
  sessions,
277
300
  events,
278
301
  hrv,
@@ -9,6 +9,13 @@ export type Unit<T extends string> = Record<T, {
9
9
  multiplier: number;
10
10
  offset: number;
11
11
  }>;
12
+ export interface ParsedRawDeveloperField {
13
+ global_message_number: number;
14
+ message_index: number;
15
+ developer_data_index: number;
16
+ field_definition_number: number;
17
+ raw_value: number[];
18
+ }
12
19
  export type Activity = 'manual' | 'auto_multi_sport' | number;
13
20
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
14
21
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2070,6 +2077,7 @@ export interface ParsedFit {
2070
2077
  protocolVersion?: number;
2071
2078
  profileVersion?: number;
2072
2079
  messages?: ParsedMessages;
2080
+ raw_developer_fields?: ParsedRawDeveloperField[];
2073
2081
  file_creator: ParsedFileCreator;
2074
2082
  device_settings: ParsedDeviceSettings;
2075
2083
  dive_summary?: ParsedDiveSummary;
@@ -175,6 +175,13 @@ function generateUtilities() {
175
175
  ]),
176
176
  ]));
177
177
  nodes.push(unitType);
178
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
179
+ generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
180
+ generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
181
+ generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
182
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
183
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
184
+ ]));
178
185
  return nodes;
179
186
  }
180
187
  function generateAdditionalFields(msg) {
@@ -258,6 +265,7 @@ function generateFitType() {
258
265
  generateProperty('protocolVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
259
266
  generateProperty('profileVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
260
267
  generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
268
+ generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
261
269
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
262
270
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
263
271
  ? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
@@ -2,6 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField } from './fit_types.js';
5
6
  export interface FitParserOptions {
6
7
  force?: boolean;
7
8
  speedUnit?: string;
@@ -10,6 +11,8 @@ export interface FitParserOptions {
10
11
  elapsedRecordField?: boolean;
11
12
  pressureUnit?: string;
12
13
  mode?: 'list' | 'cascade' | 'both';
14
+ /** Retains exact developer-field bytes for all or selected global message numbers. */
15
+ includeRawDeveloperFields?: boolean | readonly number[];
13
16
  }
14
17
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
15
18
  export default class FitParser {
@@ -3,6 +3,7 @@ import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export default class FitParser {
5
5
  constructor(options = {}) {
6
+ var _a;
6
7
  this.options = {
7
8
  force: options.force != null ? options.force : true,
8
9
  speedUnit: options.speedUnit || 'm/s',
@@ -11,6 +12,7 @@ export default class FitParser {
11
12
  elapsedRecordField: options.elapsedRecordField || false,
12
13
  pressureUnit: options.pressureUnit || 'bar',
13
14
  mode: options.mode || 'list',
15
+ includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
14
16
  };
15
17
  }
16
18
  parseAsync(content) {
@@ -26,7 +28,7 @@ export default class FitParser {
26
28
  });
27
29
  }
28
30
  parse(content, callback) {
29
- var _a, _b;
31
+ var _a, _b, _c;
30
32
  const blob = content instanceof ArrayBuffer
31
33
  ? new Uint8Array(content)
32
34
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
@@ -113,6 +115,11 @@ export default class FitParser {
113
115
  const time_in_zone = [];
114
116
  const activity_metrics = [];
115
117
  const user_metrics = [];
118
+ const rawDeveloperFields = this.options.includeRawDeveloperFields === true
119
+ || Array.isArray(this.options.includeRawDeveloperFields)
120
+ ? []
121
+ : undefined;
122
+ const messageCountsByGlobalNumber = new Map();
116
123
  let loopIndex = headerLength;
117
124
  const messageTypes = [];
118
125
  const developerFields = [];
@@ -123,12 +130,25 @@ export default class FitParser {
123
130
  let lastStopTimestamp;
124
131
  let pausedTime = 0;
125
132
  while (loopIndex < crcStart) {
126
- const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
133
+ const { globalMessageNumber, message, messageType, nextIndex, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
127
134
  loopIndex = nextIndex;
135
+ if (globalMessageNumber !== undefined) {
136
+ const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
137
+ messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
138
+ recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
139
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
140
+ global_message_number: globalMessageNumber,
141
+ message_index: messageIndex,
142
+ developer_data_index: field.developerDataIndex,
143
+ field_definition_number: field.fieldDefinitionNumber,
144
+ raw_value: field.rawValue,
145
+ });
146
+ });
147
+ }
128
148
  if (messageType !== ''
129
149
  && messageType !== 'definition'
130
150
  && message !== undefined) {
131
- ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
151
+ ((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
132
152
  }
133
153
  switch (messageType) {
134
154
  case 'lap':
@@ -263,11 +283,14 @@ export default class FitParser {
263
283
  fitObj.activity_metrics = activity_metrics;
264
284
  fitObj.user_metrics = user_metrics;
265
285
  fitObj.messages = messages;
286
+ if (rawDeveloperFields) {
287
+ fitObj.raw_developer_fields = rawDeveloperFields;
288
+ }
266
289
  if (isCascadeNeeded) {
267
290
  laps = mapDataIntoLap(laps, 'records', records);
268
291
  laps = mapDataIntoLap(laps, 'lengths', lengths);
269
292
  sessions = mapDataIntoSession(sessions, laps);
270
- fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
293
+ fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
271
294
  sessions,
272
295
  events,
273
296
  hrv,
@@ -9,6 +9,13 @@ export type Unit<T extends string> = Record<T, {
9
9
  multiplier: number;
10
10
  offset: number;
11
11
  }>;
12
+ export interface ParsedRawDeveloperField {
13
+ global_message_number: number;
14
+ message_index: number;
15
+ developer_data_index: number;
16
+ field_definition_number: number;
17
+ raw_value: number[];
18
+ }
12
19
  export type Activity = 'manual' | 'auto_multi_sport' | number;
13
20
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
14
21
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2070,6 +2077,7 @@ export interface ParsedFit {
2070
2077
  protocolVersion?: number;
2071
2078
  profileVersion?: number;
2072
2079
  messages?: ParsedMessages;
2080
+ raw_developer_fields?: ParsedRawDeveloperField[];
2073
2081
  file_creator: ParsedFileCreator;
2074
2082
  device_settings: ParsedDeviceSettings;
2075
2083
  dive_summary?: ParsedDiveSummary;
@@ -154,6 +154,13 @@ export function generateUtilities() {
154
154
  ]),
155
155
  ]));
156
156
  nodes.push(unitType);
157
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
158
+ generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
159
+ generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
160
+ generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
161
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
162
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
163
+ ]));
157
164
  return nodes;
158
165
  }
159
166
  function generateAdditionalFields(msg) {
@@ -237,6 +244,7 @@ export function generateFitType() {
237
244
  generateProperty('protocolVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
238
245
  generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
239
246
  generateProperty('messages', ts.factory.createTypeReferenceNode('ParsedMessages')),
247
+ generateArrayProperty('raw_developer_fields', ts.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
240
248
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
241
249
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
242
250
  ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "5.0.1",
4
+ "version": "5.1.0",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {