283 lines
9.1 KiB
Go
283 lines
9.1 KiB
Go
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))
|
||
}
|
||
}
|
||
}
|
||
}
|