fit-file-parser 2.2.1 → 2.2.3

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/deep_probe.js CHANGED
@@ -1,69 +1,70 @@
1
+ import fs from 'fs'
2
+ import { createRequire } from 'module'
1
3
 
2
- import fs from 'fs';
3
- import { createRequire } from 'module';
4
- const require = createRequire(import.meta.url);
5
- const FitParser = require('./dist/fit-parser.js').default;
4
+ const require = createRequire(import.meta.url)
5
+ const FitParser = require('./dist/fit-parser.js').default
6
6
 
7
- const content = fs.readFileSync('/Users/dimitrios/Projects/sports-lib/samples/fit/jumps-mtb.fit');
7
+ const content = fs.readFileSync('/Users/dimitrios/Projects/sports-lib/samples/fit/jumps-mtb.fit')
8
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
- });
9
+ force: true,
10
+ speedUnit: 'km/h',
11
+ lengthUnit: 'm',
12
+ temperatureUnit: 'celsius',
13
+ elapsedRecordField: true,
14
+ mode: 'both',
15
+ })
16
16
 
17
17
  function search(obj, path = []) {
18
- if (!obj || typeof obj !== 'object') return;
18
+ if (!obj || typeof obj !== 'object')
19
+ return
19
20
 
20
- Object.keys(obj).forEach(key => {
21
- const val = obj[key];
22
- const newPath = [...path, key];
21
+ Object.keys(obj).forEach((key) => {
22
+ const val = obj[key]
23
+ const newPath = [...path, key]
23
24
 
24
- // Check match 11 (Jump Count)
25
- if (typeof val === 'number' && Math.abs(val - 11) < 0.001) {
26
- console.log(`>>> FOUND 11 at: ${newPath.join('.')} (Value: ${val})`);
27
- }
28
- // Check match 159 (Resting Cals)
29
- if (typeof val === 'number' && Math.abs(val - 159) < 0.1) {
30
- console.log(`>>> FOUND 159 at: ${newPath.join('.')} (Value: ${val})`);
31
- }
32
- // Check match for Training Load Peak
33
- if (typeof val === 'number' && Math.abs(val - 6079174) < 100) {
34
- console.log(`>>> FOUND 6M at: ${newPath.join('.')} (Value: ${val})`);
35
- }
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
+ }
36
37
 
37
- if (typeof val === 'object') {
38
- search(val, newPath);
39
- }
40
- });
38
+ if (typeof val === 'object') {
39
+ search(val, newPath)
40
+ }
41
+ })
41
42
  }
42
43
 
43
44
  fitParser.parse(content, (error, data) => {
44
- if (error) {
45
- console.error(error);
46
- } else {
47
- console.log("Starting deep search...");
48
- search(data);
45
+ if (error) {
46
+ console.error(error)
47
+ }
48
+ else {
49
+ console.log('Starting deep search...')
50
+ search(data)
49
51
 
50
-
51
- if (data.sessions && data.sessions.length > 0) {
52
- console.log("\n=== SESSION OBJECT (First) ===");
53
- const session = data.sessions[0];
54
- Object.keys(session).forEach(key => {
55
- console.log(`${key}: ${session[key]}`);
56
- });
57
- }
58
- // Explicitly check for jump-related keys
59
- if (data.jumps) {
60
- console.log("=== JUMPS OBJECT === ");
61
- console.log(JSON.stringify(data.jumps, null, 2));
62
- }
63
- // Also check for jump events in events
64
- if (data.events) {
65
- const jumpEvents = data.events.filter(e => JSON.stringify(e).includes('jump'));
66
- console.log(`Found ${jumpEvents.length} jump events`);
67
- }
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
68
  }
69
- });
69
+ }
70
+ })
package/dist/binary.js CHANGED
@@ -228,10 +228,10 @@ function applyOptions(data, field, options) {
228
228
  case 'end_pressure':
229
229
  return convertTo(data, 'pressureUnits', options.pressureUnit);
230
230
  case 'ant_id': {
231
- const n1 = (data >>> 28) & 0xf;
232
- const n2 = (data >>> 24) & 0xf;
233
- const n3 = (data >>> 16) & 0xff;
234
- const n4 = data & 0xffff;
231
+ const n1 = (data >>> 28) & 0xF;
232
+ const n2 = (data >>> 24) & 0xF;
233
+ const n3 = (data >>> 16) & 0xFF;
234
+ const n4 = data & 0xFFFF;
235
235
  return `${n1.toString(16).toUpperCase()}-${n2.toString(16).toUpperCase()}-${n3.toString(16).toUpperCase().padStart(2, '0')}-${n4.toString(16).toUpperCase().padStart(4, '0')}`;
236
236
  }
237
237
  default:
@@ -234,10 +234,10 @@ function applyOptions(data, field, options) {
234
234
  case 'end_pressure':
235
235
  return convertTo(data, 'pressureUnits', options.pressureUnit);
236
236
  case 'ant_id': {
237
- const n1 = (data >>> 28) & 0xf;
238
- const n2 = (data >>> 24) & 0xf;
239
- const n3 = (data >>> 16) & 0xff;
240
- const n4 = data & 0xffff;
237
+ const n1 = (data >>> 28) & 0xF;
238
+ const n2 = (data >>> 24) & 0xF;
239
+ const n3 = (data >>> 16) & 0xFF;
240
+ const n4 = data & 0xFFFF;
241
241
  return `${n1.toString(16).toUpperCase()}-${n2.toString(16).toUpperCase()}-${n3.toString(16).toUpperCase().padStart(2, '0')}-${n4.toString(16).toUpperCase().padStart(4, '0')}`;
242
242
  }
243
243
  default:
package/dist/cjs/fit.js CHANGED
@@ -1734,6 +1734,13 @@ exports.FIT = {
1734
1734
  offset: 0,
1735
1735
  units: 'kGrit',
1736
1736
  },
1737
+ 182: {
1738
+ field: 'total_flow',
1739
+ type: 'float32',
1740
+ scale: 1,
1741
+ offset: 0,
1742
+ units: 'Flow',
1743
+ },
1737
1744
  183: {
1738
1745
  field: 'jump_count',
1739
1746
  type: 'uint16',
@@ -1741,6 +1748,13 @@ exports.FIT = {
1741
1748
  offset: 0,
1742
1749
  units: '',
1743
1750
  },
1751
+ 186: {
1752
+ field: 'avg_grit',
1753
+ type: 'float32',
1754
+ scale: 1,
1755
+ offset: 0,
1756
+ units: 'kGrit',
1757
+ },
1744
1758
  187: {
1745
1759
  field: 'avg_flow',
1746
1760
  type: 'float32',
@@ -4875,8 +4889,10 @@ exports.FIT = {
4875
4889
  268: 'dive_summary',
4876
4890
  285: 'jump',
4877
4891
  317: 'climb_pro',
4878
- 319: 'tank_pressure',
4892
+ 319: 'tank_update',
4879
4893
  323: 'tank_summary',
4894
+ 216: 'time_in_zone',
4895
+ 347: 'o_hr_settings',
4880
4896
  65280: 'mfg_range_min',
4881
4897
  65534: 'mfg_range_max',
4882
4898
  },
@@ -16,7 +16,7 @@ export interface MessageIndex {
16
16
  selected: boolean;
17
17
  }
18
18
  export type File = 'device' | 'settings' | 'sport' | 'activity' | 'workout' | 'course' | 'schedules' | 'weight' | 'totals' | 'goals' | 'blood_pressure' | 'monitoring_a' | 'activity_summary' | 'monitoring_daily' | 'monitoring_b' | 'segment' | 'segment_list' | 'exd_configuration' | 'mfg_range_min' | 'mfg_range_max';
19
- export type MesgNum = 'file_id' | 'capabilities' | 'device_settings' | 'user_profile' | 'hrm_profile' | 'sdm_profile' | 'bike_profile' | 'zones_target' | 'hr_zone' | 'power_zone' | 'met_zone' | 'sport' | 'goal' | 'session' | 'lap' | 'record' | 'event' | 'device_info' | 'workout' | 'workout_step' | 'schedule' | 'weight_scale' | 'course' | 'course_point' | 'totals' | 'activity' | 'software' | 'file_capabilities' | 'mesg_capabilities' | 'field_capabilities' | 'file_creator' | 'blood_pressure' | 'speed_zone' | 'monitoring' | 'training_file' | 'hrv' | 'ant_rx' | 'ant_tx' | 'ant_channel_id' | 'length' | 'monitoring_info' | 'pad' | 'slave_device' | 'connectivity' | 'weather_conditions' | 'weather_alert' | 'cadence_zone' | 'hr' | 'segment_lap' | 'memo_glob' | 'segment_id' | 'segment_leaderboard_entry' | 'segment_point' | 'segment_file' | 'workout_session' | 'watchface_settings' | 'gps_metadata' | 'camera_event' | 'timestamp_correlation' | 'gyroscope_data' | 'accelerometer_data' | 'three_d_sensor_calibration' | 'video_frame' | 'obdii_data' | 'nmea_sentence' | 'aviation_attitude' | 'video' | 'video_title' | 'video_description' | 'video_clip' | 'exd_screen_configuration' | 'exd_data_field_configuration' | 'exd_data_concept_configuration' | 'field_description' | 'developer_data_id' | 'magnetometer_data' | 'barometer_data' | 'one_d_sensor_calibration' | 'set' | 'stress_level' | 'dive_settings' | 'dive_gas' | 'dive_alarm' | 'exercise_title' | 'dive_summary' | 'jump' | 'climb_pro' | 'tank_pressure' | 'tank_summary' | 'time_in_zone' | 'mfg_range_min' | 'mfg_range_max' | 'o_hr_settings' | 'tank_update' | 'definition';
19
+ export type MesgNum = 'file_id' | 'capabilities' | 'device_settings' | 'user_profile' | 'hrm_profile' | 'sdm_profile' | 'bike_profile' | 'zones_target' | 'hr_zone' | 'power_zone' | 'met_zone' | 'sport' | 'goal' | 'session' | 'lap' | 'record' | 'event' | 'device_info' | 'workout' | 'workout_step' | 'schedule' | 'weight_scale' | 'course' | 'course_point' | 'totals' | 'activity' | 'software' | 'file_capabilities' | 'mesg_capabilities' | 'field_capabilities' | 'file_creator' | 'blood_pressure' | 'speed_zone' | 'monitoring' | 'training_file' | 'hrv' | 'ant_rx' | 'ant_tx' | 'ant_channel_id' | 'length' | 'monitoring_info' | 'pad' | 'slave_device' | 'connectivity' | 'weather_conditions' | 'weather_alert' | 'cadence_zone' | 'hr' | 'segment_lap' | 'memo_glob' | 'segment_id' | 'segment_leaderboard_entry' | 'segment_point' | 'segment_file' | 'workout_session' | 'watchface_settings' | 'gps_metadata' | 'camera_event' | 'timestamp_correlation' | 'gyroscope_data' | 'accelerometer_data' | 'three_d_sensor_calibration' | 'video_frame' | 'obdii_data' | 'nmea_sentence' | 'aviation_attitude' | 'video' | 'video_title' | 'video_description' | 'video_clip' | 'exd_screen_configuration' | 'exd_data_field_configuration' | 'exd_data_concept_configuration' | 'field_description' | 'developer_data_id' | 'magnetometer_data' | 'barometer_data' | 'one_d_sensor_calibration' | 'time_in_zone' | 'set' | 'stress_level' | 'dive_settings' | 'dive_gas' | 'dive_alarm' | 'exercise_title' | 'dive_summary' | 'jump' | 'climb_pro' | 'tank_update' | 'tank_summary' | 'o_hr_settings' | 'mfg_range_min' | 'mfg_range_max' | 'definition';
20
20
  export type Checksum = 'clear' | 'ok';
21
21
  export type FileFlags = '0' | 'read' | 'write' | 'erase';
22
22
  export type MesgCount = 'num_per_file' | 'max_per_file' | 'max_per_file_type';
@@ -363,6 +363,8 @@ export interface ParsedSession {
363
363
  training_stress_score?: number;
364
364
  intensity_factor?: number;
365
365
  left_right_balance?: LeftRightBalance100;
366
+ end_position_lat?: number;
367
+ end_position_long?: number;
366
368
  avg_stroke_count?: number;
367
369
  avg_stroke_distance?: number;
368
370
  swim_stroke?: SwimStroke;
@@ -387,10 +389,10 @@ export interface ParsedSession {
387
389
  max_pos_vertical_speed?: number;
388
390
  max_neg_vertical_speed?: number;
389
391
  min_heart_rate?: number;
390
- time_in_hr_zone?: Uint32Array;
391
- time_in_speed_zone?: Uint32Array;
392
- time_in_cadence_zone?: Uint32Array;
393
- time_in_power_zone?: Uint32Array;
392
+ time_in_hr_zone?: number[];
393
+ time_in_speed_zone?: number[];
394
+ time_in_cadence_zone?: number[];
395
+ time_in_power_zone?: number[];
394
396
  avg_lap_time?: number;
395
397
  best_lap_index?: number;
396
398
  min_altitude?: number;
@@ -418,6 +420,7 @@ export interface ParsedSession {
418
420
  avg_left_pedal_smoothness?: number;
419
421
  avg_right_pedal_smoothness?: number;
420
422
  avg_combined_pedal_smoothness?: number;
423
+ sport_profile_name?: string;
421
424
  sport_index?: number;
422
425
  time_standing?: number;
423
426
  stand_count?: number;
@@ -442,14 +445,24 @@ export interface ParsedSession {
442
445
  avg_vertical_ratio?: number;
443
446
  avg_stance_time_balance?: number;
444
447
  avg_step_length?: number;
445
- total_anaerobic_effect?: number;
448
+ total_anaerobic_training_effect?: number;
446
449
  avg_vam?: number;
450
+ recovery_advisor?: number;
451
+ min_temperature?: number;
452
+ training_load_peak?: number;
453
+ enhanced_avg_respiration_rate?: number;
454
+ enhanced_max_respiration_rate?: number;
455
+ est_sweat_loss?: number;
456
+ enhanced_min_respiration_rate?: number;
447
457
  total_grit?: number;
448
458
  total_flow?: number;
449
- workout_feel?: number;
450
- workout_rpe?: number;
459
+ jump_count?: number;
451
460
  avg_grit?: number;
452
461
  avg_flow?: number;
462
+ primary_benefit?: number;
463
+ workout_feel?: number;
464
+ workout_rpe?: number;
465
+ resting_calories?: number;
453
466
  timestamp: string;
454
467
  message_index?: MessageIndex;
455
468
  laps?: ParsedLap[];
@@ -506,10 +519,10 @@ export interface ParsedLap {
506
519
  avg_neg_vertical_speed?: number;
507
520
  max_pos_vertical_speed?: number;
508
521
  max_neg_vertical_speed?: number;
509
- time_in_hr_zone?: Uint32Array;
510
- time_in_speed_zone?: Uint32Array;
511
- time_in_cadence_zone?: Uint32Array;
512
- time_in_power_zone?: Uint32Array;
522
+ time_in_hr_zone?: number[];
523
+ time_in_speed_zone?: number[];
524
+ time_in_cadence_zone?: number[];
525
+ time_in_power_zone?: number[];
513
526
  repetition_num?: number;
514
527
  min_altitude?: number;
515
528
  min_heart_rate?: number;
@@ -666,6 +679,7 @@ export interface ParsedDeviceInfo {
666
679
  ant_transmission_type?: number;
667
680
  ant_device_number?: number;
668
681
  ant_network?: AntNetwork;
682
+ ant_id?: number;
669
683
  source_type?: SourceType;
670
684
  product_name?: string;
671
685
  battery_level?: number;
@@ -823,7 +837,7 @@ export interface ParsedMonitoring {
823
837
  timestamp: number;
824
838
  }
825
839
  export interface ParsedHrv {
826
- time?: Uint16Array;
840
+ time?: number[];
827
841
  }
828
842
  export interface ParsedLength {
829
843
  event?: Event;
@@ -858,6 +872,7 @@ export interface ParsedOHrSettings {
858
872
  timestamp: string;
859
873
  }
860
874
  export interface ParsedJump {
875
+ enhanced_mets?: number;
861
876
  distance?: number;
862
877
  height?: number;
863
878
  score?: number;
@@ -875,11 +890,30 @@ export interface ParsedFieldDescription {
875
890
  }
876
891
  export interface ParsedDeveloperDataId {
877
892
  developer_id?: number;
878
- application_id?: unknown[];
893
+ application_id?: number[];
879
894
  manufacturer_id?: Manufacturer;
880
895
  developer_data_index?: number;
881
896
  application_version?: number;
882
897
  }
898
+ export interface ParsedTimeInZone {
899
+ reference_mesg?: number;
900
+ reference_index?: number;
901
+ time_in_hr_zone?: number[];
902
+ time_in_speed_zone?: number[];
903
+ time_in_power_zone?: number[];
904
+ hr_zone_high_boundary_deprecated?: number[];
905
+ hr_zone_high_boundary?: number[];
906
+ speed_zone_high_boundary?: number[];
907
+ power_zone_high_boundary?: number[];
908
+ hr_calc_type?: HrZoneCalc;
909
+ max_heart_rate_deprecated?: number;
910
+ max_heart_rate?: number;
911
+ resting_heart_rate?: number;
912
+ threshold_heart_rate?: number;
913
+ pwr_calc_type?: PwrZoneCalc;
914
+ functional_threshold_power?: number;
915
+ timestamp: string;
916
+ }
883
917
  export interface ParsedStressLevel {
884
918
  stress_level_value?: number;
885
919
  stress_level_time?: string;
@@ -952,23 +986,6 @@ export interface ParsedTankSummary {
952
986
  end_pressure?: number;
953
987
  volume_used?: number;
954
988
  }
955
- export interface ParsedTimeInZone {
956
- timestamp: string;
957
- reference_mesg?: number;
958
- reference_index?: number;
959
- time_in_hr_zone?: number[];
960
- time_in_speed_zone?: number[];
961
- time_in_power_zone?: number[];
962
- hr_zone_high_boundary?: number[];
963
- speed_zone_high_boundary?: number[];
964
- power_zone_high_boundary?: number[];
965
- hr_calc_type?: HrZoneCalc;
966
- max_heart_rate?: number;
967
- resting_heart_rate?: number;
968
- threshold_heart_rate?: number;
969
- pwr_calc_type?: PwrZoneCalc;
970
- functional_threshold_power?: number;
971
- }
972
989
  export interface ParsedFit {
973
990
  protocolVersion?: number;
974
991
  profileVersion?: number;
@@ -60,8 +60,14 @@ function generateArrayProperty(name, type, optional = true) {
60
60
  }
61
61
  function generateTypeFromField(def) {
62
62
  switch (def.type) {
63
+ case 'uint32_array':
64
+ case 'uint16_array':
65
+ case 'uint8_array':
66
+ case 'sint32_array':
67
+ case 'sint16_array':
68
+ case 'sint8_array':
63
69
  case 'byte_array':
64
- return typescript_1.default.factory.createArrayTypeNode(typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword));
70
+ return typescript_1.default.factory.createArrayTypeNode(typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword));
65
71
  case 'bool':
66
72
  return typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.BooleanKeyword);
67
73
  case 'date_time':
@@ -104,11 +110,7 @@ function generateTypes(types) {
104
110
  const typeAlias = typescript_1.default.factory.createTypeAliasDeclaration([
105
111
  typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword),
106
112
  ], snakeToCamel(name), undefined, typescript_1.default.factory.createUnionTypeNode([...names.map(n => typescript_1.default.factory.createLiteralTypeNode(typescript_1.default.factory.createStringLiteral(String(n)))), ...(name === 'mesg_num'
107
- // TODO those are somehow missing in the FIT.types.mesg_num definition
108
- // especially 'tank_update' (319 in messages) is mapped to 'tank_pressure' a bug?
109
113
  ? [
110
- typescript_1.default.factory.createLiteralTypeNode(typescript_1.default.factory.createStringLiteral('o_hr_settings')),
111
- typescript_1.default.factory.createLiteralTypeNode(typescript_1.default.factory.createStringLiteral('tank_update')),
112
114
  typescript_1.default.factory.createLiteralTypeNode(typescript_1.default.factory.createStringLiteral('definition')),
113
115
  ]
114
116
  : [])]));
@@ -207,6 +209,7 @@ function generateFitType() {
207
209
  tank_updates: 'parsed_tank_update',
208
210
  tank_summaries: 'parsed_tank_summary',
209
211
  jumps: 'parsed_jump',
212
+ time_in_zone: 'parsed_time_in_zone',
210
213
  };
211
214
  const referenceProperties = {
212
215
  file_creator: 'parsed_file_creator',
@@ -231,13 +234,18 @@ function generateMessages(messages) {
231
234
  const nodes = [];
232
235
  Object.keys(messages).forEach((name) => {
233
236
  const msg = fit_js_1.FIT.messages[Number(name)];
237
+ const usedFields = new Set();
234
238
  const messageType = typescript_1.default.factory.createInterfaceDeclaration([
235
239
  typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword),
236
240
  ], snakeToCamel(`parsed_${msg.name}`), undefined, undefined, [
237
- ...Object.keys(msg).filter(n => n !== 'name').map((id) => {
241
+ ...Object.keys(msg).filter(n => n !== 'name').reduce((acc, id) => {
238
242
  const def = msg[Number(id)];
239
- return generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field));
240
- }),
243
+ if (!usedFields.has(def.field)) {
244
+ usedFields.add(def.field);
245
+ acc.push(generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field)));
246
+ }
247
+ return acc;
248
+ }, []),
241
249
  ...generateAdditionalFields(msg),
242
250
  ]);
243
251
  nodes.push(messageType);
package/dist/fit.js CHANGED
@@ -1731,6 +1731,13 @@ export const FIT = {
1731
1731
  offset: 0,
1732
1732
  units: 'kGrit',
1733
1733
  },
1734
+ 182: {
1735
+ field: 'total_flow',
1736
+ type: 'float32',
1737
+ scale: 1,
1738
+ offset: 0,
1739
+ units: 'Flow',
1740
+ },
1734
1741
  183: {
1735
1742
  field: 'jump_count',
1736
1743
  type: 'uint16',
@@ -1738,6 +1745,13 @@ export const FIT = {
1738
1745
  offset: 0,
1739
1746
  units: '',
1740
1747
  },
1748
+ 186: {
1749
+ field: 'avg_grit',
1750
+ type: 'float32',
1751
+ scale: 1,
1752
+ offset: 0,
1753
+ units: 'kGrit',
1754
+ },
1741
1755
  187: {
1742
1756
  field: 'avg_flow',
1743
1757
  type: 'float32',
@@ -4872,8 +4886,10 @@ export const FIT = {
4872
4886
  268: 'dive_summary',
4873
4887
  285: 'jump',
4874
4888
  317: 'climb_pro',
4875
- 319: 'tank_pressure',
4889
+ 319: 'tank_update',
4876
4890
  323: 'tank_summary',
4891
+ 216: 'time_in_zone',
4892
+ 347: 'o_hr_settings',
4877
4893
  65280: 'mfg_range_min',
4878
4894
  65534: 'mfg_range_max',
4879
4895
  },
@@ -16,7 +16,7 @@ export interface MessageIndex {
16
16
  selected: boolean;
17
17
  }
18
18
  export type File = 'device' | 'settings' | 'sport' | 'activity' | 'workout' | 'course' | 'schedules' | 'weight' | 'totals' | 'goals' | 'blood_pressure' | 'monitoring_a' | 'activity_summary' | 'monitoring_daily' | 'monitoring_b' | 'segment' | 'segment_list' | 'exd_configuration' | 'mfg_range_min' | 'mfg_range_max';
19
- export type MesgNum = 'file_id' | 'capabilities' | 'device_settings' | 'user_profile' | 'hrm_profile' | 'sdm_profile' | 'bike_profile' | 'zones_target' | 'hr_zone' | 'power_zone' | 'met_zone' | 'sport' | 'goal' | 'session' | 'lap' | 'record' | 'event' | 'device_info' | 'workout' | 'workout_step' | 'schedule' | 'weight_scale' | 'course' | 'course_point' | 'totals' | 'activity' | 'software' | 'file_capabilities' | 'mesg_capabilities' | 'field_capabilities' | 'file_creator' | 'blood_pressure' | 'speed_zone' | 'monitoring' | 'training_file' | 'hrv' | 'ant_rx' | 'ant_tx' | 'ant_channel_id' | 'length' | 'monitoring_info' | 'pad' | 'slave_device' | 'connectivity' | 'weather_conditions' | 'weather_alert' | 'cadence_zone' | 'hr' | 'segment_lap' | 'memo_glob' | 'segment_id' | 'segment_leaderboard_entry' | 'segment_point' | 'segment_file' | 'workout_session' | 'watchface_settings' | 'gps_metadata' | 'camera_event' | 'timestamp_correlation' | 'gyroscope_data' | 'accelerometer_data' | 'three_d_sensor_calibration' | 'video_frame' | 'obdii_data' | 'nmea_sentence' | 'aviation_attitude' | 'video' | 'video_title' | 'video_description' | 'video_clip' | 'exd_screen_configuration' | 'exd_data_field_configuration' | 'exd_data_concept_configuration' | 'field_description' | 'developer_data_id' | 'magnetometer_data' | 'barometer_data' | 'one_d_sensor_calibration' | 'set' | 'stress_level' | 'dive_settings' | 'dive_gas' | 'dive_alarm' | 'exercise_title' | 'dive_summary' | 'jump' | 'climb_pro' | 'tank_pressure' | 'tank_summary' | 'time_in_zone' | 'mfg_range_min' | 'mfg_range_max' | 'o_hr_settings' | 'tank_update' | 'definition';
19
+ export type MesgNum = 'file_id' | 'capabilities' | 'device_settings' | 'user_profile' | 'hrm_profile' | 'sdm_profile' | 'bike_profile' | 'zones_target' | 'hr_zone' | 'power_zone' | 'met_zone' | 'sport' | 'goal' | 'session' | 'lap' | 'record' | 'event' | 'device_info' | 'workout' | 'workout_step' | 'schedule' | 'weight_scale' | 'course' | 'course_point' | 'totals' | 'activity' | 'software' | 'file_capabilities' | 'mesg_capabilities' | 'field_capabilities' | 'file_creator' | 'blood_pressure' | 'speed_zone' | 'monitoring' | 'training_file' | 'hrv' | 'ant_rx' | 'ant_tx' | 'ant_channel_id' | 'length' | 'monitoring_info' | 'pad' | 'slave_device' | 'connectivity' | 'weather_conditions' | 'weather_alert' | 'cadence_zone' | 'hr' | 'segment_lap' | 'memo_glob' | 'segment_id' | 'segment_leaderboard_entry' | 'segment_point' | 'segment_file' | 'workout_session' | 'watchface_settings' | 'gps_metadata' | 'camera_event' | 'timestamp_correlation' | 'gyroscope_data' | 'accelerometer_data' | 'three_d_sensor_calibration' | 'video_frame' | 'obdii_data' | 'nmea_sentence' | 'aviation_attitude' | 'video' | 'video_title' | 'video_description' | 'video_clip' | 'exd_screen_configuration' | 'exd_data_field_configuration' | 'exd_data_concept_configuration' | 'field_description' | 'developer_data_id' | 'magnetometer_data' | 'barometer_data' | 'one_d_sensor_calibration' | 'time_in_zone' | 'set' | 'stress_level' | 'dive_settings' | 'dive_gas' | 'dive_alarm' | 'exercise_title' | 'dive_summary' | 'jump' | 'climb_pro' | 'tank_update' | 'tank_summary' | 'o_hr_settings' | 'mfg_range_min' | 'mfg_range_max' | 'definition';
20
20
  export type Checksum = 'clear' | 'ok';
21
21
  export type FileFlags = '0' | 'read' | 'write' | 'erase';
22
22
  export type MesgCount = 'num_per_file' | 'max_per_file' | 'max_per_file_type';
@@ -363,6 +363,8 @@ export interface ParsedSession {
363
363
  training_stress_score?: number;
364
364
  intensity_factor?: number;
365
365
  left_right_balance?: LeftRightBalance100;
366
+ end_position_lat?: number;
367
+ end_position_long?: number;
366
368
  avg_stroke_count?: number;
367
369
  avg_stroke_distance?: number;
368
370
  swim_stroke?: SwimStroke;
@@ -387,10 +389,10 @@ export interface ParsedSession {
387
389
  max_pos_vertical_speed?: number;
388
390
  max_neg_vertical_speed?: number;
389
391
  min_heart_rate?: number;
390
- time_in_hr_zone?: Uint32Array;
391
- time_in_speed_zone?: Uint32Array;
392
- time_in_cadence_zone?: Uint32Array;
393
- time_in_power_zone?: Uint32Array;
392
+ time_in_hr_zone?: number[];
393
+ time_in_speed_zone?: number[];
394
+ time_in_cadence_zone?: number[];
395
+ time_in_power_zone?: number[];
394
396
  avg_lap_time?: number;
395
397
  best_lap_index?: number;
396
398
  min_altitude?: number;
@@ -418,6 +420,7 @@ export interface ParsedSession {
418
420
  avg_left_pedal_smoothness?: number;
419
421
  avg_right_pedal_smoothness?: number;
420
422
  avg_combined_pedal_smoothness?: number;
423
+ sport_profile_name?: string;
421
424
  sport_index?: number;
422
425
  time_standing?: number;
423
426
  stand_count?: number;
@@ -442,14 +445,24 @@ export interface ParsedSession {
442
445
  avg_vertical_ratio?: number;
443
446
  avg_stance_time_balance?: number;
444
447
  avg_step_length?: number;
445
- total_anaerobic_effect?: number;
448
+ total_anaerobic_training_effect?: number;
446
449
  avg_vam?: number;
450
+ recovery_advisor?: number;
451
+ min_temperature?: number;
452
+ training_load_peak?: number;
453
+ enhanced_avg_respiration_rate?: number;
454
+ enhanced_max_respiration_rate?: number;
455
+ est_sweat_loss?: number;
456
+ enhanced_min_respiration_rate?: number;
447
457
  total_grit?: number;
448
458
  total_flow?: number;
449
- workout_feel?: number;
450
- workout_rpe?: number;
459
+ jump_count?: number;
451
460
  avg_grit?: number;
452
461
  avg_flow?: number;
462
+ primary_benefit?: number;
463
+ workout_feel?: number;
464
+ workout_rpe?: number;
465
+ resting_calories?: number;
453
466
  timestamp: string;
454
467
  message_index?: MessageIndex;
455
468
  laps?: ParsedLap[];
@@ -506,10 +519,10 @@ export interface ParsedLap {
506
519
  avg_neg_vertical_speed?: number;
507
520
  max_pos_vertical_speed?: number;
508
521
  max_neg_vertical_speed?: number;
509
- time_in_hr_zone?: Uint32Array;
510
- time_in_speed_zone?: Uint32Array;
511
- time_in_cadence_zone?: Uint32Array;
512
- time_in_power_zone?: Uint32Array;
522
+ time_in_hr_zone?: number[];
523
+ time_in_speed_zone?: number[];
524
+ time_in_cadence_zone?: number[];
525
+ time_in_power_zone?: number[];
513
526
  repetition_num?: number;
514
527
  min_altitude?: number;
515
528
  min_heart_rate?: number;
@@ -666,6 +679,7 @@ export interface ParsedDeviceInfo {
666
679
  ant_transmission_type?: number;
667
680
  ant_device_number?: number;
668
681
  ant_network?: AntNetwork;
682
+ ant_id?: number;
669
683
  source_type?: SourceType;
670
684
  product_name?: string;
671
685
  battery_level?: number;
@@ -823,7 +837,7 @@ export interface ParsedMonitoring {
823
837
  timestamp: number;
824
838
  }
825
839
  export interface ParsedHrv {
826
- time?: Uint16Array;
840
+ time?: number[];
827
841
  }
828
842
  export interface ParsedLength {
829
843
  event?: Event;
@@ -858,6 +872,7 @@ export interface ParsedOHrSettings {
858
872
  timestamp: string;
859
873
  }
860
874
  export interface ParsedJump {
875
+ enhanced_mets?: number;
861
876
  distance?: number;
862
877
  height?: number;
863
878
  score?: number;
@@ -875,11 +890,30 @@ export interface ParsedFieldDescription {
875
890
  }
876
891
  export interface ParsedDeveloperDataId {
877
892
  developer_id?: number;
878
- application_id?: unknown[];
893
+ application_id?: number[];
879
894
  manufacturer_id?: Manufacturer;
880
895
  developer_data_index?: number;
881
896
  application_version?: number;
882
897
  }
898
+ export interface ParsedTimeInZone {
899
+ reference_mesg?: number;
900
+ reference_index?: number;
901
+ time_in_hr_zone?: number[];
902
+ time_in_speed_zone?: number[];
903
+ time_in_power_zone?: number[];
904
+ hr_zone_high_boundary_deprecated?: number[];
905
+ hr_zone_high_boundary?: number[];
906
+ speed_zone_high_boundary?: number[];
907
+ power_zone_high_boundary?: number[];
908
+ hr_calc_type?: HrZoneCalc;
909
+ max_heart_rate_deprecated?: number;
910
+ max_heart_rate?: number;
911
+ resting_heart_rate?: number;
912
+ threshold_heart_rate?: number;
913
+ pwr_calc_type?: PwrZoneCalc;
914
+ functional_threshold_power?: number;
915
+ timestamp: string;
916
+ }
883
917
  export interface ParsedStressLevel {
884
918
  stress_level_value?: number;
885
919
  stress_level_time?: string;
@@ -952,23 +986,6 @@ export interface ParsedTankSummary {
952
986
  end_pressure?: number;
953
987
  volume_used?: number;
954
988
  }
955
- export interface ParsedTimeInZone {
956
- timestamp: string;
957
- reference_mesg?: number;
958
- reference_index?: number;
959
- time_in_hr_zone?: number[];
960
- time_in_speed_zone?: number[];
961
- time_in_power_zone?: number[];
962
- hr_zone_high_boundary?: number[];
963
- speed_zone_high_boundary?: number[];
964
- power_zone_high_boundary?: number[];
965
- hr_calc_type?: HrZoneCalc;
966
- max_heart_rate?: number;
967
- resting_heart_rate?: number;
968
- threshold_heart_rate?: number;
969
- pwr_calc_type?: PwrZoneCalc;
970
- functional_threshold_power?: number;
971
- }
972
989
  export interface ParsedFit {
973
990
  protocolVersion?: number;
974
991
  profileVersion?: number;
@@ -40,8 +40,14 @@ export function generateArrayProperty(name, type, optional = true) {
40
40
  }
41
41
  export function generateTypeFromField(def) {
42
42
  switch (def.type) {
43
+ case 'uint32_array':
44
+ case 'uint16_array':
45
+ case 'uint8_array':
46
+ case 'sint32_array':
47
+ case 'sint16_array':
48
+ case 'sint8_array':
43
49
  case 'byte_array':
44
- return ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
50
+ return ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword));
45
51
  case 'bool':
46
52
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
47
53
  case 'date_time':
@@ -84,11 +90,7 @@ export function generateTypes(types) {
84
90
  const typeAlias = ts.factory.createTypeAliasDeclaration([
85
91
  ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
86
92
  ], snakeToCamel(name), undefined, ts.factory.createUnionTypeNode([...names.map(n => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(String(n)))), ...(name === 'mesg_num'
87
- // TODO those are somehow missing in the FIT.types.mesg_num definition
88
- // especially 'tank_update' (319 in messages) is mapped to 'tank_pressure' a bug?
89
93
  ? [
90
- ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('o_hr_settings')),
91
- ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('tank_update')),
92
94
  ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('definition')),
93
95
  ]
94
96
  : [])]));
@@ -187,6 +189,7 @@ export function generateFitType() {
187
189
  tank_updates: 'parsed_tank_update',
188
190
  tank_summaries: 'parsed_tank_summary',
189
191
  jumps: 'parsed_jump',
192
+ time_in_zone: 'parsed_time_in_zone',
190
193
  };
191
194
  const referenceProperties = {
192
195
  file_creator: 'parsed_file_creator',
@@ -211,13 +214,18 @@ export function generateMessages(messages) {
211
214
  const nodes = [];
212
215
  Object.keys(messages).forEach((name) => {
213
216
  const msg = FIT.messages[Number(name)];
217
+ const usedFields = new Set();
214
218
  const messageType = ts.factory.createInterfaceDeclaration([
215
219
  ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
216
220
  ], snakeToCamel(`parsed_${msg.name}`), undefined, undefined, [
217
- ...Object.keys(msg).filter(n => n !== 'name').map((id) => {
221
+ ...Object.keys(msg).filter(n => n !== 'name').reduce((acc, id) => {
218
222
  const def = msg[Number(id)];
219
- return generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field));
220
- }),
223
+ if (!usedFields.has(def.field)) {
224
+ usedFields.add(def.field);
225
+ acc.push(generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field)));
226
+ }
227
+ return acc;
228
+ }, []),
221
229
  ...generateAdditionalFields(msg),
222
230
  ]);
223
231
  nodes.push(messageType);
package/eslint.config.js CHANGED
@@ -12,4 +12,9 @@ export default antfu({
12
12
  rules: {
13
13
  'unicorn/prefer-node-protocol': 'off',
14
14
  },
15
+ }, {
16
+ files: ['**/examples/*.js', '**/codegen/*.ts', '**/probe_session.js', '**/deep_probe.js'],
17
+ rules: {
18
+ 'no-console': 'off',
19
+ },
15
20
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "2.2.1",
4
+ "version": "2.2.3",
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/probe_session.js DELETED
@@ -1,44 +0,0 @@
1
-
2
- import fs from 'fs';
3
-
4
-
5
- // Note: Importing TS directly might not work with node without a loader.
6
- // Better to use the built file if available, or use ts-node.
7
- // But earlier I saw `dist/fit-parser.js`. Let's try to import that if key is default.
8
- // Actually, let's just use `fs.readFileSync` and basic node.
9
- // If `dist` is CJS, I can use `createRequire`.
10
-
11
- import { createRequire } from 'module';
12
- const require = createRequire(import.meta.url);
13
-
14
- const FitParserDist = require('./dist/fit-parser.js').default;
15
-
16
- const content = fs.readFileSync('/Users/dimitrios/Projects/sports-lib/samples/fit/jumps-mtb.fit');
17
- const fitParser = new FitParserDist({
18
- force: true,
19
- speedUnit: 'km/h',
20
- lengthUnit: 'm',
21
- temperatureUnit: 'celsius',
22
- elapsedRecordField: true,
23
- mode: 'both',
24
- });
25
-
26
- fitParser.parse(content, (error, data) => {
27
- if (error) {
28
- console.error(error);
29
- } else {
30
- const session = data.sessions[0];
31
- if (session) {
32
- console.log("=== SESSION KEYS & VALUES ===");
33
- Object.keys(session).forEach(key => {
34
- const val = session[key];
35
- console.log(`${key}: ${val}`);
36
- if (Math.abs(val - 11) < 0.1) console.log(`>>> MATCH 11 (Jump Count?): ${key}`);
37
- if (Math.abs(val - 159) < 0.1) console.log(`>>> MATCH 159 (Resting Cals?): ${key}`);
38
- if (Math.abs(val - 6079174) < 1) console.log(`>>> MATCH 6M (Training Load?): ${key}`);
39
- if (Math.abs(val - 100) < 0.1) console.log(`>>> MATCH 100 (Avg VAM?): ${key}`);
40
- if (Math.abs(val - 2756) < 1) console.log(`>>> MATCH 2756 (Avg Resp?): ${key}`);
41
- });
42
- }
43
- }
44
- });