145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package export
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/muktihari/fit/encoder"
|
|
"github.com/muktihari/fit/profile/filedef"
|
|
"github.com/muktihari/fit/profile/mesgdef"
|
|
"github.com/muktihari/fit/profile/typedef"
|
|
|
|
"rideaware/internal/workout"
|
|
)
|
|
|
|
// EncodeFITWorkout generates a FIT workout file from a Workout's structured segments.
|
|
// userFTP is the user's Functional Threshold Power in watts, used to convert
|
|
// %FTP power targets into absolute watt values for the device.
|
|
func EncodeFITWorkout(w *workout.Workout, userFTP int) ([]byte, error) {
|
|
if len(w.WorkoutData.Segments) == 0 {
|
|
return nil, fmt.Errorf("workout has no segments")
|
|
}
|
|
if userFTP <= 0 {
|
|
return nil, fmt.Errorf("user FTP must be greater than 0")
|
|
}
|
|
|
|
wktFile := filedef.NewWorkout()
|
|
|
|
wktFile.FileId.
|
|
SetType(typedef.FileWorkout).
|
|
SetManufacturer(typedef.ManufacturerDevelopment).
|
|
SetProduct(1).
|
|
SetTimeCreated(time.Now())
|
|
|
|
name := w.WorkoutData.Name
|
|
if name == "" {
|
|
name = w.Title
|
|
}
|
|
|
|
wktFile.Workout = mesgdef.NewWorkout(nil).
|
|
SetWktName(name).
|
|
SetSport(typedef.SportCycling).
|
|
SetNumValidSteps(uint16(len(w.WorkoutData.Segments)))
|
|
|
|
steps := make([]*mesgdef.WorkoutStep, 0, len(w.WorkoutData.Segments))
|
|
for i, seg := range w.WorkoutData.Segments {
|
|
step := segmentToFITStep(seg, uint16(i), userFTP)
|
|
steps = append(steps, step)
|
|
}
|
|
wktFile.WorkoutSteps = steps
|
|
|
|
fit := wktFile.ToFIT(nil)
|
|
|
|
var buf bytes.Buffer
|
|
enc := encoder.New(&buf)
|
|
if err := enc.Encode(&fit); err != nil {
|
|
return nil, fmt.Errorf("failed to encode FIT workout: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func segmentToFITStep(seg workout.WorkoutSegment, index uint16, userFTP int) *mesgdef.WorkoutStep {
|
|
step := mesgdef.NewWorkoutStep(nil).
|
|
SetMessageIndex(typedef.MessageIndex(index)).
|
|
SetDurationType(typedef.WktStepDurationTime).
|
|
SetDurationValue(uint32(seg.Duration) * 1000) // seconds to milliseconds
|
|
|
|
switch seg.Type {
|
|
case "warmup":
|
|
step.SetIntensity(typedef.IntensityWarmup)
|
|
setPowerTarget(step, seg, userFTP)
|
|
step.SetWktStepName("Warm Up")
|
|
|
|
case "steadystate":
|
|
step.SetIntensity(typedef.IntensityActive)
|
|
setPowerTargetSteady(step, seg, userFTP)
|
|
step.SetWktStepName("Steady State")
|
|
|
|
case "interval":
|
|
step.SetIntensity(typedef.IntensityInterval)
|
|
setPowerTarget(step, seg, userFTP)
|
|
step.SetWktStepName("Interval")
|
|
|
|
case "cooldown":
|
|
step.SetIntensity(typedef.IntensityCooldown)
|
|
setPowerTarget(step, seg, userFTP)
|
|
step.SetWktStepName("Cool Down")
|
|
|
|
case "ramp":
|
|
step.SetIntensity(typedef.IntensityActive)
|
|
setPowerTarget(step, seg, userFTP)
|
|
step.SetWktStepName("Ramp")
|
|
|
|
case "freeride":
|
|
step.SetIntensity(typedef.IntensityActive)
|
|
step.SetTargetType(typedef.WktStepTargetOpen)
|
|
step.SetWktStepName("Free Ride")
|
|
|
|
default:
|
|
step.SetIntensity(typedef.IntensityActive)
|
|
step.SetTargetType(typedef.WktStepTargetOpen)
|
|
}
|
|
|
|
return step
|
|
}
|
|
|
|
// setPowerTarget sets a power range target using PowerLow/PowerHigh.
|
|
// Values are converted from %FTP fractions to absolute watts with +1000 offset.
|
|
func setPowerTarget(step *mesgdef.WorkoutStep, seg workout.WorkoutSegment, userFTP int) {
|
|
if seg.PowerLow == 0 && seg.PowerHigh == 0 && seg.Power == 0 {
|
|
step.SetTargetType(typedef.WktStepTargetOpen)
|
|
return
|
|
}
|
|
|
|
step.SetTargetType(typedef.WktStepTargetPower)
|
|
step.SetTargetValue(0) // 0 = custom range
|
|
|
|
if seg.PowerLow != 0 || seg.PowerHigh != 0 {
|
|
low := uint32(float64(userFTP)*seg.PowerLow) + 1000
|
|
high := uint32(float64(userFTP)*seg.PowerHigh) + 1000
|
|
step.SetCustomTargetValueLow(low)
|
|
step.SetCustomTargetValueHigh(high)
|
|
} else if seg.Power != 0 {
|
|
watts := uint32(float64(userFTP) * seg.Power)
|
|
step.SetCustomTargetValueLow(watts - 10 + 1000)
|
|
step.SetCustomTargetValueHigh(watts + 10 + 1000)
|
|
}
|
|
}
|
|
|
|
// setPowerTargetSteady sets a power target for steady state segments using the Power field.
|
|
func setPowerTargetSteady(step *mesgdef.WorkoutStep, seg workout.WorkoutSegment, userFTP int) {
|
|
if seg.Power == 0 {
|
|
step.SetTargetType(typedef.WktStepTargetOpen)
|
|
return
|
|
}
|
|
|
|
step.SetTargetType(typedef.WktStepTargetPower)
|
|
step.SetTargetValue(0)
|
|
|
|
watts := uint32(float64(userFTP) * seg.Power)
|
|
step.SetCustomTargetValueLow(watts - 10 + 1000)
|
|
step.SetCustomTargetValueHigh(watts + 10 + 1000)
|
|
}
|