Garmin FIT Python SDK

repository·main·Indexed 41 days ago

https://github.com/garmin/fit-python-sdk

A library for reading (decoding) and writing (encoding) FIT files, the binary format used by Garmin devices to store fitness and activity data. Version 21.214.0 requires Python 3.6 or greater. It provides core classes such as Decoder for parsing files, Encoder for constructing them, and Stream for managing binary data sources, along with utilities for FIT Epoch timestamp conversion.

Tokens
7.7K
Snippets
27
Records
32
Agent score
51%

What's inside Garmin FIT Python SDK

  1. Quickstart: Decode a FIT file

    main

    To decode a FIT file, create a Stream from the file, pass it to a Decoder, and call read(). The read() method returns a tuple containing a dictionary of decoded messages and a list of errors.

    from garmin_fit_sdk import Decoder, Stream
    
    stream = Stream.from_file("Activity.fit")
    decoder = Decoder(stream)
    messages, errors = decoder.read()
    
    print(errors)
    print(messages)
  2. Decode FIT files using the Decoder class

    main

    The Decoder class is the primary interface for parsing FIT files. You initialize it with a Stream object and use the read() method to extract decoded messages.

    By default, read() performs several transformations:

    • Applies scale and offset to numeric fields.
    • Converts timestamps to Python datetime objects.
    • Converts numeric type codes to human-readable strings.
    • Expands components (bit-packed fields) and sub-fields.
    • Merges heart rate messages with record messages if enabled.

    If you need the raw values without these transformations, you can disable them via the parameters in read().

    Returns a tuple containing a dictionary of decoded messages (keyed by message type) and a list of any errors encountered during decoding.

    from garmin_fit_sdk.stream import Stream
    from garmin_fit_sdk.decoder import Decoder
    
    # Assuming 'stream' is an instance of Stream containing FIT file data
    decoder = Decoder(stream)
    
    # Read all messages
    # messages is a dict where keys are message types (e.g., 'record_mesgs')
    # errors is a list of exceptions encountered
    messages, errors = decoder.read()
    
    if errors:
        print(f"Errors encountered: {errors}")
    
    for msg_type, msgs in messages.items():
        for msg in msgs:
            print(msg)
  3. Use a mesg_listener callback during decoding

    main

    You can provide a mesg_listener to the read() method to intercept and manipulate messages as they are decoded. The message object is mutable.

    Example: Tracking all field names used in RECORD messages:

    from garmin_fit_sdk import Decoder, Stream, Profile
    
    stream = Stream.from_file("Activity.fit")
    decoder = Decoder(stream)
    
    record_fields = set()
    def mesg_listener(mesg_num, message):
        if mesg_num == Profile['mesg_num']['RECORD']:
            for field in message:
                record_fields.add(field)
    
    messages, errors = decoder.read(mesg_listener = mesg_listener)
    
    if len(errors) > 0:
        print(f"Something went wrong decoding the file: {errors}")
        return
    
    print(record_fields)
  4. Encode FIT messages with the Encoder class

    main

    The Encoder class allows you to construct FIT files. You can add messages using on_mesg() or write_mesg(), and then finalize the file with close().

    • on_mesg(mesg_num, data): Adds a message to the encoder. Unknown values in the data dictionary are ignored.
    • write_mesg(data): Expects the mesg_num to be included inside the data dictionary. Internally calls on_mesg().
    • close(): Returns the encoded data as a uint8_array (bytes).

    Note: Subfield values and unknown custom fields in the message dictionary are ignored by the encoder.

    from datetime import datetime, timezone
    from garmin_fit_sdk import Encoder, Profile
    
    encoder = Encoder()
    
    # Using on_mesg
    encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
        'manufacturer': 'development',
        'product': 1,
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
    })
    
    # Using write_mesg (mesg_num must be in the dict)
    encoder.write_mesg({
        'mesg_num': Profile['mesg_num']['FILE_ID'],
        'manufacturer': 'development',
        'product': 1,
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
    })
    
    uint8_array = encoder.close()
    
    with open('example.fit', 'wb') as f:
        f.write(uint8_array)
    from datetime import datetime, timezone
    
    from garmin_fit_sdk import Encoder, Profile
    
    encoder = Encoder()
    
    # Pass the MesgNum and message data as separate parameters to the onMesg() method
    encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
        'manufacturer': 'development',
        'product': 1,
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
    })
    
    # The writeMesg() method expects the mesgNum to be included in the message data
    # Internally, writeMesg() calls onMesg()
    encoder.write_mesg({
        'mesg_num': Profile['mesg_num']['FILE_ID'],
        'manufacturer': 'development',
        'product': 1,
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
    })
    
    # Unknown values in the message will be ignored by the Encoder
    encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
        'manufacturer': 'development',
        'product': 1,
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
        'customField': 12345, # This value will be ignored by the Encoder
    })
    
    # Subfield values in the message will be ignored by the Encoder
    encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
        'manufacturer': 'development',
        'product': 4440, # This is the main product field, which is a uint16
        'garmin_product': 'edge_1050', # This value will be ignored by the Encoder, use the main field value instead
        'time_created': datetime.now(tz=timezone.utc),
        'type': 'activity',
    })
    
    uint8_array = encoder.close()
    
    # Write the bytes to a file
    with open('example.fit', 'wb') as f:
        f.write(uint8_array)
  5. Convert FIT Epoch timestamps using Util

    main

    The Util module provides tools for handling FIT-specific data types.

    Manual Conversion

    If you did not use convert_datetimes_to_dates=True in the decoder, you can use the FIT_EPOCH_S constant (631065600) to convert FIT Epoch values to Python datetime objects:

    import datetime
    python_date = datetime.datetime.fromtimestamp(fit_datetime + Util.FIT_EPOCH_S, datetime.UTC)

    Convenience Methods

    Util provides direct methods for timestamp conversion:

    • Util.convert_timestamp_to_datetime(fit_datetime): Converts FIT Epoch to Python datetime.
    • Util.convert_datetime_to_timestamp(python_date): Converts Python datetime to FIT Epoch.
  6. Validate FIT files with is_fit and check_integrity

    main

    The Decoder class provides methods to verify if a stream contains a valid FIT file:

    • is_fit(stream): A static method that checks if the stream contains the .FIT ASCII identifier in the header (bytes 8-11).
    • check_integrity(): Performs three checks: verifies the .FIT header, ensures the file size matches the sum of Header, Data, and CRC sizes, and validates the file's CRC.

    Example usage:

    stream = Stream.from_file("activity.fit")
    print(f"is_fit: {Decoder.is_fit(stream)}")
  7. Configure Decoder.read() options

    main

    The read() method accepts several optional arguments to customize how data is decoded. All options are enabled by default. Disabling them can improve performance.

    OptionTypeDescription
    apply_scale_and_offsetboolIf True, applies scale and offset values from the FIT Profile to raw field values.
    convert_datetimes_to_datesboolIf True, converts FIT Epoch values to Python datetime objects.
    convert_types_to_stringsboolIf True, converts raw integer values to their corresponding string representations (e.g., 'activity').
    enable_crc_checkboolIf True, validates the file CRC during decoding. Disabling this improves performance.
    expand_sub_fieldsboolIf True, creates subfields for fields as defined in the FIT Profile.
    expand_componentsboolIf True, expands field components into new fields. Requires expand_sub_fields=True.
    merge_heart_ratesboolIf True, merges heart rate values from HR messages into Record messages. Requires apply_scale_and_offset and expand_components to be True.
    mesg_listenercallableAn optional callback function (mesg_num, message) used to inspect or manipulate messages after decoding.
  8. Create Stream objects from various sources

    main

    Stream objects wrap binary FIT data. You can instantiate them using several methods:

    • Stream.from_file(path): From a file on disk.
    • Stream.from_byte_array(bytearray): From a Python bytearray.
    • Stream.from_byte_io(BytesIO): From a io.BytesIO object.
    • Stream.from_buffered_reader(reader): From an io.BufferedReader.
    # From a file
    stream = Stream.from_file("activity.fit")
    
    # From a bytearray
    fit_byte_array = bytearray([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00])
    stream = Stream.from_byte_array(fit_byte_array)
  9. Write messages via on_mesg callback

    main

    The on_mesg(mesg_num, mesg) method encodes a message into the file. This method is designed to be compatible with Decoder message listener callbacks, allowing you to pipe data directly from a decoder into an encoder.

    Args:

    • mesg_num: The message number.
    • mesg: A dictionary containing the message data.
    encoder.on_mesg(mesg_num, mesg_data)
  10. Navigate and inspect a Stream

    main

    You can control the position of the stream and inspect data without advancing the pointer:

    • position(): Returns the current byte position in the stream.
    • get_length(): Returns the total length of the stream.
    • seek(position): Moves the stream to a specific byte position.
    • reset(): Moves the stream position back to the beginning (0).
    • peek_byte(): Reads one byte without advancing the stream position.
    • peek_bytes(num_bytes): Reads a specified number of bytes without advancing the stream position.
    • slice(start, end): Returns a slice of bytes between start and end without permanently changing the stream position.
    current_pos = stream.position()
    stream.seek(0)
    stream.reset()
    
    # Inspecting data without consuming it
    next_byte = stream.peek_byte()
    chunk = stream.peek_bytes(4)
    
    # Getting a specific range
    segment = stream.slice(10, 20)
  11. Convert timestamps to seconds since FIT epoch with `seconds_since_fit_epoch`

    main

    Use seconds_since_fit_epoch(timestamp) to normalize various timestamp formats into the standard FIT epoch format (seconds since the FIT epoch).

    Parameters:

    • timestamp: Can be a datetime object or a numeric value representing seconds since the FIT epoch.

    Returns:

    • A float representing the time in seconds since the FIT epoch.

    Note: If a datetime object is provided, the function subtracts util.FIT_EPOCH_S from the datetime's timestamp to align with the FIT protocol standard.

    from datetime import datetime
    
    # If timestamp is already seconds since epoch
    time_val = seconds_since_fit_epoch(1724563200.0)
    
    # If timestamp is a datetime object
    dt = datetime(2026, 8, 25, 12, 0, 0)
    time_val = seconds_since_fit_epoch(dt)