From 1c72fa0da6e51d630621bff4d1a0283a8c5c19c7 Mon Sep 17 00:00:00 2001 From: Hikmatulloh Hari Mukti Date: Tue, 2 Jan 2024 21:44:47 +0700 Subject: [PATCH 1/2] feat: add FIT File Types --- profile/filedef/activity.go | 16 ++-- profile/filedef/activity_summary.go | 88 ++++++++++++++++++ profile/filedef/activity_summary_test.go | 72 +++++++++++++++ profile/filedef/activity_test.go | 4 +- profile/filedef/blood_pressure.go | 87 ++++++++++++++++++ profile/filedef/blood_pressure_test.go | 75 ++++++++++++++++ profile/filedef/course_test.go | 2 +- profile/filedef/device.go | 93 +++++++++++++++++++ profile/filedef/device_test.go | 82 +++++++++++++++++ profile/filedef/goals.go | 75 ++++++++++++++++ profile/filedef/goals_test.go | 66 ++++++++++++++ profile/filedef/listener.go | 19 +++- profile/filedef/listener_test.go | 71 ++++++++++++++- profile/filedef/monitoring_ab.go | 91 +++++++++++++++++++ profile/filedef/monitoring_ab_test.go | 108 +++++++++++++++++++++++ profile/filedef/monitoring_daily.go | 87 ++++++++++++++++++ profile/filedef/monitoring_daily_test.go | 72 +++++++++++++++ profile/filedef/schedules.go | 74 ++++++++++++++++ profile/filedef/schedules_test.go | 66 ++++++++++++++ profile/filedef/segment.go | 97 ++++++++++++++++++++ profile/filedef/segment_list.go | 81 +++++++++++++++++ profile/filedef/segment_list_test.go | 69 +++++++++++++++ profile/filedef/segment_test.go | 75 ++++++++++++++++ profile/filedef/settings.go | 91 +++++++++++++++++++ profile/filedef/settings_test.go | 78 ++++++++++++++++ profile/filedef/sport.go | 103 +++++++++++++++++++++ profile/filedef/sport_test.go | 84 ++++++++++++++++++ profile/filedef/totals.go | 77 ++++++++++++++++ profile/filedef/totals_test.go | 66 ++++++++++++++ profile/filedef/weight.go | 87 ++++++++++++++++++ profile/filedef/weight_test.go | 72 +++++++++++++++ profile/filedef/workout.go | 6 +- profile/filedef/workout_test.go | 2 +- 33 files changed, 2215 insertions(+), 21 deletions(-) create mode 100644 profile/filedef/activity_summary.go create mode 100644 profile/filedef/activity_summary_test.go create mode 100644 profile/filedef/blood_pressure.go create mode 100644 profile/filedef/blood_pressure_test.go create mode 100644 profile/filedef/device.go create mode 100644 profile/filedef/device_test.go create mode 100644 profile/filedef/goals.go create mode 100644 profile/filedef/goals_test.go create mode 100644 profile/filedef/monitoring_ab.go create mode 100644 profile/filedef/monitoring_ab_test.go create mode 100644 profile/filedef/monitoring_daily.go create mode 100644 profile/filedef/monitoring_daily_test.go create mode 100644 profile/filedef/schedules.go create mode 100644 profile/filedef/schedules_test.go create mode 100644 profile/filedef/segment.go create mode 100644 profile/filedef/segment_list.go create mode 100644 profile/filedef/segment_list_test.go create mode 100644 profile/filedef/segment_test.go create mode 100644 profile/filedef/settings.go create mode 100644 profile/filedef/settings_test.go create mode 100644 profile/filedef/sport.go create mode 100644 profile/filedef/sport_test.go create mode 100644 profile/filedef/totals.go create mode 100644 profile/filedef/totals_test.go create mode 100644 profile/filedef/weight.go create mode 100644 profile/filedef/weight_test.go diff --git a/profile/filedef/activity.go b/profile/filedef/activity.go index 8bd7c267..f533c275 100644 --- a/profile/filedef/activity.go +++ b/profile/filedef/activity.go @@ -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 diff --git a/profile/filedef/activity_summary.go b/profile/filedef/activity_summary.go new file mode 100644 index 00000000..719d66a8 --- /dev/null +++ b/profile/filedef/activity_summary.go @@ -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 +} diff --git a/profile/filedef/activity_summary_test.go b/profile/filedef/activity_summary_test.go new file mode 100644 index 00000000..aea5eed3 --- /dev/null +++ b/profile/filedef/activity_summary_test.go @@ -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) + } +} diff --git a/profile/filedef/activity_test.go b/profile/filedef/activity_test.go index be898741..1faefb0e 100644 --- a/profile/filedef/activity_test.go +++ b/profile/filedef/activity_test.go @@ -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)), ), @@ -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 diff --git a/profile/filedef/blood_pressure.go b/profile/filedef/blood_pressure.go new file mode 100644 index 00000000..7eb37d10 --- /dev/null +++ b/profile/filedef/blood_pressure.go @@ -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 +} diff --git a/profile/filedef/blood_pressure_test.go b/profile/filedef/blood_pressure_test.go new file mode 100644 index 00000000..f59fe0d6 --- /dev/null +++ b/profile/filedef/blood_pressure_test.go @@ -0,0 +1,75 @@ +// 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 newBloodPressureMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileBloodPressure)), + 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.UserProfile).WithFields( + factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(27)), + ), + factory.CreateMesgOnly(mesgnum.BloodPressure).WithFields( + factory.CreateField(mesgnum.BloodPressure, fieldnum.BloodPressureTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), + factory.CreateField(mesgnum.BloodPressure, fieldnum.BloodPressureSystolicPressure).WithValue(uint16(110)), + factory.CreateField(mesgnum.BloodPressure, fieldnum.BloodPressureDiastolicPressure).WithValue(uint16(80)), + factory.CreateField(mesgnum.BloodPressure, fieldnum.BloodPressureHeartRate).WithValue(uint8(100)), + ), + factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).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 TestBloodPressureCorrectness(t *testing.T) { + mesgs := newBloodPressureMessageForTest(time.Now()) + + bloodPressure := filedef.NewBloodPressure(mesgs...) + if bloodPressure.FileId.Type != typedef.FileBloodPressure { + t.Fatalf("expected: %v, got: %v", typedef.FileBloodPressure, bloodPressure.FileId.Type) + } + + fit := bloodPressure.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) + } +} diff --git a/profile/filedef/course_test.go b/profile/filedef/course_test.go index 3c8ab106..7a72ad94 100644 --- a/profile/filedef/course_test.go +++ b/profile/filedef/course_test.go @@ -73,7 +73,7 @@ func TestCourseCorrectness(t *testing.T) { course := filedef.NewCourse(mesgs...) if course.FileId.Type != typedef.FileCourse { - t.Fatalf("expected: %#v, got: %#v", typedef.FileActivity, course.FileId.Type) + t.Fatalf("expected: %v, got: %v", typedef.FileActivity, course.FileId.Type) } fit := course.ToFit(nil) diff --git a/profile/filedef/device.go b/profile/filedef/device.go new file mode 100644 index 00000000..70064557 --- /dev/null +++ b/profile/filedef/device.go @@ -0,0 +1,93 @@ +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" +) + +// Device files contain information about a device’s file structure/capabilities. +type Device struct { + FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + Softwares []*mesgdef.Software + Capabilities []*mesgdef.Capabilities + FileCapabilities []*mesgdef.FileCapabilities + MesgCapabilities []*mesgdef.MesgCapabilities + FieldCapabilities []*mesgdef.FieldCapabilities + + UnrelatedMessages []proto.Message +} + +var _ File = &Device{} + +func NewDevice(mesgs ...proto.Message) *Device { + f := &Device{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Device) 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.Software: + f.Softwares = append(f.Softwares, mesgdef.NewSoftware(&mesg)) + case mesgnum.Capabilities: + f.Capabilities = append(f.Capabilities, mesgdef.NewCapabilities(&mesg)) + case mesgnum.FileCapabilities: + f.FileCapabilities = append(f.FileCapabilities, mesgdef.NewFileCapabilities(&mesg)) + case mesgnum.MesgCapabilities: + f.MesgCapabilities = append(f.MesgCapabilities, mesgdef.NewMesgCapabilities(&mesg)) + case mesgnum.FieldCapabilities: + f.FieldCapabilities = append(f.FieldCapabilities, mesgdef.NewFieldCapabilities(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Device) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 1 // non slice fields + + size += len(f.Softwares) + len(f.Capabilities) + len(f.FileCapabilities) + + len(f.MesgCapabilities) + len(f.FieldCapabilities) + + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.Software, f.Softwares) + ToMesgs(&fit.Messages, fac, mesgnum.Capabilities, f.Capabilities) + ToMesgs(&fit.Messages, fac, mesgnum.FileCapabilities, f.FileCapabilities) + ToMesgs(&fit.Messages, fac, mesgnum.MesgCapabilities, f.MesgCapabilities) + ToMesgs(&fit.Messages, fac, mesgnum.FieldCapabilities, f.FieldCapabilities) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/device_test.go b/profile/filedef/device_test.go new file mode 100644 index 00000000..5a5057f1 --- /dev/null +++ b/profile/filedef/device_test.go @@ -0,0 +1,82 @@ +// 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 newDeviceMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileDevice)), + 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.Software).WithFields( + factory.CreateField(mesgnum.Software, fieldnum.SoftwareMessageIndex).WithValue(uint16(typedef.MessageIndexReserved)), + ), + factory.CreateMesgOnly(mesgnum.Capabilities).WithFields( + factory.CreateField(mesgnum.Capabilities, fieldnum.CapabilitiesSports).WithValue([]uint8{ + uint8(typedef.SportBits0Basketball), + uint8(typedef.SportBits1AmericanFootball), + uint8(typedef.SportBits2Paddling), + }), + ), + factory.CreateMesgOnly(mesgnum.FileCapabilities).WithFields( + factory.CreateField(mesgnum.FileCapabilities, fieldnum.FileCapabilitiesType).WithValue(uint8(typedef.FileActivity)), + ), + factory.CreateMesgOnly(mesgnum.MesgCapabilities).WithFields( + factory.CreateField(mesgnum.MesgCapabilities, fieldnum.MesgCapabilitiesFile).WithValue(uint8(typedef.FileActivity)), + ), + factory.CreateMesgOnly(mesgnum.FieldCapabilities).WithFields( + factory.CreateField(mesgnum.FieldCapabilities, fieldnum.FieldCapabilitiesFile).WithValue(uint8(typedef.FileActivity)), + ), + // 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 TestDeviceCorrectness(t *testing.T) { + mesgs := newDeviceMessageForTest(time.Now()) + + device := filedef.NewDevice(mesgs...) + if device.FileId.Type != typedef.FileDevice { + t.Fatalf("expected: %v, got: %v", typedef.FileDevice, device.FileId.Type) + } + + fit := device.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) + } +} diff --git a/profile/filedef/goals.go b/profile/filedef/goals.go new file mode 100644 index 00000000..24405612 --- /dev/null +++ b/profile/filedef/goals.go @@ -0,0 +1,75 @@ +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" +) + +// Goals files allow a user to communicate their exercise/health goals. +type Goals struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + Goals []*mesgdef.Goal + UnrelatedMessages []proto.Message +} + +var _ File = &Goals{} + +func NewGoals(mesgs ...proto.Message) *Goals { + f := &Goals{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Goals) 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.Goal: + f.Goals = append(f.Goals, mesgdef.NewGoal(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Goals) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 1 // non slice fields + + size += len(f.Goals) + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.Goal, f.Goals) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/goals_test.go b/profile/filedef/goals_test.go new file mode 100644 index 00000000..6837153f --- /dev/null +++ b/profile/filedef/goals_test.go @@ -0,0 +1,66 @@ +// 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 newGoalsMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileGoals)), + 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.Goal).WithFields( + factory.CreateField(mesgnum.Goal, fieldnum.GoalSport).WithValue(uint8(typedef.SportSoccer)), + ), + // 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 TestGoalsCorrectness(t *testing.T) { + mesgs := newGoalsMessageForTest(time.Now()) + + goals := filedef.NewGoals(mesgs...) + if goals.FileId.Type != typedef.FileGoals { + t.Fatalf("expected: %v, got: %v", typedef.FileGoals, goals.FileId.Type) + } + + fit := goals.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) + } +} diff --git a/profile/filedef/listener.go b/profile/filedef/listener.go index 5954857c..4995919f 100644 --- a/profile/filedef/listener.go +++ b/profile/filedef/listener.go @@ -27,9 +27,22 @@ func defaultOptions() *options { // PredefinedFileSet is a list of default filesets used in listener, it's exported so user can append their own types and register it as an option. func PredefinedFileSet() FileSets { return FileSets{ - typedef.FileActivity: func() File { return NewActivity() }, - typedef.FileCourse: func() File { return NewCourse() }, - typedef.FileWorkout: func() File { return NewWorkout() }, + typedef.FileDevice: func() File { return NewDevice() }, + typedef.FileSettings: func() File { return NewSettings() }, + typedef.FileSport: func() File { return NewSport() }, + typedef.FileBloodPressure: func() File { return NewBloodPressure() }, + typedef.FileWeight: func() File { return NewWeight() }, + typedef.FileWorkout: func() File { return NewWorkout() }, + typedef.FileActivity: func() File { return NewActivity() }, + typedef.FileCourse: func() File { return NewCourse() }, + typedef.FileGoals: func() File { return NewGoals() }, + typedef.FileTotals: func() File { return NewTotals() }, + typedef.FileSchedules: func() File { return NewSchedules() }, + typedef.FileMonitoringA: func() File { return NewMonitoringAB() }, + typedef.FileMonitoringB: func() File { return NewMonitoringAB() }, + typedef.FileMonitoringDaily: func() File { return NewMonitoringDaily() }, + typedef.FileSegment: func() File { return NewSegment() }, + typedef.FileSegmentList: func() File { return NewSegmentList() }, } } diff --git a/profile/filedef/listener_test.go b/profile/filedef/listener_test.go index 19fa2a36..886cfbdf 100644 --- a/profile/filedef/listener_test.go +++ b/profile/filedef/listener_test.go @@ -37,6 +37,36 @@ func TestListenerForSingleFitFile(t *testing.T) { mesgs []proto.Message result filedef.File }{ + { + name: "default listener for device", + mesgs: newDeviceMessageForTest(now), + result: filedef.NewDevice(newDeviceMessageForTest(now)...), + }, + { + name: "default listener for settings", + mesgs: newSettingsMessageForTest(now), + result: filedef.NewSettings(newSettingsMessageForTest(now)...), + }, + { + name: "default listener for sport", + mesgs: newSportMessageForTest(now), + result: filedef.NewSport(newSportMessageForTest(now)...), + }, + { + name: "default listener for blood pressure", + mesgs: newBloodPressureMessageForTest(now), + result: filedef.NewBloodPressure(newBloodPressureMessageForTest(now)...), + }, + { + name: "default listener for weight", + mesgs: newWeightMessageForTest(now), + result: filedef.NewWeight(newWeightMessageForTest(now)...), + }, + { + name: "default listener for workout", + mesgs: newWorkoutMessageForTest(now), + result: filedef.NewWorkout(newWorkoutMessageForTest(now)...), + }, { name: "default listener for activity", mesgs: newActivityMessageForTest(now), @@ -48,9 +78,44 @@ func TestListenerForSingleFitFile(t *testing.T) { result: filedef.NewCourse(newCourseMessageForTest(now)...), }, { - name: "default listener for workout", - mesgs: newWorkoutMessageForTest(now), - result: filedef.NewWorkout(newWorkoutMessageForTest(now)...), + name: "default listener for goals", + mesgs: newGoalsMessageForTest(now), + result: filedef.NewGoals(newGoalsMessageForTest(now)...), + }, + { + name: "default listener for totals", + mesgs: newTotalsMessageForTest(now), + result: filedef.NewTotals(newTotalsMessageForTest(now)...), + }, + { + name: "default listener for schedules", + mesgs: newSchedulesMessageForTest(now), + result: filedef.NewSchedules(newSchedulesMessageForTest(now)...), + }, + { + name: "default listener for monitoring A", + mesgs: newMonitoringAMessageForTest(now), + result: filedef.NewMonitoringAB(newMonitoringAMessageForTest(now)...), + }, + { + name: "default listener for monitoring B", + mesgs: newMonitoringBMessageForTest(now), + result: filedef.NewMonitoringAB(newMonitoringBMessageForTest(now)...), + }, + { + name: "default listener for monitoring daily", + mesgs: newMonitoringDailyMessageForTest(now), + result: filedef.NewMonitoringDaily(newMonitoringDailyMessageForTest(now)...), + }, + { + name: "default listener for segment", + mesgs: newSegmentMessageForTest(now), + result: filedef.NewSegment(newSegmentMessageForTest(now)...), + }, + { + name: "default listener for segment list", + mesgs: newSegmentListMessageForTest(now), + result: filedef.NewSegmentList(newSegmentListMessageForTest(now)...), }, { name: "listener for not specified fileset, course", diff --git a/profile/filedef/monitoring_ab.go b/profile/filedef/monitoring_ab.go new file mode 100644 index 00000000..1a1ccd3a --- /dev/null +++ b/profile/filedef/monitoring_ab.go @@ -0,0 +1,91 @@ +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" +) + +// MonitoringAB (Monitoring A and Monitoring B) files are used to store data that is logged over varying time intervals. +// The two monitoring file formats are identical apart from supporting different conventions for file_id.number and the start of accumulating data values. +// +// The FIT file_id.type = 15 for a monitoring_a file and +// the FIT file_id.type = 32 for a monitoring_b file +type MonitoringAB struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + MonitoringInfo *mesgdef.MonitoringInfo + Monitorings []*mesgdef.Monitoring + DeviceInfos []*mesgdef.DeviceInfo + + UnrelatedMessages []proto.Message +} + +var _ File = &MonitoringAB{} + +func NewMonitoringAB(mesgs ...proto.Message) *MonitoringAB { + f := &MonitoringAB{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *MonitoringAB) 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.MonitoringInfo: + f.MonitoringInfo = mesgdef.NewMonitoringInfo(&mesg) + case mesgnum.Monitoring: + f.Monitorings = append(f.Monitorings, mesgdef.NewMonitoring(&mesg)) + case mesgnum.DeviceInfo: + f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *MonitoringAB) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 2 // non slice fields + + size += len(f.Monitorings) + 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.MonitoringInfo != nil { + fit.Messages = append(fit.Messages, f.MonitoringInfo.ToMesg(fac)) + } + + ToMesgs(&fit.Messages, fac, mesgnum.Monitoring, f.Monitorings) + ToMesgs(&fit.Messages, fac, mesgnum.DeviceInfo, f.DeviceInfos) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/monitoring_ab_test.go b/profile/filedef/monitoring_ab_test.go new file mode 100644 index 00000000..df067fce --- /dev/null +++ b/profile/filedef/monitoring_ab_test.go @@ -0,0 +1,108 @@ +// 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" + "golang.org/x/exp/slices" +) + +func newMonitoringAMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileMonitoringA)), + 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.MonitoringInfo).WithFields( + factory.CreateField(mesgnum.MonitoringInfo, fieldnum.MonitoringInfoActivityType).WithValue([]uint8{ + uint8(typedef.ActivityTypeCycling), + uint8(typedef.ActivityTypeRunning), + }), + ), + factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringActivityType).WithValue(uint8(typedef.ActivityTypeCycling)), + ), + factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringActivityType).WithValue(uint8(typedef.ActivityTypeRunning)), + ), + factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), + factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoBatteryStatus).WithValue(uint8(typedef.BatteryStatusGood)), + ), + // 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 newMonitoringBMessageForTest(now time.Time) []proto.Message { + mesgsB := slices.Clone(newMonitoringAMessageForTest(time.Now())) + ftype := mesgsB[0].FieldByNum(fieldnum.FileIdType) + ftype.Value = uint8(typedef.FileMonitoringB) + return mesgsB +} + +func TestMonitoringABCorrectness(t *testing.T) { + mesgsA := newMonitoringAMessageForTest(time.Now()) + + monitoringA := filedef.NewMonitoringAB(mesgsA...) + if monitoringA.FileId.Type != typedef.FileMonitoringA { + t.Fatalf("expected: %v, got: %v", typedef.FileActivity, monitoringA.FileId.Type) + } + + fit := monitoringA.ToFit(nil) // use standard factory + + if diff := cmp.Diff(mesgsA, 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) + } + + mesgsB := newMonitoringBMessageForTest(time.Now()) + ftype := mesgsB[0].FieldByNum(fieldnum.FileIdType) + ftype.Value = uint8(typedef.FileMonitoringB) + + monitoringB := filedef.NewMonitoringAB(mesgsB...) + if monitoringB.FileId.Type != typedef.FileMonitoringB { + t.Fatalf("expected: %v, got: %v", typedef.FileMonitoringB, monitoringA.FileId.Type) + } + + fit = monitoringB.ToFit(nil) // use standard factory + + if diff := cmp.Diff(mesgsB, 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) + } +} diff --git a/profile/filedef/monitoring_daily.go b/profile/filedef/monitoring_daily.go new file mode 100644 index 00000000..864814b6 --- /dev/null +++ b/profile/filedef/monitoring_daily.go @@ -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" +) + +// MonitoringDaily files follow the same format as monitoring files, however data is logged at 24 hour time intervals. +type MonitoringDaily struct { + FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number, time_created, number + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + MonitoringInfo *mesgdef.MonitoringInfo + Monitorings []*mesgdef.Monitoring // required fields: timestamp + DeviceInfos []*mesgdef.DeviceInfo + + UnrelatedMessages []proto.Message +} + +var _ File = &MonitoringDaily{} + +func NewMonitoringDaily(mesgs ...proto.Message) *MonitoringDaily { + f := &MonitoringDaily{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *MonitoringDaily) 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.MonitoringInfo: + f.MonitoringInfo = mesgdef.NewMonitoringInfo(&mesg) + case mesgnum.Monitoring: + f.Monitorings = append(f.Monitorings, mesgdef.NewMonitoring(&mesg)) + case mesgnum.DeviceInfo: + f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *MonitoringDaily) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 2 // non slice fields + + size += len(f.Monitorings) + 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.MonitoringInfo != nil { + fit.Messages = append(fit.Messages, f.MonitoringInfo.ToMesg(fac)) + } + + ToMesgs(&fit.Messages, fac, mesgnum.Monitoring, f.Monitorings) + ToMesgs(&fit.Messages, fac, mesgnum.DeviceInfo, f.DeviceInfos) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/monitoring_daily_test.go b/profile/filedef/monitoring_daily_test.go new file mode 100644 index 00000000..ece689a8 --- /dev/null +++ b/profile/filedef/monitoring_daily_test.go @@ -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 newMonitoringDailyMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileMonitoringDaily)), + 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.MonitoringInfo).WithFields( + factory.CreateField(mesgnum.MonitoringInfo, fieldnum.MonitoringInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), + ), + factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringIntensity).WithValue(uint8(typedef.IntensityActive)), + ), + factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).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 TestDailyMonitoringCorrectness(t *testing.T) { + mesgs := newMonitoringDailyMessageForTest(time.Now()) + + monitoringDaily := filedef.NewMonitoringDaily(mesgs...) + if monitoringDaily.FileId.Type != typedef.FileMonitoringDaily { + t.Fatalf("expected: %v, got: %v", typedef.FileMonitoringDaily, monitoringDaily.FileId.Type) + } + + fit := monitoringDaily.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) + } +} diff --git a/profile/filedef/schedules.go b/profile/filedef/schedules.go new file mode 100644 index 00000000..330c60da --- /dev/null +++ b/profile/filedef/schedules.go @@ -0,0 +1,74 @@ +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" +) + +// Schedule files are used to schedule a user’s workouts and may contain multiple schedule messages each representing the start time of a workout. +type Schedule struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + Schedules []*mesgdef.Schedule + + UnrelatedMessages []proto.Message +} + +var _ File = &Schedule{} + +func NewSchedules(mesgs ...proto.Message) *Schedule { + f := &Schedule{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Schedule) 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.Schedule: + f.Schedules = append(f.Schedules, mesgdef.NewSchedule(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Schedule) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 1 // non slice fields + + size += len(f.Schedules) + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.Schedule, f.Schedules) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + return fit +} diff --git a/profile/filedef/schedules_test.go b/profile/filedef/schedules_test.go new file mode 100644 index 00000000..04df0ffe --- /dev/null +++ b/profile/filedef/schedules_test.go @@ -0,0 +1,66 @@ +// 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 newSchedulesMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileSchedules)), + 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.Schedule).WithFields( + factory.CreateField(mesgnum.Schedule, fieldnum.ScheduleCompleted).WithValue(true), + ), + // 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 TestSchedulesCorrectness(t *testing.T) { + mesgs := newSchedulesMessageForTest(time.Now()) + + schedules := filedef.NewSchedules(mesgs...) + if schedules.FileId.Type != typedef.FileSchedules { + t.Fatalf("expected: %v, got: %v", typedef.FileSchedules, schedules.FileId.Type) + } + + fit := schedules.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) + } +} diff --git a/profile/filedef/segment.go b/profile/filedef/segment.go new file mode 100644 index 00000000..d823cca8 --- /dev/null +++ b/profile/filedef/segment.go @@ -0,0 +1,97 @@ +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" +) + +// Segment files contain data defining a route and timing information to gauge progress against previous performances or other users +type Segment struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + SegmentId *mesgdef.SegmentId + SegmentLeaderboardEntry *mesgdef.SegmentLeaderboardEntry + SegmentLap *mesgdef.SegmentLap + SegmentPoints []*mesgdef.SegmentPoint + + UnrelatedMessages []proto.Message +} + +var _ File = &Segment{} + +func NewSegment(mesgs ...proto.Message) *Segment { + f := &Segment{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Segment) 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.SegmentId: + f.SegmentId = mesgdef.NewSegmentId(&mesg) + case mesgnum.SegmentLeaderboardEntry: + f.SegmentLeaderboardEntry = mesgdef.NewSegmentLeaderboardEntry(&mesg) + case mesgnum.SegmentLap: + f.SegmentLap = mesgdef.NewSegmentLap(&mesg) + case mesgnum.SegmentPoint: + f.SegmentPoints = append(f.SegmentPoints, mesgdef.NewSegmentPoint(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Segment) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 4 // non slice fields + + size += len(f.SegmentPoints) + 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.SegmentId != nil { + fit.Messages = append(fit.Messages, f.SegmentId.ToMesg(fac)) + } + + if f.SegmentLeaderboardEntry != nil { + fit.Messages = append(fit.Messages, f.SegmentLeaderboardEntry.ToMesg(fac)) + } + + if f.SegmentLap != nil { + fit.Messages = append(fit.Messages, f.SegmentLap.ToMesg(fac)) + } + + ToMesgs(&fit.Messages, fac, mesgnum.SegmentPoint, f.SegmentPoints) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/segment_list.go b/profile/filedef/segment_list.go new file mode 100644 index 00000000..2f61daf3 --- /dev/null +++ b/profile/filedef/segment_list.go @@ -0,0 +1,81 @@ +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" +) + +// SegmentList files maintain a list of available segments on the device. +type SegmentList struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + FileCreator *mesgdef.FileCreator + SegmentFiles []*mesgdef.SegmentFile + + UnrelatedMessages []proto.Message +} + +var _ File = &SegmentList{} + +func NewSegmentList(mesgs ...proto.Message) *SegmentList { + f := &SegmentList{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *SegmentList) 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.FileCreator: + f.FileCreator = mesgdef.NewFileCreator(&mesg) + case mesgnum.SegmentFile: + f.SegmentFiles = append(f.SegmentFiles, mesgdef.NewSegmentFile(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *SegmentList) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 2 // non slice fields + + size += len(f.SegmentFiles) + 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.FileCreator != nil { + fit.Messages = append(fit.Messages, f.FileCreator.ToMesg(fac)) + } + + ToMesgs(&fit.Messages, fac, mesgnum.SegmentFile, f.SegmentFiles) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + return fit +} diff --git a/profile/filedef/segment_list_test.go b/profile/filedef/segment_list_test.go new file mode 100644 index 00000000..fc5ae307 --- /dev/null +++ b/profile/filedef/segment_list_test.go @@ -0,0 +1,69 @@ +// 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 newSegmentListMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileSegmentList)), + 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.FileCreator).WithFields( + factory.CreateField(mesgnum.FileCreator, fieldnum.FileCreatorSoftwareVersion).WithValue(uint16(1)), + ), + factory.CreateMesgOnly(mesgnum.SegmentFile).WithFields( + factory.CreateField(mesgnum.SegmentFile, fieldnum.SegmentFileEnabled).WithValue(true), + ), + // 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 TestSegmentListCorrectness(t *testing.T) { + mesgs := newSegmentListMessageForTest(time.Now()) + + segmentList := filedef.NewSegmentList(mesgs...) + if segmentList.FileId.Type != typedef.FileSegmentList { + t.Fatalf("expected: %v, got: %v", typedef.FileSegmentList, segmentList.FileId.Type) + } + + fit := segmentList.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) + } +} diff --git a/profile/filedef/segment_test.go b/profile/filedef/segment_test.go new file mode 100644 index 00000000..d4b1b201 --- /dev/null +++ b/profile/filedef/segment_test.go @@ -0,0 +1,75 @@ +// 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 newSegmentMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileSegment)), + 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.SegmentId).WithFields( + factory.CreateField(mesgnum.SegmentId, fieldnum.SegmentIdEnabled).WithValue(true), + ), + factory.CreateMesgOnly(mesgnum.SegmentLeaderboardEntry).WithFields( + factory.CreateField(mesgnum.SegmentLeaderboardEntry, fieldnum.SegmentLeaderboardEntryName).WithValue("entry test"), + ), + factory.CreateMesgOnly(mesgnum.SegmentLap).WithFields( + factory.CreateField(mesgnum.SegmentLap, fieldnum.SegmentLapName).WithValue("lap test"), + ), + factory.CreateMesgOnly(mesgnum.SegmentPoint).WithFields( + factory.CreateField(mesgnum.SegmentPoint, fieldnum.SegmentPointAltitude).WithValue(uint16(10000)), + ), + // 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 TestSegmentCorrectness(t *testing.T) { + mesgs := newSegmentMessageForTest(time.Now()) + + segment := filedef.NewSegment(mesgs...) + if segment.FileId.Type != typedef.FileSegment { + t.Fatalf("expected: %v, got: %v", typedef.FileSegment, segment.FileId.Type) + } + + fit := segment.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) + } +} diff --git a/profile/filedef/settings.go b/profile/filedef/settings.go new file mode 100644 index 00000000..2287e32a --- /dev/null +++ b/profile/filedef/settings.go @@ -0,0 +1,91 @@ +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" +) + +// Settings files contain user and device information in the form of profiles. +type Settings struct { + FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + UserProfiles []*mesgdef.UserProfile + HrmProfiles []*mesgdef.HrmProfile + SdmProfiles []*mesgdef.SdmProfile + BikeProfiles []*mesgdef.BikeProfile + DeviceSettings []*mesgdef.DeviceSettings + + UnrelatedMessages []proto.Message +} + +var _ File = &Settings{} + +func NewSettings(mesgs ...proto.Message) *Settings { + f := &Settings{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Settings) 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.UserProfiles = append(f.UserProfiles, mesgdef.NewUserProfile(&mesg)) + case mesgnum.HrmProfile: + f.HrmProfiles = append(f.HrmProfiles, mesgdef.NewHrmProfile(&mesg)) + case mesgnum.SdmProfile: + f.SdmProfiles = append(f.SdmProfiles, mesgdef.NewSdmProfile(&mesg)) + case mesgnum.BikeProfile: + f.BikeProfiles = append(f.BikeProfiles, mesgdef.NewBikeProfile(&mesg)) + case mesgnum.DeviceSettings: + f.DeviceSettings = append(f.DeviceSettings, mesgdef.NewDeviceSettings(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Settings) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 1 // non slice fields + + size += len(f.UserProfiles) + len(f.HrmProfiles) + len(f.SdmProfiles) + + len(f.BikeProfiles) + len(f.DeviceSettings) + + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.UserProfile, f.UserProfiles) + ToMesgs(&fit.Messages, fac, mesgnum.HrmProfile, f.HrmProfiles) + ToMesgs(&fit.Messages, fac, mesgnum.SdmProfile, f.SdmProfiles) + ToMesgs(&fit.Messages, fac, mesgnum.BikeProfile, f.BikeProfiles) + ToMesgs(&fit.Messages, fac, mesgnum.DeviceSettings, f.DeviceSettings) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + return fit +} diff --git a/profile/filedef/settings_test.go b/profile/filedef/settings_test.go new file mode 100644 index 00000000..2d239814 --- /dev/null +++ b/profile/filedef/settings_test.go @@ -0,0 +1,78 @@ +// 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 newSettingsMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileSettings)), + 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.UserProfile).WithFields( + factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(29)), + ), + factory.CreateMesgOnly(mesgnum.HrmProfile).WithFields( + factory.CreateField(mesgnum.HrmProfile, fieldnum.HrmProfileEnabled).WithValue(true), + ), + factory.CreateMesgOnly(mesgnum.SdmProfile).WithFields( + factory.CreateField(mesgnum.SdmProfile, fieldnum.SdmProfileEnabled).WithValue(true), + ), + factory.CreateMesgOnly(mesgnum.BikeProfile).WithFields( + factory.CreateField(mesgnum.BikeProfile, fieldnum.BikeProfileEnabled).WithValue(true), + ), + factory.CreateMesgOnly(mesgnum.DeviceSettings).WithFields( + factory.CreateField(mesgnum.DeviceSettings, fieldnum.DeviceSettingsBacklightMode).WithValue(uint8(typedef.BacklightModeAutoBrightness)), + ), + // 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 TestSettingsCorrectness(t *testing.T) { + mesgs := newSettingsMessageForTest(time.Now()) + + settings := filedef.NewSettings(mesgs...) + if settings.FileId.Type != typedef.FileSettings { + t.Fatalf("expected: %v, got: %v", typedef.FileSettings, settings.FileId.Type) + } + + fit := settings.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) + } +} diff --git a/profile/filedef/sport.go b/profile/filedef/sport.go new file mode 100644 index 00000000..273bdef9 --- /dev/null +++ b/profile/filedef/sport.go @@ -0,0 +1,103 @@ +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" +) + +// Sport files contain information about the user’s desired target zones. +type Sport struct { + FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + ZonesTargets []*mesgdef.ZonesTarget + Sport *mesgdef.Sport + HrZones []*mesgdef.HrZone + PowerZones []*mesgdef.PowerZone + MetZones []*mesgdef.MetZone + SpeedZones []*mesgdef.SpeedZone + CadenceZones []*mesgdef.CadenceZone + + UnrelatedMessages []proto.Message +} + +var _ File = &Sport{} + +func NewSport(mesgs ...proto.Message) *Sport { + f := &Sport{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Sport) 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.ZonesTarget: + f.ZonesTargets = append(f.ZonesTargets, mesgdef.NewZonesTarget(&mesg)) + case mesgnum.Sport: + f.Sport = mesgdef.NewSport(&mesg) + case mesgnum.HrZone: + f.HrZones = append(f.HrZones, mesgdef.NewHrZone(&mesg)) + case mesgnum.PowerZone: + f.PowerZones = append(f.PowerZones, mesgdef.NewPowerZone(&mesg)) + case mesgnum.MetZone: + f.MetZones = append(f.MetZones, mesgdef.NewMetZone(&mesg)) + case mesgnum.SpeedZone: + f.SpeedZones = append(f.SpeedZones, mesgdef.NewSpeedZone(&mesg)) + case mesgnum.CadenceZone: + f.CadenceZones = append(f.CadenceZones, mesgdef.NewCadenceZone(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Sport) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 2 // non slice fields + + size += len(f.ZonesTargets) + len(f.HrZones) + len(f.PowerZones) + + len(f.MetZones) + len(f.SpeedZones) + len(f.CadenceZones) + + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.ZonesTarget, f.ZonesTargets) + + if f.Sport != nil { + fit.Messages = append(fit.Messages, f.Sport.ToMesg(fac)) + } + + ToMesgs(&fit.Messages, fac, mesgnum.HrZone, f.HrZones) + ToMesgs(&fit.Messages, fac, mesgnum.PowerZone, f.PowerZones) + ToMesgs(&fit.Messages, fac, mesgnum.MetZone, f.MetZones) + ToMesgs(&fit.Messages, fac, mesgnum.SpeedZone, f.SpeedZones) + ToMesgs(&fit.Messages, fac, mesgnum.CadenceZone, f.CadenceZones) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + return fit +} diff --git a/profile/filedef/sport_test.go b/profile/filedef/sport_test.go new file mode 100644 index 00000000..c861cb56 --- /dev/null +++ b/profile/filedef/sport_test.go @@ -0,0 +1,84 @@ +// 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 newSportMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileSport)), + 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.ZonesTarget).WithFields( + factory.CreateField(mesgnum.ZonesTarget, fieldnum.ZonesTargetMaxHeartRate).WithValue(uint8(190)), + ), + factory.CreateMesgOnly(mesgnum.Sport).WithFields( + factory.CreateField(mesgnum.Sport, fieldnum.SportSport).WithValue(uint8(typedef.SportAmericanFootball)), + ), + factory.CreateMesgOnly(mesgnum.HrZone).WithFields( + factory.CreateField(mesgnum.HrZone, fieldnum.HrZoneHighBpm).WithValue(uint8(177)), + ), + factory.CreateMesgOnly(mesgnum.PowerZone).WithFields( + factory.CreateField(mesgnum.PowerZone, fieldnum.PowerZoneHighValue).WithValue(uint16(200)), + ), + factory.CreateMesgOnly(mesgnum.MetZone).WithFields( + factory.CreateField(mesgnum.MetZone, fieldnum.MetZoneHighBpm).WithValue(uint8(178)), + ), + factory.CreateMesgOnly(mesgnum.SpeedZone).WithFields( + factory.CreateField(mesgnum.SpeedZone, fieldnum.SpeedZoneHighValue).WithValue(uint16(10000)), + ), + factory.CreateMesgOnly(mesgnum.CadenceZone).WithFields( + factory.CreateField(mesgnum.CadenceZone, fieldnum.CadenceZoneHighValue).WithValue(uint8(100)), + ), + // 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 TestSportCorrectness(t *testing.T) { + mesgs := newSportMessageForTest(time.Now()) + + sport := filedef.NewSport(mesgs...) + if sport.FileId.Type != typedef.FileSport { + t.Fatalf("expected: %v, got: %v", typedef.FileSport, sport.FileId.Type) + } + + fit := sport.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) + } +} diff --git a/profile/filedef/totals.go b/profile/filedef/totals.go new file mode 100644 index 00000000..4cfc498d --- /dev/null +++ b/profile/filedef/totals.go @@ -0,0 +1,77 @@ +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" +) + +// Totals files are used to summarize a user’s activities and may contain multiple totals messages each representing +// summaries of a different activity type/sport +type Totals struct { + FileId mesgdef.FileId + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + Totals []*mesgdef.Totals + + UnrelatedMessages []proto.Message +} + +var _ File = &Totals{} + +func NewTotals(mesgs ...proto.Message) *Totals { + f := &Totals{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Totals) 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.Totals: + f.Totals = append(f.Totals, mesgdef.NewTotals(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Totals) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 3 // non slice fields + + size += len(f.Totals) + 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) + + ToMesgs(&fit.Messages, fac, mesgnum.Totals, f.Totals) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/totals_test.go b/profile/filedef/totals_test.go new file mode 100644 index 00000000..ec91ee90 --- /dev/null +++ b/profile/filedef/totals_test.go @@ -0,0 +1,66 @@ +// 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 newTotalsMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileTotals)), + 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.Totals).WithFields( + factory.CreateField(mesgnum.Totals, fieldnum.TotalsSport).WithValue(uint8(typedef.SportSoccer)), + ), + // 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 TestTotalsCorrectness(t *testing.T) { + mesgs := newTotalsMessageForTest(time.Now()) + + totals := filedef.NewTotals(mesgs...) + if totals.FileId.Type != typedef.FileTotals { + t.Fatalf("expected: %v, got: %v", typedef.FileTotals, totals.FileId.Type) + } + + fit := totals.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) + } +} diff --git a/profile/filedef/weight.go b/profile/filedef/weight.go new file mode 100644 index 00000000..dd4d3bce --- /dev/null +++ b/profile/filedef/weight.go @@ -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" +) + +// Weight contains time-stamped discrete measurement data of weight. +type Weight struct { + FileId mesgdef.FileId // required fields: type, manufacturer, product, serial_number + + // Developer Data Lookup + DeveloperDataIds []*mesgdef.DeveloperDataId + FieldDescriptions []*mesgdef.FieldDescription + + UserProfile *mesgdef.UserProfile + WeightScales []*mesgdef.WeightScale + DeviceInfos []*mesgdef.DeviceInfo + + UnrelatedMessages []proto.Message +} + +var _ File = &Weight{} + +func NewWeight(mesgs ...proto.Message) *Weight { + f := &Weight{} + for i := range mesgs { + f.Add(mesgs[i]) + } + + return f +} + +func (f *Weight) 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.WeightScale: + f.WeightScales = append(f.WeightScales, mesgdef.NewWeightScale(&mesg)) + case mesgnum.DeviceInfo: + f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg)) + default: + f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) + } +} + +func (f *Weight) ToFit(fac mesgdef.Factory) proto.Fit { + if fac == nil { + fac = factory.StandardFactory() + } + + var size = 2 // non slice fields + + size += len(f.WeightScales) + 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.WeightScale, f.WeightScales) + ToMesgs(&fit.Messages, fac, mesgnum.DeviceInfo, f.DeviceInfos) + + fit.Messages = append(fit.Messages, f.UnrelatedMessages...) + + SortMessagesByTimestamp(fit.Messages) + + return fit +} diff --git a/profile/filedef/weight_test.go b/profile/filedef/weight_test.go new file mode 100644 index 00000000..65ebcddf --- /dev/null +++ b/profile/filedef/weight_test.go @@ -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 newWeightMessageForTest(now time.Time) []proto.Message { + return []proto.Message{ + factory.CreateMesgOnly(mesgnum.FileId).WithFields( + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileWeight)), + 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.UserProfile).WithFields( + factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(27)), + ), + factory.CreateMesgOnly(mesgnum.WeightScale).WithFields( + factory.CreateField(mesgnum.WeightScale, fieldnum.WeightScaleBmi).WithValue(uint16(1000)), + ), + factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).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 TestWeightCorrectness(t *testing.T) { + mesgs := newWeightMessageForTest(time.Now()) + + weight := filedef.NewWeight(mesgs...) + if weight.FileId.Type != typedef.FileWeight { + t.Fatalf("expected: %v, got: %v", typedef.FileWeight, weight.FileId.Type) + } + + fit := weight.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) + } +} diff --git a/profile/filedef/workout.go b/profile/filedef/workout.go index ad3f7561..193cb16b 100644 --- a/profile/filedef/workout.go +++ b/profile/filedef/workout.go @@ -15,15 +15,15 @@ import ( // // ref: https://developer.garmin.com/fit/file-types/workout/ type Workout 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 - Workout *mesgdef.Workout - WorkoutSteps []*mesgdef.WorkoutStep + Workout *mesgdef.Workout // required fields: num_valid_steps + WorkoutSteps []*mesgdef.WorkoutStep // required fields: message_index, duration_type, target_type // Messages not related to Workout UnrelatedMessages []proto.Message diff --git a/profile/filedef/workout_test.go b/profile/filedef/workout_test.go index 6fb1e392..0da7ef06 100644 --- a/profile/filedef/workout_test.go +++ b/profile/filedef/workout_test.go @@ -55,7 +55,7 @@ func TestWorkoutCorrectness(t *testing.T) { workout := filedef.NewWorkout(mesgs...) if workout.FileId.Type != typedef.FileWorkout { - t.Fatalf("expected: %#v, got: %#v", typedef.FileActivity, workout.FileId.Type) + t.Fatalf("expected: %v, got: %v", typedef.FileActivity, workout.FileId.Type) } fit := workout.ToFit(nil) From ea92e5e52c102f66fe01f46c1669400ce888914c Mon Sep 17 00:00:00 2001 From: Hikmatulloh Hari Mukti Date: Tue, 2 Jan 2024 21:47:46 +0700 Subject: [PATCH 2/2] docs: update documentation --- docs/usage.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 841c5505..549eb636 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -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. @@ -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)