fit-file-parser 6.0.2 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Change Log
2
2
 
3
+ ## 6.1.1 - 2026-09-21
4
+
5
+ ### Added
6
+
7
+ - Export lookup helpers for FIT manufacturer, Garmin product, sport, and
8
+ sub-sport identifiers from the package root.
9
+ - Export the same helpers from the lightweight `fit-file-parser/profile`
10
+ entry point for lookup-only consumers.
11
+ - Add reverse sport, sub-sport, and course-point identifier lookups.
12
+ - Add lightweight `fit-file-parser/raw` and `fit-file-parser/encoder` entry
13
+ points for profile-independent message scanning and encoding.
14
+ - Add a strict raw-message reader with CRC, definition, developer-field, and
15
+ compressed-timestamp validation.
16
+ - Export an opt-in Garmin product display-name helper without changing parsed
17
+ `product` or `product_name` fields.
18
+ - Decode mountain enduro and mountain downhill sub-sport identifiers.
19
+
20
+ ### Compatibility
21
+
22
+ - Existing parsed output remains unchanged except that sub-sport IDs 153 and
23
+ 154 now resolve to `mountain_enduro` and `mountain_downhill`.
24
+ - Existing Garmin product display names ported from SportsLib are preserved.
25
+
3
26
  ## 6.0.2 - 2026-09-21
4
27
 
5
28
  ### Changed
package/README.md CHANGED
@@ -257,6 +257,57 @@ unknown enum IDs remain numbers, and invalid entries retained inside FIT arrays
257
257
  are `null`. All profile fields are optional because each FIT message definition
258
258
  chooses which fields are present.
259
259
 
260
+ Applications that need profile lookups independently of parsing can use the
261
+ exported manufacturer, Garmin product, sport, sub-sport, and course-point helpers:
262
+
263
+ ```javascript
264
+ import {
265
+ getFitCoursePointId,
266
+ getFitGarminProductDisplayName,
267
+ getFitManufacturerName,
268
+ getFitSportId,
269
+ getFitSportName,
270
+ getFitSubSportId,
271
+ getFitSubSportName,
272
+ } from 'fit-file-parser'
273
+
274
+ getFitManufacturerName(1) // "garmin"
275
+ getFitGarminProductDisplayName(4655) // "Edge MTB"
276
+ getFitSportName(2) // "cycling"
277
+ getFitSubSportName(153) // "mountain_enduro"
278
+ getFitSportId('cycling') // 2
279
+ getFitSubSportId('indoor_cycling') // 6
280
+ getFitCoursePointId('rest_area') // 29
281
+ ```
282
+
283
+ These helpers read the same maintained profile used by the decoder. The product
284
+ display helper is opt-in and does not synthesize or overwrite parsed
285
+ `product_name` values.
286
+
287
+ Lookup-only consumers can import the same helpers from
288
+ `fit-file-parser/profile` without loading the parser entry point.
289
+
290
+ ### Lightweight raw messages
291
+
292
+ Consumers that need strict FIT framing, CRC validation, compressed timestamps,
293
+ and exact fields without loading the semantic profile can use the lightweight
294
+ raw entry point:
295
+
296
+ ```javascript
297
+ import { readFitMessages } from 'fit-file-parser/raw'
298
+
299
+ const { messages } = readFitMessages(content, {
300
+ messageNumbers: [18, 26, 72],
301
+ maxInputBytes: 64 * 1024 * 1024,
302
+ })
303
+ ```
304
+
305
+ The result retains native and developer fields as defensive `Uint8Array`
306
+ copies. It does not apply names, enum formatting, scales, units, or
307
+ provider-specific interpretation. Invalid input throws `FitMessageReaderError`
308
+ with a stable `code` such as `invalid_header`, `invalid_crc`, or
309
+ `invalid_structure`.
310
+
260
311
  ## Inputs
261
312
 
262
313
  Both parser methods accept:
@@ -283,7 +334,7 @@ base types, sizes, and values in their raw FIT representation. Applying FIT
283
334
  scales and offsets is the caller's responsibility.
284
335
 
285
336
  ```javascript
286
- import { FitBaseType, FitEncoder } from 'fit-file-parser'
337
+ import { FitBaseType, FitEncoder } from 'fit-file-parser/encoder'
287
338
 
288
339
  const encoder = new FitEncoder()
289
340
  encoder.writeMessage(0, [
@@ -319,6 +370,9 @@ Scalar 64-bit values use `bigint`. Strings, numeric arrays, and other
319
370
  variable-length values use exact-size `Uint8Array` values. Invalid field
320
371
  definitions or numeric ranges throw before a partial message is written.
321
372
 
373
+ The encoder remains available from the package root; the `/encoder` entry
374
+ point avoids loading the decoder and semantic profile.
375
+
322
376
  ## TypeScript and module formats
323
377
 
324
378
  The package includes TypeScript declarations and exports
@@ -3,6 +3,9 @@ import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
5
  export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
6
+ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
7
+ export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
8
+ export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
6
9
  export interface FitParserOptions {
7
10
  force?: boolean;
8
11
  speedUnit?: string;
@@ -1,11 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FitEncoder = exports.FitBaseType = void 0;
3
+ exports.readFitUnsignedField = exports.readFitStringField = exports.readFitMessages = exports.getFitBaseTypeId = exports.fitTimestampToUnixMilliseconds = exports.FitMessageReaderError = exports.getFitSubSportName = exports.getFitSubSportId = exports.getFitSportName = exports.getFitSportId = exports.getFitManufacturerName = exports.getFitGarminProductName = exports.getFitGarminProductDisplayName = exports.getFitCoursePointId = exports.FitEncoder = exports.FitBaseType = void 0;
4
4
  const binary_js_1 = require("./binary.js");
5
5
  const helper_js_1 = require("./helper.js");
6
6
  var fit_encoder_js_1 = require("./fit-encoder.js");
7
7
  Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function () { return fit_encoder_js_1.FitBaseType; } });
8
8
  Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
9
+ var profile_lookup_js_1 = require("./profile-lookup.js");
10
+ Object.defineProperty(exports, "getFitCoursePointId", { enumerable: true, get: function () { return profile_lookup_js_1.getFitCoursePointId; } });
11
+ Object.defineProperty(exports, "getFitGarminProductDisplayName", { enumerable: true, get: function () { return profile_lookup_js_1.getFitGarminProductDisplayName; } });
12
+ Object.defineProperty(exports, "getFitGarminProductName", { enumerable: true, get: function () { return profile_lookup_js_1.getFitGarminProductName; } });
13
+ Object.defineProperty(exports, "getFitManufacturerName", { enumerable: true, get: function () { return profile_lookup_js_1.getFitManufacturerName; } });
14
+ Object.defineProperty(exports, "getFitSportId", { enumerable: true, get: function () { return profile_lookup_js_1.getFitSportId; } });
15
+ Object.defineProperty(exports, "getFitSportName", { enumerable: true, get: function () { return profile_lookup_js_1.getFitSportName; } });
16
+ Object.defineProperty(exports, "getFitSubSportId", { enumerable: true, get: function () { return profile_lookup_js_1.getFitSubSportId; } });
17
+ Object.defineProperty(exports, "getFitSubSportName", { enumerable: true, get: function () { return profile_lookup_js_1.getFitSubSportName; } });
18
+ var raw_message_reader_js_1 = require("./raw-message-reader.js");
19
+ Object.defineProperty(exports, "FitMessageReaderError", { enumerable: true, get: function () { return raw_message_reader_js_1.FitMessageReaderError; } });
20
+ Object.defineProperty(exports, "fitTimestampToUnixMilliseconds", { enumerable: true, get: function () { return raw_message_reader_js_1.fitTimestampToUnixMilliseconds; } });
21
+ Object.defineProperty(exports, "getFitBaseTypeId", { enumerable: true, get: function () { return raw_message_reader_js_1.getFitBaseTypeId; } });
22
+ Object.defineProperty(exports, "readFitMessages", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitMessages; } });
23
+ Object.defineProperty(exports, "readFitStringField", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitStringField; } });
24
+ Object.defineProperty(exports, "readFitUnsignedField", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitUnsignedField; } });
9
25
  class FitParser {
10
26
  constructor(options = {}) {
11
27
  var _a, _b, _c, _d;
@@ -213,7 +213,7 @@ export type SportEvent = 'uncategorized' | 'geocaching' | 'fitness' | 'recreatio
213
213
  export type SquatExerciseName = 'leg_press' | 'back_squat_with_body_bar' | 'back_squats' | 'weighted_back_squats' | 'balancing_squat' | 'weighted_balancing_squat' | 'barbell_back_squat' | 'barbell_box_squat' | 'barbell_front_squat' | 'barbell_hack_squat' | 'barbell_hang_squat_snatch' | 'barbell_lateral_step_up' | 'barbell_quarter_squat' | 'barbell_siff_squat' | 'barbell_squat_snatch' | 'barbell_squat_with_heels_raised' | 'barbell_stepover' | 'barbell_step_up' | 'bench_squat_with_rotational_chop' | 'weighted_bench_squat_with_rotational_chop' | 'body_weight_wall_squat' | 'weighted_wall_squat' | 'box_step_squat' | 'weighted_box_step_squat' | 'braced_squat' | 'crossed_arm_barbell_front_squat' | 'crossover_dumbbell_step_up' | 'dumbbell_front_squat' | 'dumbbell_split_squat' | 'dumbbell_squat' | 'dumbbell_squat_clean' | 'dumbbell_stepover' | 'dumbbell_step_up' | 'elevated_single_leg_squat' | 'weighted_elevated_single_leg_squat' | 'figure_four_squats' | 'weighted_figure_four_squats' | 'goblet_squat' | 'kettlebell_squat' | 'kettlebell_swing_overhead' | 'kettlebell_swing_with_flip_to_squat' | 'lateral_dumbbell_step_up' | 'one_legged_squat' | 'overhead_dumbbell_squat' | 'overhead_squat' | 'partial_single_leg_squat' | 'weighted_partial_single_leg_squat' | 'pistol_squat' | 'weighted_pistol_squat' | 'plie_slides' | 'weighted_plie_slides' | 'plie_squat' | 'weighted_plie_squat' | 'prisoner_squat' | 'weighted_prisoner_squat' | 'single_leg_bench_get_up' | 'weighted_single_leg_bench_get_up' | 'single_leg_bench_squat' | 'weighted_single_leg_bench_squat' | 'single_leg_squat_on_swiss_ball' | 'weighted_single_leg_squat_on_swiss_ball' | 'squat' | 'weighted_squat' | 'squats_with_band' | 'staggered_squat' | 'weighted_staggered_squat' | 'step_up' | 'weighted_step_up' | 'suitcase_squats' | 'sumo_squat' | 'sumo_squat_slide_in' | 'weighted_sumo_squat_slide_in' | 'sumo_squat_to_high_pull' | 'sumo_squat_to_stand' | 'weighted_sumo_squat_to_stand' | 'sumo_squat_with_rotation' | 'weighted_sumo_squat_with_rotation' | 'swiss_ball_body_weight_wall_squat' | 'weighted_swiss_ball_wall_squat' | 'thrusters' | 'uneven_squat' | 'weighted_uneven_squat' | 'waist_slimming_squat' | 'wall_ball' | 'wide_stance_barbell_squat' | 'wide_stance_goblet_squat' | 'zercher_squat' | 'kbs_overhead' | 'squat_and_side_kick' | 'squat_jumps_in_n_out' | 'pilates_plie_squats_parallel_turned_out_flat_and_heels' | 'releve_straight_leg_and_knee_bent_with_one_leg_variation' | 'alternating_box_dumbbell_step_ups' | 'dumbbell_overhead_squat_single_arm' | 'dumbbell_squat_snatch' | 'medicine_ball_squat' | 'wall_ball_squat_and_press' | 'squat_american_swing' | 'air_squat' | 'dumbbell_thrusters' | 'overhead_barbell_squat' | number;
214
214
  export type StairStepperExerciseName = 'stair_stepper' | number;
215
215
  export type StrokeType = 'no_event' | 'other' | 'serve' | 'forehand' | 'backhand' | 'smash' | number;
216
- export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'all' | number;
216
+ export type SubSport = 'generic' | 'treadmill' | 'street' | 'trail' | 'track' | 'spin' | 'indoor_cycling' | 'road' | 'mountain' | 'downhill' | 'recumbent' | 'cyclocross' | 'hand_cycling' | 'track_cycling' | 'indoor_rowing' | 'elliptical' | 'stair_climbing' | 'lap_swimming' | 'open_water' | 'flexibility_training' | 'strength_training' | 'warm_up' | 'match' | 'exercise' | 'challenge' | 'indoor_skiing' | 'cardio_training' | 'indoor_walking' | 'e_bike_fitness' | 'bmx' | 'casual_walking' | 'speed_walking' | 'bike_to_run_transition' | 'run_to_bike_transition' | 'swim_to_bike_transition' | 'atv' | 'motocross' | 'backcountry' | 'resort' | 'rc_drone' | 'wingsuit' | 'whitewater' | 'skate_skiing' | 'yoga' | 'pilates' | 'indoor_running' | 'gravel_cycling' | 'e_bike_mountain' | 'commuting' | 'mixed_surface' | 'navigate' | 'track_me' | 'map' | 'single_gas_diving' | 'multi_gas_diving' | 'gauge_diving' | 'apnea_diving' | 'apnea_hunting' | 'virtual_activity' | 'obstacle' | 'breathing' | 'ccr_diving' | 'sail_race' | 'expedition' | 'ultra' | 'indoor_climbing' | 'bouldering' | 'hiit' | 'indoor_grinding' | 'hunting_with_dogs' | 'amrap' | 'emom' | 'tabata' | 'esport' | 'triathlon' | 'duathlon' | 'brick' | 'swim_run' | 'adventure_race' | 'trucker_workout' | 'pickleball' | 'padel' | 'indoor_wheelchair_walk' | 'indoor_wheelchair_run' | 'indoor_hand_cycling' | 'field' | 'ice' | 'ultimate' | 'platform' | 'squash' | 'badminton' | 'racquetball' | 'table_tennis' | 'overland' | 'trolling_motor' | 'fly_canopy' | 'fly_paraglide' | 'fly_paramotor' | 'fly_pressurized' | 'fly_navigate' | 'fly_timer' | 'fly_altimeter' | 'fly_wx' | 'fly_vfr' | 'fly_ifr' | 'dynamic_apnea' | 'enduro' | 'rucking' | 'rally' | 'pool_triathlon' | 'e_bike_enduro' | 'mountain_enduro' | 'mountain_downhill' | 'all' | number;
217
217
  export type SupportedExdScreenLayouts = 'full_screen' | 'half_vertical' | 'half_horizontal' | 'half_vertical_right_split' | 'half_horizontal_bottom_split' | 'full_quarter_split' | 'half_vertical_left_split' | 'half_horizontal_top_split' | number;
218
218
  export type SuspensionExerciseName = 'chest_fly' | 'chest_press' | 'crunch' | 'curl' | 'dip' | 'face_pull' | 'glute_bridge' | 'hamstring_curl' | 'hip_drop' | 'inverted_row' | 'knee_drive_jump' | 'knee_to_chest' | 'lat_pullover' | 'lunge' | 'mountain_climber' | 'pendulum' | 'pike' | 'plank' | 'power_pull' | 'pull_up' | 'push_up' | 'reverse_mountain_climber' | 'reverse_plank' | 'rollout' | 'row' | 'side_lunge' | 'side_plank' | 'single_leg_deadlift' | 'single_leg_squat' | 'sit_up' | 'split' | 'squat' | 'squat_jump' | 'tricep_press' | 'y_fly' | number;
219
219
  export type SwimStroke = 'freestyle' | 'backstroke' | 'breaststroke' | 'butterfly' | 'drill' | 'mixed' | 'im' | 'im_by_round' | 'rimo' | number;
@@ -0,0 +1,19 @@
1
+ /** Resolves a FIT manufacturer identifier to its canonical profile name. */
2
+ export declare function getFitManufacturerName(value: number | string | null | undefined): string | null;
3
+ /** Resolves a Garmin product identifier to its canonical profile name. */
4
+ export declare function getFitGarminProductName(value: number | string | null | undefined): string | null;
5
+ /** Resolves a FIT sport identifier to its canonical profile name. */
6
+ export declare function getFitSportName(value: number | string | null | undefined): string | null;
7
+ /** Resolves a FIT sub-sport identifier to its canonical profile name. */
8
+ export declare function getFitSubSportName(value: number | string | null | undefined): string | null;
9
+ /** Resolves a FIT sport name to its numeric profile identifier. */
10
+ export declare function getFitSportId(value: string | null | undefined): number | null;
11
+ /** Resolves a FIT sub-sport name to its numeric profile identifier. */
12
+ export declare function getFitSubSportId(value: string | null | undefined): number | null;
13
+ /** Resolves a FIT course-point name to its numeric profile identifier. */
14
+ export declare function getFitCoursePointId(value: string | null | undefined): number | null;
15
+ /**
16
+ * Resolves a Garmin product identifier to a human-readable device name.
17
+ * This does not alter parsed `product` or `product_name` fields.
18
+ */
19
+ export declare function getFitGarminProductDisplayName(value: number | string | null | undefined): string | null;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFitManufacturerName = getFitManufacturerName;
4
+ exports.getFitGarminProductName = getFitGarminProductName;
5
+ exports.getFitSportName = getFitSportName;
6
+ exports.getFitSubSportName = getFitSubSportName;
7
+ exports.getFitSportId = getFitSportId;
8
+ exports.getFitSubSportId = getFitSubSportId;
9
+ exports.getFitCoursePointId = getFitCoursePointId;
10
+ exports.getFitGarminProductDisplayName = getFitGarminProductDisplayName;
11
+ const profile_js_1 = require("./profile.js");
12
+ function normalizeProfileName(value) {
13
+ return value.trim().toLowerCase().replace(/[\s_-]/g, '');
14
+ }
15
+ const FIT_PROFILE_MANUFACTURERS = profile_js_1.PROFILE_TYPES.manufacturer;
16
+ const FIT_PROFILE_GARMIN_PRODUCTS = profile_js_1.PROFILE_TYPES.garmin_product;
17
+ const FIT_PROFILE_SPORTS = profile_js_1.PROFILE_TYPES.sport;
18
+ const FIT_PROFILE_SUB_SPORTS = profile_js_1.PROFILE_TYPES.sub_sport;
19
+ const FIT_PROFILE_COURSE_POINTS = profile_js_1.PROFILE_TYPES.course_point;
20
+ function createProfileIdMap(mapping) {
21
+ return new Map(Object.entries(mapping)
22
+ .filter((entry) => typeof entry[1] === 'string')
23
+ .map(([id, name]) => [normalizeProfileName(name), Number(id)]));
24
+ }
25
+ const FIT_PROFILE_SPORT_IDS = createProfileIdMap(FIT_PROFILE_SPORTS);
26
+ const FIT_PROFILE_SUB_SPORT_IDS = createProfileIdMap(FIT_PROFILE_SUB_SPORTS);
27
+ const FIT_PROFILE_COURSE_POINT_IDS = createProfileIdMap(FIT_PROFILE_COURSE_POINTS);
28
+ function getProfileName(mapping, value) {
29
+ if (value === null || value === undefined) {
30
+ return null;
31
+ }
32
+ if (typeof value !== 'number' && typeof value !== 'string') {
33
+ return null;
34
+ }
35
+ const normalizedValue = typeof value === 'string' ? value.trim() : value;
36
+ if (normalizedValue === '' || (typeof normalizedValue === 'string' && !/^\d+$/.test(normalizedValue))) {
37
+ return null;
38
+ }
39
+ const id = Number(normalizedValue);
40
+ if (!Number.isSafeInteger(id) || id < 0) {
41
+ return null;
42
+ }
43
+ const name = mapping[id];
44
+ return typeof name === 'string' ? name : null;
45
+ }
46
+ function getProfileId(mapping, value) {
47
+ var _a;
48
+ if (typeof value !== 'string') {
49
+ return null;
50
+ }
51
+ const name = normalizeProfileName(value);
52
+ if (name === '') {
53
+ return null;
54
+ }
55
+ return (_a = mapping.get(name)) !== null && _a !== void 0 ? _a : null;
56
+ }
57
+ /** Resolves a FIT manufacturer identifier to its canonical profile name. */
58
+ function getFitManufacturerName(value) {
59
+ return getProfileName(FIT_PROFILE_MANUFACTURERS, value);
60
+ }
61
+ /** Resolves a Garmin product identifier to its canonical profile name. */
62
+ function getFitGarminProductName(value) {
63
+ return getProfileName(FIT_PROFILE_GARMIN_PRODUCTS, value);
64
+ }
65
+ /** Resolves a FIT sport identifier to its canonical profile name. */
66
+ function getFitSportName(value) {
67
+ return getProfileName(FIT_PROFILE_SPORTS, value);
68
+ }
69
+ /** Resolves a FIT sub-sport identifier to its canonical profile name. */
70
+ function getFitSubSportName(value) {
71
+ return getProfileName(FIT_PROFILE_SUB_SPORTS, value);
72
+ }
73
+ /** Resolves a FIT sport name to its numeric profile identifier. */
74
+ function getFitSportId(value) {
75
+ return getProfileId(FIT_PROFILE_SPORT_IDS, value);
76
+ }
77
+ /** Resolves a FIT sub-sport name to its numeric profile identifier. */
78
+ function getFitSubSportId(value) {
79
+ return getProfileId(FIT_PROFILE_SUB_SPORT_IDS, value);
80
+ }
81
+ /** Resolves a FIT course-point name to its numeric profile identifier. */
82
+ function getFitCoursePointId(value) {
83
+ return getProfileId(FIT_PROFILE_COURSE_POINT_IDS, value);
84
+ }
85
+ /**
86
+ * Resolves a Garmin product identifier to a human-readable device name.
87
+ * This does not alter parsed `product` or `product_name` fields.
88
+ */
89
+ function getFitGarminProductDisplayName(value) {
90
+ const name = getFitGarminProductName(value);
91
+ if (!name) {
92
+ return null;
93
+ }
94
+ // Preserve the established display spelling for the optical heart-rate product.
95
+ const displaySource = name === 'o_hr' ? 'o_h_r' : name;
96
+ const formatted = displaySource
97
+ .replace(/^fr(\d+)/i, 'Forerunner $1')
98
+ .replace(/^fenix(\d+)/i, 'Fenix $1')
99
+ .replace(/^edge(\d+)/i, 'Edge $1')
100
+ .replace(/^vivoactive/i, 'VivoActive')
101
+ .replace(/^vivosmart/i, 'VivoSmart')
102
+ .replace(/^vivofit/i, 'VivoFit')
103
+ .replace(/^vivomove/i, 'VivoMove')
104
+ .replace(/^vivosport/i, 'VivoSport')
105
+ .replace(/^approach([A-Z\d])/i, 'Approach $1')
106
+ .replace(/^marq([A-Z])/i, 'Marq $1')
107
+ .replace(/^hrm/i, 'HRM ')
108
+ .replace(/_/g, ' ')
109
+ .replace(/([a-z])([A-Z0-9])/g, '$1 $2')
110
+ .replace(/(\d)([a-z])/gi, '$1 $2');
111
+ return formatted
112
+ .split(' ')
113
+ .map((word) => {
114
+ const lower = word.toLowerCase();
115
+ if (lower === 'apac')
116
+ return 'APAC';
117
+ if (lower === 'xt')
118
+ return 'XT';
119
+ if (lower === 'lte')
120
+ return 'LTE';
121
+ if (lower === 'hr')
122
+ return 'HR';
123
+ if (lower === 'gps')
124
+ return 'GPS';
125
+ if (lower === 'mtb')
126
+ return 'MTB';
127
+ if (lower === 'ii')
128
+ return 'II';
129
+ if (lower === 'iii')
130
+ return 'III';
131
+ if (lower === 'm' && name.toLowerCase().includes('645m'))
132
+ return 'Music';
133
+ if (lower === 'jpn')
134
+ return 'Japan';
135
+ if (lower === 'chn')
136
+ return 'China';
137
+ if (lower === 'twn')
138
+ return 'Taiwan';
139
+ if (lower === 'kor')
140
+ return 'Korea';
141
+ if (lower === 'rus')
142
+ return 'Russia';
143
+ if (lower === 'sea')
144
+ return 'SEA';
145
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
146
+ })
147
+ .join(' ')
148
+ .replace(/Vivo Active/g, 'VivoActive')
149
+ .trim();
150
+ }
@@ -17479,6 +17479,8 @@ exports.PROFILE_TYPES = {
17479
17479
  125: 'rally',
17480
17480
  126: 'pool_triathlon',
17481
17481
  127: 'e_bike_enduro',
17482
+ 153: 'mountain_enduro',
17483
+ 154: 'mountain_downhill',
17482
17484
  254: 'all',
17483
17485
  },
17484
17486
  supported_exd_screen_layouts: {
@@ -0,0 +1,60 @@
1
+ export type FitMessageReaderErrorCode = 'invalid_input' | 'input_limit' | 'invalid_header' | 'invalid_crc' | 'invalid_structure';
2
+ export type FitMessageReaderIssueCode = 'invalid_timestamp';
3
+ export interface FitRawField {
4
+ fieldNumber: number;
5
+ size: number;
6
+ baseType: number;
7
+ bytes: Uint8Array;
8
+ }
9
+ export interface FitRawDeveloperField {
10
+ fieldNumber: number;
11
+ size: number;
12
+ developerDataIndex: number;
13
+ bytes: Uint8Array;
14
+ }
15
+ export interface FitRawMessage {
16
+ globalMessageNumber: number;
17
+ messageIndex: number;
18
+ littleEndian: boolean;
19
+ /** Native FIT timestamp in seconds since the FIT epoch, when available. */
20
+ timestamp?: number;
21
+ /** Reconstructed native timestamp when the record used a compressed header. */
22
+ compressedTimestamp?: number;
23
+ fields: FitRawField[];
24
+ developerFields: FitRawDeveloperField[];
25
+ }
26
+ export interface FitMessageReaderIssue {
27
+ code: FitMessageReaderIssueCode;
28
+ globalMessageNumber: number;
29
+ messageIndex: number;
30
+ }
31
+ export interface FitMessageReaderResult {
32
+ protocolVersion: number;
33
+ profileVersion: number;
34
+ messages: FitRawMessage[];
35
+ issues: FitMessageReaderIssue[];
36
+ }
37
+ export interface FitMessageReaderOptions {
38
+ /** Retain only these global message numbers. Omit to retain every message. */
39
+ messageNumbers?: readonly number[];
40
+ /** Reject larger inputs before allocating retained message data. */
41
+ maxInputBytes?: number;
42
+ }
43
+ /** Error thrown when the strict raw-message reader rejects a FIT input. */
44
+ export declare class FitMessageReaderError extends Error {
45
+ readonly code: FitMessageReaderErrorCode;
46
+ constructor(code: FitMessageReaderErrorCode);
47
+ }
48
+ /** Returns the five-bit FIT base-type identifier, or null for reserved/unknown values. */
49
+ export declare function getFitBaseTypeId(baseType: number): number | null;
50
+ /** Reads an unsigned one-, two-, or four-byte raw FIT field. */
51
+ export declare function readFitUnsignedField(field: FitRawField | undefined, expectedBaseType: number, size: 1 | 2 | 4, littleEndian: boolean): number | undefined;
52
+ /** Reads UTF-8 bytes, removes trailing NUL padding, and preserves interior NUL separators. */
53
+ export declare function readFitStringField(field: FitRawField | undefined): string | undefined;
54
+ /**
55
+ * Strictly validates a FIT file and retains raw fields for selected messages.
56
+ * This entry point does not load the semantic FIT profile or format field values.
57
+ */
58
+ export declare function readFitMessages(input: ArrayBuffer | Uint8Array, options?: FitMessageReaderOptions): FitMessageReaderResult;
59
+ /** Converts a native FIT timestamp to a JavaScript epoch millisecond value. */
60
+ export declare function fitTimestampToUnixMilliseconds(timestamp: number): number;