335 lines
8.8 KiB
Markdown
335 lines
8.8 KiB
Markdown
# 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.
|