Skip to content

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

License

Notifications You must be signed in to change notification settings

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

No packages published

Contributors 2

  •  
  •