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!: update filedef implementation #42

Merged
merged 1 commit into from
Dec 11, 2023
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
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