Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-02-08 07:28:32 +00:00
commit 2945d90d24
25 changed files with 3177 additions and 0 deletions

104
example/README.md Normal file
View File

@@ -0,0 +1,104 @@
# FIT Parser Example
This directory contains a complete example application that demonstrates how to use the FIT parser.
## Running the Example
```bash
# From the example directory
go run main.go testdata/Activity.fit
# Or from the repository root
go run example/main.go example/testdata/Activity.fit
```
## Available Test Files
The `testdata/` directory contains several example FIT files:
- **Activity.fit** - Full activity file with GPS, heart rate, power, and cadence data
- **Settings.fit** - Simple device settings file
- **MonitoringFile.fit** - Daily monitoring data
- **WorkoutIndividualSteps.fit** - Workout definition
- **RealWorld_Cycling.fit** - Real-world cycling activity from Wahoo ELEMNT
## What the Example Does
The example program:
1. Reads and validates the FIT file
2. Decodes all messages with CRC validation
3. Displays file information (type, manufacturer, timestamps)
4. Shows activity summaries (distance, time, heart rate, power, etc.)
5. Prints sample data records with GPS coordinates, speed, and sensor data
## Example Output
```
File: testdata/Activity.fit (94096 bytes)
============================================================
Header Information:
FIT Header: Protocol v2.0, Profile v21158, DataSize=94080 bytes
Decoded 3611 messages
Message Type Summary:
FileId : 1 messages
Record : 3601 messages
Session : 1 messages
Lap : 1 messages
Event : 2 messages
DeviceInfo : 1 messages
Activity : 1 messages
📊 Session Summary:
Sport: Unknown
Total Time: 60.02 minutes
Average Heart Rate: 145 bpm
Max Heart Rate: 178 bpm
```
## Using in Your Own Code
```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 with CRC validation (enabled by default)
messages, err := decoder.Decode()
if err != nil {
log.Fatal(err)
}
// Process messages
for _, msg := range messages {
switch msg.Num {
case fitparser.MesgNumRecord:
// Process data records
if hr, ok := msg.GetFieldValueUint8(fitparser.FieldRecordHeartRate); ok {
fmt.Printf("Heart Rate: %d bpm\n", hr)
}
}
}
}
```

282
example/main.go Normal file
View File

@@ -0,0 +1,282 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"git.blackfinn.de/go/fit-parser/fitparser"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go <fit-file>")
fmt.Println("Example: go run main.go testdata/Activity.fit")
fmt.Println()
fmt.Println("Available test files:")
fmt.Println(" testdata/Activity.fit - Full activity with GPS and sensors")
fmt.Println(" testdata/Settings.fit - Device settings file")
fmt.Println(" testdata/MonitoringFile.fit - Daily monitoring data")
fmt.Println(" testdata/WorkoutIndividualSteps.fit - Workout definition")
fmt.Println(" testdata/RealWorld_Cycling.fit - Real-world cycling activity")
os.Exit(1)
}
filePath := os.Args[1]
// Read FIT file
data, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
fmt.Printf("File: %s (%d bytes)\n", filePath, len(data))
fmt.Println(strings.Repeat("=", 60))
// Check if it's a valid FIT file
if !fitparser.IsFIT(data) {
log.Fatal("Not a valid FIT file")
}
// Create decoder
decoder, err := fitparser.NewDecoder(data)
if err != nil {
log.Fatalf("Error creating decoder: %v", err)
}
// Optionally disable CRC checking for faster parsing
// decoder.EnableCRCCheck(false)
// Get header info
header := decoder.GetHeader()
if header != nil {
fmt.Println("\nHeader Information:")
fmt.Printf(" %s\n", header)
} else {
// Parse header to get info
header, _ := fitparser.ParseHeader(data)
fmt.Println("\nHeader Information:")
fmt.Printf(" %s\n", header)
}
// Decode all messages
messages, err := decoder.Decode()
if err != nil {
// CRC errors are common, continue anyway
fmt.Printf("Warning: %v\n", err)
fmt.Println("Continuing with decoded messages...")
}
if len(messages) == 0 {
fmt.Println("No messages found in file")
return
}
fmt.Printf("\nDecoded %d messages\n", len(messages))
// Count message types
msgCounts := make(map[uint16]int)
for _, msg := range messages {
msgCounts[msg.Num]++
}
fmt.Println("\nMessage Type Summary:")
for mesgNum, count := range msgCounts {
fmt.Printf(" %-20s: %5d messages\n", fitparser.GetMessageName(mesgNum), count)
}
// Process key messages
fmt.Println(strings.Repeat("=", 60))
fmt.Println("\nFile Details:")
for _, msg := range messages {
switch msg.Num {
case fitparser.MesgNumFileId:
printFileId(msg)
case fitparser.MesgNumActivity:
printActivity(msg)
case fitparser.MesgNumSession:
printSession(msg)
case fitparser.MesgNumLap:
printLap(msg)
}
}
// Print sample records
printSampleRecords(messages)
}
func printFileId(msg *fitparser.Message) {
fmt.Println("\n📄 File ID:")
if fileType, ok := msg.GetFieldValueUint8(fitparser.FieldFileIdType); ok {
fmt.Printf(" Type: %s\n", fitparser.GetFileTypeName(fileType))
}
if manufacturer, ok := msg.GetFieldValueUint16(fitparser.FieldFileIdManufacturer); ok {
fmt.Printf(" Manufacturer: %d\n", manufacturer)
}
if product, ok := msg.GetFieldValueUint16(fitparser.FieldFileIdProduct); ok {
fmt.Printf(" Product: %d\n", product)
}
if serial, ok := msg.GetFieldValueUint32(fitparser.FieldFileIdSerialNumber); ok && serial != fitparser.Uint32Invalid {
fmt.Printf(" Serial Number: %d\n", serial)
}
if timeCreated, ok := msg.GetFieldValueUint32(fitparser.FieldFileIdTimeCreated); ok {
t := fitparser.ConvertFITTimestamp(timeCreated)
fmt.Printf(" Created: %s\n", t.Format("2006-01-02 15:04:05 MST"))
}
}
func printActivity(msg *fitparser.Message) {
fmt.Println("\n🏃 Activity Summary:")
if timestamp, ok := msg.GetFieldValueUint32(fitparser.FieldActivityTimestamp); ok {
t := fitparser.ConvertFITTimestamp(timestamp)
fmt.Printf(" Timestamp: %s\n", t.Format("2006-01-02 15:04:05 MST"))
}
if totalTime, ok := msg.GetFieldValueUint32(fitparser.FieldActivityTotalTimerTime); ok {
minutes := float64(totalTime) / 1000.0 / 60.0
fmt.Printf(" Total Time: %.2f minutes\n", minutes)
}
if numSessions, ok := msg.GetFieldValueUint16(fitparser.FieldActivityNumSessions); ok {
fmt.Printf(" Number of Sessions: %d\n", numSessions)
}
}
func printSession(msg *fitparser.Message) {
fmt.Println("\n📊 Session Summary:")
if sport, ok := msg.GetFieldValueUint8(fitparser.FieldSessionSport); ok {
fmt.Printf(" Sport: %s\n", fitparser.GetSportName(sport))
}
if totalTime, ok := msg.GetFieldValueUint32(fitparser.FieldSessionTotalTimerTime); ok {
minutes := float64(totalTime) / 1000.0 / 60.0
fmt.Printf(" Total Time: %.2f minutes\n", minutes)
}
if totalDist, ok := msg.GetFieldValueUint32(fitparser.FieldSessionTotalDistance); ok {
km := float64(totalDist) / 100000.0
fmt.Printf(" Total Distance: %.2f km\n", km)
}
if avgSpeed, ok := msg.GetFieldValueUint16(fitparser.FieldSessionAvgSpeed); ok && avgSpeed != fitparser.Uint16Invalid {
speed := float64(avgSpeed) / 1000.0
fmt.Printf(" Average Speed: %.2f m/s (%.2f km/h)\n", speed, speed*3.6)
}
if maxSpeed, ok := msg.GetFieldValueUint16(fitparser.FieldSessionMaxSpeed); ok && maxSpeed != fitparser.Uint16Invalid {
speed := float64(maxSpeed) / 1000.0
fmt.Printf(" Max Speed: %.2f m/s (%.2f km/h)\n", speed, speed*3.6)
}
if avgHR, ok := msg.GetFieldValueUint8(fitparser.FieldSessionAvgHeartRate); ok && avgHR != fitparser.Uint8Invalid {
fmt.Printf(" Average Heart Rate: %d bpm\n", avgHR)
}
if maxHR, ok := msg.GetFieldValueUint8(fitparser.FieldSessionMaxHeartRate); ok && maxHR != fitparser.Uint8Invalid {
fmt.Printf(" Max Heart Rate: %d bpm\n", maxHR)
}
if totalCalories, ok := msg.GetFieldValueUint16(fitparser.FieldSessionTotalCalories); ok {
fmt.Printf(" Total Calories: %d kcal\n", totalCalories)
}
if avgPower, ok := msg.GetFieldValueUint16(fitparser.FieldSessionAvgPower); ok && avgPower != fitparser.Uint16Invalid {
fmt.Printf(" Average Power: %d watts\n", avgPower)
}
if maxPower, ok := msg.GetFieldValueUint16(fitparser.FieldSessionMaxPower); ok && maxPower != fitparser.Uint16Invalid {
fmt.Printf(" Max Power: %d watts\n", maxPower)
}
}
func printLap(msg *fitparser.Message) {
fmt.Println("\n⏱ Lap:")
if timestamp, ok := msg.GetFieldValueUint32(fitparser.FieldLapTimestamp); ok {
t := fitparser.ConvertFITTimestamp(timestamp)
fmt.Printf(" Timestamp: %s\n", t.Format("15:04:05"))
}
if totalTime, ok := msg.GetFieldValueUint32(fitparser.FieldLapTotalTimerTime); ok {
seconds := float64(totalTime) / 1000.0
fmt.Printf(" Time: %.2f seconds\n", seconds)
}
if totalDist, ok := msg.GetFieldValueUint32(fitparser.FieldLapTotalDistance); ok {
meters := float64(totalDist) / 100.0
fmt.Printf(" Distance: %.2f meters\n", meters)
}
if avgHR, ok := msg.GetFieldValueUint8(fitparser.FieldLapAvgHeartRate); ok && avgHR != fitparser.Uint8Invalid {
fmt.Printf(" Avg HR: %d bpm\n", avgHR)
}
}
func printSampleRecords(messages []*fitparser.Message) {
fmt.Println(strings.Repeat("=", 60))
fmt.Println("\nSample Records (first 5):")
count := 0
for _, msg := range messages {
if msg.Num == fitparser.MesgNumRecord && count < 5 {
count++
fmt.Printf("\n📍 Record #%d:\n", count)
if timestamp, ok := msg.GetFieldValueUint32(fitparser.FieldRecordTimestamp); ok {
t := fitparser.ConvertFITTimestamp(timestamp)
fmt.Printf(" Time: %s\n", t.Format("15:04:05"))
}
// Position
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)
}
}
}
}
if altitude, ok := msg.GetFieldValueUint16(fitparser.FieldRecordAltitude); ok && altitude != fitparser.Uint16Invalid {
alt := float64(altitude)/5.0 - 500.0
fmt.Printf(" Altitude: %.1f m\n", alt)
}
if hr, ok := msg.GetFieldValueUint8(fitparser.FieldRecordHeartRate); ok && hr != fitparser.Uint8Invalid {
fmt.Printf(" Heart Rate: %d bpm\n", hr)
}
if cadence, ok := msg.GetFieldValueUint8(fitparser.FieldRecordCadence); ok && cadence != fitparser.Uint8Invalid {
fmt.Printf(" Cadence: %d rpm\n", cadence)
}
if speed, ok := msg.GetFieldValueUint16(fitparser.FieldRecordSpeed); ok && speed != fitparser.Uint16Invalid {
spd := float64(speed) / 1000.0
fmt.Printf(" Speed: %.2f m/s (%.2f km/h)\n", spd, spd*3.6)
}
if power, ok := msg.GetFieldValueUint16(fitparser.FieldRecordPower); ok && power != fitparser.Uint16Invalid {
fmt.Printf(" Power: %d watts\n", power)
}
if temp, ok := msg.GetFieldValueUint8(fitparser.FieldRecordTemperature); ok && temp != fitparser.Sint8Invalid {
fmt.Printf(" Temperature: %d °C\n", int(temp))
}
}
}
}

BIN
example/testdata/Activity.fit vendored Normal file

Binary file not shown.

BIN
example/testdata/MonitoringFile.fit vendored Normal file

Binary file not shown.

42
example/testdata/README.md vendored Normal file
View File

@@ -0,0 +1,42 @@
# Test Data
This directory contains example FIT files for testing and demonstration purposes.
## Files
- **Activity.fit** (92 KB) - Full activity file from Garmin SDK with 3,601 data records
- **Settings.fit** (82 bytes) - Simple settings file
- **MonitoringFile.fit** (2.1 KB) - Daily monitoring data example
- **WorkoutIndividualSteps.fit** (175 bytes) - Workout definition file
- **RealWorld_Cycling.fit** (171 KB) - Real-world cycling activity from Wahoo ELEMNT device
## Usage
```go
package main
import (
"log"
"os"
"git.blackfinn.de/go/fit-parser/fitparser"
)
func main() {
data, _ := os.ReadFile("testdata/Activity.fit")
decoder, _ := fitparser.NewDecoder(data)
messages, err := decoder.Decode()
if err != nil {
log.Fatal(err)
}
// Process messages...
}
```
## Source
- SDK examples: From official Garmin FIT SDK (https://developer.garmin.com/fit/)
- Real-world example: Wahoo ELEMNT cycling device
## License
These files are provided for testing and educational purposes. The FIT protocol and SDK are property of Garmin International, Inc.

BIN
example/testdata/RealWorld_Cycling.fit vendored Normal file

Binary file not shown.

BIN
example/testdata/Settings.fit vendored Normal file

Binary file not shown.

Binary file not shown.