feat: extend equipment and workout models with service tracking
This commit is contained in:
94
internal/activity/fit_parser.go
Normal file
94
internal/activity/fit_parser.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package activity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/muktihari/fit/decoder"
|
||||
"github.com/muktihari/fit/profile/basetype"
|
||||
"github.com/muktihari/fit/profile/mesgdef"
|
||||
"github.com/muktihari/fit/profile/typedef"
|
||||
)
|
||||
|
||||
// ParseFIT decodes a FIT activity file and extracts session-level metrics.
|
||||
func ParseFIT(data []byte) (*ParsedActivity, error) {
|
||||
dec := decoder.New(bytes.NewReader(data))
|
||||
|
||||
if !dec.Next() {
|
||||
return nil, fmt.Errorf("empty or invalid FIT file")
|
||||
}
|
||||
|
||||
fit, err := dec.Decode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode FIT file: %w", err)
|
||||
}
|
||||
|
||||
result := &ParsedActivity{}
|
||||
foundSession := false
|
||||
|
||||
for i := range fit.Messages {
|
||||
if fit.Messages[i].Num != typedef.MesgNumSession {
|
||||
continue
|
||||
}
|
||||
|
||||
foundSession = true
|
||||
session := mesgdef.NewSession(&fit.Messages[i])
|
||||
|
||||
// total_elapsed_time: FIT stores as uint32 with scale 1000 (ms -> seconds)
|
||||
if session.TotalElapsedTime != basetype.Uint32Invalid {
|
||||
result.Duration = int(session.TotalElapsedTime / 1000)
|
||||
}
|
||||
|
||||
// total_distance: FIT stores as uint32 with scale 100 (centimeters -> meters)
|
||||
// Convert to km for our model
|
||||
if session.TotalDistance != basetype.Uint32Invalid {
|
||||
result.Distance = float64(session.TotalDistance) / 100.0 / 1000.0
|
||||
}
|
||||
|
||||
// Power (no scale)
|
||||
if session.AvgPower != basetype.Uint16Invalid {
|
||||
result.AvgPower = int(session.AvgPower)
|
||||
}
|
||||
if session.MaxPower != basetype.Uint16Invalid {
|
||||
result.MaxPower = int(session.MaxPower)
|
||||
}
|
||||
|
||||
// Heart rate (no scale)
|
||||
if session.AvgHeartRate != basetype.Uint8Invalid {
|
||||
result.AvgHR = int(session.AvgHeartRate)
|
||||
}
|
||||
if session.MaxHeartRate != basetype.Uint8Invalid {
|
||||
result.MaxHR = int(session.MaxHeartRate)
|
||||
}
|
||||
|
||||
// Calories (no scale)
|
||||
if session.TotalCalories != basetype.Uint16Invalid {
|
||||
result.CaloriesBurned = int(session.TotalCalories)
|
||||
}
|
||||
|
||||
// Elevation gain (no scale, meters)
|
||||
if session.TotalAscent != basetype.Uint16Invalid {
|
||||
result.ElevGain = int(session.TotalAscent)
|
||||
}
|
||||
|
||||
// Cadence (no scale)
|
||||
if session.AvgCadence != basetype.Uint8Invalid {
|
||||
result.AvgCadence = int(session.AvgCadence)
|
||||
}
|
||||
|
||||
// Start time
|
||||
if session.StartTime.IsZero() {
|
||||
result.StartTime = session.Timestamp
|
||||
} else {
|
||||
result.StartTime = session.StartTime
|
||||
}
|
||||
|
||||
break // use first session
|
||||
}
|
||||
|
||||
if !foundSession {
|
||||
return nil, fmt.Errorf("no session data found in FIT file")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user