# 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 ```bash go get git.blackfinn.de/go/fit-parser/fitparser ``` Or clone and use locally: ```bash git clone https://git.blackfinn.de/go/fit-parser.git cd fitparser go test ./fitparser ``` ## Quick Start ```go 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 ```go // 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 ```go // 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 ```go 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 ```go // 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 ```go 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: ```bash cd fitparser go test -v ``` Run benchmarks: ```bash 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 - [Official FIT SDK](https://developer.garmin.com/fit/) - [FIT Protocol Documentation](https://developer.garmin.com/fit/protocol/) - [FIT File Format Specification](https://developer.garmin.com/fit/file-types/) ## Acknowledgments This Go implementation was created by studying the C, C++, and Python implementations from the official Garmin FIT SDK.