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

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.