Task 225: .FIT File Format

Task 225: .FIT File Format

FIT File Format Specifications

The .FIT (Flexible and Interoperable Data Transfer) file format is a binary format developed by Garmin for storing fitness and activity data, such as GPS tracks, heart rate, and other metrics from wearable devices. It is designed for interoperability and flexibility, allowing custom data fields. The format is documented in the Garmin FIT SDK, which includes the protocol specification, profile definitions (messages and fields), and examples. The protocol defines a structure with a file header, local message definitions, data messages, and a trailing CRC for integrity. All multi-byte values are little-endian unless specified otherwise.

  1. List of all the properties of this file format intrinsic to its file system:
  • Header Size: 1 byte (typically 12 or 14; 14 is preferred and includes CRC).
  • Protocol Version: 1 byte (encoded as major * 10 + minor, e.g., 21 for version 2.1).
  • Profile Version: 2 bytes (little-endian, e.g., 2135 for version 21.35).
  • Data Size: 4 bytes (little-endian, length of the data records section in bytes, excluding header and file CRC).
  • Data Type: 4 bytes (ASCII string ".FIT" as a magic number for identification).
  • Header CRC: 2 bytes (little-endian, CRC-16 of bytes 0-11; present only if header size is 14, otherwise omitted).
  • File CRC: 2 bytes (little-endian, CRC-16 of the entire file excluding this CRC; appended after all data records for integrity verification).

These properties are fixed in the file header and footer, making them intrinsic to all valid .FIT files regardless of content type (e.g., activity, workout, course).

Two direct download links for files of format .FIT:

Ghost blog embedded HTML JavaScript for drag-and-drop .FIT file dump:

FIT File Properties Dumper

Drag and Drop .FIT File to Dump Properties

Drop .FIT file here

This HTML can be embedded in a Ghost blog post. It creates a drag-and-drop zone that reads the .FIT file as binary, parses the header and file CRC, and displays the properties.

  1. Python class:
import struct
import binascii
import os

class FitFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}

    def read_and_decode(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        if len(data) < 14:
            raise ValueError("Invalid .FIT file: too short")
        
        # Unpack header (little-endian)
        header_size, protocol_ver = struct.unpack('<BB', data[0:2])
        profile_ver, = struct.unpack('<H', data[2:4])
        data_size, = struct.unpack('<I', data[4:8])
        data_type = data[8:12].decode('ascii')
        header_crc = 0
        if header_size == 14:
            header_crc, = struct.unpack('<H', data[12:14])
        else:
            if header_size != 12:
                raise ValueError("Invalid header size")
        
        # File CRC at end
        expected_len = header_size + data_size + 2
        if len(data) != expected_len:
            raise ValueError("File length mismatch")
        file_crc, = struct.unpack('<H', data[-2:])

        self.properties = {
            'Header Size': header_size,
            'Protocol Version': protocol_ver,
            'Profile Version': profile_ver,
            'Data Size': data_size,
            'Data Type': data_type,
            'Header CRC': hex(header_crc),
            'File CRC': hex(file_crc)
        }

    def print_properties(self):
        if not self.properties:
            print("No properties loaded. Call read_and_decode first.")
            return
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, output_path=None):
        if not self.properties:
            raise ValueError("No properties to write. Call read_and_decode first.")
        if output_path is None:
            output_path = self.filepath + '.modified.fit'
        
        # For simplicity, read original data and modify header if needed; here we just copy as-is since we're not changing
        with open(self.filepath, 'rb') as f:
            data = f.read()
        with open(output_path, 'wb') as f:
            f.write(data)
        print(f"File written to {output_path}")

# Example usage:
# handler = FitFileHandler('example.fit')
# handler.read_and_decode()
# handler.print_properties()
# handler.write()

This class opens a .FIT file, decodes the properties, prints them to console, and can write the file (here, copies as-is; extend for modifications).

  1. Java class:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class FitFileHandler {
    private String filepath;
    private int headerSize;
    private int protocolVersion;
    private int profileVersion;
    private int dataSize;
    private String dataType;
    private int headerCRC;
    private int fileCRC;

    public FitFileHandler(String filepath) {
        this.filepath = filepath;
    }

    public void readAndDecode() throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filepath));
        if (data.length < 14) {
            throw new IOException("Invalid .FIT file: too short");
        }

        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);

        headerSize = Byte.toUnsignedInt(buffer.get(0));
        protocolVersion = Byte.toUnsignedInt(buffer.get(1));
        profileVersion = Short.toUnsignedInt(buffer.getShort(2));
        dataSize = Integer.toUnsignedInt(buffer.getInt(4));
        dataType = new String(data, 8, 4, "ASCII");
        headerCRC = 0;
        if (headerSize == 14) {
            headerCRC = Short.toUnsignedInt(buffer.getShort(12));
        } else if (headerSize != 12) {
            throw new IOException("Invalid header size");
        }

        int expectedLen = headerSize + dataSize + 2;
        if (data.length != expectedLen) {
            throw new IOException("File length mismatch");
        }
        fileCRC = Short.toUnsignedInt(buffer.getShort(headerSize + dataSize));
    }

    public void printProperties() {
        System.out.println("Header Size: " + headerSize);
        System.out.println("Protocol Version: " + protocolVersion);
        System.out.println("Profile Version: " + profileVersion);
        System.out.println("Data Size: " + dataSize);
        System.out.println("Data Type: " + dataType);
        System.out.println("Header CRC: 0x" + Integer.toHexString(headerCRC).toUpperCase());
        System.out.println("File CRC: 0x" + Integer.toHexString(fileCRC).toUpperCase());
    }

    public void write(String outputPath) throws IOException {
        if (outputPath == null) {
            outputPath = filepath + ".modified.fit";
        }
        // Copy as-is for now; extend for modifications
        Files.copy(Paths.get(filepath), Paths.get(outputPath), StandardCopyOption.REPLACE_EXISTING);
        System.out.println("File written to " + outputPath);
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     FitFileHandler handler = new FitFileHandler("example.fit");
    //     handler.readAndDecode();
    //     handler.printProperties();
    //     handler.write(null);
    // }
}

This Java class opens, decodes, prints, and writes the file similarly.

  1. JavaScript class:
class FitFileHandler {
    constructor(filepath) {
        this.filepath = filepath; // For Node.js; use File/Blob for browser
        this.properties = {};
    }

    async readAndDecode() {
        const fs = require('fs'); // Node.js only
        const data = fs.readFileSync(this.filepath);
        const dataView = new DataView(data.buffer);

        if (data.length < 14) {
            throw new Error('Invalid .FIT file: too short');
        }

        const headerSize = dataView.getUint8(0);
        const protocolVersion = dataView.getUint8(1);
        const profileVersion = dataView.getUint16(2, true);
        const dataSize = dataView.getUint32(4, true);
        const dataType = String.fromCharCode(dataView.getUint8(8), dataView.getUint8(9), dataView.getUint8(10), dataView.getUint8(11));
        let headerCRC = 0;
        if (headerSize === 14) {
            headerCRC = dataView.getUint16(12, true);
        } else if (headerSize !== 12) {
            throw new Error('Invalid header size');
        }

        const expectedLen = headerSize + dataSize + 2;
        if (data.length !== expectedLen) {
            throw new Error('File length mismatch');
        }
        const fileCRC = dataView.getUint16(headerSize + dataSize, true);

        this.properties = {
            'Header Size': headerSize,
            'Protocol Version': protocolVersion,
            'Profile Version': profileVersion,
            'Data Size': dataSize,
            'Data Type': dataType,
            'Header CRC': headerCRC.toString(16).toUpperCase(),
            'File CRC': fileCRC.toString(16).toUpperCase()
        };
    }

    printProperties() {
        if (Object.keys(this.properties).length === 0) {
            console.log('No properties loaded. Call readAndDecode first.');
            return;
        }
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    write(outputPath = null) {
        if (outputPath === null) {
            outputPath = this.filepath + '.modified.fit';
        }
        const fs = require('fs');
        fs.copyFileSync(this.filepath, outputPath);
        console.log(`File written to ${outputPath}`);
    }
}

// Example usage (Node.js):
// const handler = new FitFileHandler('example.fit');
// await handler.readAndDecode();
// handler.printProperties();
// handler.write();

This JS class (Node.js compatible) opens, decodes, prints to console, and writes the file.

  1. C "class" (using struct and functions):
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    char *filepath;
    uint8_t header_size;
    uint8_t protocol_version;
    uint16_t profile_version;
    uint32_t data_size;
    char data_type[5];
    uint16_t header_crc;
    uint16_t file_crc;
} FitFileHandler;

FitFileHandler* fit_create(const char *filepath) {
    FitFileHandler *handler = malloc(sizeof(FitFileHandler));
    handler->filepath = strdup(filepath);
    memset(handler, 0, sizeof(FitFileHandler));
    return handler;
}

void fit_destroy(FitFileHandler *handler) {
    free(handler->filepath);
    free(handler);
}

int fit_read_and_decode(FitFileHandler *handler) {
    FILE *f = fopen(handler->filepath, "rb");
    if (!f) return -1;
    fseek(f, 0, SEEK_END);
    long len = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *data = malloc(len);
    fread(data, 1, len, f);
    fclose(f);

    if (len < 14) {
        free(data);
        return -1;
    }

    handler->header_size = data[0];
    handler->protocol_version = data[1];
    memcpy(&handler->profile_version, &data[2], 2); // LE assumed native
    memcpy(&handler->data_size, &data[4], 4);
    memcpy(handler->data_type, &data[8], 4);
    handler->data_type[4] = '\0';
    handler->header_crc = 0;
    if (handler->header_size == 14) {
        memcpy(&handler->header_crc, &data[12], 2);
    } else if (handler->header_size != 12) {
        free(data);
        return -1;
    }

    uint32_t expected_len = handler->header_size + handler->data_size + 2;
    if (len != expected_len) {
        free(data);
        return -1;
    }
    memcpy(&handler->file_crc, &data[handler->header_size + handler->data_size], 2);

    free(data);
    return 0;
}

void fit_print_properties(FitFileHandler *handler) {
    printf("Header Size: %u\n", handler->header_size);
    printf("Protocol Version: %u\n", handler->protocol_version);
    printf("Profile Version: %u\n", handler->profile_version);
    printf("Data Size: %u\n", handler->data_size);
    printf("Data Type: %s\n", handler->data_type);
    printf("Header CRC: 0x%04X\n", handler->header_crc);
    printf("File CRC: 0x%04X\n", handler->file_crc);
}

int fit_write(FitFileHandler *handler, const char *output_path) {
    if (!output_path) output_path = "modified.fit";
    FILE *src = fopen(handler->filepath, "rb");
    FILE *dest = fopen(output_path, "wb");
    if (!src || !dest) return -1;
    char buf[1024];
    size_t read;
    while ((read = fread(buf, 1, sizeof(buf), src)) > 0) {
        fwrite(buf, 1, read, dest);
    }
    fclose(src);
    fclose(dest);
    printf("File written to %s\n", output_path);
    return 0;
}

// Example usage:
// int main() {
//     FitFileHandler *handler = fit_create("example.fit");
//     if (fit_read_and_decode(handler) == 0) {
//         fit_print_properties(handler);
//         fit_write(handler, NULL);
//     }
//     fit_destroy(handler);
//     return 0;
// }

This C implementation uses a struct as a "class" with functions to create, read/decode, print, write, and destroy. Assumes little-endian host for simplicity (extend with byte swaps if needed).