Signed-off-by: Thomas Klaehn <tkl@blackfinn.de>
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 identificationMesgNumRecord- Activity data points (GPS, heart rate, power, etc.)MesgNumLap- Lap summariesMesgNumSession- Session summariesMesgNumActivity- Activity summariesMesgNumEvent- Events (start, stop, lap button, etc.)MesgNumDeviceInfo- Device informationMesgNumUserProfile- User profile data
Field Numbers
Common field numbers are defined as constants:
FileId Message
FieldFileIdType- File typeFieldFileIdManufacturer- Manufacturer IDFieldFileIdProduct- Product IDFieldFileIdSerialNumber- Device serial numberFieldFileIdTimeCreated- File creation timestamp
Record Message
FieldRecordTimestamp- Record timestampFieldRecordPositionLat- Latitude (semicircles)FieldRecordPositionLong- Longitude (semicircles)FieldRecordAltitude- AltitudeFieldRecordHeartRate- 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.
Languages
Go
100%