Skip to content

Commit

Permalink
feat: add FIT File Types (#88)
Browse files Browse the repository at this point in the history
* feat: add FIT File Types

* docs: update documentation
  • Loading branch information
muktihari authored Jan 2, 2024
1 parent 944a724 commit 8d5788d
Show file tree
Hide file tree
Showing 34 changed files with 2,217 additions and 25 deletions.
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

0 comments on commit 8d5788d

Please sign in to comment.