fit-file-parser 2.2.4 → 2.2.6

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.
@@ -0,0 +1,35 @@
1
+ # FIT Parser Development Rules
2
+
3
+ You are working on the `fit-parser` library. Follow these rules strictly to ensure correctness and stability.
4
+
5
+ ## Core Principles
6
+
7
+ 1. **Trust the Official SDK**:
8
+ * Always verify Message IDs and Field IDs against the Official Garmin FIT SDK (`@garmin/fitsdk`).
9
+ * **NEVER** guess or reuse existing IDs if they seem wrong (e.g., Message 140 vs 285).
10
+ * If `@garmin/fitsdk` is available in `node_modules`, use it as the source of truth.
11
+
12
+ 2. **Investigation First**:
13
+ * If a field is missing, use `npm run inspect <file>` or `npm run probe <file> <value>` BEFORE modifying code.
14
+ * Confirm the field exists in the file and identify its raw value.
15
+
16
+ 3. **Testing is Mandatory**:
17
+ * Every new message or field definition MUST have a corresponding test case in `test/`.
18
+ * Test against real FIT files whenever possible (use `examples/` directory).
19
+ * Use `npm run test` to verify changes.
20
+
21
+ 4. **Code Generation**:
22
+ * `src/fit_types.ts` is AUTO-GENERATED.
23
+ * If you modify `src/fit.ts`, you **MUST** run `npm run codegen` to update types.
24
+ * Do not edit `src/fit_types.ts` manually.
25
+
26
+ ## Workflow checklist
27
+
28
+ When adding a new message or fixing a field:
29
+
30
+ - [ ] Locate official Message ID in Garmin SDK
31
+ - [ ] Locate Field IDs and Types in Garmin SDK
32
+ - [ ] Update `src/fit.ts` with new definition
33
+ - [ ] Run `npm run codegen`
34
+ - [ ] Add/Update test in `test/`
35
+ - [ ] Run `npm run build && npm run test`
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: add-fit-message
3
+ description: Add a new FIT message or update an existing one
4
+ ---
5
+
6
+ # Add/Update FIT Message
7
+
8
+ ## Prerequisites
9
+
10
+ - Identify Message ID (e.g., 285 for Jump)
11
+ - Identify Field IDs and Types from Garmin SDK
12
+
13
+ ## Steps
14
+
15
+ 1. **Modify `src/fit.ts`** - Add message definition
16
+ 2. **Run `npm run codegen`** - Updates `src/fit_types.ts`
17
+ 3. **Add Test** - Create test in `test/`
18
+ 4. **Verify** - Run `npm run build && npm run test`
19
+
20
+ ## Example Entry
21
+
22
+ ```typescript
23
+ 285: {
24
+ name: 'jump',
25
+ 253: { field: 'timestamp', type: 'date_time', scale: null, offset: 0, units: 's' },
26
+ 0: { field: 'distance', type: 'float32', scale: null, offset: 0, units: 'm' },
27
+ 5: { field: 'position_lat', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
28
+ }
29
+ ```
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: common fit-parser development workflows
3
+ ---
4
+
5
+ # Inspect FIT File
6
+
7
+ // turbo
8
+ 1. node scripts/inspect_fit.js <path_to_fit_file> [message_key]
9
+
10
+ # Probe FIT File for Value
11
+
12
+ // turbo
13
+ 1. node scripts/deep_probe.js <path_to_fit_file> <value> [tolerance]
14
+
15
+ # Run Tests
16
+
17
+ // turbo
18
+ 1. npm run build && npm run test
19
+
20
+ # Add New Message
21
+
22
+ 1. Locate Message ID in `@garmin/fitsdk/src/profile.js`
23
+ 2. Update `src/fit.ts` with message definition
24
+ // turbo
25
+ 3. npm run codegen
26
+ 4. Add test in `test/`
27
+ // turbo
28
+ 5. npm run build && npm run test
@@ -0,0 +1,50 @@
1
+ # Investigating FIT Files
2
+
3
+ This guide explains how to investigate FIT files when you suspect fields are missing or incorrectly parsed, and how to add support for new messages.
4
+
5
+ ## Tools Included
6
+
7
+ We provide scripts in the `scripts/` directory to help you probe FIT files.
8
+
9
+ ### 1. Inspecting Parsed Data (`scripts/inspect_fit.js`)
10
+
11
+ Use this script to see what the parser currently outputs for a file.
12
+
13
+ ```bash
14
+ # View summary of all messages in file
15
+ node scripts/inspect_fit.js examples/jumps-mtb.fit
16
+
17
+ # View specific message details
18
+ node scripts/inspect_fit.js examples/jumps-mtb.fit jumps
19
+ node scripts/inspect_fit.js examples/jumps-mtb.fit session
20
+ ```
21
+
22
+ ### 2. Probing for Unknown Data (`scripts/deep_probe.js`)
23
+
24
+ Use this script when you know a value exists (e.g., from Garmin Connect or another tool) but can't find it in the parser output. It recursively searches the raw parsed structure for that value.
25
+
26
+ ```bash
27
+ # Edit the script to set the value you're looking for, then run:
28
+ node scripts/deep_probe.js
29
+ ```
30
+
31
+ ## Workflow for Missing Fields
32
+
33
+ If you suspect a field is missing (e.g., "Jump Hang Time"):
34
+
35
+ 1. **Verify it exists**: Check the file with an external tool or online viewer. Note the exact value (e.g., `0.36` seconds).
36
+ 2. **Probe**: Use `deep_probe.js` or inspect the `examples/` output to see if the value appears in an unknown field (e.g., `field_123`).
37
+ 3. **Identify Message ID**:
38
+ * Check `src/fit.ts` for the message definition.
39
+ * If the message ID seems wrong, or fields are missing, compare with the **Official Garmin FIT SDK**.
40
+ * *Tip: You can find the official SDK in `@garmin/fitsdk` if installed, or search online.*
41
+ 4. **Fix**:
42
+ * Update `src/fit.ts` with the correct Message ID and Field IDs.
43
+ * Run `npm run codegen` to update types.
44
+ * Add a test case in `test/` relative to your new message.
45
+
46
+ ## Common Issues
47
+
48
+ * **Wrong Message ID**: Sometimes `fit-parser` has legacy or guessed IDs (e.g., `jump` was 140, but official is 285).
49
+ * **Missing Fields**: New devices add new fields. They often appear as `unknown_field_X`.
50
+ * **Scale/Offset**: If values look huge (e.g., 20838184 instead of 20.838), check if they need a scale factor (e.g., `semicircles` to `degrees`).
package/check_ids.js ADDED
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import FitParser from './dist/fit-parser.js';
3
+
4
+ const content = fs.readFileSync('../sports-lib/samples/fit/jumps-mtb.fit');
5
+ const fitParser = new (FitParser.default || FitParser)({ force: true, mode: 'both' });
6
+
7
+ fitParser.parse(content, (error, data) => {
8
+ if (error) { console.error(error); return; }
9
+
10
+ // The parser stores messages in data[messageName]
11
+ // Any unknown messages are in data[messageId] (if numeric)
12
+ console.log('Keys in data:', Object.keys(data));
13
+
14
+ // Let's check for anything that looks like a message ID
15
+ for (const key of Object.keys(data)) {
16
+ if (!isNaN(parseInt(key))) {
17
+ console.log(`Unknown Message ID found: ${key} (${data[key].length} items)`);
18
+ }
19
+ }
20
+
21
+ if (data.jumps) {
22
+ console.log('Jumps fields:', Object.keys(data.jumps[0]));
23
+ }
24
+ });
@@ -0,0 +1,46 @@
1
+ import fs from 'fs'
2
+ import { createRequire } from 'module'
3
+
4
+ const require = createRequire(import.meta.url)
5
+ const FitParser = require('./dist/fit-parser.js').default
6
+
7
+ const content = fs.readFileSync('./examples/jumps-mtb.fit')
8
+ const fitParser = new FitParser({
9
+ force: true,
10
+ speedUnit: 'm/s',
11
+ lengthUnit: 'm',
12
+ temperatureUnit: 'celsius',
13
+ elapsedRecordField: true,
14
+ mode: 'both',
15
+ })
16
+
17
+ fitParser.parse(content, (error, data) => {
18
+ if (error) {
19
+ console.error(error)
20
+ return
21
+ }
22
+
23
+ console.log('=== JUMPS ARRAY ===')
24
+ if (data.jumps && data.jumps.length > 0) {
25
+ data.jumps.forEach((jump, i) => {
26
+ console.log(`\nJump ${i + 1}:`)
27
+ Object.keys(jump).forEach((key) => {
28
+ console.log(` ${key}: ${JSON.stringify(jump[key])}`)
29
+ })
30
+ })
31
+ }
32
+ else {
33
+ console.log('No jumps found')
34
+ }
35
+
36
+ // Also check definitions array for message 140 (jump)
37
+ console.log('\n=== DEFINITIONS FOR MESSAGE 140 (JUMP) ===')
38
+ if (data.definitions) {
39
+ const jumpDefs = data.definitions.filter(d => d && d.messageType === 140)
40
+ console.log(`Found ${jumpDefs.length} jump definitions`)
41
+ jumpDefs.forEach((def, i) => {
42
+ console.log(`\nDefinition ${i + 1}:`)
43
+ console.log(JSON.stringify(def, null, 2))
44
+ })
45
+ }
46
+ })
package/dist/binary.js CHANGED
@@ -187,8 +187,23 @@ function convertTo(data, unitsList, unitName) {
187
187
  const unit = options[unitName];
188
188
  return unit ? data * unit.multiplier + unit.offset : data;
189
189
  }
190
- function applyOptions(data, field, options) {
190
+ function applyOptions(data, field, options, fields) {
191
191
  switch (field) {
192
+ case 'device_type': {
193
+ const isLocal = fields.source_type === 'local' || fields.source_type === 5;
194
+ const isBLE = fields.source_type === 'bluetooth_low_energy' || fields.source_type === 3 || fields.source_type === 'bluetooth' || fields.source_type === 2;
195
+ const isANT = fields.source_type === 'antplus' || fields.source_type === 1 || fields.source_type === 'ant' || fields.source_type === 0;
196
+ if (isLocal) {
197
+ return FIT.types.local_device_type[data] || data;
198
+ }
199
+ if (isBLE) {
200
+ return FIT.types.ble_device_type[data] || data;
201
+ }
202
+ if (isANT) {
203
+ return FIT.types.antplus_device_type[data] || data;
204
+ }
205
+ return data;
206
+ }
192
207
  case 'speed':
193
208
  case 'enhanced_speed':
194
209
  case 'vertical_speed':
@@ -332,31 +347,41 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
332
347
  let readDataFromIndex = startIndex + 1;
333
348
  const fields = {};
334
349
  const message = getFitMessage(messageType.globalMessageNumber);
350
+ const rawFields = [];
335
351
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
336
352
  const fDef = messageType.fieldDefs[i];
337
353
  const data = readData(blob, fDef, readDataFromIndex, options);
338
354
  if (!isInvalidValue(data, fDef.type)) {
339
- if (fDef.isDeveloperField) {
340
- const field = fDef.name;
341
- const { type } = fDef;
342
- const { scale } = fDef;
343
- const { offset } = fDef;
344
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options);
345
- }
346
- else {
347
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
348
- if (field !== 'unknown' && field !== '' && field !== undefined) {
349
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options);
350
- }
351
- }
352
- if (message.name === 'record' && options.elapsedRecordField) {
353
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
354
- fields.timer_time = fields.elapsed_time - pausedTime;
355
- }
355
+ rawFields.push({ fDef, data });
356
356
  }
357
357
  readDataFromIndex += fDef.size;
358
358
  messageSize += fDef.size;
359
359
  }
360
+ for (const { fDef, data } of rawFields) {
361
+ const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
362
+ if (field !== 'unknown' && field !== '' && field !== undefined) {
363
+ fields[field] = data;
364
+ }
365
+ }
366
+ for (const { fDef, data } of rawFields) {
367
+ if (fDef.isDeveloperField) {
368
+ const field = fDef.name;
369
+ const { type } = fDef;
370
+ const { scale } = fDef;
371
+ const { offset } = fDef;
372
+ fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
373
+ }
374
+ else {
375
+ const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
376
+ if (field !== 'unknown' && field !== '' && field !== undefined) {
377
+ fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
378
+ }
379
+ }
380
+ if (message.name === 'record' && options.elapsedRecordField) {
381
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
382
+ fields.timer_time = fields.elapsed_time - pausedTime;
383
+ }
384
+ }
360
385
  if (message.name === 'field_description') {
361
386
  developerFields[fields.developer_data_index]
362
387
  = developerFields[fields.developer_data_index] || [];
@@ -193,8 +193,23 @@ function convertTo(data, unitsList, unitName) {
193
193
  const unit = options[unitName];
194
194
  return unit ? data * unit.multiplier + unit.offset : data;
195
195
  }
196
- function applyOptions(data, field, options) {
196
+ function applyOptions(data, field, options, fields) {
197
197
  switch (field) {
198
+ case 'device_type': {
199
+ const isLocal = fields.source_type === 'local' || fields.source_type === 5;
200
+ const isBLE = fields.source_type === 'bluetooth_low_energy' || fields.source_type === 3 || fields.source_type === 'bluetooth' || fields.source_type === 2;
201
+ const isANT = fields.source_type === 'antplus' || fields.source_type === 1 || fields.source_type === 'ant' || fields.source_type === 0;
202
+ if (isLocal) {
203
+ return fit_js_1.FIT.types.local_device_type[data] || data;
204
+ }
205
+ if (isBLE) {
206
+ return fit_js_1.FIT.types.ble_device_type[data] || data;
207
+ }
208
+ if (isANT) {
209
+ return fit_js_1.FIT.types.antplus_device_type[data] || data;
210
+ }
211
+ return data;
212
+ }
198
213
  case 'speed':
199
214
  case 'enhanced_speed':
200
215
  case 'vertical_speed':
@@ -338,31 +353,41 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
338
353
  let readDataFromIndex = startIndex + 1;
339
354
  const fields = {};
340
355
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
356
+ const rawFields = [];
341
357
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
342
358
  const fDef = messageType.fieldDefs[i];
343
359
  const data = readData(blob, fDef, readDataFromIndex, options);
344
360
  if (!isInvalidValue(data, fDef.type)) {
345
- if (fDef.isDeveloperField) {
346
- const field = fDef.name;
347
- const { type } = fDef;
348
- const { scale } = fDef;
349
- const { offset } = fDef;
350
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options);
351
- }
352
- else {
353
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
354
- if (field !== 'unknown' && field !== '' && field !== undefined) {
355
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options);
356
- }
357
- }
358
- if (message.name === 'record' && options.elapsedRecordField) {
359
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
360
- fields.timer_time = fields.elapsed_time - pausedTime;
361
- }
361
+ rawFields.push({ fDef, data });
362
362
  }
363
363
  readDataFromIndex += fDef.size;
364
364
  messageSize += fDef.size;
365
365
  }
366
+ for (const { fDef, data } of rawFields) {
367
+ const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
368
+ if (field !== 'unknown' && field !== '' && field !== undefined) {
369
+ fields[field] = data;
370
+ }
371
+ }
372
+ for (const { fDef, data } of rawFields) {
373
+ if (fDef.isDeveloperField) {
374
+ const field = fDef.name;
375
+ const { type } = fDef;
376
+ const { scale } = fDef;
377
+ const { offset } = fDef;
378
+ fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
379
+ }
380
+ else {
381
+ const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
382
+ if (field !== 'unknown' && field !== '' && field !== undefined) {
383
+ fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
384
+ }
385
+ }
386
+ if (message.name === 'record' && options.elapsedRecordField) {
387
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
388
+ fields.timer_time = fields.elapsed_time - pausedTime;
389
+ }
390
+ }
366
391
  if (message.name === 'field_description') {
367
392
  developerFields[fields.developer_data_index]
368
393
  = developerFields[fields.developer_data_index] || [];
package/dist/cjs/fit.js CHANGED
@@ -3005,13 +3005,18 @@ exports.FIT = {
3005
3005
  units: 'percent',
3006
3006
  },
3007
3007
  },
3008
- 140: {
3008
+ 285: {
3009
3009
  name: 'jump',
3010
3010
  253: { field: 'timestamp', type: 'date_time', scale: null, offset: 0, units: 's' },
3011
- 9: { field: 'distance', type: 'uint16', scale: 100, offset: 0, units: 'm' },
3012
- 10: { field: 'height', type: 'uint16', scale: 1000, offset: 0, units: 'm' },
3013
- 37: { field: 'score', type: 'sint32', scale: null, offset: 0, units: '' },
3014
- 7: { field: 'enhanced_mets', type: 'uint32', scale: 65536, offset: 0, units: 'METs' },
3011
+ 0: { field: 'distance', type: 'float32', scale: null, offset: 0, units: 'm' },
3012
+ 1: { field: 'height', type: 'float32', scale: null, offset: 0, units: 'm' },
3013
+ 2: { field: 'rotations', type: 'uint8', scale: null, offset: 0, units: '' },
3014
+ 3: { field: 'hang_time', type: 'float32', scale: null, offset: 0, units: 's' },
3015
+ 4: { field: 'score', type: 'float32', scale: null, offset: 0, units: '' },
3016
+ 5: { field: 'position_lat', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
3017
+ 6: { field: 'position_long', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
3018
+ 7: { field: 'speed', type: 'uint16', scale: 1000, offset: 0, units: 'm/s' },
3019
+ 8: { field: 'enhanced_speed', type: 'uint32', scale: 1000, offset: 0, units: 'm/s' },
3015
3020
  },
3016
3021
  21: {
3017
3022
  name: 'event',
@@ -3101,7 +3106,7 @@ exports.FIT = {
3101
3106
  },
3102
3107
  1: {
3103
3108
  field: 'device_type',
3104
- type: 'antplus_device_type',
3109
+ type: 'uint8',
3105
3110
  scale: null,
3106
3111
  offset: 0,
3107
3112
  units: '',
@@ -5973,7 +5978,6 @@ exports.FIT = {
5973
5978
  65534: 'connect',
5974
5979
  },
5975
5980
  antplus_device_type: {
5976
- 0: 0,
5977
5981
  1: 'antfs',
5978
5982
  11: 'bike_power',
5979
5983
  12: 'environment_sensor_legacy',
@@ -5987,10 +5991,12 @@ exports.FIT = {
5987
5991
  26: 'racquet',
5988
5992
  27: 'control_hub',
5989
5993
  31: 'muscle_oxygen',
5994
+ 34: 'shifting',
5990
5995
  35: 'bike_light_main',
5991
5996
  36: 'bike_light_shared',
5992
5997
  38: 'exd',
5993
5998
  40: 'bike_radar',
5999
+ 46: 'bike_aero',
5994
6000
  119: 'weight_scale',
5995
6001
  120: 'heart_rate',
5996
6002
  121: 'bike_speed_cadence',
@@ -5998,6 +6004,26 @@ exports.FIT = {
5998
6004
  123: 'bike_speed',
5999
6005
  124: 'stride_speed_distance',
6000
6006
  },
6007
+ local_device_type: {
6008
+ 0: 'gps',
6009
+ 1: 'glonass',
6010
+ 2: 'gps_glonass',
6011
+ 3: 'accelerometer',
6012
+ 4: 'barometer',
6013
+ 5: 'temperature',
6014
+ 10: 'whr',
6015
+ 12: 'sensor_hub',
6016
+ },
6017
+ ble_device_type: {
6018
+ 0: 'connected_gps',
6019
+ 1: 'heart_rate',
6020
+ 2: 'bike_power',
6021
+ 3: 'bike_speed_cadence',
6022
+ 4: 'bike_speed',
6023
+ 5: 'bike_cadence',
6024
+ 6: 'footpod',
6025
+ 7: 'bike_trainer',
6026
+ },
6001
6027
  ant_network: {
6002
6028
  0: 'public',
6003
6029
  1: 'antplus',
@@ -72,7 +72,9 @@ export type Schedule = 'workout' | 'course';
72
72
  export type CoursePoint = 'generic' | 'summit' | 'valley' | 'water' | 'food' | 'danger' | 'left' | 'right' | 'straight' | 'first_aid' | 'fourth_category' | 'third_category' | 'second_category' | 'first_category' | 'hors_category' | 'sprint' | 'left_fork' | 'right_fork' | 'middle_fork' | 'slight_left' | 'sharp_left' | 'slight_right' | 'sharp_right' | 'u_turn' | 'segment_start' | 'segment_end';
73
73
  export type Manufacturer = '0' | 'garmin' | 'garmin_fr405_antfs' | 'zephyr' | 'dayton' | 'idt' | 'srm' | 'quarq' | 'ibike' | 'saris' | 'spark_hk' | 'tanita' | 'echowell' | 'dynastream_oem' | 'nautilus' | 'dynastream' | 'timex' | 'metrigear' | 'xelic' | 'beurer' | 'cardiosport' | 'a_and_d' | 'hmm' | 'suunto' | 'thita_elektronik' | 'gpulse' | 'clean_mobile' | 'pedal_brain' | 'peaksware' | 'saxonar' | 'lemond_fitness' | 'dexcom' | 'wahoo_fitness' | 'octane_fitness' | 'archinoetics' | 'the_hurt_box' | 'citizen_systems' | 'magellan' | 'osynce' | 'holux' | 'concept2' | 'one_giant_leap' | 'ace_sensor' | 'brim_brothers' | 'xplova' | 'perception_digital' | 'bf1systems' | 'pioneer' | 'spantec' | 'metalogics' | '4iiiis' | 'seiko_epson' | 'seiko_epson_oem' | 'ifor_powell' | 'maxwell_guider' | 'star_trac' | 'breakaway' | 'alatech_technology_ltd' | 'mio_technology_europe' | 'rotor' | 'geonaute' | 'id_bike' | 'specialized' | 'wtek' | 'physical_enterprises' | 'north_pole_engineering' | 'bkool' | 'cateye' | 'stages_cycling' | 'sigmasport' | 'tomtom' | 'peripedal' | 'wattbike' | 'moxy' | 'ciclosport' | 'powerbahn' | 'acorn_projects_aps' | 'lifebeam' | 'bontrager' | 'wellgo' | 'scosche' | 'magura' | 'woodway' | 'elite' | 'nielsen_kellerman' | 'dk_city' | 'tacx' | 'direction_technology' | 'magtonic' | '1partcarbon' | 'inside_ride_technologies' | 'sound_of_motion' | 'stryd' | 'icg' | 'mipulse' | 'bsx_athletics' | 'look' | 'campagnolo_srl' | 'body_bike_smart' | 'praxisworks' | 'limits_technology' | 'topaction_technology' | 'cosinuss' | 'fitcare' | 'magene' | 'giant_manufacturing_co' | 'tigrasport' | 'salutron' | 'technogym' | 'bryton_sensors' | 'latitude_limited' | 'soaring_technology' | 'igpsport' | 'thinkrider' | 'gopher_sport' | 'waterrower' | 'orangetheory' | 'inpeak' | 'kinetic' | 'johnson_health_tech' | 'polar_electro' | 'seesense' | 'nci_technology' | 'development' | 'healthandlife' | 'lezyne' | 'scribe_labs' | 'zwift' | 'watteam' | 'recon' | 'favero_electronics' | 'dynovelo' | 'strava' | 'precor' | 'bryton' | 'sram' | 'navman' | 'cobi' | 'spivi' | 'mio_magellan' | 'evesports' | 'sensitivus_gauge' | 'podoon' | 'life_time_fitness' | 'falco_e_motors' | 'minoura' | 'cycliq' | 'luxottica' | 'trainer_road' | 'the_sufferfest' | 'fullspeedahead' | 'virtualtraining' | 'feedbacksports' | 'omata' | 'vdo' | 'magneticdays' | 'hammerhead' | 'kinetic_by_kurt' | 'shapelog' | 'dabuziduo' | 'jetblack' | 'coros' | 'virtugo' | 'velosense' | 'actigraphcorp';
74
74
  export type GarminProduct = 'hrm_bike' | 'hrm1' | 'axh01' | 'axb01' | 'axb02' | 'hrm2ss' | 'dsi_alf02' | 'hrm3ss' | 'hrm_run_single_byte_product_id' | 'bsm' | 'bcm' | 'axs01' | 'hrm_tri_single_byte_product_id' | 'fr225_single_byte_product_id' | 'fr301_china' | 'fr301_japan' | 'fr301_korea' | 'fr301_taiwan' | 'fr405' | 'fr50' | 'fr405_japan' | 'fr60' | 'dsi_alf01' | 'fr310xt' | 'edge500' | 'fr110' | 'edge800' | 'edge500_taiwan' | 'edge500_japan' | 'chirp' | 'fr110_japan' | 'edge200' | 'fr910xt' | 'edge800_taiwan' | 'edge800_japan' | 'alf04' | 'fr610' | 'fr210_japan' | 'vector_ss' | 'vector_cp' | 'edge800_china' | 'edge500_china' | 'fr610_japan' | 'edge500_korea' | 'fr70' | 'fr310xt_4t' | 'amx' | 'fr10' | 'edge800_korea' | 'swim' | 'fr910xt_china' | 'fenix' | 'edge200_taiwan' | 'edge510' | 'edge810' | 'tempe' | 'fr910xt_japan' | 'fr620' | 'fr220' | 'fr910xt_korea' | 'fr10_japan' | 'edge810_japan' | 'virb_elite' | 'edge_touring' | 'edge510_japan' | 'hrm_tri' | 'hrm_run' | 'fr920xt' | 'edge510_asia' | 'edge810_china' | 'edge810_taiwan' | 'edge1000' | 'vivo_fit' | 'virb_remote' | 'vivo_ki' | 'fr15' | 'vivo_active' | 'edge510_korea' | 'fr620_japan' | 'fr620_china' | 'fr220_japan' | 'fr220_china' | 'approach_s6' | 'vivo_smart' | 'fenix2' | 'epix' | 'fenix3' | 'edge1000_taiwan' | 'edge1000_japan' | 'fr15_japan' | 'edge520' | 'edge1000_china' | 'fr620_russia' | 'fr220_russia' | 'vector_s' | 'edge1000_korea' | 'fr920xt_taiwan' | 'fr920xt_china' | 'fr920xt_japan' | 'virbx' | 'vivo_smart_apac' | 'etrex_touch' | 'edge25' | 'fr25' | 'vivo_fit2' | 'fr225' | 'fr630' | 'fr230' | 'fr735xt' | 'vivo_active_apac' | 'vector_2' | 'vector_2s' | 'virbxe' | 'fr620_taiwan' | 'fr220_taiwan' | 'truswing' | 'fenix3_china' | 'fenix3_twn' | 'varia_headlight' | 'varia_taillight_old' | 'edge_explore_1000' | 'fr225_asia' | 'varia_radar_taillight' | 'varia_radar_display' | 'edge20' | 'd2_bravo' | 'approach_s20' | 'varia_remote' | 'hrm4_run' | 'vivo_active_hr' | 'vivo_smart_hr' | 'vivo_move' | 'varia_vision' | 'vivo_fit3' | 'fenix3_hr' | 'virb_ultra_30' | 'index_smart_scale' | 'fr235' | 'fenix3_chronos' | 'oregon7xx' | 'rino7xx' | 'nautix' | 'edge_820' | 'edge_explore_820' | 'fenix5s' | 'd2_bravo_titanium' | 'varia_ut800' | 'running_dynamics_pod' | 'fenix5x' | 'vivo_fit_jr' | 'fr935' | 'fenix5' | 'descent' | 'sdm4' | 'edge_remote' | 'training_center' | 'connectiq_simulator' | 'android_antplus_plugin' | 'connect';
75
- export type AntplusDeviceType = '0' | 'antfs' | 'bike_power' | 'environment_sensor_legacy' | 'multi_sport_speed_distance' | 'control' | 'fitness_equipment' | 'blood_pressure' | 'geocache_node' | 'light_electric_vehicle' | 'env_sensor' | 'racquet' | 'control_hub' | 'muscle_oxygen' | 'bike_light_main' | 'bike_light_shared' | 'exd' | 'bike_radar' | 'weight_scale' | 'heart_rate' | 'bike_speed_cadence' | 'bike_cadence' | 'bike_speed' | 'stride_speed_distance';
75
+ export type AntplusDeviceType = '0' | 'antfs' | 'bike_power' | 'environment_sensor_legacy' | 'multi_sport_speed_distance' | 'control' | 'fitness_equipment' | 'blood_pressure' | 'geocache_node' | 'light_electric_vehicle' | 'env_sensor' | 'racquet' | 'control_hub' | 'muscle_oxygen' | 'shifting' | 'bike_light_main' | 'bike_light_shared' | 'exd' | 'bike_radar' | 'weight_scale' | 'heart_rate' | 'bike_speed_cadence' | 'bike_cadence' | 'bike_speed' | 'stride_speed_distance';
76
+ export type LocalDeviceType = 'gps' | 'glonass' | 'beidou' | 'galileo' | 'waas_egnos' | 'local' | 'barometer' | 'accelerometer' | 'gyroscope' | 'compass';
77
+ export type BleDeviceType = '0' | 'heart_rate' | 'cycling_speed_cadence' | 'cycling_power';
76
78
  export type AntNetwork = 'public' | 'antplus' | 'antfs' | 'private';
77
79
  export type WorkoutCapabilities = '0' | 'interval' | 'custom' | 'fitness_equipment' | 'firstbeat' | 'new_leaf' | 'tcx' | 'speed' | 'heart_rate' | 'distance' | 'cadence' | 'power' | 'grade' | 'resistance' | 'protected';
78
80
  export type BatteryStatus = '0' | 'new' | 'good' | 'ok' | 'low' | 'critical' | 'charging' | 'unknown';
@@ -871,13 +873,6 @@ export interface ParsedOHrSettings {
871
873
  enabled?: number;
872
874
  timestamp: string;
873
875
  }
874
- export interface ParsedJump {
875
- enhanced_mets?: number;
876
- distance?: number;
877
- height?: number;
878
- score?: number;
879
- timestamp: string;
880
- }
881
876
  export interface ParsedFieldDescription {
882
877
  developer_data_index?: number;
883
878
  field_definition_number?: number;
@@ -975,6 +970,18 @@ export interface ParsedDiveSummary {
975
970
  bottom_time?: number;
976
971
  timestamp: string;
977
972
  }
973
+ export interface ParsedJump {
974
+ distance?: number;
975
+ height?: number;
976
+ rotations?: number;
977
+ hang_time?: number;
978
+ score?: number;
979
+ position_lat?: number;
980
+ position_long?: number;
981
+ speed?: number;
982
+ enhanced_speed?: number;
983
+ timestamp: string;
984
+ }
978
985
  export interface ParsedTankUpdate {
979
986
  sensor?: number;
980
987
  pressure?: number;
package/dist/fit.js CHANGED
@@ -3002,13 +3002,18 @@ export const FIT = {
3002
3002
  units: 'percent',
3003
3003
  },
3004
3004
  },
3005
- 140: {
3005
+ 285: {
3006
3006
  name: 'jump',
3007
3007
  253: { field: 'timestamp', type: 'date_time', scale: null, offset: 0, units: 's' },
3008
- 9: { field: 'distance', type: 'uint16', scale: 100, offset: 0, units: 'm' },
3009
- 10: { field: 'height', type: 'uint16', scale: 1000, offset: 0, units: 'm' },
3010
- 37: { field: 'score', type: 'sint32', scale: null, offset: 0, units: '' },
3011
- 7: { field: 'enhanced_mets', type: 'uint32', scale: 65536, offset: 0, units: 'METs' },
3008
+ 0: { field: 'distance', type: 'float32', scale: null, offset: 0, units: 'm' },
3009
+ 1: { field: 'height', type: 'float32', scale: null, offset: 0, units: 'm' },
3010
+ 2: { field: 'rotations', type: 'uint8', scale: null, offset: 0, units: '' },
3011
+ 3: { field: 'hang_time', type: 'float32', scale: null, offset: 0, units: 's' },
3012
+ 4: { field: 'score', type: 'float32', scale: null, offset: 0, units: '' },
3013
+ 5: { field: 'position_lat', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
3014
+ 6: { field: 'position_long', type: 'sint32', scale: null, offset: 0, units: 'semicircles' },
3015
+ 7: { field: 'speed', type: 'uint16', scale: 1000, offset: 0, units: 'm/s' },
3016
+ 8: { field: 'enhanced_speed', type: 'uint32', scale: 1000, offset: 0, units: 'm/s' },
3012
3017
  },
3013
3018
  21: {
3014
3019
  name: 'event',
@@ -3098,7 +3103,7 @@ export const FIT = {
3098
3103
  },
3099
3104
  1: {
3100
3105
  field: 'device_type',
3101
- type: 'antplus_device_type',
3106
+ type: 'uint8',
3102
3107
  scale: null,
3103
3108
  offset: 0,
3104
3109
  units: '',
@@ -5970,7 +5975,6 @@ export const FIT = {
5970
5975
  65534: 'connect',
5971
5976
  },
5972
5977
  antplus_device_type: {
5973
- 0: 0,
5974
5978
  1: 'antfs',
5975
5979
  11: 'bike_power',
5976
5980
  12: 'environment_sensor_legacy',
@@ -5984,10 +5988,12 @@ export const FIT = {
5984
5988
  26: 'racquet',
5985
5989
  27: 'control_hub',
5986
5990
  31: 'muscle_oxygen',
5991
+ 34: 'shifting',
5987
5992
  35: 'bike_light_main',
5988
5993
  36: 'bike_light_shared',
5989
5994
  38: 'exd',
5990
5995
  40: 'bike_radar',
5996
+ 46: 'bike_aero',
5991
5997
  119: 'weight_scale',
5992
5998
  120: 'heart_rate',
5993
5999
  121: 'bike_speed_cadence',
@@ -5995,6 +6001,26 @@ export const FIT = {
5995
6001
  123: 'bike_speed',
5996
6002
  124: 'stride_speed_distance',
5997
6003
  },
6004
+ local_device_type: {
6005
+ 0: 'gps',
6006
+ 1: 'glonass',
6007
+ 2: 'gps_glonass',
6008
+ 3: 'accelerometer',
6009
+ 4: 'barometer',
6010
+ 5: 'temperature',
6011
+ 10: 'whr',
6012
+ 12: 'sensor_hub',
6013
+ },
6014
+ ble_device_type: {
6015
+ 0: 'connected_gps',
6016
+ 1: 'heart_rate',
6017
+ 2: 'bike_power',
6018
+ 3: 'bike_speed_cadence',
6019
+ 4: 'bike_speed',
6020
+ 5: 'bike_cadence',
6021
+ 6: 'footpod',
6022
+ 7: 'bike_trainer',
6023
+ },
5998
6024
  ant_network: {
5999
6025
  0: 'public',
6000
6026
  1: 'antplus',
@@ -72,7 +72,9 @@ export type Schedule = 'workout' | 'course';
72
72
  export type CoursePoint = 'generic' | 'summit' | 'valley' | 'water' | 'food' | 'danger' | 'left' | 'right' | 'straight' | 'first_aid' | 'fourth_category' | 'third_category' | 'second_category' | 'first_category' | 'hors_category' | 'sprint' | 'left_fork' | 'right_fork' | 'middle_fork' | 'slight_left' | 'sharp_left' | 'slight_right' | 'sharp_right' | 'u_turn' | 'segment_start' | 'segment_end';
73
73
  export type Manufacturer = '0' | 'garmin' | 'garmin_fr405_antfs' | 'zephyr' | 'dayton' | 'idt' | 'srm' | 'quarq' | 'ibike' | 'saris' | 'spark_hk' | 'tanita' | 'echowell' | 'dynastream_oem' | 'nautilus' | 'dynastream' | 'timex' | 'metrigear' | 'xelic' | 'beurer' | 'cardiosport' | 'a_and_d' | 'hmm' | 'suunto' | 'thita_elektronik' | 'gpulse' | 'clean_mobile' | 'pedal_brain' | 'peaksware' | 'saxonar' | 'lemond_fitness' | 'dexcom' | 'wahoo_fitness' | 'octane_fitness' | 'archinoetics' | 'the_hurt_box' | 'citizen_systems' | 'magellan' | 'osynce' | 'holux' | 'concept2' | 'one_giant_leap' | 'ace_sensor' | 'brim_brothers' | 'xplova' | 'perception_digital' | 'bf1systems' | 'pioneer' | 'spantec' | 'metalogics' | '4iiiis' | 'seiko_epson' | 'seiko_epson_oem' | 'ifor_powell' | 'maxwell_guider' | 'star_trac' | 'breakaway' | 'alatech_technology_ltd' | 'mio_technology_europe' | 'rotor' | 'geonaute' | 'id_bike' | 'specialized' | 'wtek' | 'physical_enterprises' | 'north_pole_engineering' | 'bkool' | 'cateye' | 'stages_cycling' | 'sigmasport' | 'tomtom' | 'peripedal' | 'wattbike' | 'moxy' | 'ciclosport' | 'powerbahn' | 'acorn_projects_aps' | 'lifebeam' | 'bontrager' | 'wellgo' | 'scosche' | 'magura' | 'woodway' | 'elite' | 'nielsen_kellerman' | 'dk_city' | 'tacx' | 'direction_technology' | 'magtonic' | '1partcarbon' | 'inside_ride_technologies' | 'sound_of_motion' | 'stryd' | 'icg' | 'mipulse' | 'bsx_athletics' | 'look' | 'campagnolo_srl' | 'body_bike_smart' | 'praxisworks' | 'limits_technology' | 'topaction_technology' | 'cosinuss' | 'fitcare' | 'magene' | 'giant_manufacturing_co' | 'tigrasport' | 'salutron' | 'technogym' | 'bryton_sensors' | 'latitude_limited' | 'soaring_technology' | 'igpsport' | 'thinkrider' | 'gopher_sport' | 'waterrower' | 'orangetheory' | 'inpeak' | 'kinetic' | 'johnson_health_tech' | 'polar_electro' | 'seesense' | 'nci_technology' | 'development' | 'healthandlife' | 'lezyne' | 'scribe_labs' | 'zwift' | 'watteam' | 'recon' | 'favero_electronics' | 'dynovelo' | 'strava' | 'precor' | 'bryton' | 'sram' | 'navman' | 'cobi' | 'spivi' | 'mio_magellan' | 'evesports' | 'sensitivus_gauge' | 'podoon' | 'life_time_fitness' | 'falco_e_motors' | 'minoura' | 'cycliq' | 'luxottica' | 'trainer_road' | 'the_sufferfest' | 'fullspeedahead' | 'virtualtraining' | 'feedbacksports' | 'omata' | 'vdo' | 'magneticdays' | 'hammerhead' | 'kinetic_by_kurt' | 'shapelog' | 'dabuziduo' | 'jetblack' | 'coros' | 'virtugo' | 'velosense' | 'actigraphcorp';
74
74
  export type GarminProduct = 'hrm_bike' | 'hrm1' | 'axh01' | 'axb01' | 'axb02' | 'hrm2ss' | 'dsi_alf02' | 'hrm3ss' | 'hrm_run_single_byte_product_id' | 'bsm' | 'bcm' | 'axs01' | 'hrm_tri_single_byte_product_id' | 'fr225_single_byte_product_id' | 'fr301_china' | 'fr301_japan' | 'fr301_korea' | 'fr301_taiwan' | 'fr405' | 'fr50' | 'fr405_japan' | 'fr60' | 'dsi_alf01' | 'fr310xt' | 'edge500' | 'fr110' | 'edge800' | 'edge500_taiwan' | 'edge500_japan' | 'chirp' | 'fr110_japan' | 'edge200' | 'fr910xt' | 'edge800_taiwan' | 'edge800_japan' | 'alf04' | 'fr610' | 'fr210_japan' | 'vector_ss' | 'vector_cp' | 'edge800_china' | 'edge500_china' | 'fr610_japan' | 'edge500_korea' | 'fr70' | 'fr310xt_4t' | 'amx' | 'fr10' | 'edge800_korea' | 'swim' | 'fr910xt_china' | 'fenix' | 'edge200_taiwan' | 'edge510' | 'edge810' | 'tempe' | 'fr910xt_japan' | 'fr620' | 'fr220' | 'fr910xt_korea' | 'fr10_japan' | 'edge810_japan' | 'virb_elite' | 'edge_touring' | 'edge510_japan' | 'hrm_tri' | 'hrm_run' | 'fr920xt' | 'edge510_asia' | 'edge810_china' | 'edge810_taiwan' | 'edge1000' | 'vivo_fit' | 'virb_remote' | 'vivo_ki' | 'fr15' | 'vivo_active' | 'edge510_korea' | 'fr620_japan' | 'fr620_china' | 'fr220_japan' | 'fr220_china' | 'approach_s6' | 'vivo_smart' | 'fenix2' | 'epix' | 'fenix3' | 'edge1000_taiwan' | 'edge1000_japan' | 'fr15_japan' | 'edge520' | 'edge1000_china' | 'fr620_russia' | 'fr220_russia' | 'vector_s' | 'edge1000_korea' | 'fr920xt_taiwan' | 'fr920xt_china' | 'fr920xt_japan' | 'virbx' | 'vivo_smart_apac' | 'etrex_touch' | 'edge25' | 'fr25' | 'vivo_fit2' | 'fr225' | 'fr630' | 'fr230' | 'fr735xt' | 'vivo_active_apac' | 'vector_2' | 'vector_2s' | 'virbxe' | 'fr620_taiwan' | 'fr220_taiwan' | 'truswing' | 'fenix3_china' | 'fenix3_twn' | 'varia_headlight' | 'varia_taillight_old' | 'edge_explore_1000' | 'fr225_asia' | 'varia_radar_taillight' | 'varia_radar_display' | 'edge20' | 'd2_bravo' | 'approach_s20' | 'varia_remote' | 'hrm4_run' | 'vivo_active_hr' | 'vivo_smart_hr' | 'vivo_move' | 'varia_vision' | 'vivo_fit3' | 'fenix3_hr' | 'virb_ultra_30' | 'index_smart_scale' | 'fr235' | 'fenix3_chronos' | 'oregon7xx' | 'rino7xx' | 'nautix' | 'edge_820' | 'edge_explore_820' | 'fenix5s' | 'd2_bravo_titanium' | 'varia_ut800' | 'running_dynamics_pod' | 'fenix5x' | 'vivo_fit_jr' | 'fr935' | 'fenix5' | 'descent' | 'sdm4' | 'edge_remote' | 'training_center' | 'connectiq_simulator' | 'android_antplus_plugin' | 'connect';
75
- export type AntplusDeviceType = '0' | 'antfs' | 'bike_power' | 'environment_sensor_legacy' | 'multi_sport_speed_distance' | 'control' | 'fitness_equipment' | 'blood_pressure' | 'geocache_node' | 'light_electric_vehicle' | 'env_sensor' | 'racquet' | 'control_hub' | 'muscle_oxygen' | 'bike_light_main' | 'bike_light_shared' | 'exd' | 'bike_radar' | 'weight_scale' | 'heart_rate' | 'bike_speed_cadence' | 'bike_cadence' | 'bike_speed' | 'stride_speed_distance';
75
+ export type AntplusDeviceType = '0' | 'antfs' | 'bike_power' | 'environment_sensor_legacy' | 'multi_sport_speed_distance' | 'control' | 'fitness_equipment' | 'blood_pressure' | 'geocache_node' | 'light_electric_vehicle' | 'env_sensor' | 'racquet' | 'control_hub' | 'muscle_oxygen' | 'shifting' | 'bike_light_main' | 'bike_light_shared' | 'exd' | 'bike_radar' | 'weight_scale' | 'heart_rate' | 'bike_speed_cadence' | 'bike_cadence' | 'bike_speed' | 'stride_speed_distance';
76
+ export type LocalDeviceType = 'gps' | 'glonass' | 'beidou' | 'galileo' | 'waas_egnos' | 'local' | 'barometer' | 'accelerometer' | 'gyroscope' | 'compass';
77
+ export type BleDeviceType = '0' | 'heart_rate' | 'cycling_speed_cadence' | 'cycling_power';
76
78
  export type AntNetwork = 'public' | 'antplus' | 'antfs' | 'private';
77
79
  export type WorkoutCapabilities = '0' | 'interval' | 'custom' | 'fitness_equipment' | 'firstbeat' | 'new_leaf' | 'tcx' | 'speed' | 'heart_rate' | 'distance' | 'cadence' | 'power' | 'grade' | 'resistance' | 'protected';
78
80
  export type BatteryStatus = '0' | 'new' | 'good' | 'ok' | 'low' | 'critical' | 'charging' | 'unknown';
@@ -871,13 +873,6 @@ export interface ParsedOHrSettings {
871
873
  enabled?: number;
872
874
  timestamp: string;
873
875
  }
874
- export interface ParsedJump {
875
- enhanced_mets?: number;
876
- distance?: number;
877
- height?: number;
878
- score?: number;
879
- timestamp: string;
880
- }
881
876
  export interface ParsedFieldDescription {
882
877
  developer_data_index?: number;
883
878
  field_definition_number?: number;
@@ -975,6 +970,18 @@ export interface ParsedDiveSummary {
975
970
  bottom_time?: number;
976
971
  timestamp: string;
977
972
  }
973
+ export interface ParsedJump {
974
+ distance?: number;
975
+ height?: number;
976
+ rotations?: number;
977
+ hang_time?: number;
978
+ score?: number;
979
+ position_lat?: number;
980
+ position_long?: number;
981
+ speed?: number;
982
+ enhanced_speed?: number;
983
+ timestamp: string;
984
+ }
978
985
  export interface ParsedTankUpdate {
979
986
  sensor?: number;
980
987
  pressure?: number;
package/find_field.js ADDED
@@ -0,0 +1,23 @@
1
+ const fs = require('fs');
2
+ const FitParser = require('./dist/fit-parser.js').default;
3
+
4
+ const content = fs.readFileSync('../sports-lib/samples/fit/jumps-mtb.fit');
5
+ const fitParser = new FitParser({ force: true, mode: 'both' });
6
+
7
+ fitParser.parse(content, (error, data) => {
8
+ if (error) { console.error(error); return; }
9
+
10
+ function search(obj, path = '') {
11
+ if (!obj || typeof obj !== 'object') return;
12
+ for (const [key, value] of Object.entries(obj)) {
13
+ const currentPath = path ? `${path}.${key}` : key;
14
+ if (key === 'enhanced_mets') {
15
+ console.log(`FOUND enhanced_mets at ${currentPath}: ${value}`);
16
+ }
17
+ search(value, currentPath);
18
+ }
19
+ }
20
+
21
+ search(data);
22
+ console.log('Search finished.');
23
+ });
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import FitParser from './dist/fit-parser.js';
3
+
4
+ const content = fs.readFileSync('../sports-lib/samples/fit/jumps-mtb.fit');
5
+ // Try both ways to instantiate
6
+ const fitParser = new (FitParser.default || FitParser)({ force: true, mode: 'both' });
7
+
8
+ fitParser.parse(content, (error, data) => {
9
+ if (error) { console.error(error); return; }
10
+
11
+ function search(obj, path = '') {
12
+ if (!obj || typeof obj !== 'object') return;
13
+ if (Array.isArray(obj)) {
14
+ obj.forEach((item, index) => search(item, `${path}[${index}]`));
15
+ return;
16
+ }
17
+ for (const [key, value] of Object.entries(obj)) {
18
+ const currentPath = path ? `${path}.${key}` : key;
19
+ if (key === 'enhanced_mets') {
20
+ console.log(`FOUND enhanced_mets at ${currentPath}: ${value}`);
21
+ }
22
+ search(value, currentPath);
23
+ }
24
+ }
25
+
26
+ search(data);
27
+ console.log('Search finished.');
28
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "2.2.4",
4
+ "version": "2.2.6",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {
@@ -75,4 +75,4 @@
75
75
  "typescript": "^5.9.3",
76
76
  "vitest": "^4.0.13"
77
77
  }
78
- }
78
+ }
package/scan_jumps.js ADDED
@@ -0,0 +1,29 @@
1
+ import fs from 'fs';
2
+ import FitParser from './dist/fit-parser.js';
3
+
4
+ const content = fs.readFileSync('../sports-lib/samples/fit/jumps-mtb.fit');
5
+ const fitParser = new (FitParser.default || FitParser)({ force: true, mode: 'both' });
6
+
7
+ fitParser.parse(content, (error, data) => {
8
+ if (error) { console.error(error); return; }
9
+
10
+ if (data.jumps && data.jumps.length > 0) {
11
+ console.log(`Found ${data.jumps.length} jumps.`);
12
+ data.jumps.forEach((j, i) => {
13
+ // check for values close to 16.3038
14
+ const target = 16.3038;
15
+ const epsilon = 0.1;
16
+
17
+ Object.entries(j).forEach(([key, val]) => {
18
+ if (typeof val === 'number' && Math.abs(val - target) < epsilon) {
19
+ console.log(`MATCH INDEX ${i} KEY ${key}: ${val}`);
20
+ }
21
+ });
22
+
23
+ // Also just print field 7 ('speed' in new def)
24
+ console.log(`Jump ${i}: speed=${j.speed}`);
25
+ });
26
+ } else {
27
+ console.log('No jumps found.');
28
+ }
29
+ });
@@ -0,0 +1,74 @@
1
+ import fs from 'fs'
2
+ import { createRequire } from 'module'
3
+
4
+ const require = createRequire(import.meta.url)
5
+ // Try to load from local build or node_modules
6
+ let FitParser
7
+ try {
8
+ FitParser = require('../dist/fit-parser.js').default
9
+ }
10
+ catch (e) {
11
+ FitParser = require('fit-file-parser').default
12
+ }
13
+
14
+ const args = process.argv.slice(2)
15
+ if (args.length < 2) {
16
+ console.log('Usage: node deep_probe.js <path_to_fit_file> <search_value> [tolerance]')
17
+ console.log('Example: node deep_probe.js ../examples/jumps-mtb.fit 11 0.1')
18
+ process.exit(1)
19
+ }
20
+
21
+ const filePath = args[0]
22
+ const targetValue = Number.parseFloat(args[1])
23
+ const tolerance = args[2] ? Number.parseFloat(args[2]) : 0.001
24
+
25
+ if (isNaN(targetValue)) {
26
+ console.error('Error: Search value must be a number')
27
+ process.exit(1)
28
+ }
29
+
30
+ try {
31
+ const content = fs.readFileSync(filePath)
32
+ const fitParser = new FitParser({
33
+ force: true,
34
+ speedUnit: 'm/s',
35
+ lengthUnit: 'm',
36
+ temperatureUnit: 'celsius',
37
+ elapsedRecordField: true,
38
+ mode: 'both',
39
+ })
40
+
41
+ function search(obj, path = []) {
42
+ if (!obj || typeof obj !== 'object')
43
+ return
44
+
45
+ Object.keys(obj).forEach((key) => {
46
+ const val = obj[key]
47
+ const newPath = [...path, key]
48
+
49
+ if (typeof val === 'number') {
50
+ if (Math.abs(val - targetValue) <= tolerance) {
51
+ console.log(`>>> FOUND MATCH at: ${newPath.join('.')} (Value: ${val})`)
52
+ }
53
+ }
54
+
55
+ if (typeof val === 'object') {
56
+ search(val, newPath)
57
+ }
58
+ })
59
+ }
60
+
61
+ fitParser.parse(content, (error, data) => {
62
+ if (error) {
63
+ console.error(error)
64
+ }
65
+ else {
66
+ console.log(`Searching for value ${targetValue} (±${tolerance}) in ${filePath}...`)
67
+ search(data)
68
+ console.log('Search complete.')
69
+ }
70
+ })
71
+ }
72
+ catch (e) {
73
+ console.error('Error:', e.message)
74
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'fs'
2
+ import { createRequire } from 'module'
3
+
4
+ const require = createRequire(import.meta.url)
5
+ // Try to load from local build or node_modules
6
+ let FitParser
7
+ try {
8
+ FitParser = require('../dist/fit-parser.js').default
9
+ }
10
+ catch (e) {
11
+ FitParser = require('fit-file-parser').default
12
+ }
13
+
14
+ const args = process.argv.slice(2)
15
+ if (args.length < 1) {
16
+ console.log('Usage: node inspect_fit.js <path_to_fit_file> [message_key]')
17
+ console.log('Example: node inspect_fit.js ../examples/jumps-mtb.fit jumps')
18
+ process.exit(1)
19
+ }
20
+
21
+ const filePath = args[0]
22
+ const messageKey = args[1]
23
+
24
+ try {
25
+ const content = fs.readFileSync(filePath)
26
+ const fitParser = new FitParser({
27
+ force: true,
28
+ mode: 'both',
29
+ speedUnit: 'm/s',
30
+ lengthUnit: 'm',
31
+ })
32
+
33
+ fitParser.parse(content, (error, data) => {
34
+ if (error) {
35
+ console.error('Error parsing FIT file:', error)
36
+ return
37
+ }
38
+
39
+ if (messageKey) {
40
+ if (data[messageKey]) {
41
+ console.log(`\n=== ${messageKey.toUpperCase()} (${Array.isArray(data[messageKey]) ? data[messageKey].length : 'object'}) ===`)
42
+ console.log(JSON.stringify(data[messageKey], null, 2))
43
+ }
44
+ else if (data.activity && data.activity[messageKey]) {
45
+ console.log(`\n=== ACTIVITY.${messageKey.toUpperCase()} ===`)
46
+ console.log(JSON.stringify(data.activity[messageKey], null, 2))
47
+ }
48
+ else {
49
+ console.log(`\nMessage key '${messageKey}' not found in root or activity object.`)
50
+ console.log('Available keys:', Object.keys(data).join(', '))
51
+ }
52
+ }
53
+ else {
54
+ console.log('\n=== ROOT KEYS ===')
55
+ console.log(Object.keys(data).join('\n'))
56
+
57
+ // Print summary of counts
58
+ console.log('\n=== SUMMARY ===')
59
+ Object.keys(data).forEach((key) => {
60
+ if (Array.isArray(data[key])) {
61
+ console.log(`${key}: ${data[key].length} items`)
62
+ }
63
+ })
64
+ }
65
+ })
66
+ }
67
+ catch (e) {
68
+ console.error('Error reading file:', e.message)
69
+ }
package/deep_probe.js DELETED
@@ -1,70 +0,0 @@
1
- import fs from 'fs'
2
- import { createRequire } from 'module'
3
-
4
- const require = createRequire(import.meta.url)
5
- const FitParser = require('./dist/fit-parser.js').default
6
-
7
- const content = fs.readFileSync('/Users/dimitrios/Projects/sports-lib/samples/fit/jumps-mtb.fit')
8
- const fitParser = new FitParser({
9
- force: true,
10
- speedUnit: 'km/h',
11
- lengthUnit: 'm',
12
- temperatureUnit: 'celsius',
13
- elapsedRecordField: true,
14
- mode: 'both',
15
- })
16
-
17
- function search(obj, path = []) {
18
- if (!obj || typeof obj !== 'object')
19
- return
20
-
21
- Object.keys(obj).forEach((key) => {
22
- const val = obj[key]
23
- const newPath = [...path, key]
24
-
25
- // Check match 11 (Jump Count)
26
- if (typeof val === 'number' && Math.abs(val - 11) < 0.001) {
27
- console.log(`>>> FOUND 11 at: ${newPath.join('.')} (Value: ${val})`)
28
- }
29
- // Check match 159 (Resting Cals)
30
- if (typeof val === 'number' && Math.abs(val - 159) < 0.1) {
31
- console.log(`>>> FOUND 159 at: ${newPath.join('.')} (Value: ${val})`)
32
- }
33
- // Check match for Training Load Peak
34
- if (typeof val === 'number' && Math.abs(val - 6079174) < 100) {
35
- console.log(`>>> FOUND 6M at: ${newPath.join('.')} (Value: ${val})`)
36
- }
37
-
38
- if (typeof val === 'object') {
39
- search(val, newPath)
40
- }
41
- })
42
- }
43
-
44
- fitParser.parse(content, (error, data) => {
45
- if (error) {
46
- console.error(error)
47
- }
48
- else {
49
- console.log('Starting deep search...')
50
- search(data)
51
-
52
- if (data.sessions && data.sessions.length > 0) {
53
- console.log('\n=== SESSION OBJECT (First) ===')
54
- const session = data.sessions[0]
55
- Object.keys(session).forEach((key) => {
56
- console.log(`${key}: ${session[key]}`)
57
- })
58
- }
59
- // Explicitly check for jump-related keys
60
- if (data.jumps) {
61
- console.log('=== JUMPS OBJECT === ')
62
- console.log(JSON.stringify(data.jumps, null, 2))
63
- }
64
- // Also check for jump events in events
65
- if (data.events) {
66
- const jumpEvents = data.events.filter(e => JSON.stringify(e).includes('jump'))
67
- console.log(`Found ${jumpEvents.length} jump events`)
68
- }
69
- }
70
- })