Thomas Klaehn 2945d90d24 Initial commit
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00
2026-02-08 07:30:31 +00:00

FIT File Parser for Go

A pure Go implementation of a FIT (Flexible and Interoperable Data Transfer) file parser. FIT is a binary file format used by Garmin and other fitness device manufacturers to store activity data, workouts, courses, and more.

Features

  • Full FIT file decoder
  • Support for all FIT base types (integers, floats, strings, arrays)
  • Complete CRC-16 validation (tested on 170+ real-world FIT files)
  • Support for both little-endian and big-endian architectures
  • Header parsing (12 and 14-byte headers with proper CRC handling)
  • Message definitions and data records
  • Developer field support
  • Profile definitions for common message types
  • Timestamp conversion utilities
  • Semicircles to degrees conversion
  • Zero dependencies (uses only Go standard library)

Installation

go get git.blackfinn.de/go/fit-parser/fitparser

Or clone and use locally:

git clone https://git.blackfinn.de/go/fit-parser.git
cd fitparser
go test ./fitparser

Quick Start

package main

import (
	"fmt"
	"log"
	"os"

	"git.blackfinn.de/go/fit-parser/fitparser"
)

func main() {
	// Read FIT file
	data, err := os.ReadFile("activity.fit")
	if err != nil {
		log.Fatal(err)
	}

	// Create decoder
	decoder, err := fitparser.NewDecoder(data)
	if err != nil {
		log.Fatal(err)
	}

	// Decode messages
	messages, err := decoder.Decode()
	if err != nil {
		log.Fatal(err)
	}

	// Process messages
	fmt.Printf("Decoded %d messages\n", len(messages))

	for _, msg := range messages {
		switch msg.Num {
		case fitparser.MesgNumFileId:
			if fileType, ok := msg.GetFieldValueUint8(fitparser.FieldFileIdType); ok {
				fmt.Printf("File Type: %s\n", fitparser.GetFileTypeName(fileType))
			}

		case fitparser.MesgNumRecord:
			// Get timestamp
			if timestamp, ok := msg.GetFieldValueUint32(fitparser.FieldRecordTimestamp); ok {
				t := fitparser.ConvertFITTimestamp(timestamp)
				fmt.Printf("Record at %s\n", t.Format("2006-01-02 15:04:05"))
			}

			// Get heart rate
			if hr, ok := msg.GetFieldValueUint8(fitparser.FieldRecordHeartRate); ok && hr != fitparser.Uint8Invalid {
				fmt.Printf("  Heart Rate: %d bpm\n", hr)
			}

			// Get position (if available)
			if lat, ok := msg.Fields[fitparser.FieldRecordPositionLat]; ok {
				if latInt, ok := lat.(int32); ok && latInt != fitparser.Sint32Invalid {
					latDeg := fitparser.ConvertSemicirclesToDegrees(latInt)
					if lon, ok := msg.Fields[fitparser.FieldRecordPositionLong]; ok {
						if lonInt, ok := lon.(int32); ok && lonInt != fitparser.Sint32Invalid {
							lonDeg := fitparser.ConvertSemicirclesToDegrees(lonInt)
							fmt.Printf("  Position: %.6f, %.6f\n", latDeg, lonDeg)
						}
					}
				}
			}
		}
	}
}

API Documentation

Creating a Decoder

// From byte slice
data, _ := os.ReadFile("activity.fit")
decoder, err := fitparser.NewDecoder(data)

// Enable/disable CRC checking (enabled by default)
decoder.EnableCRCCheck(false)

Decoding Files

// Decode all messages
messages, err := decoder.Decode()

// Check file integrity only
err := fitparser.CheckIntegrity(data)

// Check if data is a FIT file
if fitparser.IsFIT(data) {
    // Process FIT file
}

Working with Messages

for _, msg := range messages {
    // Get message type name
    name := fitparser.GetMessageName(msg.Num)

    // Access fields by field number
    if value, ok := msg.GetFieldValue(fieldNum); ok {
        // Process value
    }

    // Type-safe field accessors
    uint8Val, ok := msg.GetFieldValueUint8(fieldNum)
    uint16Val, ok := msg.GetFieldValueUint16(fieldNum)
    uint32Val, ok := msg.GetFieldValueUint32(fieldNum)
    stringVal, ok := msg.GetFieldValueString(fieldNum)

    // Direct field access
    if value, ok := msg.Fields[fieldNum]; ok {
        // Type assert as needed
        switch v := value.(type) {
        case uint8:
            // Handle uint8
        case uint32:
            // Handle uint32
        case []uint8:
            // Handle byte array
        case string:
            // Handle string
        }
    }
}

Utility Functions

// Convert FIT timestamp to Go time.Time
timestamp := uint32(12345678)
t := fitparser.ConvertFITTimestamp(timestamp)

// Convert semicircles to degrees (for GPS coordinates)
latSemicircles := int32(464800000)
latDegrees := fitparser.ConvertSemicirclesToDegrees(latSemicircles)  // ~19.47 degrees

// Get human-readable names
fileTypeName := fitparser.GetFileTypeName(fitparser.FileTypeActivity)  // "Activity"
sportName := fitparser.GetSportName(fitparser.SportRunning)           // "Running"
messageName := fitparser.GetMessageName(fitparser.MesgNumRecord)       // "Record"

Common Message Types

The parser includes constants for common FIT message types:

  • MesgNumFileId - File identification
  • MesgNumRecord - Activity data points (GPS, heart rate, power, etc.)
  • MesgNumLap - Lap summaries
  • MesgNumSession - Session summaries
  • MesgNumActivity - Activity summaries
  • MesgNumEvent - Events (start, stop, lap button, etc.)
  • MesgNumDeviceInfo - Device information
  • MesgNumUserProfile - User profile data

Field Numbers

Common field numbers are defined as constants:

FileId Message

  • FieldFileIdType - File type
  • FieldFileIdManufacturer - Manufacturer ID
  • FieldFileIdProduct - Product ID
  • FieldFileIdSerialNumber - Device serial number
  • FieldFileIdTimeCreated - File creation timestamp

Record Message

  • FieldRecordTimestamp - Record timestamp
  • FieldRecordPositionLat - Latitude (semicircles)
  • FieldRecordPositionLong - Longitude (semicircles)
  • FieldRecordAltitude - Altitude
  • FieldRecordHeartRate - Heart rate (bpm)
  • FieldRecordCadence - Cadence (rpm)
  • FieldRecordDistance - Distance (meters)
  • FieldRecordSpeed - Speed (m/s)
  • FieldRecordPower - Power (watts)
  • FieldRecordTemperature - Temperature (°C)

Example: Processing an Activity File

package main

import (
	"fmt"
	"log"
	"os"

	"git.blackfinn.de/go/fit-parser/fitparser"
)

func main() {
	data, err := os.ReadFile("activity.fit")
	if err != nil {
		log.Fatal(err)
	}

	decoder, err := fitparser.NewDecoder(data)
	if err != nil {
		log.Fatal(err)
	}

	messages, err := decoder.Decode()
	if err != nil {
		log.Fatal(err)
	}

	// Count message types
	msgCounts := make(map[uint16]int)
	for _, msg := range messages {
		msgCounts[msg.Num]++
	}

	fmt.Println("Message Statistics:")
	for mesgNum, count := range msgCounts {
		fmt.Printf("  %s: %d\n", fitparser.GetMessageName(mesgNum), count)
	}

	// Process session data
	for _, msg := range messages {
		if msg.Num == fitparser.MesgNumSession {
			if sport, ok := msg.GetFieldValueUint8(fitparser.FieldSessionSport); ok {
				fmt.Printf("\nSport: %s\n", fitparser.GetSportName(sport))
			}

			if totalTime, ok := msg.GetFieldValueUint32(fitparser.FieldSessionTotalTimerTime); ok {
				fmt.Printf("Total Time: %.2f minutes\n", float64(totalTime)/1000.0/60.0)
			}

			if totalDist, ok := msg.GetFieldValueUint32(fitparser.FieldSessionTotalDistance); ok {
				fmt.Printf("Total Distance: %.2f km\n", float64(totalDist)/100000.0)
			}

			if avgHR, ok := msg.GetFieldValueUint8(fitparser.FieldSessionAvgHeartRate); ok {
				fmt.Printf("Average Heart Rate: %d bpm\n", avgHR)
			}

			if maxHR, ok := msg.GetFieldValueUint8(fitparser.FieldSessionMaxHeartRate); ok {
				fmt.Printf("Max Heart Rate: %d bpm\n", maxHR)
			}
		}
	}
}

Performance

The parser is designed for efficiency:

  • Decodes activity files with thousands of records in milliseconds
  • Zero-allocation field decoding where possible
  • Optional CRC checking can be disabled for faster parsing
  • Memory-efficient streaming approach

Testing

Run the test suite:

cd fitparser
go test -v

Run benchmarks:

cd fitparser
go test -bench=.

Limitations

  • Write/encode functionality not yet implemented (decode only)
  • Some advanced features like subfields and components are not fully implemented
  • Developer fields are decoded but not interpreted

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

This implementation is based on the official Garmin FIT SDK. Please refer to the FIT Protocol License for usage terms.

References

Acknowledgments

This Go implementation was created by studying the C, C++, and Python implementations from the official Garmin FIT SDK.

Description
No description provided
Readme MIT 208 KiB
Languages
Go 100%