Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FIT File Types #88

Merged
merged 2 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ If you are uncertain if it's a chained fit file. Create a loop and use dec.Next(

### Decode to Common File Types

Decode to Common File Types enables us to interact with FIT files through common file types such as Activity Files, Course Files, Workout Files, and more, which group protocol messages based on specific purposes.

_Note: Currently only 3 common file types are defined: Activity, Course & Workout, but you can create your own file types using our building block and register it in the listener as an option using WithFileSets()_
Decode to Common File Types enables us to interact with FIT files through common file types such as Activity Files, Course Files, Workout Files, and [more](../profile/filedef/doc.go), which group protocol messages based on specific purposes.

1. To get started, the simpliest way to create an common file type is to decode the FIT file in its raw protocol messages then pass the messages to create the desired common file type.

Expand Down Expand Up @@ -316,7 +314,7 @@ func main() {
}

// After invoking CheckIntegrity and users want to reuse `dec` to Decode the FIT file,
// `f` should be reset since `f` has been fully read. The following method with do:
// `f` should be reset since `f` has been fully read. The following method will do:
_, err = f.Seek(0, io.SeekStart)
if err != nil {
panic(err)
Expand Down
16 changes: 8 additions & 8 deletions profile/filedef/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,29 @@ import (
//
// ref: https://developer.garmin.com/fit/file-types/activity/
type Activity struct {
FileId mesgdef.FileId // must have mesg
FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number, time_created

// Developer Data Lookup
DeveloperDataIds []*mesgdef.DeveloperDataId
FieldDescriptions []*mesgdef.FieldDescription

// Required Messages
Activity *mesgdef.Activity
Sessions []*mesgdef.Session
Laps []*mesgdef.Lap
Records []*mesgdef.Record
Activity *mesgdef.Activity // required fields: timestamp, num_sessions, type, event, event_type
Sessions []*mesgdef.Session // required fields: timestamp, start_time, total_elapsed_time, sport, event, event_type
Laps []*mesgdef.Lap // required fields: timestamp, event, event_type
Records []*mesgdef.Record // required fields: timestamp

// Optional Messages
UserProfile *mesgdef.UserProfile
DeviceInfos []*mesgdef.DeviceInfo
DeviceInfos []*mesgdef.DeviceInfo // required fields: timestamp
Events []*mesgdef.Event
Lengths []*mesgdef.Length
Lengths []*mesgdef.Length // required fields: timestamp, event, event_type
SegmentLap []*mesgdef.SegmentLap
ZonesTargets []*mesgdef.ZonesTarget
Workouts []*mesgdef.Workout
WorkoutSteps []*mesgdef.WorkoutStep
HRs []*mesgdef.Hr
HRVs []*mesgdef.Hrv
HRVs []*mesgdef.Hrv // required fields: time

// Messages not related to Activity
UnrelatedMessages []proto.Message
Expand Down
88 changes: 88 additions & 0 deletions profile/filedef/activity_summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package filedef

import (
"github.com/muktihari/fit/factory"
"github.com/muktihari/fit/profile/mesgdef"
"github.com/muktihari/fit/profile/untyped/mesgnum"
"github.com/muktihari/fit/proto"
)

// ActivitySummary is a compact version of the activity file and contain only activity, session and lap messages
type ActivitySummary struct {
FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number, time_created

// Developer Data Lookup
DeveloperDataIds []*mesgdef.DeveloperDataId
FieldDescriptions []*mesgdef.FieldDescription

Activity *mesgdef.Activity
Sessions []*mesgdef.Session
Laps []*mesgdef.Lap

// Messages not related to Activity
UnrelatedMessages []proto.Message
}

var _ File = &ActivitySummary{}

func NewActivitySummary(mesgs ...proto.Message) *ActivitySummary {
f := &ActivitySummary{}
for i := range mesgs {
f.Add(mesgs[i])
}

return f
}

func (f *ActivitySummary) Add(mesg proto.Message) {
switch mesg.Num {
case mesgnum.FileId:
f.FileId = *mesgdef.NewFileId(&mesg)
case mesgnum.DeveloperDataId:
f.DeveloperDataIds = append(f.DeveloperDataIds, mesgdef.NewDeveloperDataId(&mesg))
case mesgnum.FieldDescription:
f.FieldDescriptions = append(f.FieldDescriptions, mesgdef.NewFieldDescription(&mesg))
case mesgnum.Activity:
f.Activity = mesgdef.NewActivity(&mesg)
case mesgnum.Session:
f.Sessions = append(f.Sessions, mesgdef.NewSession(&mesg))
case mesgnum.Lap:
f.Laps = append(f.Laps, mesgdef.NewLap(&mesg))
default:
f.UnrelatedMessages = append(f.UnrelatedMessages, mesg)
}
}

func (f *ActivitySummary) ToFit(fac mesgdef.Factory) proto.Fit {
if fac == nil {
fac = factory.StandardFactory()
}

var size = 2 // non slice fields

size += len(f.Sessions) + len(f.Laps) + len(f.DeveloperDataIds) +
len(f.FieldDescriptions) + len(f.UnrelatedMessages)

fit := proto.Fit{
Messages: make([]proto.Message, 0, size),
}

// Should be as ordered: FieldId, DeveloperDataId and FieldDescription
fit.Messages = append(fit.Messages, f.FileId.ToMesg(fac))

ToMesgs(&fit.Messages, fac, mesgnum.DeveloperDataId, f.DeveloperDataIds)
ToMesgs(&fit.Messages, fac, mesgnum.FieldDescription, f.FieldDescriptions)

if f.Activity != nil {
fit.Messages = append(fit.Messages, f.Activity.ToMesg(fac))
}

ToMesgs(&fit.Messages, fac, mesgnum.Session, f.Sessions)
ToMesgs(&fit.Messages, fac, mesgnum.Lap, f.Laps)

fit.Messages = append(fit.Messages, f.UnrelatedMessages...)

SortMessagesByTimestamp(fit.Messages)

return fit
}
72 changes: 72 additions & 0 deletions profile/filedef/activity_summary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2023 The Fit SDK for Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package filedef_test

import (
"fmt"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/muktihari/fit/factory"
"github.com/muktihari/fit/kit/datetime"
"github.com/muktihari/fit/profile/filedef"
"github.com/muktihari/fit/profile/typedef"
"github.com/muktihari/fit/profile/untyped/fieldnum"
"github.com/muktihari/fit/profile/untyped/mesgnum"
"github.com/muktihari/fit/proto"
)

func newActivitySummaryMessageForTest(now time.Time) []proto.Message {
return []proto.Message{
factory.CreateMesgOnly(mesgnum.FileId).WithFields(
factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileActivitySummary)),
factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)),
),
factory.CreateMesgOnly(mesgnum.DeveloperDataId).WithFields(
factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)),
),
factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields(
factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)),
),
factory.CreateMesgOnly(mesgnum.Lap).WithFields(
factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(datetime.ToUint32(now)), // intentionally using same timestamp as last messag)e
),
factory.CreateMesgOnly(mesgnum.Session).WithFields(
factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))),
),
factory.CreateMesgOnly(mesgnum.Activity).WithFields(
factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))),
),
// Unrelated messages
factory.CreateMesgOnly(mesgnum.BarometerData).WithFields(
factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))),
),
factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields(
factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))),
),
}
}

func TestActivitySummaryCorrectness(t *testing.T) {
mesgs := newActivitySummaryMessageForTest(time.Now())

activitySummary := filedef.NewActivitySummary(mesgs...)
if activitySummary.FileId.Type != typedef.FileActivitySummary {
t.Fatalf("expected: %v, got: %v", typedef.FileActivitySummary, activitySummary.FileId.Type)
}

fit := activitySummary.ToFit(nil) // use standard factory

if diff := cmp.Diff(mesgs, fit.Messages, createFieldComparer()); diff != "" {
fmt.Println("messages order:")
for i := range fit.Messages {
mesg := fit.Messages[i]
fmt.Printf("%d: %s\n", mesg.Num, mesg.Num)
}
fmt.Println("")
t.Fatal(diff)
}
}
4 changes: 2 additions & 2 deletions profile/filedef/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func newActivityMessageForTest(now time.Time) []proto.Message {
factory.CreateMesgOnly(mesgnum.Activity).WithFields(
factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))),
),
// Unordered pptional Messages
// Unordered optional Messages
factory.CreateMesgOnly(mesgnum.Length).WithFields(
factory.CreateField(mesgnum.Length, fieldnum.LengthAvgSpeed).WithValue(uint16(1000)),
),
Expand Down Expand Up @@ -132,7 +132,7 @@ func TestActivityCorrectness(t *testing.T) {

activity := filedef.NewActivity(mesgs...)
if activity.FileId.Type != typedef.FileActivity {
t.Fatalf("expected: %#v, got: %#v", typedef.FileActivity, activity.FileId.Type)
t.Fatalf("expected: %v, got: %v", typedef.FileActivity, activity.FileId.Type)
}

fit := activity.ToFit(nil) // use standard factory
Expand Down
87 changes: 87 additions & 0 deletions profile/filedef/blood_pressure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package filedef

import (
"github.com/muktihari/fit/factory"
"github.com/muktihari/fit/profile/mesgdef"
"github.com/muktihari/fit/profile/untyped/mesgnum"
"github.com/muktihari/fit/proto"
)

// BloodPressure files contain time-stamped discrete measurement data of blood pressure.
type BloodPressure struct {
FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number

// Developer Data Lookup
DeveloperDataIds []*mesgdef.DeveloperDataId
FieldDescriptions []*mesgdef.FieldDescription

UserProfile *mesgdef.UserProfile
BloodPressures []*mesgdef.BloodPressure
DeviceInfos []*mesgdef.DeviceInfo

UnrelatedMessages []proto.Message
}

var _ File = &BloodPressure{}

func NewBloodPressure(mesgs ...proto.Message) *BloodPressure {
f := &BloodPressure{}
for i := range mesgs {
f.Add(mesgs[i])
}

return f
}

func (f *BloodPressure) Add(mesg proto.Message) {
switch mesg.Num {
case mesgnum.FileId:
f.FileId = *mesgdef.NewFileId(&mesg)
case mesgnum.DeveloperDataId:
f.DeveloperDataIds = append(f.DeveloperDataIds, mesgdef.NewDeveloperDataId(&mesg))
case mesgnum.FieldDescription:
f.FieldDescriptions = append(f.FieldDescriptions, mesgdef.NewFieldDescription(&mesg))
case mesgnum.UserProfile:
f.UserProfile = mesgdef.NewUserProfile(&mesg)
case mesgnum.BloodPressure:
f.BloodPressures = append(f.BloodPressures, mesgdef.NewBloodPressure(&mesg))
case mesgnum.DeviceInfo:
f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg))
default:
f.UnrelatedMessages = append(f.UnrelatedMessages, mesg)
}
}

func (f *BloodPressure) ToFit(fac mesgdef.Factory) proto.Fit {
if fac == nil {
fac = factory.StandardFactory()
}

var size = 2 // non slice fields

size += len(f.BloodPressures) + len(f.DeviceInfos) +
len(f.DeveloperDataIds) + len(f.FieldDescriptions) + len(f.UnrelatedMessages)

fit := proto.Fit{
Messages: make([]proto.Message, 0, size),
}

// Should be as ordered: FieldId, DeveloperDataId and FieldDescription
fit.Messages = append(fit.Messages, f.FileId.ToMesg(fac))

ToMesgs(&fit.Messages, fac, mesgnum.DeveloperDataId, f.DeveloperDataIds)
ToMesgs(&fit.Messages, fac, mesgnum.FieldDescription, f.FieldDescriptions)

if f.UserProfile != nil {
fit.Messages = append(fit.Messages, f.UserProfile.ToMesg(fac))
}

ToMesgs(&fit.Messages, fac, mesgnum.BloodPressure, f.BloodPressures)
ToMesgs(&fit.Messages, fac, mesgnum.DeviceInfo, f.DeviceInfos)

fit.Messages = append(fit.Messages, f.UnrelatedMessages...)

SortMessagesByTimestamp(fit.Messages)

return fit
}
Loading