Skip to content

rootrootde/python_fit_tool

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Python FIT Tool - Enhanced Fork

A Python library for reading and writing Garmin FIT (Flexible and Interoperable Data Transfer) files, enhanced with support for undocumented messages and field types.

Table of Contents

Fork History

  • Original: stagescycling/python_fit_tool (last updated October 2022)
  • Previous Fork: Stuart.Lynne@gmail.com (2024-09-17)
  • Current Fork: Enhanced with undocumented messages/types support

Key Enhancements in This Fork

1. Support for Undocumented Messages and Types

This fork includes support for undocumented FIT messages and types that are not part of the official Garmin SDK specification:

Undocumented Messages:

  • Message 162 (mesg_162)
  • Message 233 (mesg_233)
  • Message 288 (mesg_288)
  • Message 324 (mesg_324)
  • Message 325 (mesg_325)
  • Message 327 (mesg_327)

New Message Types Added:

  • sport_settings (13)
  • data_screen (14)
  • alert (16)
  • range_alert (17)
  • device_used (22)
  • location (29)
  • map_layer (70)
  • routing (71)
  • user_metrics (79)
  • open_water_event (89)
  • device_status (104)
  • best_effort (113)
  • personal_record (114)
  • activity_metrics (140)
  • epo_status (141)
  • multisport_settings (143)
  • multisport_activity (144)
  • sensor_settings (147)
  • metronome (152)
  • connect_iq_field (170)
  • clubs (173)
  • waypoint_handling (189)
  • golf_course (190)
  • golf_stats (191)
  • score (192)
  • hole (193)
  • shot (194)
  • music_info (243)
  • mtb_cx (309)
  • race (310)
  • split_time (311)
  • power_mode (321)
  • gps_event (326)
  • race_event (358)
  • sleep_schedule (379)
  • cpe_status (394)
  • workout_schedule (428)

New Field Types Added:

  • Additional file types: locations, records, multi_sport, clubs, score_card, metrics, sleep, pace_band, calendar, hrv_status, lha_backup, ptd_backup, schedule
  • Extended event types: performance_condition_alert
  • New sport/activity enums: exercise categories (move, pose, banded_exercises)
  • GPS modes and types with multi-band support
  • Enhanced navigation and routing types
  • Custom field definitions preserved in Additions.xlsx

2. Enhanced Profile Generation System

  • Merge and Generate Script: merge_and_generate.py combines official Garmin profiles with custom additions
  • Automatic Deployment: Generated files are automatically copied to the appropriate directories
  • Command-line Support: Flexible profile generation with custom input files
  • Error Handling: Robust validation and error reporting during profile generation

3. Field Scaling Fixes

  • Corrected altitude and enhanced_altitude field scaling in location messages
  • Proper handling of semicircle to degrees conversion
  • Custom scale/offset values preserved through profile regeneration

4. Updated to Latest FIT SDK

  • Profile version: 21.171 (latest as of 2025)
  • Protocol version: 2.3
  • Includes all recent message types and field definitions from Garmin

Features

  • Read and Write: Full support for reading and writing FIT files
  • Type Safety: Auto-generated message classes with proper typing
  • Developer Fields: Support for custom developer-defined fields
  • CSV Export: Convert FIT files to human-readable CSV format
  • Programmatic Creation: Build FIT files programmatically with FitFileBuilder
  • Examples: Comprehensive examples for common use cases

Background

The Flexible and Interoperable Data Transfer (FIT) protocol is designed specifically for the storing and sharing of data that originates from sport, fitness and health devices. The FIT protocol defines a set of data storage templates (FIT messages) that can be used to store information such as user profiles, activity data, courses, and workouts. It is specifically designed to be compact, interoperable and extensible.

More info...

Installation

From Source (Recommended for this fork)

git clone <your-repo-url> cd python_fit_tool pip install -e .

Dependencies

pip install openpyxl==2.5.12 bitstruct==8.11.1

For Development

# Install with development dependencies pip install -e ".[dev]"

How to Merge and Generate New Profiles

This fork includes an enhanced profile generation system that combines official Garmin FIT profiles with custom additions. Here's how to use it:

Quick Start - Generate with Current Profile

cd fit_tool/gen python merge_and_generate.py

This will:

  1. Merge Profile_21.171.xlsx (official Garmin profile) with Additions.xlsx (custom messages/types)
  2. Generate Python message classes in ./messages/ directory
  3. Generate profile types in profile.py
  4. Automatically copy generated files to fit_tool/profile/ directory

Using Custom Profile Files

# Use a different Garmin profile version python merge_and_generate.py --profile Profile_22.0.xlsx # Use different additions file python merge_and_generate.py --additions MyCustom.xlsx # Use both custom files python merge_and_generate.py --profile Profile_22.0.xlsx --additions MyCustom.xlsx

Adding New Messages or Types

  1. Edit Additions.xlsx: Open fit_tool/gen/Additions.xlsx in Excel or LibreOffice

  2. Add Messages (Messages sheet):

    • Column A: Message Name (e.g., "my_custom_message")
    • Column B: Field Def # (field number within message)
    • Column C: Field Name (e.g., "timestamp", "data_value")
    • Column D: Field Type (e.g., "date_time", "uint16", "sint32")
    • Column E: Array (TRUE/FALSE for array fields)
    • Column F: Components (semicolon-separated for complex fields)
    • Column G: Scale (scaling factor, e.g., 100)
    • Column H: Offset (offset value)
    • Column I: Units (e.g., "m", "s", "deg")
    • Column Q: Global Message Number (unique message ID)
  3. Add Types (Types sheet):

    • Column A: Type Name (e.g., "my_custom_enum")
    • Column B: Base Type (e.g., "enum", "uint8")
    • Column C: Value Name (enum value name)
    • Column D: Value (numeric value for enum)
    • Column E: Comment (optional description)
  4. Regenerate: Run python merge_and_generate.py to apply changes

Example: Adding a Custom Message

# In Messages sheet: Message Name | Field Def # | Field Name | Field Type | Global Message Number my_sensor_data | 253 | timestamp | date_time | 500 my_sensor_data | 0 | sensor_value | uint16 | 500 my_sensor_data | 1 | sensor_type | enum | 500 
# In Types sheet: Type Name | Base Type | Value Name | Value sensor_type | enum | temperature | 0 sensor_type | enum | humidity | 1 sensor_type | enum | pressure | 2 

Generated Files

After running the merge script, you'll get:

  • Message Classes: fit_tool/profile/messages/my_sensor_data_message.py
  • Updated Profile Types: Enhanced fit_tool/profile/profile_type.py with new enums
  • Message Factory: Updated fit_tool/profile/messages/message_factory.py

Validation and Error Handling

The script includes comprehensive validation:

  • Checks for duplicate message numbers
  • Validates field types against known base types
  • Reports missing required columns
  • Warns about potential conflicts

Best Practices

  1. Backup First: Always backup your Additions.xlsx before major changes
  2. Unique Message Numbers: Use message numbers > 400 to avoid conflicts with future Garmin updates
  3. Test After Generation: Run tests to ensure generated code compiles correctly
  4. Document Changes: Add comments in the Excel file to document custom additions

Command Line Interface

usage: fittool [-h] [-v] [-o OUTPUT] [-l LOG] [-t TYPE] FILE Tool for managing FIT files. positional arguments:  FILE FIT file to process optional arguments:  -h, --help show this help message and exit  -v, --verbose specify verbose output  -o OUTPUT, --output OUTPUT  Output filename.  -l LOG, --log LOG Log filename.  -t TYPE, --type TYPE Output format type. Options: csv, fit.

Convert file to CSV

./bin/fittool oldstage.fit 

Library Usage

Reading a FIT file

The following code reads all the bytes from an activity FIT file and then decodes these bytes to create a FIT file object. We then convert the FIT data to a human-readable CSV file.

from fit_tool.fit_file import FitFile def main(): """ The following code reads all the bytes from a FIT formatted file and then decodes these bytes to  create a FIT file object. We then convert the FIT data to a human-readable CSV file.  """ path = '../tests/data/sdk/Activity.fit' fit_file = FitFile.from_file(path) out_path = '../tests/data/sdk/Activity.csv' fit_file.to_csv(out_path) if __name__ == "__main__": main()

Reading a FIT file and plotting some data

import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np from fit_tool.fit_file import FitFile from fit_tool.profile.messages.record_message import RecordMessage def main(): """ Analyze a FIT file  """ mpl.style.use('seaborn') print(f'Loading activity file...') app_fit = FitFile.from_file('./activity_20211102_133232.fit') timestamp1 = [] power1 = [] distance1 = [] speed1 = [] cadence1 = [] for record in app_fit.records: message = record.message if isinstance(message, RecordMessage): timestamp1.append(message.timestamp) distance1.append(message.distance) power1.append(message.power) speed1.append(message.speed) cadence1.append(message.cadence) start_timestamp = timestamp1[0] time1 = np.array(timestamp1) power1 = np.array(power1) speed1 = np.array(speed1) cadence1 = np.array(cadence1) time1 = (time1 - start_timestamp) / 1000.0 # seconds # # Plot the data # ax1 = plt.subplot(311) ax1.plot(time1, power1, '-o', label='app [W]') ax1.legend(loc="upper right") plt.xlabel('Time (s)') plt.ylabel('Power (W)') plt.subplot(312, sharex=ax1) plt.plot(time1, speed1, '-o', label='app [m/s]') plt.legend(loc="upper right") plt.xlabel('Time (s)') plt.ylabel('speed (m/s)') plt.subplot(313, sharex=ax1) plt.plot(time1, cadence1, '-o', label='app [rpm]') plt.legend(loc="upper right") plt.xlabel('Time (s)') plt.ylabel('cadence (rpm)') plt.show() if __name__ == "__main__": main()

Writing a Workout

import datetime from fit_tool.fit_file_builder import FitFileBuilder from fit_tool.profile.messages.file_id_message import FileIdMessage from fit_tool.profile.messages.workout_message import WorkoutMessage from fit_tool.profile.messages.workout_step_message import WorkoutStepMessage from fit_tool.profile.profile_type import Sport, Intensity, WorkoutStepDuration, WorkoutStepTarget, Manufacturer, FileType def main(): file_id_message = FileIdMessage() file_id_message.type = FileType.WORKOUT file_id_message.manufacturer = Manufacturer.DEVELOPMENT.value file_id_message.product = 0 file_id_message.time_created = round(datetime.datetime.now().timestamp() * 1000) file_id_message.serial_number = 0x12345678 workout_steps = [] step = WorkoutStepMessage() step.workout_step_name = 'Warm up 10min in Heart Rate Zone 1' step.intensity = Intensity.WARMUP step.duration_type = WorkoutStepDuration.TIME step.duration_time = 600.0 step.target_type = WorkoutStepTarget.HEART_RATE step.target_hr_zone = 1 workout_steps.append(step) step = WorkoutStepMessage() step.workout_step_name = 'Bike 40min Power Zone 3' step.intensity = Intensity.ACTIVE step.duration_type = WorkoutStepDuration.TIME step.duration_time = 24000.0 step.target_type = WorkoutStepTarget.POWER step.target_power_zone = 3 workout_steps.append(step) step = WorkoutStepMessage() step.workout_step_name = 'Cool Down Until Lap Button Pressed' step.intensity = Intensity.COOLDOWN step.duration_type = WorkoutStepDuration.OPEN step.durationValue = 0 step.target_type = WorkoutStepTarget.OPEN step.target_value = 0 workout_steps.append(step) workout_message = WorkoutMessage() workout_message.workoutName = 'Tempo Bike' workout_message.sport = Sport.CYCLING workout_message.num_valid_steps = len(workout_steps) # We set autoDefine to true, so that the builder creates the required # Definition Messages for us. builder = FitFileBuilder(auto_define=True, min_string_size=50) builder.add(file_id_message) builder.add(workout_message) builder.add_all(workout_steps) fit_file = builder.build() out_path = '../tests/out/tempo_bike_workout.fit' fit_file.to_file(out_path) if __name__ == "__main__": main()

Writing a Course

import datetime import gpxpy from geopy.distance import geodesic from fit_tool.fit_file_builder import FitFileBuilder from fit_tool.profile.messages.course_message import CourseMessage from fit_tool.profile.messages.course_point_message import CoursePointMessage from fit_tool.profile.messages.event_message import EventMessage from fit_tool.profile.messages.file_id_message import FileIdMessage from fit_tool.profile.messages.lap_message import LapMessage from fit_tool.profile.messages.record_message import RecordMessage from fit_tool.profile.profile_type import FileType, Manufacturer, Sport, Event, EventType, CoursePoint def main(): # Set auto_define to true, so that the builder creates the required Definition Messages for us. builder = FitFileBuilder(auto_define=True, min_string_size=50) # Read position data from a GPX file gpx_file = open('../tests/data/old_stage_left_hand_lee.gpx', 'r') gpx = gpxpy.parse(gpx_file) message = FileIdMessage() message.type = FileType.COURSE message.manufacturer = Manufacturer.DEVELOPMENT.value message.product = 0 message.timeCreated = round(datetime.datetime.now().timestamp() * 1000) message.serialNumber = 0x12345678 builder.add(message) # Every FIT course file MUST contain a Course message message = CourseMessage() message.courseName = 'old stage' message.sport = Sport.CYCLING builder.add(message) # Timer Events are REQUIRED for FIT course files start_timestamp = round(datetime.datetime.now().timestamp() * 1000) message = EventMessage() message.event = Event.TIMER message.event_type = EventType.START message.timestamp = start_timestamp builder.add(message) distance = 0.0 timestamp = start_timestamp course_records = [] # track points prev_coordinate = None for track_point in gpx.tracks[0].segments[0].points: current_coordinate = (track_point.latitude, track_point.longitude) # calculate distance from previous coordinate and accumulate distance if prev_coordinate: delta = geodesic(prev_coordinate, current_coordinate).meters else: delta = 0.0 distance += delta message = RecordMessage() message.position_lat = track_point.latitude message.position_long = track_point.longitude message.distance = distance message.timestamp = timestamp course_records.append(message) timestamp += 10000 prev_coordinate = current_coordinate builder.add_all(course_records) # Add start and end course points (i.e. way points) # message = CoursePointMessage() message.timestamp = course_records[0].timestamp message.position_lat = course_records[0].position_lat message.position_long = course_records[0].position_long message.type = CoursePoint.SEGMENT_START message.course_point_name = 'start' builder.add(message) message = CoursePointMessage() message.timestamp = course_records[-1].timestamp message.position_lat = course_records[-1].position_lat message.position_long = course_records[-1].position_long message.type = CoursePoint.SEGMENT_END message.course_point_name = 'end' builder.add(message) # stop event message = EventMessage() message.event = Event.TIMER message.eventType = EventType.STOP_ALL message.timestamp = timestamp builder.add(message) # Every FIT course file MUST contain a Lap message elapsed_time = timestamp - start_timestamp message = LapMessage() message.timestamp = timestamp message.start_time = start_timestamp message.total_elapsed_time = elapsed_time message.total_timer_time = elapsed_time message.start_position_lat = course_records[0].position_lat message.start_position_long = course_records[0].position_long message.end_position_lat = course_records[-1].position_lat message.endPositionLong = course_records[-1].position_long message.total_distance = course_records[-1].distance # Finally build the FIT file object and write it to a file fit_file = builder.build() out_path = '../tests/out/old_stage_course.fit' fit_file.to_file(out_path) csv_path = '../tests/out/old_stage_course.csv' fit_file.to_csv(csv_path) if __name__ == "__main__": main()

About

A Python library for reading and writing Garmin FIT (Flexible and Interoperable Data Transfer) files, enhanced with support for undocumented messages and field types.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors