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.
@@ -31,23 +31,23 @@ class FitParser {
31
31
  });
32
32
  }
33
33
  parse(content, callback) {
34
- var _a;
34
+ var _a, _b;
35
35
  const blob = content instanceof ArrayBuffer
36
36
  ? new Uint8Array(content)
37
37
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
38
38
  const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
39
39
  if (blob.length < 12) {
40
40
  callback('File to small to be a FIT file', undefined);
41
- if (!this.options.force) {
42
- return;
43
- }
41
+ return;
44
42
  }
45
43
  const headerLength = blob[0];
46
44
  if (headerLength !== 14 && headerLength !== 12) {
47
45
  callback('Incorrect header size', undefined);
48
- if (!this.options.force) {
49
- return;
50
- }
46
+ return;
47
+ }
48
+ if (blob.length < headerLength) {
49
+ callback('File to small to be a FIT file', undefined);
50
+ return;
51
51
  }
52
52
  let fileTypeString = '';
53
53
  for (let i = 8; i < 12; i++) {
@@ -55,29 +55,33 @@ class FitParser {
55
55
  }
56
56
  if (fileTypeString !== '.FIT') {
57
57
  callback('Missing \'.FIT\' in header', undefined);
58
- if (!this.options.force) {
59
- return;
60
- }
58
+ return;
61
59
  }
62
60
  if (headerLength === 14 && !this.options.force) {
63
61
  const crcHeader = blob[12] + (blob[13] << 8);
64
62
  const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
65
- if (crcHeader !== crcHeaderCalc) {
66
- // callback('Header CRC mismatch', {});
67
- // TODO: fix Header CRC check
63
+ if (crcHeader !== 0 && crcHeader !== crcHeaderCalc) {
64
+ callback('Header CRC mismatch', undefined);
68
65
  return;
69
66
  }
70
67
  }
71
68
  const protocolVersion = blob[1];
72
69
  const profileVersion = blob[2] + (blob[3] << 8);
73
- const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
70
+ const dataLength = dataView.getUint32(4, true);
74
71
  const crcStart = dataLength + headerLength;
72
+ if (crcStart > blob.length) {
73
+ callback('File data exceeds input length', undefined);
74
+ return;
75
+ }
76
+ if (!this.options.force && crcStart + 2 > blob.length) {
77
+ callback('File CRC missing', undefined);
78
+ return;
79
+ }
75
80
  if (!this.options.force) {
76
81
  const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
77
- const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
82
+ const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, 0, crcStart);
78
83
  if (crcFile !== crcFileCalc) {
79
- // callback('File CRC mismatch', {});
80
- // TODO: fix File CRC check
84
+ callback('File CRC mismatch', undefined);
81
85
  return;
82
86
  }
83
87
  }
@@ -85,6 +89,7 @@ class FitParser {
85
89
  profileVersion,
86
90
  protocolVersion,
87
91
  };
92
+ const messages = {};
88
93
  let sessions = [];
89
94
  let laps = [];
90
95
  const records = [];
@@ -107,6 +112,7 @@ class FitParser {
107
112
  const tank_updates = [];
108
113
  const tank_summaries = [];
109
114
  const jumps = [];
115
+ const sets = [];
110
116
  const splits = [];
111
117
  const split_summaries = [];
112
118
  const time_in_zone = [];
@@ -115,14 +121,20 @@ class FitParser {
115
121
  let loopIndex = headerLength;
116
122
  const messageTypes = [];
117
123
  const developerFields = [];
124
+ const decoderState = {};
118
125
  const isModeCascade = this.options.mode === 'cascade';
119
126
  const isCascadeNeeded = isModeCascade || this.options.mode === 'both';
120
127
  let startDate;
121
128
  let lastStopTimestamp;
122
129
  let pausedTime = 0;
123
130
  while (loopIndex < crcStart) {
124
- const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
131
+ const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
125
132
  loopIndex = nextIndex;
133
+ if (messageType !== ''
134
+ && messageType !== 'definition'
135
+ && message !== undefined) {
136
+ ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
137
+ }
126
138
  switch (messageType) {
127
139
  case 'lap':
128
140
  laps.push(message);
@@ -210,6 +222,9 @@ class FitParser {
210
222
  case 'jump':
211
223
  jumps.push(message);
212
224
  break;
225
+ case 'set':
226
+ sets.push(message);
227
+ break;
213
228
  case 'split':
214
229
  splits.push(message);
215
230
  break;
@@ -250,15 +265,17 @@ class FitParser {
250
265
  fitObj.time_in_zone = time_in_zone;
251
266
  fitObj.activity_metrics = activity_metrics;
252
267
  fitObj.user_metrics = user_metrics;
268
+ fitObj.messages = messages;
253
269
  if (isCascadeNeeded) {
254
270
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
255
271
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
256
272
  sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
257
- fitObj.activity = Object.assign(Object.assign({}, ((_a = fitObj.activity) !== null && _a !== void 0 ? _a : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
273
+ 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
258
274
  sessions,
259
275
  events,
260
276
  hrv,
261
277
  device_infos, developer_data_ids: applications, field_descriptions: fieldDescriptions, sports,
278
+ sets,
262
279
  splits,
263
280
  split_summaries });
264
281
  }
@@ -272,6 +289,7 @@ class FitParser {
272
289
  fitObj.developer_data_ids = applications;
273
290
  fitObj.field_descriptions = fieldDescriptions;
274
291
  fitObj.hrv = hrv;
292
+ fitObj.sets = sets;
275
293
  }
276
294
  callback(undefined, fitObj);
277
295
  }
package/dist/cjs/fit.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { FitOptions, MesgNum } from './fit_types.js';
2
2
  export interface FieldDefinition {
3
3
  type: string | number;
4
+ rawType?: string | number;
4
5
  fDefNo: number;
5
6
  size: number;
7
+ array?: boolean;
6
8
  endianAbility: boolean;
7
9
  littleEndian: boolean;
8
10
  baseTypeNo: number;
@@ -13,13 +15,17 @@ export interface FieldDefinition {
13
15
  requiresBoundedDataView?: boolean;
14
16
  developerDataIndex?: number;
15
17
  isDeveloperField?: boolean;
18
+ aliases?: MessageObject[];
16
19
  }
17
20
  export interface MessageObject {
18
21
  field: string;
19
22
  type: string;
23
+ baseType?: string;
24
+ array?: boolean;
20
25
  scale: number | null;
21
26
  offset: number;
22
27
  units: string;
28
+ aliases?: MessageObject[];
23
29
  }
24
30
  export interface Message {
25
31
  name: MesgNum;
package/dist/cjs/fit.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FIT = void 0;
4
+ const garmin_profile_generated_js_1 = require("./garmin_profile.generated.js");
4
5
  const metersInOneKilometer = 1000;
5
6
  const secondsInOneHour = 3600;
6
7
  // according to https://en.wikipedia.org/wiki/Mile
@@ -11,7 +12,7 @@ const psiInOneBar = 14.5037738;
11
12
  // which contains all the types and values
12
13
  // i would wish for some kind of inference mechanism here but for now we leave it as any
13
14
  // maybe zod could help here
14
- exports.FIT = {
15
+ const FIT_OVERRIDES = {
15
16
  scConst: 180 / Math.pow(2, 31),
16
17
  options: {
17
18
  speedUnits: {
@@ -49,6 +50,11 @@ exports.FIT = {
49
50
  },
50
51
  },
51
52
  temperatureUnits: {
53
+ // canonical native temperature unit: degree Celsius [°C]
54
+ 'celsius': {
55
+ multiplier: 1,
56
+ offset: 0,
57
+ },
52
58
  // native temperature unit: degree Celsius [°C]
53
59
  '°C': {
54
60
  multiplier: 1,
@@ -57,7 +63,7 @@ exports.FIT = {
57
63
  // kelvin [K]
58
64
  'kelvin': {
59
65
  multiplier: 1,
60
- offset: -273.15,
66
+ offset: 273.15,
61
67
  },
62
68
  // degree fahrenheit [°F]
63
69
  'fahrenheit': {
@@ -1797,20 +1803,6 @@ exports.FIT = {
1797
1803
  offset: 0,
1798
1804
  units: 'percent',
1799
1805
  },
1800
- 214: {
1801
- field: 'avg_grit',
1802
- type: 'float32',
1803
- scale: null,
1804
- offset: 0,
1805
- units: '',
1806
- },
1807
- 215: {
1808
- field: 'avg_flow',
1809
- type: 'float32',
1810
- scale: null,
1811
- offset: 0,
1812
- units: '',
1813
- },
1814
1806
  140: {
1815
1807
  field: 'recovery_advisor',
1816
1808
  type: 'uint16', // Minutes?
@@ -4135,22 +4127,25 @@ exports.FIT = {
4135
4127
  },
4136
4128
  2: {
4137
4129
  field: 'distance',
4138
- type: 'float32',
4139
- scale: null,
4130
+ type: 'uint32',
4131
+ baseType: 'uint32',
4132
+ scale: 100,
4140
4133
  offset: 0,
4141
4134
  units: 'm',
4142
4135
  },
4143
4136
  3: {
4144
4137
  field: 'cycles',
4145
- type: 'float32',
4146
- scale: null,
4138
+ type: 'uint32',
4139
+ baseType: 'uint32',
4140
+ scale: 2,
4147
4141
  offset: 0,
4148
4142
  units: 'cycles',
4149
4143
  },
4150
4144
  4: {
4151
4145
  field: 'active_time',
4152
- type: 'float32',
4153
- scale: null,
4146
+ type: 'uint32',
4147
+ baseType: 'uint32',
4148
+ scale: 1000,
4154
4149
  offset: 0,
4155
4150
  units: 's',
4156
4151
  },
@@ -4205,28 +4200,33 @@ exports.FIT = {
4205
4200
  },
4206
4201
  12: {
4207
4202
  field: 'temperature',
4208
- type: 'float32',
4209
- scale: null,
4203
+ type: 'sint16',
4204
+ baseType: 'sint16',
4205
+ scale: 100,
4210
4206
  offset: 0,
4211
4207
  units: 'C',
4212
4208
  },
4213
4209
  14: {
4214
4210
  field: 'temperature_min',
4215
- type: 'float32',
4216
- scale: null,
4211
+ type: 'sint16',
4212
+ baseType: 'sint16',
4213
+ scale: 100,
4217
4214
  offset: 0,
4218
4215
  units: 'C',
4219
4216
  },
4220
4217
  15: {
4221
4218
  field: 'temperature_max',
4222
- type: 'float32',
4223
- scale: null,
4219
+ type: 'sint16',
4220
+ baseType: 'sint16',
4221
+ scale: 100,
4224
4222
  offset: 0,
4225
4223
  units: 'C',
4226
4224
  },
4227
4225
  16: {
4228
4226
  field: 'activity_time',
4229
- type: 'int32',
4227
+ type: 'uint16',
4228
+ baseType: 'uint16',
4229
+ array: true,
4230
4230
  scale: null,
4231
4231
  offset: 0,
4232
4232
  units: '',
@@ -4597,17 +4597,6 @@ exports.FIT = {
4597
4597
  units: '',
4598
4598
  },
4599
4599
  },
4600
- 108: {
4601
- name: 'o_hr_settings',
4602
- 253: {
4603
- field: 'timestamp',
4604
- type: 'date_time',
4605
- scale: null,
4606
- offset: 0,
4607
- units: '',
4608
- },
4609
- 0: { field: 'enabled', type: 'byte', scale: null, offset: 0, units: '' },
4610
- },
4611
4600
  140: {
4612
4601
  name: 'activity_metrics',
4613
4602
  1: {
@@ -4742,6 +4731,86 @@ exports.FIT = {
4742
4731
  units: '',
4743
4732
  },
4744
4733
  },
4734
+ 225: {
4735
+ name: 'set',
4736
+ 254: {
4737
+ field: 'timestamp',
4738
+ type: 'date_time',
4739
+ scale: null,
4740
+ offset: 0,
4741
+ units: '',
4742
+ },
4743
+ 0: {
4744
+ field: 'duration',
4745
+ type: 'uint32',
4746
+ scale: 1000,
4747
+ offset: 0,
4748
+ units: 's',
4749
+ },
4750
+ 3: {
4751
+ field: 'repetitions',
4752
+ type: 'uint16',
4753
+ scale: null,
4754
+ offset: 0,
4755
+ units: '',
4756
+ },
4757
+ 4: {
4758
+ field: 'weight',
4759
+ type: 'uint16',
4760
+ scale: 16,
4761
+ offset: 0,
4762
+ units: 'kg',
4763
+ },
4764
+ 5: {
4765
+ field: 'set_type',
4766
+ type: 'set_type',
4767
+ scale: null,
4768
+ offset: 0,
4769
+ units: '',
4770
+ },
4771
+ 6: {
4772
+ field: 'start_time',
4773
+ type: 'date_time',
4774
+ scale: null,
4775
+ offset: 0,
4776
+ units: '',
4777
+ },
4778
+ 7: {
4779
+ field: 'category',
4780
+ type: 'exercise_category_array',
4781
+ scale: null,
4782
+ offset: 0,
4783
+ units: '',
4784
+ },
4785
+ 8: {
4786
+ field: 'category_subtype',
4787
+ type: 'uint16_array',
4788
+ scale: null,
4789
+ offset: 0,
4790
+ units: '',
4791
+ },
4792
+ 9: {
4793
+ field: 'weight_display_unit',
4794
+ type: 'fit_base_unit',
4795
+ scale: null,
4796
+ offset: 0,
4797
+ units: '',
4798
+ },
4799
+ 10: {
4800
+ field: 'message_index',
4801
+ type: 'message_index',
4802
+ scale: null,
4803
+ offset: 0,
4804
+ units: '',
4805
+ },
4806
+ 11: {
4807
+ field: 'wkt_step_index',
4808
+ type: 'message_index',
4809
+ scale: null,
4810
+ offset: 0,
4811
+ units: '',
4812
+ },
4813
+ },
4745
4814
  227: {
4746
4815
  name: 'stress_level',
4747
4816
  0: {
@@ -5203,7 +5272,7 @@ exports.FIT = {
5203
5272
  units: 's',
5204
5273
  },
5205
5274
  4: {
5206
- field: 'time_in_power_zone',
5275
+ field: 'time_in_cadence_zone',
5207
5276
  type: 'uint32_array',
5208
5277
  scale: 1000,
5209
5278
  offset: 0,
@@ -5231,11 +5300,11 @@ exports.FIT = {
5231
5300
  units: 'm/s',
5232
5301
  },
5233
5302
  8: {
5234
- field: 'power_zone_high_boundary',
5235
- type: 'uint16_array',
5303
+ field: 'cadence_zone_high_boundary',
5304
+ type: 'uint8_array',
5236
5305
  scale: null,
5237
5306
  offset: 0,
5238
- units: 'watts',
5307
+ units: 'rpm',
5239
5308
  },
5240
5309
  9: {
5241
5310
  field: 'power_zone_high_boundary',
@@ -9088,3 +9157,57 @@ exports.FIT = {
9088
9157
  },
9089
9158
  },
9090
9159
  };
9160
+ function equivalentProfileName(left, right) {
9161
+ return String(left).replace(/_/g, '').toLowerCase()
9162
+ === String(right).replace(/_/g, '').toLowerCase();
9163
+ }
9164
+ function mergeMessages() {
9165
+ const messages = Object.assign({}, FIT_OVERRIDES.messages);
9166
+ Object.entries(garmin_profile_generated_js_1.GARMIN_MESSAGES).forEach(([messageId, generatedMessage]) => {
9167
+ const overrideMessage = FIT_OVERRIDES.messages[Number(messageId)];
9168
+ if (!overrideMessage) {
9169
+ messages[Number(messageId)] = generatedMessage;
9170
+ return;
9171
+ }
9172
+ const mergedMessage = Object.assign(Object.assign({}, overrideMessage), generatedMessage);
9173
+ Object.entries(generatedMessage).forEach(([fieldId, generatedField]) => {
9174
+ var _a;
9175
+ if (fieldId === 'name') {
9176
+ return;
9177
+ }
9178
+ const overrideField = overrideMessage[Number(fieldId)];
9179
+ const hasCompatibleName = overrideField
9180
+ && equivalentProfileName(overrideField.field, generatedField.field);
9181
+ mergedMessage[Number(fieldId)] = hasCompatibleName
9182
+ ? Object.assign(Object.assign(Object.assign({}, generatedField), overrideField), { baseType: generatedField.baseType, array: (_a = overrideField.array) !== null && _a !== void 0 ? _a : generatedField.array }) : Object.assign(Object.assign(Object.assign({}, overrideField), generatedField), { aliases: overrideField ? [overrideField] : undefined });
9183
+ });
9184
+ messages[Number(messageId)] = mergedMessage;
9185
+ });
9186
+ return messages;
9187
+ }
9188
+ function mergeTypes() {
9189
+ const typeNames = new Set([
9190
+ ...Object.keys(garmin_profile_generated_js_1.GARMIN_TYPES),
9191
+ ...Object.keys(FIT_OVERRIDES.types),
9192
+ ]);
9193
+ return Object.fromEntries([...typeNames].map((typeName) => {
9194
+ var _a, _b;
9195
+ const generatedValues = (_a = garmin_profile_generated_js_1.GARMIN_TYPES[typeName]) !== null && _a !== void 0 ? _a : {};
9196
+ const overrideValues = (_b = FIT_OVERRIDES.types[typeName]) !== null && _b !== void 0 ? _b : {};
9197
+ const values = Object.assign({}, generatedValues);
9198
+ Object.entries(overrideValues).forEach(([valueId, overrideValue]) => {
9199
+ const generatedValue = generatedValues[Number(valueId)];
9200
+ if (generatedValue === undefined
9201
+ || equivalentProfileName(overrideValue, generatedValue)) {
9202
+ values[Number(valueId)] = overrideValue;
9203
+ }
9204
+ });
9205
+ return [typeName, values];
9206
+ }));
9207
+ }
9208
+ exports.FIT = {
9209
+ scConst: FIT_OVERRIDES.scConst,
9210
+ options: FIT_OVERRIDES.options,
9211
+ messages: mergeMessages(),
9212
+ types: mergeTypes(),
9213
+ };