104
example/README.md
Normal file
104
example/README.md
Normal 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
282
example/main.go
Normal 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
BIN
example/testdata/Activity.fit
vendored
Normal file
Binary file not shown.
BIN
example/testdata/MonitoringFile.fit
vendored
Normal file
BIN
example/testdata/MonitoringFile.fit
vendored
Normal file
Binary file not shown.
42
example/testdata/README.md
vendored
Normal file
42
example/testdata/README.md
vendored
Normal 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
BIN
example/testdata/RealWorld_Cycling.fit
vendored
Normal file
Binary file not shown.
BIN
example/testdata/Settings.fit
vendored
Normal file
BIN
example/testdata/Settings.fit
vendored
Normal file
Binary file not shown.
BIN
example/testdata/WorkoutIndividualSteps.fit
vendored
Normal file
BIN
example/testdata/WorkoutIndividualSteps.fit
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user