334
README.md
Normal file
334
README.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user