Skip to content

Commit

Permalink
feat!: update filedef implementation (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
muktihari committed Dec 11, 2023
1 parent 59059d6 commit 074874a
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 62 deletions.
11 changes: 7 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ func main() {

// The listener will receive every decoded message from the decoder as soon as it is decoded,
// The Activity Listener will transform the messages into an Activity File.
al := filedef.NewListener(filedef.NewActivity())
al := filedef.NewListener[filedef.Activity]()
defer al.Close() // release channel used by listener

dec := decoder.New(bufio.NewReader(f),
decoder.WithMesgListener(al), // Add activity listener to the decoder
decoder.WithBroadcastOnly(), // Direct the decoder to only broadcast the messages without retaining them.
Expand Down Expand Up @@ -171,7 +173,9 @@ func main() {
}
defer f.Close()

al := filedef.NewListener(filedef.NewActivity())
al := filedef.NewListener[filedef.Activity]()
defer al.Close() // release channel used by listener

dec := decoder.New(bufio.NewReader(f),
decoder.WithMesgListener(al),
decoder.WithBroadcastOnly(),
Expand All @@ -188,7 +192,6 @@ func main() {
// File Type: activity

if fileId.Type != typedef.FileActivity {
_ = al.File() // Note: It is recommended to call this method to release the listener's channel.
return // Let's stop.
}

Expand Down Expand Up @@ -347,7 +350,7 @@ func main() {

### Encode Common File Types

Currently, only Activity Files are supported. Please note that this feature is still under development and has not been tested yet.
Please note that this feature is still under development and has not been fully tested yet.

```go
...
Expand Down
81 changes: 50 additions & 31 deletions profile/filedef/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
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"
)

// Activity is a common file type that most wearable device or cycling computer uses to record activities.
//
// Please note since we group the same mesgdef types in a slice, we lose the arrival order of the messages.
// But for messages that have timestamp, we can reconstruct the messages by timestamp order.
//
// ref: https://developer.garmin.com/fit/file-types/activity/
type Activity struct {
FileId *mesgdef.FileId
Activity *mesgdef.Activity
Expand All @@ -18,8 +25,8 @@ type Activity struct {
Records []*mesgdef.Record

// Optional Messages
DeviceInfo *mesgdef.DeviceInfo
UserProfile *mesgdef.UserProfile
DeviceInfos []*mesgdef.DeviceInfo
Events []*mesgdef.Event
Lengths []*mesgdef.Length
SegmentLap []*mesgdef.SegmentLap
Expand All @@ -32,8 +39,13 @@ type Activity struct {
// Developer Data Lookup
DeveloperDataIds []*mesgdef.DeveloperDataId
FieldDescriptions []*mesgdef.FieldDescription

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

var _ File = &Activity{}

func NewActivity(mesgs ...proto.Message) *Activity {
f := &Activity{}
for i := range mesgs {
Expand All @@ -56,7 +68,7 @@ func (f *Activity) Add(mesg proto.Message) {
case mesgnum.Record:
f.Records = append(f.Records, mesgdef.NewRecord(mesg))
case mesgnum.DeviceInfo:
f.DeviceInfo = mesgdef.NewDeviceInfo(mesg)
f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(mesg))
case mesgnum.UserProfile:
f.UserProfile = mesgdef.NewUserProfile(mesg)
case mesgnum.Event:
Expand All @@ -77,59 +89,66 @@ func (f *Activity) Add(mesg proto.Message) {
f.DeveloperDataIds = append(f.DeveloperDataIds, mesgdef.NewDeveloperDataId(mesg))
case mesgnum.FieldDescription:
f.FieldDescriptions = append(f.FieldDescriptions, mesgdef.NewFieldDescription(mesg))
default:
f.UnrelatedMessages = append(f.UnrelatedMessages, mesg)
}
}

func (f *Activity) ToFit(factory Factory) proto.Fit {
var size = 4 // non slice fields
func (f *Activity) ToFit(fac Factory) proto.Fit {
if fac == nil {
fac = factory.StandardFactory()
}

var size = 3 // non slice fields

size += len(f.Sessions) + len(f.Laps) + len(f.Records) +
size += len(f.Sessions) + len(f.Laps) + len(f.Records) + len(f.DeviceInfos) +
len(f.Events) + len(f.Lengths) + len(f.SegmentLap) + len(f.ZoneTargets) +
len(f.Workouts) + len(f.WorkoutSteps) + len(f.HRs) + len(f.HRVs)
len(f.Workouts) + len(f.WorkoutSteps) + len(f.HRs) + len(f.HRVs) +
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
if f.FileId != nil {
mesg := factory.CreateMesg(mesgnum.FileId)
mesg := fac.CreateMesg(mesgnum.FileId)
f.FileId.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

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

if f.Activity != nil {
mesg := factory.CreateMesg(mesgnum.Activity)
f.Activity.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}
PutMessages(fac, &fit.Messages, mesgnum.DeviceInfo, f.DeviceInfos)

if f.DeviceInfo != nil {
mesg := factory.CreateMesg(mesgnum.DeviceInfo)
f.DeviceInfo.PutMessage(&mesg)
if f.UserProfile != nil {
mesg := fac.CreateMesg(mesgnum.UserProfile)
f.UserProfile.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

if f.UserProfile != nil {
mesg := factory.CreateMesg(mesgnum.UserProfile)
f.UserProfile.PutMessage(&mesg)
if f.Activity != nil {
mesg := fac.CreateMesg(mesgnum.Activity)
f.Activity.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

PutMessages(factory, &fit.Messages, mesgnum.Session, f.Sessions)
PutMessages(factory, &fit.Messages, mesgnum.Lap, f.Laps)
PutMessages(factory, &fit.Messages, mesgnum.Record, f.Records)
PutMessages(factory, &fit.Messages, mesgnum.Event, f.Events)
PutMessages(factory, &fit.Messages, mesgnum.Length, f.Lengths)
PutMessages(factory, &fit.Messages, mesgnum.SegmentLap, f.SegmentLap)
PutMessages(factory, &fit.Messages, mesgnum.ZonesTarget, f.ZoneTargets)
PutMessages(factory, &fit.Messages, mesgnum.Workout, f.Workouts)
PutMessages(factory, &fit.Messages, mesgnum.WorkoutStep, f.WorkoutSteps)
PutMessages(factory, &fit.Messages, mesgnum.Hr, f.HRs)
PutMessages(factory, &fit.Messages, mesgnum.Hrv, f.HRVs)
PutMessages(fac, &fit.Messages, mesgnum.Session, f.Sessions)
PutMessages(fac, &fit.Messages, mesgnum.Lap, f.Laps)
PutMessages(fac, &fit.Messages, mesgnum.Record, f.Records)
PutMessages(fac, &fit.Messages, mesgnum.Event, f.Events)
PutMessages(fac, &fit.Messages, mesgnum.Length, f.Lengths)
PutMessages(fac, &fit.Messages, mesgnum.SegmentLap, f.SegmentLap)
PutMessages(fac, &fit.Messages, mesgnum.ZonesTarget, f.ZoneTargets)
PutMessages(fac, &fit.Messages, mesgnum.Workout, f.Workouts)
PutMessages(fac, &fit.Messages, mesgnum.WorkoutStep, f.WorkoutSteps)
PutMessages(fac, &fit.Messages, mesgnum.Hr, f.HRs)
PutMessages(fac, &fit.Messages, mesgnum.Hrv, f.HRVs)

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

SortMessagesByTimestamp(fit.Messages)

return fit
}
70 changes: 66 additions & 4 deletions profile/filedef/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
package filedef

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

type CourseFile struct {
// Course is a common file type used as points of courses to assist with on- and off-road navigation,
// to provide turn by turn directions, or with virtual training applications to simulate real-world activities.
//
// Please note since we group the same mesgdef types in a slice, we lose the arrival order of the messages.
// But for messages that have timestamp, we can reconstruct the messages by timestamp order.
//
// ref: https://developer.garmin.com/fit/file-types/course/
type Course struct {
FileId *mesgdef.FileId
Course *mesgdef.Course
Lap *mesgdef.Lap
Expand All @@ -24,10 +32,15 @@ type CourseFile struct {
// Developer Data Lookup
DeveloperDataIds []*mesgdef.DeveloperDataId
FieldDescriptions []*mesgdef.FieldDescription

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

func NewCourseFile(mesgs ...proto.Message) (f *CourseFile, ok bool) {
f = &CourseFile{}
var _ File = &Course{}

func NewCourse(mesgs ...proto.Message) (f *Course, ok bool) {
f = &Course{}
for i := range mesgs {
f.Add(mesgs[i])
}
Expand All @@ -39,7 +52,7 @@ func NewCourseFile(mesgs ...proto.Message) (f *CourseFile, ok bool) {
return f, true
}

func (f *CourseFile) Add(mesg proto.Message) {
func (f *Course) Add(mesg proto.Message) {
switch mesg.Num {
case mesgnum.FileId:
f.FileId = mesgdef.NewFileId(mesg)
Expand All @@ -57,5 +70,54 @@ func (f *CourseFile) Add(mesg proto.Message) {
f.DeveloperDataIds = append(f.DeveloperDataIds, mesgdef.NewDeveloperDataId(mesg))
case mesgnum.FieldDescription:
f.FieldDescriptions = append(f.FieldDescriptions, mesgdef.NewFieldDescription(mesg))
default:
f.UnrelatedMessages = append(f.UnrelatedMessages, mesg)
}
}

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

size := 3 /* non slice fields */

size += len(f.Records) + len(f.Events) + len(f.CoursePoints) +
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
if f.FileId != nil {
mesg := fac.CreateMesg(mesgnum.FileId)
f.FileId.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

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

if f.Course != nil {
mesg := fac.CreateMesg(mesgnum.Course)
f.Course.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

if f.Lap != nil {
mesg := fac.CreateMesg(mesgnum.Lap)
f.Lap.PutMessage(&mesg)
fit.Messages = append(fit.Messages, mesg)
}

PutMessages(fac, &fit.Messages, mesgnum.Record, f.Records)
PutMessages(fac, &fit.Messages, mesgnum.Event, f.Events)
PutMessages(fac, &fit.Messages, mesgnum.CoursePoint, f.CoursePoints)

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

SortMessagesByTimestamp(fit.Messages)

return fit
}
47 changes: 43 additions & 4 deletions profile/filedef/filedef.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,65 @@
package filedef

import (
"github.com/muktihari/fit/kit/typeconv"
"github.com/muktihari/fit/profile/typedef"
"github.com/muktihari/fit/proto"
"golang.org/x/exp/slices"
)

// File is an interface for defining common type file, any defined common file type should implement
// the following methods to be able to work with Listener (and other building block in filedef package).
type File interface {
// Add adds message into file structure.
Add(mesg proto.Message)
// ToFit converts file back to proto.Fit structure.
ToFit(fac Factory) proto.Fit
}

// Factory is factory interface used in filedef package to convert a common type file back to its proto.Fit representation.
type Factory interface {
CreateMesg(num typedef.MesgNum) proto.Message
}

type PutMessage interface {
PutMessage(mesg *proto.Message)
}

// PutMessages bulks put messages
func PutMessages[S []E, E PutMessage](factory Factory, messages *[]proto.Message, mesgNum typedef.MesgNum, s S) {
for i := range s {
mesg := factory.CreateMesg(mesgNum)
s[i].PutMessage(&mesg)
*messages = append(*messages, mesg)
}
}

// PutMessage is a type constraint to retrieve all mesgdef structures which implement PutMessage method.
type PutMessage interface {
PutMessage(mesg *proto.Message)
}

// SortMessagesByTimestamp sorts messages by timestamp only if the message has timestamp field.
// When a message has no timestamp field, its order will not be changed.
func SortMessagesByTimestamp(messages []proto.Message) {
slices.SortFunc(messages, func(m1, m2 proto.Message) int {
value1 := m1.FieldValueByNum(proto.FieldNumTimestamp)
if value1 == nil {
return 0
}

value2 := m2.FieldValueByNum(proto.FieldNumTimestamp)
if value2 == nil {
return 0
}

timestamp1 := typeconv.ToUint32[uint32](value1)
timestamp2 := typeconv.ToUint32[uint32](value2)

if timestamp1 == timestamp2 {
return 0
}

if timestamp1 < timestamp2 {
return -1
}

return 1
})
}
Loading

0 comments on commit 074874a

Please sign in to comment.