From 20bd95113f4bb13f7a14a233eed90b25109be40f Mon Sep 17 00:00:00 2001 From: Mukti Date: Mon, 9 Sep 2024 15:47:02 +0700 Subject: [PATCH 01/12] docs: update README.md (#410) --- README.md | 5 ++--- cmd/fitactivity/README.md | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 93c87c1..76b2861 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![CodeCov](https://codecov.io/gh/muktihari/fit/branch/master/graph/badge.svg)](https://codecov.io/gh/muktihari/fit) [![Go Report Card](https://goreportcard.com/badge/github.com/muktihari/fit)](https://goreportcard.com/report/github.com/muktihari/fit) [![Profile Version](https://img.shields.io/badge/profile-v21.141-lightblue.svg?style=flat)](https://developer.garmin.com/fit/download) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8731/badge)](https://www.bestpractices.dev/projects/8731) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/muktihari/fit/badge)](https://securityscorecards.dev/viewer/?uri=github.com/muktihari/fit) This project hosts the Go implementation for [The Flexible and Interoperable Data Transfer (FIT) protocol](https://developer.garmin.com/fit), which is a protocol developed by Garmin for storing and sharing data originating from sports, fitness, and health devices. Activities recorded using devices such as smartwatch and cycling computer are now mostly in a FIT file format (\*.fit). @@ -14,7 +13,7 @@ This project hosts the Go implementation for [The Flexible and Interoperable Dat The FIT protocol, known for its compact size as a binary file format, is the preferred choice for manufacturers to use in their embedded devices. However, despite its widespread adoption, Garmin has not yet released an official SDK for Go, and existing third-party libraries for decoding and encoding the FIT protocol lack the semantics of the official SDK. -One of the key semantics they are missing is the ability for users to retrieve raw protocol messages; instead, they decode directly into predefined message structs grouped by [common file types](https://developer.garmin.com/fit/file-types) which makes users unable to extend some functionalities. Furthermore, existing third-party libraries do not seem to fully support FIT Protocol V2, and their ability to produce variant options of the FIT protocol is limited. For instance, creating FIT files with compressed timestamps or FIT files with multiple local message types, which significantly reduces the resulting FIT files' size, is missing. +One of the key semantics they are missing is the ability for users to retrieve protocol messages. Instead, these messages are unmarshalled directly into predefined structures based on [common file types](https://developer.garmin.com/fit/file-types). This results in the loss of the message arrival order, inability to handle 'unknown messages', and limits the ability to extend certain functionalities. Furthermore, existing third-party libraries do not seem to fully support FIT Protocol V2, and their ability to produce variant options of the FIT protocol is limited. For instance, creating FIT files with compressed timestamps or FIT files with multiple local message types, which significantly reduces the resulting FIT files' size, is missing. Without diminishing respect for the existing libraries created nearly a decade ago, at a time when the capabilities of Go were limited, we believe a new approach is necessary. This is where this SDK comes in, bridging the gap and enabling Go developers to seamlessly interact with the FIT protocol. @@ -292,7 +291,7 @@ Here is the sample of what **Developer Fields** would look like in a **.fit** th We provide some CLI programs to interact with FIT files that can be found in [cmd](/cmd/doc.go) folder. -1. **fitactivity**: Combines multiple FIT activity files into one continuous FIT activity (and conceal the start and end GPS positions for privacy). [README.md](/cmd/fitactivity/README.md) +1. **fitactivity**: A program to manage FIT activity files, including combining multiple FIT activity files into a single continuous activity, concealing start and end GPS positions for privacy, and reducing or removing messages. [README.md](/cmd/fitactivity/README.md) 2. **fitconv**: Converts FIT files to CSV format, enabling us to read the FIT data in a human-readable format. Conversely, it also converts CSV files back to FIT format, enabling us to create or edit FIT files in CSV form. The programs is designed to work seamlessly with CSVs produced by the Official FIT SDK's _FitCSVTool.jar_. [README.md](/cmd/fitconv/README.md) 3. **fitprint**: Generates comprehensive human-readable **\*.txt** file containing details extracted from FIT files. [README.md](/cmd/fitprint/README.md) 4. **fitdump**: Dumps the FIT file(s) into segmented bytes in a **\*.txt** file format. [README.md](/cmd/fitdump/README.md) diff --git a/cmd/fitactivity/README.md b/cmd/fitactivity/README.md index 10105aa..bf8e6cd 100644 --- a/cmd/fitactivity/README.md +++ b/cmd/fitactivity/README.md @@ -1,6 +1,8 @@ # FIT Activity CLI -A program to handle FIT files based on provided command: +A program to manage FIT activity files, including combining multiple FIT activity files into a single continuous activity, concealing start and end GPS positions for privacy, and reducing or removing messages. + +Available commands: 1. **combine**: combine multiple activities into one continuous activity. 1. **conceal**: conceal first or last x meters GPS positions for privacy. From 06762d7232cce79c73c345d0b01e05778c8d2ca4 Mon Sep 17 00:00:00 2001 From: Mukti Date: Tue, 10 Sep 2024 00:21:15 +0700 Subject: [PATCH 02/12] cli: fitactivity improve concealer (#411) * feat: imporove fitactivity concealer * docs: update README.md --- cmd/fitactivity/README.md | 10 +- cmd/fitactivity/concealer/conceal_test.go | 668 ++++++++++++++++++++++ cmd/fitactivity/concealer/concealer.go | 313 +++++----- cmd/fitactivity/main.go | 8 +- 4 files changed, 830 insertions(+), 169 deletions(-) create mode 100644 cmd/fitactivity/concealer/conceal_test.go diff --git a/cmd/fitactivity/README.md b/cmd/fitactivity/README.md index bf8e6cd..53e4bd7 100644 --- a/cmd/fitactivity/README.md +++ b/cmd/fitactivity/README.md @@ -82,11 +82,11 @@ _NOTE: Combining FIT activity files is NOT the same as merging multiple files in ## How We Conceal GPS Positions 1. Conceal Start Position - We will iterate from the beginning of FIT Messages up to the desired conceal distance and for every record found, we will remove the `position_lat` and `position_long` fields. And also, we will update the corresponding session fields: `start_position_lat` and `start_position_long`. + We will iterate from the beginning of FIT Messages up to the desired conceal distance and for every record found, we will remove the `position_lat` and `position_long` fields. 1. Conceal End Position - We will backward-iterate from the end of the FIT messages up to the desired conceal distance and for every record found, we will remove the `position_lat` and `position_long` fields. And also, we will update the corresponding session fields: `end_position_lat` and `end_position_long`. + We will backward-iterate from the end of the FIT messages up to the desired conceal distance and for every record found, we will remove the `position_lat` and `position_long` fields. -We will remove `start_position_lat`, `start_position_long`, `end_position_lat`, and `end_position_long` fields from Laps. But why? GPS Positions saved in lap messages can be vary, user may set new lap every 500m or new lap every 1 hour for example, we don't know the exact distance for each lap. If user want to conceal 1km, we need to find all laps within the conceal distance and decide whether to remove it or change it with new positions, this will add complexity. So, let's just remove it for now, if our upload target is Strava, they don't specify positions in lap message anyway. +For Laps and Sessions, we will update these following fields: `start_position_lat`, `start_position_long`, `end_position_lat`, and `end_position_long` to match the new desired positions. ## How We Reduce Record Messages @@ -106,6 +106,10 @@ We reduce record messages based on provided method, you can only select one of t The reduced record messages are simply removed; no aggregation is performed. +## How We Remove Messages + +Removing messages in straightforward, we will remove messages based on given message numbers. For instance, giving **160** as input will remove **gps_metadata** message ([List os message numbers ](../../profile/typedef/mesg_num_gen.go)). For unknown messages, we consider message that are not defined by Global Profile (Profile.xlsx) as unknown messages. For developer data, we will remove `developer_data_id` and `field_description` messages, additionally all `DeveloperFields` in all messages will be removed as well. + ## Install or Build _Prerequisite: Install golang: [https://go.dev/doc/install](https://go.dev/doc/install)_ diff --git a/cmd/fitactivity/concealer/conceal_test.go b/cmd/fitactivity/concealer/conceal_test.go new file mode 100644 index 0000000..bb3c9f5 --- /dev/null +++ b/cmd/fitactivity/concealer/conceal_test.go @@ -0,0 +1,668 @@ +package concealer + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/muktihari/fit/factory" + "github.com/muktihari/fit/profile/untyped/fieldnum" + "github.com/muktihari/fit/profile/untyped/mesgnum" + "github.com/muktihari/fit/proto" +) + +func TestConceal(t *testing.T) { + tt := []struct { + name string + mesgs []proto.Message + startThreshold uint32 + endThreshold uint32 + expected []proto.Message + }{ + { + name: "conceal overlap (full conceal), 4 records 2 laps, conceal up to 3rd record, 1st lap fully concealed, 2nd lap partially, session changed", + mesgs: []proto.Message{ + 0: {Num: mesgnum.FileId}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + 3: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(1)), + }}, + 4: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + 5: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + 6: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + 7: {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong).WithValue(int32(3)), + }}, + 8: {Num: mesgnum.Activity}, + }, + startThreshold: 3, + endThreshold: 3, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + }}, + {Num: mesgnum.Activity}, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + Conceal(tc.mesgs, tc.startThreshold, tc.endThreshold) + if diff := cmp.Diff(tc.mesgs, tc.expected, + cmp.Transformer("Value", func(v proto.Value) interface{} { return v.Any() }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} + +func TestConcealStartPosition(t *testing.T) { + tt := []struct { + name string + mesgs []proto.Message + lapIndices []int + sessionIndices []int + threshold uint32 + expectedIndex int + expected []proto.Message + }{ + {name: "threshold zero", threshold: 0}, + { + name: "4 records 2 laps, conceal up to 3rd record, 1st lap fully concealed, 2nd lap partially, session changed", + mesgs: []proto.Message{ + 0: {Num: mesgnum.FileId}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + 3: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(1)), + }}, + 4: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + 5: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + 6: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + 7: {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong).WithValue(int32(3)), + }}, + 8: {Num: mesgnum.Activity}, + }, + lapIndices: []int{3, 6}, + sessionIndices: []int{7}, + threshold: 3, + expectedIndex: 5, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + // GPS Positions are fully concealed. + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(3)), // New Start Lat + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(3)), // New Start Long + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Activity}, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + lastConcealStartIndex := concealStartPosition(tc.mesgs, tc.lapIndices, tc.sessionIndices, tc.threshold) + if lastConcealStartIndex != tc.expectedIndex { + t.Fatalf("expected last conceal start's record index is: %d, got: %d", + tc.expectedIndex, lastConcealStartIndex) + } + if diff := cmp.Diff(tc.mesgs, tc.expected, + cmp.Transformer("Value", func(v proto.Value) interface{} { return v.Any() }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} + +func TestUpdateStartPosition(t *testing.T) { + tt := []struct { + name string + mesgs []proto.Message + indices []int + fieldNums placeholder + recordIndex int + expected []proto.Message + }{ + { + name: "laps: 4 records 2 laps, conceal up to 3rd record, 1st lap fully concealed, 2nd lap partially", + mesgs: []proto.Message{ + 0: {Num: mesgnum.FileId}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + 3: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(1)), + }}, + 4: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + 5: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + 6: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + 7: {Num: mesgnum.Activity}, + }, + indices: []int{3, 6}, + fieldNums: lapPlaceholder, + recordIndex: 5, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + // GPS Positions are fully concealed. + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(3)), // New Start Lat + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(3)), // New Start Long + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Activity}, + }, + }, + { + name: "1 laps, 2 records all without gps", + mesgs: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Activity}, + }, + indices: []int{3}, + fieldNums: lapPlaceholder, + recordIndex: 2, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapTotalTimerTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Activity}, + }, + }, + } + + for i, tc := range tt { + if i != len(tt)-1 { + continue + } + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + updateStartPosition(tc.mesgs, tc.indices, tc.fieldNums, tc.recordIndex) + if diff := cmp.Diff(tc.mesgs, tc.expected, + cmp.Transformer("Value", func(v proto.Value) interface{} { return v.Any() }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} + +func TestConcealEndPosition(t *testing.T) { + tt := []struct { + name string + mesgs []proto.Message + lapIndices []int + sessionIndices []int + threshold uint32 + expected []proto.Message + }{ + {name: "threshold zero", threshold: 0}, + { + name: "laps: 4 records 2 laps, conceal up to 3rd record, 1st lap fully concealed, 2nd lap partially, session changed", + mesgs: []proto.Message{ + 0: {Num: mesgnum.FileId}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + 3: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(1)), + }}, + 4: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + 5: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + 6: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + 7: {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong).WithValue(int32(3)), + }}, + 8: {Num: mesgnum.Activity}, + }, + lapIndices: []int{3, 6}, + sessionIndices: []int{7}, + threshold: 3, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(0)), // New Start Lat + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(0)), // New Start Long + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalTimerTime).WithValue(uint32(4)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong).WithValue(int32(0)), + }}, + {Num: mesgnum.Activity}, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + concealEndPosition(tc.mesgs, tc.lapIndices, tc.sessionIndices, 0, tc.threshold) + if diff := cmp.Diff(tc.mesgs, tc.expected, + cmp.Transformer("Value", func(v proto.Value) interface{} { return v.Any() }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} + +func TestUpdateEndPosition(t *testing.T) { + tt := []struct { + name string + mesgs []proto.Message + indices []int + fieldNums placeholder + startRecordIndex int + recordIndex int + expected []proto.Message + }{ + { + name: "laps: 4 records 2 laps, conceal up to 3rd record, 1st lap fully concealed, 2nd lap partially", + mesgs: []proto.Message{ + 0: {Num: mesgnum.FileId}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + 3: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(1)), + }}, + 4: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + 5: {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + 6: {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(2)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(3)), + }}, + 7: {Num: mesgnum.Activity}, + }, + indices: []int{3, 6}, + fieldNums: lapPlaceholder, + startRecordIndex: 0, + recordIndex: 1, + expected: []proto.Message{ + {Num: mesgnum.FileId}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(0)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(1)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartPositionLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLat).WithValue(int32(0)), // New Start Lat + factory.CreateField(mesgnum.Lap, fieldnum.LapEndPositionLong).WithValue(int32(0)), // New Start Long + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(2)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(2)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(3)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(3)), + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ + factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(uint32(3)), + factory.CreateField(mesgnum.Lap, fieldnum.LapStartTime).WithValue(uint32(2)), + }}, + {Num: mesgnum.Activity}, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + updateEndPosition(tc.mesgs, tc.indices, tc.fieldNums, tc.startRecordIndex, tc.recordIndex) + if diff := cmp.Diff(tc.mesgs, tc.expected, + cmp.Transformer("Value", func(v proto.Value) interface{} { return v.Any() }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} diff --git a/cmd/fitactivity/concealer/concealer.go b/cmd/fitactivity/concealer/concealer.go index d77afb0..fed1ea1 100644 --- a/cmd/fitactivity/concealer/concealer.go +++ b/cmd/fitactivity/concealer/concealer.go @@ -5,205 +5,196 @@ package concealer import ( - "github.com/muktihari/fit/factory" "github.com/muktihari/fit/profile/basetype" "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/profile/untyped/mesgnum" "github.com/muktihari/fit/proto" ) -// Conceal removes coordinates (lat, long) as far as start distance and end distance from the given FIT file. -// If startDistance and endDistance == 0, it will not do anything, nil will be returned. -// This will also remove StartPositionLat, StartPositionLong, EndPositionLat and EndPositionLong from any lap messages. -func Conceal(fit *proto.FIT, startDistance, endDistance uint32) error { - if err := concealPositionStart(fit, startDistance); err != nil { - return err - } - if err := concealPositionEnd(fit, endDistance); err != nil { - return err +// Conceal removes Latitude and Longitude data of first and last meters. +// Affected Laps and Sessions will be updated accordingly. +func Conceal(mesgs []proto.Message, first, last uint32) { + var lapIndices, sessionIndices []int + for i := range mesgs { + switch mesgs[i].Num { + case mesgnum.Lap: + lapIndices = append(lapIndices, i) + case mesgnum.Session: + sessionIndices = append(sessionIndices, i) + } } - return nil + lastIndex := concealStartPosition(mesgs, lapIndices, sessionIndices, first) + concealEndPosition(mesgs, lapIndices, sessionIndices, lastIndex, last) } -// concealPositionStart removes coordinates (lat, long) as far as start distance from the given FIT file. -// If concealDistance == 0, it will not do anything, nil will be returned. -func concealPositionStart(fit *proto.FIT, concealDistance uint32) error { - if concealDistance == 0 { - return nil - } - - var sessionIndex = -1 - var newStartRecordIndex = -1 +// placeholder for replacing Lap and Session field numbers since both share +// the same field name but have different field numbers. +type placeholder struct { + startTime byte + totalTimerTime byte + startPositionLat byte + startPositionLong byte + endPositionLat byte + endPositionLong byte +} -loop: - for i := range fit.Messages { - switch fit.Messages[i].Num { - case mesgnum.Lap: - fit.Messages[i].RemoveFieldByNum(fieldnum.LapStartPositionLat) - fit.Messages[i].RemoveFieldByNum(fieldnum.LapStartPositionLong) - case mesgnum.Session: - if sessionIndex == -1 { - sessionIndex = i - } - case mesgnum.Record: - if fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLat).Int32() == basetype.Sint32Invalid || - fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLong).Int32() == basetype.Sint32Invalid { - continue loop - } +var lapPlaceholder = placeholder{ + startTime: fieldnum.LapStartTime, + totalTimerTime: fieldnum.LapTotalTimerTime, + startPositionLat: fieldnum.LapStartPositionLat, + startPositionLong: fieldnum.LapStartPositionLong, + endPositionLat: fieldnum.LapEndPositionLat, + endPositionLong: fieldnum.LapEndPositionLong, +} - distance := fit.Messages[i].FieldValueByNum(fieldnum.RecordDistance).Uint32() - if distance != basetype.Uint32Invalid { - continue loop - } - if distance > concealDistance { - newStartRecordIndex = i - break loop - } +var sessionPlaceholder = placeholder{ + startTime: fieldnum.SessionStartTime, + totalTimerTime: fieldnum.SessionTotalTimerTime, + startPositionLat: fieldnum.SessionStartPositionLat, + startPositionLong: fieldnum.SessionStartPositionLong, + endPositionLat: fieldnum.SessionEndPositionLat, + endPositionLong: fieldnum.SessionEndPositionLong, +} - fit.Messages[i].RemoveFieldByNum(fieldnum.RecordPositionLat) - fit.Messages[i].RemoveFieldByNum(fieldnum.RecordPositionLong) - } +func concealStartPosition(mesgs []proto.Message, lapIndices, sessionIndices []int, threshold uint32) (lastConcealStartIndex int) { + if threshold == 0 { + return 0 } - if sessionIndex == -1 { // no session found during first iteration - for i := newStartRecordIndex + 1; i < len(fit.Messages); i++ { - if fit.Messages[i].Num == mesgnum.Session { - sessionIndex = i - break + lastConcealStartIndex = -1 + for i := range mesgs { + mesg := &mesgs[i] + if mesg.Num == mesgnum.Record { + d := mesg.FieldValueByNum(fieldnum.RecordDistance).Uint32() + if d < threshold { + mesg.RemoveFieldByNum(fieldnum.RecordPositionLat) + mesg.RemoveFieldByNum(fieldnum.RecordPositionLong) + continue } - } - if sessionIndex == -1 { - return nil // no session found to update + lastConcealStartIndex = i + break } } - if newStartRecordIndex == -1 { // all record are concealed - fit.Messages[sessionIndex].RemoveFieldByNum(fieldnum.SessionEndPositionLat) - fit.Messages[sessionIndex].RemoveFieldByNum(fieldnum.SessionEndPositionLong) - } else { - newLat, newLong := basetype.Sint32Invalid, basetype.Sint32Invalid - for i := newStartRecordIndex; i < len(fit.Messages); i++ { // find new start record that has newLat and long - newLat = fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLat).Int32() - newLong = fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLong).Int32() - if newLat != basetype.Sint32Invalid && newLong != basetype.Sint32Invalid { - break - } + updateStartPosition(mesgs, lapIndices, lapPlaceholder, lastConcealStartIndex) // Update Laps + updateStartPosition(mesgs, sessionIndices, sessionPlaceholder, lastConcealStartIndex) // Update Sessions + + return lastConcealStartIndex +} + +// updateStartPosition update start position of Laps or Sessions. +func updateStartPosition(mesgs []proto.Message, indices []int, ph placeholder, recordIndex int) { + var rec proto.Message + if recordIndex != -1 { + rec = mesgs[recordIndex] + } + + var ( + recTimestamp = rec.FieldValueByNum(fieldnum.RecordTimestamp).Uint32() + recLat = rec.FieldValueByNum(fieldnum.RecordPositionLat).Int32() + recLong = rec.FieldValueByNum(fieldnum.RecordPositionLong).Int32() + ) + for i := range indices { + var ( + mesg = &mesgs[indices[i]] + startTime = mesg.FieldValueByNum(ph.startTime).Uint32() + totalTimerTime = mesg.FieldValueByNum(ph.totalTimerTime).Uint32() + ) + if startTime == basetype.Uint32Invalid || totalTimerTime == basetype.Uint32Invalid || + startTime+totalTimerTime < recTimestamp { + mesg.RemoveFieldByNum(ph.startPositionLat) + mesg.RemoveFieldByNum(ph.startPositionLong) + mesg.RemoveFieldByNum(ph.endPositionLat) + mesg.RemoveFieldByNum(ph.endPositionLong) + continue } - fieldStartPositionLat := fit.Messages[sessionIndex].FieldByNum(fieldnum.SessionStartPositionLat) - if fieldStartPositionLat == nil { - fit.Messages[sessionIndex].Fields = append(fit.Messages[sessionIndex].Fields, - factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLat), - ) - lastIndex := len(fit.Messages[sessionIndex].Fields) - 1 - fieldStartPositionLat = &fit.Messages[sessionIndex].Fields[lastIndex] + if recLat == basetype.Sint32Invalid { + mesg.RemoveFieldByNum(ph.startPositionLat) + } else if field := mesg.FieldByNum(ph.startPositionLat); field != nil { + field.Value = proto.Int32(recLat) } - fieldStartPositionLat.Value = proto.Int32(newLat) - - fieldStartPositionLong := fit.Messages[sessionIndex].FieldByNum(fieldnum.SessionStartPositionLong) - if fieldStartPositionLong == nil { - fit.Messages[sessionIndex].Fields = append(fit.Messages[sessionIndex].Fields, - factory.CreateField(mesgnum.Session, fieldnum.SessionStartPositionLong), - ) - lastIndex := len(fit.Messages[sessionIndex].Fields) - 1 - fieldStartPositionLong = &fit.Messages[sessionIndex].Fields[lastIndex] + + if recLong == basetype.Sint32Invalid { + mesg.RemoveFieldByNum(ph.startPositionLong) + } else if field := mesg.FieldByNum(ph.startPositionLong); field != nil { + field.Value = proto.Int32(recLong) } - fieldStartPositionLong.Value = proto.Int32(newLong) + break } - - return nil } -// concealPositionEnd removes coordinates (lat, long) as far as end distance from the given FIT file. -// If concealDistance == 0, it will not do anything, nil will be returned. -func concealPositionEnd(fit *proto.FIT, concealDistance uint32) error { - if concealDistance == 0 { - return nil +func concealEndPosition(mesgs []proto.Message, lapIndices, sessionIndices []int, lastConcealStartIndex int, threshold uint32) { + if threshold == 0 { + return } - var sessionIndex = -1 - var newEndRecordIndex = -1 - var lastDistance uint32 - -loop: - for i := len(fit.Messages) - 1; i >= 0; i-- { - switch fit.Messages[i].Num { - case mesgnum.Lap: - fit.Messages[i].RemoveFieldByNum(fieldnum.LapEndPositionLat) - fit.Messages[i].RemoveFieldByNum(fieldnum.LapEndPositionLong) - case mesgnum.Session: - if sessionIndex == -1 { - sessionIndex = i - } - case mesgnum.Record: - if fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLat).Int32() == basetype.Sint32Invalid || - fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLong).Int32() == basetype.Sint32Invalid { - continue loop + var ( + lastConcealEndIndex = -1 + lastRecDist = basetype.Uint32Invalid + ) + for i := len(mesgs) - 1; i >= 0; i-- { + mesg := &mesgs[i] + if mesg.Num == mesgnum.Record { + d := mesg.FieldValueByNum(fieldnum.RecordDistance).Uint32() + if lastRecDist == basetype.Uint32Invalid { + lastRecDist = d } - distance := fit.Messages[i].FieldValueByNum(fieldnum.RecordDistance).Uint32() - if distance != basetype.Uint32Invalid { - continue loop - } - - if lastDistance == 0 { // first valid last distance - lastDistance = distance + if lastRecDist-d < threshold { + mesg.RemoveFieldByNum(fieldnum.RecordPositionLat) + mesg.RemoveFieldByNum(fieldnum.RecordPositionLong) + continue } + lastConcealEndIndex = i + break + } + } - if lastDistance-distance > concealDistance { - newEndRecordIndex = i - break loop - } + updateEndPosition(mesgs, lapIndices, lapPlaceholder, lastConcealStartIndex, lastConcealEndIndex) // Update Laps + updateEndPosition(mesgs, sessionIndices, sessionPlaceholder, lastConcealStartIndex, lastConcealEndIndex) // Update Sessions +} - fit.Messages[i].RemoveFieldByNum(fieldnum.RecordPositionLat) - fit.Messages[i].RemoveFieldByNum(fieldnum.RecordPositionLong) - } +// updateEndPosition update end position of Laps or Sessions. +func updateEndPosition(mesgs []proto.Message, indices []int, ph placeholder, lastConcealStartIndex, recordIndex int) { + var rec proto.Message + if recordIndex != -1 { + rec = mesgs[recordIndex] } - if sessionIndex == -1 { // no session found during first iteration - for i := newEndRecordIndex - 1; i >= 0; i-- { // find session to update - if fit.Messages[i].Num == mesgnum.Session { - sessionIndex = i - break - } + var ( + recTimestamp = rec.FieldValueByNum(fieldnum.RecordTimestamp).Uint32() + recLat = rec.FieldValueByNum(fieldnum.RecordPositionLat).Int32() + recLong = rec.FieldValueByNum(fieldnum.RecordPositionLong).Int32() + ) + for i := len(indices) - 1; i >= 0; i-- { + var ( + mesg = &mesgs[indices[i]] + startTime = mesg.FieldValueByNum(ph.startTime).Uint32() + ) + if startTime == basetype.Uint32Invalid || startTime > recTimestamp { + mesg.RemoveFieldByNum(ph.startPositionLat) + mesg.RemoveFieldByNum(ph.startPositionLong) + mesg.RemoveFieldByNum(ph.endPositionLat) + mesg.RemoveFieldByNum(ph.endPositionLong) + continue } - if sessionIndex == -1 { - return nil // no session found to update - } - } - if newEndRecordIndex == -1 { // all record are concealed - fit.Messages[sessionIndex].RemoveFieldByNum(fieldnum.SessionEndPositionLat) - fit.Messages[sessionIndex].RemoveFieldByNum(fieldnum.SessionEndPositionLong) - } else { - newLat, newLong := basetype.Sint32Invalid, basetype.Sint32Invalid - for i := newEndRecordIndex; i > 0; i++ { // find new end record that has newLat and long - newLat = fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLat).Int32() - newLong = fit.Messages[i].FieldValueByNum(fieldnum.RecordPositionLong).Int32() - if newLat != basetype.Sint32Invalid && newLong != basetype.Sint32Invalid { - break - } + if lastConcealStartIndex > recordIndex { // Overlap + mesg.RemoveFieldByNum(ph.startPositionLat) + mesg.RemoveFieldByNum(ph.startPositionLong) } - fieldEndPositionLat := fit.Messages[sessionIndex].FieldByNum(fieldnum.SessionEndPositionLat) - if fieldEndPositionLat == nil { - fit.Messages[sessionIndex].Fields = append(fit.Messages[sessionIndex].Fields, - factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLat), - ) - lastIndex := len(fit.Messages[sessionIndex].Fields) - 1 - fieldEndPositionLat = &fit.Messages[sessionIndex].Fields[lastIndex] + if recLat == basetype.Sint32Invalid { + mesg.RemoveFieldByNum(ph.endPositionLat) + } else if field := mesg.FieldByNum(ph.endPositionLat); field != nil { + field.Value = proto.Int32(recLat) } - fieldEndPositionLat.Value = proto.Int32(newLat) - - fieldEndPositionLong := fit.Messages[sessionIndex].FieldByNum(fieldnum.SessionEndPositionLong) - if fieldEndPositionLong == nil { - fit.Messages[sessionIndex].Fields = append(fit.Messages[sessionIndex].Fields, - factory.CreateField(mesgnum.Session, fieldnum.SessionEndPositionLong), - ) - lastIndex := len(fit.Messages[sessionIndex].Fields) - 1 - fieldEndPositionLong = &fit.Messages[sessionIndex].Fields[lastIndex] + + if recLong == basetype.Sint32Invalid { + mesg.RemoveFieldByNum(ph.endPositionLong) + } else if field := mesg.FieldByNum(ph.endPositionLong); field != nil { + field.Value = proto.Int32(recLong) } - fieldEndPositionLong.Value = proto.Int32(newLong) + break } - - return nil } diff --git a/cmd/fitactivity/main.go b/cmd/fitactivity/main.go index e4b6fec..322da2d 100644 --- a/cmd/fitactivity/main.go +++ b/cmd/fitactivity/main.go @@ -55,7 +55,7 @@ const ( ) var mainUsage = `About: - ` + cli + ` is a program to handle FIT files based on provided command. + ` + cli + ` is a program to manage FIT activity files based on provided command. Usage: ` + cli + ` [command] @@ -334,7 +334,7 @@ loop: formatThousand(int(concealFirst)), formatThousand(int(concealLast))) verboserun(msg, func() { - err = concealer.Conceal(fit, uint32(concealFirst)*100, uint32(concealLast)*100) + concealer.Conceal(fit.Messages, uint32(concealFirst)*100, uint32(concealLast)*100) }) if err != nil { return err @@ -521,9 +521,7 @@ func conceal(fs *flag.FlagSet, args []string) (err error) { verboserun(fmt.Sprintf("[%d] Concealing", i), func() { for _, fit := range fits { - if err = concealer.Conceal(fit, uint32(first)*100, uint32(last)*100); err != nil { - return - } + concealer.Conceal(fit.Messages, uint32(first)*100, uint32(last)*100) } }) if err != nil { From 26acfd75cb4f2d9d3eff91a9a02cf6ede5ed1e94 Mon Sep 17 00:00:00 2001 From: Mukti Date: Tue, 10 Sep 2024 11:15:27 +0700 Subject: [PATCH 03/12] fix: limit concurrently to the lesser of len(paths) or num CPU (#412) --- cmd/fitactivity/main.go | 2 +- cmd/fitactivity/opener/opener.go | 76 ++++++++++++++++++--------- cmd/fitactivity/opener/opener_test.go | 34 ++++++++++++ 3 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 cmd/fitactivity/opener/opener_test.go diff --git a/cmd/fitactivity/main.go b/cmd/fitactivity/main.go index 322da2d..5aecf0c 100644 --- a/cmd/fitactivity/main.go +++ b/cmd/fitactivity/main.go @@ -196,7 +196,7 @@ func combine(fs *flag.FlagSet, args []string) (err error) { subcommandRemove = "remove" ) - subcommands := make([]string, 0, 2) + var subcommands []string var i int loop: for i = range args { diff --git a/cmd/fitactivity/opener/opener.go b/cmd/fitactivity/opener/opener.go index bfae762..4f483ac 100644 --- a/cmd/fitactivity/opener/opener.go +++ b/cmd/fitactivity/opener/opener.go @@ -7,80 +7,104 @@ package opener import ( "context" "os" + "runtime" "sync" "github.com/muktihari/fit/decoder" + "github.com/muktihari/fit/profile/mesgdef" "github.com/muktihari/fit/profile/typedef" "github.com/muktihari/fit/proto" ) -// Open opens all given paths concurrently. -func Open(paths []string) ([]*proto.FIT, error) { +var numCPU = runtime.NumCPU() + +// Open opens all paths concurrently using a number of workers equal to the lesser value of len(paths) or runtime.NumCPU(). +func Open(paths []string) (fits []*proto.FIT, err error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - resultc := make(chan result, len(paths)) - var wg sync.WaitGroup + n := len(paths) + if n > numCPU { + n = numCPU + } + + var ( + jobs = make(chan string, n) + results = make(chan result, n) + ) - wg.Add(len(paths)) - for i := range paths { - path := paths[i] - go worker(ctx, path, resultc, &wg) + for i := 0; i < n; i++ { + go worker(ctx, jobs, results) } go func() { - wg.Wait() - close(resultc) + for _, path := range paths { + jobs <- path + } + close(jobs) }() - fits := make([]*proto.FIT, 0, len(paths)) - for res := range resultc { + // Most files has one sequence of FIT activity, grow as needed. + fits = make([]*proto.FIT, 0, len(paths)) + for i := 0; i < len(paths); i++ { + res := <-results if res.err != nil { return nil, res.err } - fits = append(fits, res.fit) + fits = append(fits, res.fits...) } return fits, nil } type result struct { - fit *proto.FIT - err error + fits []*proto.FIT + err error } -func worker(ctx context.Context, path string, resultc chan<- result, wg *sync.WaitGroup) { - defer wg.Done() +func worker(ctx context.Context, jobs <-chan string, results chan<- result) { + for path := range jobs { + fits, err := decode(ctx, path) + results <- result{fits: fits, err: err} + } +} + +var pool = sync.Pool{New: func() interface{} { return decoder.New(nil) }} +func decode(ctx context.Context, path string) (fits []*proto.FIT, err error) { f, err := os.Open(path) if err != nil { - resultc <- result{err: err} return } defer f.Close() - dec := decoder.New(f) + dec := pool.Get().(*decoder.Decoder) + defer pool.Put(dec) + + dec.Reset(f) for dec.Next() { - fileId, err := dec.PeekFileId() + var fileId *mesgdef.FileId + fileId, err = dec.PeekFileId() if err != nil { - resultc <- result{err: err} return } if fileId.Type != typedef.FileActivity { - if err := dec.Discard(); err != nil { - resultc <- result{err: err} + if err = dec.Discard(); err != nil { + return } continue } - fit, err := dec.DecodeWithContext(ctx) + var fit *proto.FIT + fit, err = dec.DecodeWithContext(ctx) if err != nil { - resultc <- result{err: err} return } - resultc <- result{fit: fit} + fits = append(fits, fit) } + + return } diff --git a/cmd/fitactivity/opener/opener_test.go b/cmd/fitactivity/opener/opener_test.go new file mode 100644 index 0000000..e2f6b70 --- /dev/null +++ b/cmd/fitactivity/opener/opener_test.go @@ -0,0 +1,34 @@ +package opener + +import ( + "path/filepath" + "runtime" + "testing" +) + +var ( + _, filename, _, _ = runtime.Caller(0) + cd = filepath.Dir(filename) + testdata = filepath.Join(cd, "..", "..", "..", "testdata") + fromGarminForums = filepath.Join(testdata, "from_garmin_forums") + fromOfficialSDK = filepath.Join(testdata, "from_official_sdk") +) + +func TestOpen(t *testing.T) { + numCPU = 2 // Override NumCPU + + paths := []string{ + filepath.Join(fromGarminForums, "triathlon_summary_first.fit"), + filepath.Join(fromGarminForums, "triathlon_summary_last.fit"), + filepath.Join(fromOfficialSDK, "activity_developerdata.fit"), + filepath.Join(fromOfficialSDK, "Activity.fit"), + } + + fits, err := Open(paths) + if err != nil { + t.Fatalf("expected error nil, got: %v", err) + } + if len(paths) != len(fits) { + t.Fatalf("expected len(fits) is %d, got: %d", len(paths), len(fits)) + } +} From 6ce504387c80af0fd9c80f748035141addebfac8 Mon Sep 17 00:00:00 2001 From: Mukti Date: Tue, 10 Sep 2024 11:20:09 +0700 Subject: [PATCH 04/12] chore: fitactivity fix typo (#413) --- cmd/fitactivity/aggregator/aggregator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/fitactivity/aggregator/aggregator.go b/cmd/fitactivity/aggregator/aggregator.go index 8f5c932..1b94eeb 100644 --- a/cmd/fitactivity/aggregator/aggregator.go +++ b/cmd/fitactivity/aggregator/aggregator.go @@ -44,7 +44,7 @@ func Aggregate[T any](dst, src T) { case strings.HasPrefix(f.Name, "Max") || strings.HasPrefix(f.Name, "EnhancedMax"): max(dv.Field(i), sv.Field(i)) // MaxHeartRate, MaxCadence, EnhancedMaxRespirationRate, etc. case strings.HasPrefix(f.Name, "Min") || strings.HasPrefix(f.Name, "EnhancedMin"): - min(dv.Field(i), sv.Field(i)) // MinHeartRate, MinCadence, EnhancedAltitude, etc. + min(dv.Field(i), sv.Field(i)) // MinHeartRate, MinCadence, EnhancedMinAltitude, etc. case strings.HasPrefix(f.Name, "Avg") || strings.HasPrefix(f.Name, "EnhancedAvg"): avg(dv.Field(i), sv.Field(i)) // AvgHeartRate, AvgCadence, EnhancedAvgSpeed, etc.. default: From 8d15b50801f6703a1330f81c1b649ee223c790b1 Mon Sep 17 00:00:00 2001 From: Mukti Date: Tue, 10 Sep 2024 12:06:38 +0700 Subject: [PATCH 05/12] feat: fitactivity implement context propagation (#414) --- cmd/fitactivity/main.go | 106 ++++++++++++++++++++++---- cmd/fitactivity/opener/opener.go | 4 +- cmd/fitactivity/opener/opener_test.go | 3 +- 3 files changed, 94 insertions(+), 19 deletions(-) diff --git a/cmd/fitactivity/main.go b/cmd/fitactivity/main.go index 5aecf0c..b3004c7 100644 --- a/cmd/fitactivity/main.go +++ b/cmd/fitactivity/main.go @@ -5,10 +5,12 @@ package main import ( + "context" "errors" "flag" "fmt" "os" + "os/signal" "path/filepath" "strconv" "strings" @@ -72,6 +74,18 @@ Flags: ` func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + + go func() { + sig := <-quit + cancel() + fmt.Printf(" %v ", sig) + }() + fs := flag.NewFlagSet("main", flag.ExitOnError) fs.Usage = func() { fmt.Fprint(os.Stderr, mainUsage) } @@ -97,16 +111,16 @@ func main() { switch command { case "combine": fs := flag.NewFlagSet(command, flag.ExitOnError) - printerror(fs, command, combine(fs, args[1:])) + printerror(fs, command, combine(ctx, fs, args[1:])) case "conceal": fs := flag.NewFlagSet(command, flag.ExitOnError) - printerror(fs, command, conceal(fs, args[1:])) + printerror(fs, command, conceal(ctx, fs, args[1:])) case "reduce": fs := flag.NewFlagSet(command, flag.ExitOnError) - printerror(fs, command, reduce(fs, args[1:])) + printerror(fs, command, reduce(ctx, fs, args[1:])) case "remove": fs := flag.NewFlagSet(command, flag.ExitOnError) - printerror(fs, command, remove(fs, args[1:])) + printerror(fs, command, remove(ctx, fs, args[1:])) default: printerror(fs, command, fmt.Errorf("command provided but not defined: %s", command)) } @@ -187,7 +201,7 @@ Examples: ` + cli + ` combine conceal reduce -o result.fit --last 1000 --time 5 part1.fit part2.fit ` -func combine(fs *flag.FlagSet, args []string) (err error) { +func combine(ctx context.Context, fs *flag.FlagSet, args []string) (err error) { fs.Usage = func() { fmt.Fprint(os.Stderr, combineUsage) } const ( @@ -310,12 +324,18 @@ loop: var fits []*proto.FIT verboserun(fmt.Sprintf("Decoding %d files", len(files)), func() { - fits, err = opener.Open(files) + fits, err = opener.Open(ctx, files) }) if err != nil { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + var fit *proto.FIT verboserun(fmt.Sprintf("Combining %d files", len(files)), func() { fit, err = combiner.Combine(fits) @@ -328,6 +348,12 @@ loop: fit.FileHeader.ProfileVersion = latestProfileVersion(fits) for _, subcommand := range subcommands { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + switch subcommand { case subcommandConceal: msg := fmt.Sprintf("Concealing [start: %sm, end: %sm]", @@ -403,6 +429,12 @@ loop: headerOption = encoder.WithCompressedTimestampHeader() } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + msg := fmt.Sprintf("Encoding [%s]", headerInfo) verboserun(msg, func() { var f *os.File @@ -412,7 +444,10 @@ loop: } defer f.Close() enc := encoder.New(f, headerOption) - err = enc.Encode(fit) + err = enc.EncodeWithContext(ctx, fit) + if err != nil { + _ = os.Remove(out) + } }) return err @@ -438,7 +473,7 @@ Examples: ` + cli + ` conceal --first 1000 --last 1000 a.fit b.fit ` -func conceal(fs *flag.FlagSet, args []string) (err error) { +func conceal(ctx context.Context, fs *flag.FlagSet, args []string) (err error) { fs.Usage = func() { fmt.Fprint(os.Stderr, concealUsage) } var interleave int @@ -508,7 +543,7 @@ func conceal(fs *flag.FlagSet, args []string) (err error) { var fit *proto.FIT for dec.Next() { - fit, err = dec.Decode() + fit, err = dec.DecodeWithContext(ctx) if err != nil { return } @@ -519,6 +554,12 @@ func conceal(fs *flag.FlagSet, args []string) (err error) { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + verboserun(fmt.Sprintf("[%d] Concealing", i), func() { for _, fit := range fits { concealer.Conceal(fit.Messages, uint32(first)*100, uint32(last)*100) @@ -528,6 +569,12 @@ func conceal(fs *flag.FlagSet, args []string) (err error) { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + verboserun(fmt.Sprintf("[%d] Encoding [%s]", i, headerInfo), func() { name := fmt.Sprintf("%s_concealed_%d_%d.fit", strings.TrimSuffix(path, filepath.Ext(path)), first, last) @@ -542,7 +589,8 @@ func conceal(fs *flag.FlagSet, args []string) (err error) { enc.Reset(f, headerOption) for _, fit := range fits { - if err = enc.Encode(fit); err != nil { + if err = enc.EncodeWithContext(ctx, fit); err != nil { + _ = os.Remove(name) return } } @@ -587,7 +635,7 @@ Examples: ` + cli + ` reduce --time 5 a.fit b.fit ` -func reduce(fs *flag.FlagSet, args []string) (err error) { +func reduce(ctx context.Context, fs *flag.FlagSet, args []string) (err error) { fs.Usage = func() { fmt.Fprint(os.Stderr, reduceUsage) } var interleave int @@ -683,7 +731,7 @@ func reduce(fs *flag.FlagSet, args []string) (err error) { var fit *proto.FIT for dec.Next() { - fit, err = dec.Decode() + fit, err = dec.DecodeWithContext(ctx) if err != nil { return } @@ -694,6 +742,12 @@ func reduce(fs *flag.FlagSet, args []string) (err error) { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + var msgs []string verboserun(fmt.Sprintf("[%d] Reducing", i), func() { for _, fit := range fits { @@ -712,6 +766,12 @@ func reduce(fs *flag.FlagSet, args []string) (err error) { fmt.Fprintf(os.Stderr, " %s\n", msg) } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + verboserun(fmt.Sprintf("[%d] Encoding [%s]", i, headerInfo), func() { name := fmt.Sprintf("%s_reduced_%s.fit", strings.TrimSuffix(path, filepath.Ext(path)), nameSuffix) @@ -726,7 +786,8 @@ func reduce(fs *flag.FlagSet, args []string) (err error) { enc.Reset(f, headerOption) for _, fit := range fits { - if err = enc.Encode(fit); err != nil { + if err = enc.EncodeWithContext(ctx, fit); err != nil { + _ = os.Remove(name) return } } @@ -763,7 +824,7 @@ Examples: ` + cli + ` remove --unknown --nums 160,162 --devdata a.fit b.fit ` -func remove(fs *flag.FlagSet, args []string) (err error) { +func remove(ctx context.Context, fs *flag.FlagSet, args []string) (err error) { fs.Usage = func() { fmt.Fprint(os.Stderr, removeUsage) } var interleave int @@ -856,7 +917,7 @@ func remove(fs *flag.FlagSet, args []string) (err error) { var fit *proto.FIT for dec.Next() { - fit, err = dec.Decode() + fit, err = dec.DecodeWithContext(ctx) if err != nil { return } @@ -867,6 +928,12 @@ func remove(fs *flag.FlagSet, args []string) (err error) { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + var msgs []string verboserun(fmt.Sprintf("[%d] Removing", i), func() { for _, fit := range fits { @@ -896,6 +963,12 @@ func remove(fs *flag.FlagSet, args []string) (err error) { fmt.Fprintf(os.Stderr, " %s\n", msg) } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + verboserun(fmt.Sprintf("[%d] Encoding [%s]", i, headerInfo), func() { name := fmt.Sprintf("%s_removed_%s.fit", strings.TrimSuffix(path, filepath.Ext(path)), nameSuffix) @@ -910,7 +983,8 @@ func remove(fs *flag.FlagSet, args []string) (err error) { enc.Reset(f, headerOption) for _, fit := range fits { - if err = enc.Encode(fit); err != nil { + if err = enc.EncodeWithContext(ctx, fit); err != nil { + _ = os.Remove(name) return } } diff --git a/cmd/fitactivity/opener/opener.go b/cmd/fitactivity/opener/opener.go index 4f483ac..e36ac33 100644 --- a/cmd/fitactivity/opener/opener.go +++ b/cmd/fitactivity/opener/opener.go @@ -19,8 +19,8 @@ import ( var numCPU = runtime.NumCPU() // Open opens all paths concurrently using a number of workers equal to the lesser value of len(paths) or runtime.NumCPU(). -func Open(paths []string) (fits []*proto.FIT, err error) { - ctx, cancel := context.WithCancel(context.Background()) +func Open(ctx context.Context, paths []string) (fits []*proto.FIT, err error) { + ctx, cancel := context.WithCancel(ctx) defer cancel() n := len(paths) diff --git a/cmd/fitactivity/opener/opener_test.go b/cmd/fitactivity/opener/opener_test.go index e2f6b70..3958b31 100644 --- a/cmd/fitactivity/opener/opener_test.go +++ b/cmd/fitactivity/opener/opener_test.go @@ -1,6 +1,7 @@ package opener import ( + "context" "path/filepath" "runtime" "testing" @@ -24,7 +25,7 @@ func TestOpen(t *testing.T) { filepath.Join(fromOfficialSDK, "Activity.fit"), } - fits, err := Open(paths) + fits, err := Open(context.Background(), paths) if err != nil { t.Fatalf("expected error nil, got: %v", err) } From fd72df2eb3fda5eece3812c6d066669045c56761 Mon Sep 17 00:00:00 2001 From: Mukti Date: Tue, 10 Sep 2024 19:29:34 +0700 Subject: [PATCH 06/12] perf: optimize encoder buffer size (#415) --- encoder/encoder.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/encoder/encoder.go b/encoder/encoder.go index 7eee7ec..777d7f8 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -69,8 +69,11 @@ type Encoder struct { // and will change every rollover event occurrence. timestampReference uint32 - mesgDef proto.MessageDefinition // Temporary message definition to reduce alloc. - bytesArray [proto.MaxBytesPerMessage]byte // General purpose array for encoding process. + mesgDef proto.MessageDefinition // Temporary message definition to reduce alloc. + + // Dynamic-sized buffer for encoding, starting at 1537 bytes (the maximum size of Message Definition). + // It starts small but grows as needed and may only grow when using Message's MarshalAppend. + buf []byte } type options struct { @@ -187,6 +190,7 @@ func New(w io.Writer, opts ...Option) *Encoder { crc16: crc16.New(nil), protocolValidator: new(proto.Validator), localMesgNumLRU: new(lru), + buf: make([]byte, 0, proto.MaxBytesPerMessageDefinition), } e.Reset(w, opts...) return e @@ -319,7 +323,7 @@ func (e *Encoder) encodeFileHeader(header *proto.FileHeader) error { header.DataType = proto.DataTypeFIT header.CRC = 0 // recalculated - b, _ := header.MarshalAppend(e.bytesArray[:0]) + b, _ := header.MarshalAppend(e.buf[:0]) if header.Size != 14 { n, err := e.w.Write(b[:header.Size]) @@ -350,7 +354,7 @@ func (e *Encoder) updateFileHeader(header *proto.FileHeader) (err error) { header.DataSize = e.dataSize - b, _ := header.MarshalAppend(e.bytesArray[:0]) + b, _ := header.MarshalAppend(e.buf[:0]) if header.Size == 14 { _, _ = e.crc16.Write(b[:12]) // recalculate CRC Checksum since FileHeader is changed. @@ -426,11 +430,11 @@ func (e *Encoder) encodeMessages(messages []proto.Message) error { } // encodeMessage marshals and encodes message definition and its message into w. -func (e *Encoder) encodeMessage(mesg *proto.Message) error { +func (e *Encoder) encodeMessage(mesg *proto.Message) (err error) { mesg.Header = proto.MesgNormalHeaderMask mesg.Architecture = e.options.endianness - if err := e.options.messageValidator.Validate(mesg); err != nil { + if err = e.options.messageValidator.Validate(mesg); err != nil { return fmt.Errorf("message validation failed: %w", err) } @@ -464,15 +468,16 @@ func (e *Encoder) encodeMessage(mesg *proto.Message) error { return err } - b, _ := e.mesgDef.MarshalAppend(e.bytesArray[:0]) + b, _ := e.mesgDef.MarshalAppend(e.buf[:0]) localMesgNum, isNewMesgDef := e.localMesgNumLRU.Put(b) // This might alloc memory since we need to copy the item. if e.options.headerOption == headerOptionNormal { b[0] = (b[0] &^ proto.LocalMesgNumMask) | localMesgNum // Update the message definition header. mesg.Header = (mesg.Header &^ proto.LocalMesgNumMask) | localMesgNum } + var n int if isNewMesgDef { - n, err := e.w.Write(b) + n, err = e.w.Write(b) e.n, e.dataSize = e.n+int64(n), e.dataSize+uint32(n) if err != nil { return fmt.Errorf("write message definition failed: %w", err) @@ -480,17 +485,18 @@ func (e *Encoder) encodeMessage(mesg *proto.Message) error { _, _ = e.crc16.Write(b) } - b, err := mesg.MarshalAppend(e.bytesArray[:0]) + // At this point, e.buf may grow. Re-assign e.buf in case slice has grown. + e.buf, err = mesg.MarshalAppend(e.buf[:0]) if err != nil { return fmt.Errorf("marshal mesg failed: %w", err) } - n, err := e.w.Write(b) + n, err = e.w.Write(e.buf) e.n, e.dataSize = e.n+int64(n), e.dataSize+uint32(n) if err != nil { return fmt.Errorf("write message failed: %w", err) } - _, _ = e.crc16.Write(b) + _, _ = e.crc16.Write(e.buf) return nil } @@ -525,7 +531,7 @@ func (e *Encoder) compressTimestampIntoHeader(mesg *proto.Message) { } func (e *Encoder) encodeCRC() error { - b := e.bytesArray[:2] + b := e.buf[:2] binary.LittleEndian.PutUint16(b, e.crc16.Sum16()) n, err := e.w.Write(b) From 0616e921cfc46891f32e81ac19aa39cf54f86bf2 Mon Sep 17 00:00:00 2001 From: Mukti Date: Wed, 11 Sep 2024 11:55:08 +0700 Subject: [PATCH 07/12] perf: round encoder buffer size to nearest sizeclasses (#416) --- encoder/encoder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/encoder/encoder.go b/encoder/encoder.go index 777d7f8..3ccbb12 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -71,7 +71,7 @@ type Encoder struct { mesgDef proto.MessageDefinition // Temporary message definition to reduce alloc. - // Dynamic-sized buffer for encoding, starting at 1537 bytes (the maximum size of Message Definition). + // Dynamic-sized buffer for encoding, starting at 1536 bytes (see PR #415 and #416 for details). // It starts small but grows as needed and may only grow when using Message's MarshalAppend. buf []byte } @@ -190,7 +190,7 @@ func New(w io.Writer, opts ...Option) *Encoder { crc16: crc16.New(nil), protocolValidator: new(proto.Validator), localMesgNumLRU: new(lru), - buf: make([]byte, 0, proto.MaxBytesPerMessageDefinition), + buf: make([]byte, 0, 1536), } e.Reset(w, opts...) return e From c1bf3fa68c60cb77ad08c7d2357c8e253020638d Mon Sep 17 00:00:00 2001 From: Mukti Date: Wed, 11 Sep 2024 21:10:01 +0700 Subject: [PATCH 08/12] fix: encoder dynamic protocol version validator (#417) --- encoder/encoder.go | 2 +- encoder/encoder_test.go | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/encoder/encoder.go b/encoder/encoder.go index 3ccbb12..59acb0a 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -215,7 +215,6 @@ func (e *Encoder) Reset(w io.Writer, opts ...Option) { } e.reset() - e.protocolValidator.SetProtocolVersion(e.options.protocolVersion) var lruSize byte = 1 if e.options.headerOption == headerOptionNormal && e.options.multipleLocalMessageType > 0 { @@ -319,6 +318,7 @@ func (e *Encoder) encodeFileHeader(header *proto.FileHeader) error { } else if header.ProtocolVersion == 0 { // Default when not specified in FileHeader. header.ProtocolVersion = byte(proto.V1) } + e.protocolValidator.SetProtocolVersion(proto.Version(header.ProtocolVersion)) header.DataType = proto.DataTypeFIT header.CRC = 0 // recalculated diff --git a/encoder/encoder_test.go b/encoder/encoder_test.go index ad98591..08faeaf 100644 --- a/encoder/encoder_test.go +++ b/encoder/encoder_test.go @@ -735,6 +735,7 @@ func TestUpdateHeader(t *testing.T) { func TestEncodeHeader(t *testing.T) { tt := []struct { name string + opts []Option protocolVersion proto.Version header proto.FileHeader b []byte @@ -760,6 +761,7 @@ func TestEncodeHeader(t *testing.T) { return b }(), + protocolVersion: proto.V1, }, { name: "header 12 legacy", @@ -820,20 +822,45 @@ func TestEncodeHeader(t *testing.T) { 247, 38, }, }, + { + name: "force use protocol version from Option", + protocolVersion: proto.V2, + opts: []Option{ + WithProtocolVersion(proto.V2), + }, + header: proto.FileHeader{ + Size: 14, + ProtocolVersion: byte(proto.V1), + ProfileVersion: 2135, + DataSize: 136830, + DataType: ".FIT", + CRC: 21830, + }, + b: []byte{ + 14, + 32, // Previously 16 + 87, 8, + 126, 22, 2, 0, + 46, 70, 73, 84, + 185, 85, // Previously was 70, 85, + }, + }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { bytebuf := new(bytes.Buffer) - enc := New(bytebuf, WithWriteBufferSize(0)) - if tc.protocolVersion != 0 { - enc.options.protocolVersion = tc.protocolVersion - } + enc := New(bytebuf, append(tc.opts, WithWriteBufferSize(0))...) _ = enc.encodeFileHeader(&tc.header) if diff := cmp.Diff(bytebuf.Bytes(), tc.b); diff != "" { t.Fatal(diff) } + + if enc.protocolValidator.ProtocolVersion() != tc.protocolVersion { + t.Fatalf("expected protocol version: %v, got: %v", + tc.protocolVersion, enc.options.protocolVersion) + } }) } } @@ -970,6 +997,9 @@ func TestEncodeMessage(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.opts = append(tc.opts, WithWriteBufferSize(0)) enc := New(tc.w, tc.opts...) + // Protocol Version now is set on encodeFileHeader as we allow dynamic protocol version + // based on FileHeader. This by pass it since we don't encode file header. + enc.protocolValidator.SetProtocolVersion(enc.options.protocolVersion) err := enc.encodeMessage(&tc.mesg) if !errors.Is(err, tc.err) { t.Fatalf("expected: %v, got: %v", tc.err, err) From 3e5a59e0ea147a47704218c707e7b019a753fa18 Mon Sep 17 00:00:00 2001 From: Mukti Date: Thu, 12 Sep 2024 14:28:39 +0700 Subject: [PATCH 09/12] refactor!: remove not particularly useful features (#418) * proto: remove CreateMessageDefinition, CreateMessageDefinitionTo and WithMessages * proto: remove Message's WithFields and WithDeveloperFields method * remove internal/kit * remove (Message) WithFieldValues * remove all Clone() method * encoder: add unit test for newMessageDefinition * remove factory CreateMesgOnly * decoder: make Accumulator values private and update docs * encoder: allocate mesgDef fields and developerFields upfront size 255 * proto: remove constant MaxBytesPerMessage and MaxBytesPerMessageDefinition * proto: change FileHeader.ProtocolVersion type to proto.Version instead of byte * chore: update Factory interface code docs * decoder: rename ErrNotAFitFile to ErrNotFITFile * decoder: remove idempotent guarantee for PeekFileHeader and PeekFileId as we might change the behavior later * docs: update documentation * fuzz: ignore mesgDef reserved * basetype: remove IsInteger() EndianAbility() method, add EndianAbilityMask * remove: Reserved and Architecture fields from Message * proto: move LocalMesgNum and NewMessageDefinition functions to bottom * chore: clean up proto_marshal_test.go code * encoder: simplify encodeCRC * decoder: reset on Decode, make Next as optional --- cmd/fitactivity/README.md | 3 +- cmd/fitactivity/main.go | 8 +- cmd/fitprint/printer/printer.go | 64 ++-- decoder/accumulator.go | 65 ++-- decoder/accumulator_test.go | 8 +- decoder/decoder.go | 52 +-- decoder/decoder_test.go | 160 ++++----- decoder/raw.go | 4 +- decoder/raw_test.go | 101 +++--- docs/usage.md | 62 ++-- encoder/encoder.go | 63 +++- encoder/encoder_bench_test.go | 115 +++---- encoder/encoder_test.go | 409 ++++++++++++++++------- encoder/stream_test.go | 16 +- encoder/validator.go | 5 +- encoder/validator_test.go | 226 ++++++------- factory/exported_gen.go | 9 +- factory/factory_gen.go | 9 +- fuzz_test.go | 9 +- internal/cmd/benchfit/go.mod | 4 +- internal/cmd/benchfit/go.sum | 8 +- internal/cmd/fitgen/factory/factory.tmpl | 18 +- internal/cmd/testgen/big_activity.go | 108 +++--- internal/cmd/testgen/main.go | 29 +- internal/kit/kit.go | 8 - internal/kit/kit_test.go | 21 -- profile/basetype/basetype.go | 138 +++----- profile/basetype/basetype_test.go | 68 ---- profile/filedef/activity_summary_test.go | 32 +- profile/filedef/activity_test.go | 96 +++--- profile/filedef/blood_pressure_test.go | 32 +- profile/filedef/course_test.go | 52 +-- profile/filedef/device_test.go | 40 +-- profile/filedef/filedef_test.go | 40 +-- profile/filedef/goals_test.go | 24 +- profile/filedef/listener_test.go | 12 +- profile/filedef/monitoring_ab_test.go | 36 +- profile/filedef/monitoring_daily_test.go | 32 +- profile/filedef/schedules_test.go | 24 +- profile/filedef/segment_list_test.go | 28 +- profile/filedef/segment_test.go | 36 +- profile/filedef/settings_test.go | 40 +-- profile/filedef/sport_test.go | 48 +-- profile/filedef/totals_test.go | 24 +- profile/filedef/weight_test.go | 32 +- profile/filedef/workout_test.go | 32 +- profile/mesgdef/mesgdef.go | 3 +- profile/mesgdef/mesgdef_test.go | 11 +- profile/mesgdef/record_gen_test.go | 10 +- proto/proto.go | 226 +++++-------- proto/proto_internal_test.go | 184 ++++++++++ proto/proto_marshal.go | 68 +--- proto/proto_marshal_test.go | 98 +++--- proto/proto_test.go | 321 ++---------------- proto/value_marshal.go | 32 +- proto/value_marshal_test.go | 105 +++--- proto/value_unmarshal.go | 32 +- proto/version.go | 12 +- proto/version_test.go | 2 +- 59 files changed, 1704 insertions(+), 1850 deletions(-) delete mode 100644 internal/kit/kit.go delete mode 100644 internal/kit/kit_test.go diff --git a/cmd/fitactivity/README.md b/cmd/fitactivity/README.md index 53e4bd7..4bc8508 100644 --- a/cmd/fitactivity/README.md +++ b/cmd/fitactivity/README.md @@ -167,7 +167,7 @@ Output: ```sh About: - fitactivity is a program to handle FIT files based on provided command. + fitactivity is a program to manage FIT files based on provided command. Usage: fitactivity [command] @@ -228,6 +228,7 @@ Subcommand Flags (only if subcommand is provided): --rdp float64 reduce method: RDP [Ramer-Douglas-Peucker] based on GPS points, epsilon > 0 --distance float64 reduce method: distance interval in meters --time uint32 reduce method: time interval in seconds + remove: (select at least one) --unknown bool remove unknown messages --nums string remove message numbers (value separated by comma) diff --git a/cmd/fitactivity/main.go b/cmd/fitactivity/main.go index b3004c7..b4e5cdf 100644 --- a/cmd/fitactivity/main.go +++ b/cmd/fitactivity/main.go @@ -344,7 +344,7 @@ loop: return err } - fit.FileHeader.ProtocolVersion = byte(latestProtocolVersion(fits)) + fit.FileHeader.ProtocolVersion = latestProtocolVersion(fits) fit.FileHeader.ProfileVersion = latestProfileVersion(fits) for _, subcommand := range subcommands { @@ -1050,14 +1050,14 @@ func formatThousand(v int) string { return result.String() } -func latestProtocolVersion(fits []*proto.FIT) byte { - var version = byte(proto.V1) +func latestProtocolVersion(fits []*proto.FIT) proto.Version { + var version = proto.V1 for i := range fits { if fits[i].FileHeader.ProtocolVersion > version { version = fits[i].FileHeader.ProtocolVersion } } - return byte(proto.Version(version)) + return version } func latestProfileVersion(fits []*proto.FIT) uint16 { diff --git a/cmd/fitprint/printer/printer.go b/cmd/fitprint/printer/printer.go index cbfe53c..1684813 100644 --- a/cmd/fitprint/printer/printer.go +++ b/cmd/fitprint/printer/printer.go @@ -74,6 +74,7 @@ func Print(path string) error { defer p.Close() dec := decoder.New(f, + decoder.WithMesgDefListener(p), decoder.WithMesgListener(p), decoder.WithBroadcastOnly(), decoder.WithIgnoreChecksum(), @@ -127,16 +128,23 @@ File Header: const channelBuffer = 1000 type printer struct { - w io.Writer - poolc chan proto.Message - mesgc chan proto.Message - done chan struct{} - active bool - count int + w io.Writer + localMessageDefinitions [proto.LocalMesgNumMask + 1]proto.MessageDefinition + poolc chan proto.Message + messagec chan message + done chan struct{} + active bool + count int fieldDescriptions []*mesgdef.FieldDescription } +type message struct { + proto.Message + Reserved byte + Architecture byte +} + func New(w io.Writer) *printer { p := &printer{w: w} p.reset() @@ -151,20 +159,20 @@ func New(w io.Writer) *printer { } func (p *printer) loop() { - for mesg := range p.mesgc { - switch mesg.Num { + for m := range p.messagec { + switch m.Num { case mesgnum.FieldDescription: - p.fieldDescriptions = append(p.fieldDescriptions, mesgdef.NewFieldDescription(&mesg)) + p.fieldDescriptions = append(p.fieldDescriptions, mesgdef.NewFieldDescription(&m.Message)) } - p.print(mesg) - p.poolc <- mesg + p.print(m) + p.poolc <- m.Message p.count++ } close(p.done) } func (p *printer) reset() { - p.mesgc = make(chan proto.Message, channelBuffer) + p.messagec = make(chan message, channelBuffer) p.done = make(chan struct{}) p.count = 0 p.active = true @@ -174,7 +182,7 @@ func (p *printer) Wait() { if !p.active { return } - close(p.mesgc) + close(p.messagec) <-p.done p.active = false } @@ -184,6 +192,12 @@ func (p *printer) Close() { close(p.poolc) } +var _ decoder.MesgDefListener = (*printer)(nil) + +func (p *printer) OnMesgDef(mesgDef proto.MessageDefinition) { + p.localMessageDefinitions[proto.LocalMesgNum(mesgDef.Header)] = mesgDef +} + var _ decoder.MesgListener = (*printer)(nil) func (p *printer) OnMesg(mesg proto.Message) { @@ -192,10 +206,10 @@ func (p *printer) OnMesg(mesg proto.Message) { go p.loop() p.active = true } - p.mesgc <- p.prep(mesg) + p.messagec <- p.prep(mesg) } -func (p *printer) prep(mesg proto.Message) proto.Message { +func (p *printer) prep(mesg proto.Message) message { m := <-p.poolc if cap(m.Fields) < len(mesg.Fields) { @@ -212,20 +226,22 @@ func (p *printer) prep(mesg proto.Message) proto.Message { copy(m.DeveloperFields, mesg.DeveloperFields) mesg.DeveloperFields = m.DeveloperFields - return mesg + mesgDef := p.localMessageDefinitions[proto.LocalMesgNum(mesg.Header)] + + return message{Message: mesg, Reserved: mesgDef.Reserved, Architecture: mesgDef.Architecture} } -func (p *printer) print(mesg proto.Message) { - numstr := mesg.Num.String() +func (p *printer) print(m message) { + numstr := m.Num.String() if strings.HasPrefix(numstr, "MesgNumInvalid") { numstr = factory.NameUnknown } fmt.Fprintf(p.w, "%s (num: %d, arch: %d, fields[-]: %d, developerFields[+]: %d) [%d]:\n", - numstr, mesg.Num, mesg.Architecture, len(mesg.Fields), len(mesg.DeveloperFields), p.count) + numstr, m.Num, m.Architecture, len(m.Fields), len(m.DeveloperFields), p.count) - for j := range mesg.Fields { - field := &mesg.Fields[j] + for j := range m.Fields { + field := &m.Fields[j] var ( isDynamicField = false @@ -237,7 +253,7 @@ func (p *printer) print(mesg proto.Message) { units = field.Units ) - if subField := field.SubFieldSubtitution(&mesg); subField != nil { + if subField := field.SubFieldSubtitution(&m.Message); subField != nil { isDynamicField = true name = subField.Name baseType = subField.Type.BaseType() @@ -291,8 +307,8 @@ func (p *printer) print(mesg proto.Message) { ) } - for i := range mesg.DeveloperFields { - devField := &mesg.DeveloperFields[i] + for i := range m.DeveloperFields { + devField := &m.DeveloperFields[i] fieldDesc := p.getFieldDescription(devField.DeveloperDataIndex, devField.Num) if fieldDesc == nil { continue diff --git a/decoder/accumulator.go b/decoder/accumulator.go index 24468f9..5b741b4 100644 --- a/decoder/accumulator.go +++ b/decoder/accumulator.go @@ -8,53 +8,60 @@ import ( "github.com/muktihari/fit/profile/typedef" ) +// Accumulator is value accumulator. type Accumulator struct { - AccumulatedValues []AccumulatedValue // use slice over map since len(values) is relatively small + values []value // use slice over map since len(values) is relatively small } +// NewAccumulator creates new accumulator. func NewAccumulator() *Accumulator { - return &Accumulator{} // No need to make AccumulatedValues as it will be created on append anyway. + return &Accumulator{} } -func (a *Accumulator) Collect(mesgNum typedef.MesgNum, destFieldNum byte, value uint32) { - for i := range a.AccumulatedValues { - field := &a.AccumulatedValues[i] - if field.MesgNum == mesgNum && field.DestFieldNum == destFieldNum { - field.Value = value - field.Last = value +// Collect collects value, it will either append the value when not exist or replace existing one. +func (a *Accumulator) Collect(mesgNum typedef.MesgNum, destFieldNum byte, val uint32) { + for i := range a.values { + av := &a.values[i] + if av.mesgNum == mesgNum && av.fieldNum == destFieldNum { + av.value = val + av.last = val return } } - a.AccumulatedValues = append(a.AccumulatedValues, AccumulatedValue{ - MesgNum: mesgNum, - DestFieldNum: destFieldNum, - Value: value, - Last: value, + a.values = append(a.values, value{ + mesgNum: mesgNum, + fieldNum: destFieldNum, + value: val, + last: val, }) } -func (a *Accumulator) Accumulate(mesgNum typedef.MesgNum, destFieldNum byte, value uint32, bits byte) uint32 { - for i := range a.AccumulatedValues { - av := &a.AccumulatedValues[i] - if av.MesgNum == mesgNum && av.DestFieldNum == destFieldNum { - return av.Accumulate(value, bits) +// Accumulate calculates the accumulated value and update accordingly. It returns the original value +// when the corresponding value does not exist. +func (a *Accumulator) Accumulate(mesgNum typedef.MesgNum, destFieldNum byte, val uint32, bits byte) uint32 { + for i := range a.values { + av := &a.values[i] + if av.mesgNum == mesgNum && av.fieldNum == destFieldNum { + return av.accumulate(val, bits) } } - return value + return val } -func (a *Accumulator) Reset() { a.AccumulatedValues = a.AccumulatedValues[:0] } +// Reset resets the accumulator. Tt retains the underlying storage for use by +// future use to reduce memory allocs. +func (a *Accumulator) Reset() { a.values = a.values[:0] } -type AccumulatedValue struct { - MesgNum typedef.MesgNum - DestFieldNum byte - Last uint32 - Value uint32 +type value struct { + mesgNum typedef.MesgNum + fieldNum byte + last uint32 + value uint32 } -func (a *AccumulatedValue) Accumulate(value uint32, bits byte) uint32 { +func (a *value) accumulate(val uint32, bits byte) uint32 { var mask uint32 = (1 << bits) - 1 - a.Value += (value - a.Last) & mask - a.Last = value - return a.Value + a.value += (val - a.last) & mask + a.last = val + return a.value } diff --git a/decoder/accumulator_test.go b/decoder/accumulator_test.go index 85d7399..a9db1bb 100644 --- a/decoder/accumulator_test.go +++ b/decoder/accumulator_test.go @@ -90,13 +90,13 @@ func TestAccumulatorReset(t *testing.T) { accumu := NewAccumulator() accumu.Collect(mesgnum.Record, fieldnum.RecordSpeed, 1000) - if len(accumu.AccumulatedValues) != 1 { - t.Fatalf("expected AccumulatedValues is 1, got: %d", len(accumu.AccumulatedValues)) + if len(accumu.values) != 1 { + t.Fatalf("expected AccumulatedValues is 1, got: %d", len(accumu.values)) } accumu.Reset() - if len(accumu.AccumulatedValues) != 0 { - t.Fatalf("expected AccumulatedValues is 0 after reset, got: %d", len(accumu.AccumulatedValues)) + if len(accumu.values) != 0 { + t.Fatalf("expected AccumulatedValues is 0 after reset, got: %d", len(accumu.values)) } } diff --git a/decoder/decoder.go b/decoder/decoder.go index b983fdb..e6aa1a2 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -30,7 +30,7 @@ func (e errorString) Error() string { return string(e) } const ( // Integrity errors - ErrNotAFitFile = errorString("not a FIT file") + ErrNotFITFile = errorString("not a FIT file") ErrDataSizeZero = errorString("data size zero") ErrCRCChecksumMismatch = errorString("crc checksum mismatch") @@ -40,8 +40,6 @@ const ( ErrInvalidBaseType = errorString("invalid basetype") ) -const littleEndian = 0 - // Decoder is FIT file decoder. See New() for details. type Decoder struct { readBuffer *readBuffer // read from io.Reader with buffer without extra copying. @@ -81,7 +79,8 @@ type Decoder struct { // Factory defines a contract that any Factory containing these method can be used by the Decoder. type Factory interface { - // CreateField create new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. + // CreateField creates new field based on defined messages in the factory. + // If not found, it returns new field with "unknown" name. CreateField(mesgNum typedef.MesgNum, num byte) proto.Field } @@ -183,7 +182,8 @@ func WithReadBufferSize(size int) Option { // The FIT protocol allows for multiple FIT files to be chained together in a single FIT file. // Each FIT file in the chain must be a properly formatted FIT file (header, data records, CRC). // -// To decode chained FIT files, use Next() to check if r hasn't reach EOF and next bytes are still a valid FIT sequences. +// To decode a chained FIT file containing multiple FIT data, invoke Decode() or DecodeWithContext() +// method multiple times. For convenience, we can wrap it with the Next() method as follows (optional): // // for dec.Next() { // fit, err := dec.Decode() @@ -327,8 +327,7 @@ func (d *Decoder) discardMessages() (err error) { // PeekFileHeader decodes only up to FileHeader (first 12-14 bytes) without decoding the whole reader. // -// After this method is invoked, Decode picks up where this left then continue decoding next messages instead of starting from zero. -// This method is idempotent and can be invoked even after Decode has been invoked. +// If we choose to continue, Decode picks up where this left then continue decoding next messages instead of starting from zero. func (d *Decoder) PeekFileHeader() (*proto.FileHeader, error) { if d.err != nil { return nil, d.err @@ -342,8 +341,7 @@ func (d *Decoder) PeekFileHeader() (*proto.FileHeader, error) { // PeekFileId decodes only up to FileId message without decoding the whole reader. // FileId message should be the first message of any FIT file, otherwise return an error. // -// After this method is invoked, Decode picks up where this left then continue decoding next messages instead of starting from zero. -// This method is idempotent and can be invoked even after Decode has been invoked. +// If we choose to continue, Decode picks up where this left then continue decoding next messages instead of starting from zero. func (d *Decoder) PeekFileId() (*mesgdef.FileId, error) { if d.err != nil { return nil, d.err @@ -367,14 +365,14 @@ func (d *Decoder) Next() bool { if !d.sequenceCompleted { return true } - d.reset() // reset values for the next chained FIT file + d.sequenceCompleted = false // err is saved in the func, any exported will call this func anyway. return d.decodeFileHeaderOnce() == nil } // Decode method decodes `r` into FIT data. One invocation will produce one valid FIT data or // an error if it occurs. To decode a chained FIT file containing multiple FIT data, invoke this -// method multiple times, however, the invocation must be wrapped with Next() method as follows: +// method multiple times. For convenience, we can wrap it with the Next() method as follows (optional): // // for dec.Next() { // fit, err := dec.Decode() @@ -396,12 +394,14 @@ func (d *Decoder) Decode() (*proto.FIT, error) { if d.err = d.decodeCRC(); d.err != nil { return nil, d.err } - d.sequenceCompleted = true - return &proto.FIT{ + fit := &proto.FIT{ FileHeader: d.fileHeader, Messages: d.messages, CRC: d.crc, - }, nil + } + d.reset() + d.sequenceCompleted = true + return fit, nil } // Discard discards a single FIT file sequence and returns any error encountered. This method directs the Decoder to @@ -443,6 +443,7 @@ func (d *Decoder) Discard() error { if _, d.err = d.readN(2); d.err != nil { // Discard File CRC return d.err } + d.reset() d.sequenceCompleted = true return d.err } @@ -463,7 +464,7 @@ func (d *Decoder) decodeFileHeader() error { size := b[0] if size != 12 && size != 14 { // current spec is either 12 or 14 - return fmt.Errorf("file header size [%d] is invalid: %w", size, ErrNotAFitFile) + return fmt.Errorf("file header size [%d] is invalid: %w", size, ErrNotFITFile) } _, _ = d.crc16.Write(b) @@ -476,12 +477,12 @@ func (d *Decoder) decodeFileHeader() error { // PERF: Neither string(b[7:11]) nor assigning proto.DataTypeFIT constant to a variable escape to the heap. if string(b[7:11]) != proto.DataTypeFIT { - return ErrNotAFitFile + return ErrNotFITFile } d.fileHeader = proto.FileHeader{ Size: size, - ProtocolVersion: b[0], + ProtocolVersion: proto.Version(b[0]), ProfileVersion: binary.LittleEndian.Uint16(b[1:3]), DataSize: binary.LittleEndian.Uint32(b[3:7]), DataType: proto.DataTypeFIT, @@ -556,7 +557,7 @@ func (d *Decoder) decodeMessageDefinition(header byte) error { mesgDef.Header = header mesgDef.Reserved = b[0] mesgDef.Architecture = b[1] - if mesgDef.Architecture == littleEndian { + if mesgDef.Architecture == proto.LittleEndian { mesgDef.MesgNum = typedef.MesgNum(binary.LittleEndian.Uint16(b[2:4])) } else { mesgDef.MesgNum = typedef.MesgNum(binary.BigEndian.Uint16(b[2:4])) @@ -610,7 +611,10 @@ func (d *Decoder) decodeMessageDefinition(header byte) error { d.localMessageDefinitions[localMesgNum] = mesgDef if len(d.options.mesgDefListeners) > 0 { - mesgDef := mesgDef.Clone() // Clone since we don't have control of the object lifecycle outside Decoder. + // Clone since we don't have control of the object lifecycle outside Decoder. + mesgDef := *mesgDef + mesgDef.FieldDefinitions = append(mesgDef.FieldDefinitions[:0:0], mesgDef.FieldDefinitions...) + mesgDef.DeveloperFieldDefinitions = append(mesgDef.DeveloperFieldDefinitions[:0:0], mesgDef.DeveloperFieldDefinitions...) for i := range d.options.mesgDefListeners { d.options.mesgDefListeners[i].OnMesgDef(mesgDef) // blocking or non-blocking depends on listeners' implementation. } @@ -631,8 +635,6 @@ func (d *Decoder) decodeMessageData(header byte) (err error) { mesg := proto.Message{Num: mesgDef.MesgNum} mesg.Header = header - mesg.Reserved = mesgDef.Reserved - mesg.Architecture = mesgDef.Architecture mesg.Fields = d.fieldsArray[:0] if (header & proto.MesgCompressedHeaderMask) == proto.MesgCompressedHeaderMask { // Compressed Timestamp Message Data @@ -1011,12 +1013,14 @@ func (d *Decoder) DecodeWithContext(ctx context.Context) (*proto.FIT, error) { if d.err = d.decodeCRC(); d.err != nil { return nil, d.err } - d.sequenceCompleted = true - return &proto.FIT{ + fit := &proto.FIT{ FileHeader: d.fileHeader, Messages: d.messages, CRC: d.crc, - }, nil + } + d.reset() + d.sequenceCompleted = true + return fit, nil } func checkContext(ctx context.Context) error { diff --git a/decoder/decoder_test.go b/decoder/decoder_test.go index 12ff6b8..590eabc 100644 --- a/decoder/decoder_test.go +++ b/decoder/decoder_test.go @@ -25,7 +25,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/muktihari/fit/factory" - "github.com/muktihari/fit/internal/kit" "github.com/muktihari/fit/kit/datetime" "github.com/muktihari/fit/kit/hash" "github.com/muktihari/fit/kit/hash/crc16" @@ -484,12 +483,12 @@ func TestCheckIntegrity(t *testing.T) { r: func() io.Reader { h := proto.FileHeader{ Size: 14, - ProtocolVersion: byte(proto.V2), + ProtocolVersion: proto.V2, ProfileVersion: profile.Version, DataSize: 0, DataType: proto.DataTypeFIT, } - b, _ := h.MarshalBinary() + b, _ := h.MarshalAppend(nil) crc := crc16.New(nil) crc.Write(b[:12]) binary.LittleEndian.PutUint16(b[12:14], crc.Sum16()) @@ -568,13 +567,13 @@ func TestCheckIntegrity(t *testing.T) { // Chained FIT File but with next sequence header is b := append(b[:0:0], b...) h := headerForTest() - nextb, _ := h.MarshalBinary() + nextb, _ := h.MarshalAppend(nil) nextb[0] = 100 // alter FileHeader's Size b = append(b, nextb...) return bytes.NewReader(b) }(), n: 1, - err: ErrNotAFitFile, + err: ErrNotFITFile, }, } @@ -610,62 +609,60 @@ func createFitForTest() (proto.FIT, []byte) { fit := proto.FIT{ FileHeader: headerForTest(), Messages: []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileActivity)), - ), - factory.CreateMesgOnly(mesgnum.DeveloperDataId).WithFields( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue([]string{"Heart Rate"}), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - ), - factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ), - factory.CreateMesgOnly(mesgnum.Record). - WithFields( - factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), - factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(77)), - ). - WithDeveloperFields( - proto.DeveloperField{ - DeveloperDataIndex: 0, - Num: 0, - Value: proto.Uint8(100), - }, - ), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(77)), + }, DeveloperFields: []proto.DeveloperField{ + { + DeveloperDataIndex: 0, + Num: 0, + Value: proto.Uint8(100), + }, + }}, }, } for i := 0; i < 100; i++ { fit.Messages = append(fit.Messages, - factory.CreateMesgOnly(mesgnum.Record).WithFields( - factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32((i+1)*1000)), + proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32((i + 1) * 1000)), factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ), + }}, ) } bytesbuffer := new(bytes.Buffer) - b, _ := fit.FileHeader.MarshalBinary() + b, _ := fit.FileHeader.MarshalAppend(nil) bytesbuffer.Write(b) // Marshal and calculate data size and crc checksum crc16checker := crc16.New(nil) for i := range fit.Messages { mesg := fit.Messages[i] - mesgDef := proto.CreateMessageDefinition(&mesg) - b, _ := mesgDef.MarshalBinary() + mesgDef, _ := proto.NewMessageDefinition(&mesg) + b, _ := mesgDef.MarshalAppend(nil) bytesbuffer.Write(b) crc16checker.Write(b) - b, err := mesg.MarshalBinary() + b, err := mesg.MarshalAppend(nil, proto.LittleEndian) if err != nil { panic(err) } @@ -851,7 +848,7 @@ func TestNext(t *testing.T) { // New header of the next chained FIT sequences. header := headerForTest() - b, _ := header.MarshalBinary() + b, _ := header.MarshalAppend(nil) buf = append(buf, b...) r := func() io.Reader { @@ -890,8 +887,8 @@ func TestNext(t *testing.T) { t.Fatalf("should have next, return false") } - if len(dec.accumulator.AccumulatedValues) != 0 { - t.Fatalf("expected accumulator's AccumulatedValues is 0, got: %d", len(dec.accumulator.AccumulatedValues)) + if len(dec.accumulator.values) != 0 { + t.Fatalf("expected accumulator's AccumulatedValues is 0, got: %d", len(dec.accumulator.values)) } if dec.crc16.Sum16() != 0 { // not necessary since reset every decode header anyway, but let's just add it @@ -984,7 +981,7 @@ func makeDecodeTableTest() []decodeTestCase { return }) }(), - err: ErrNotAFitFile, + err: ErrNotFITFile, }, { name: "decode messages return error", @@ -1127,7 +1124,7 @@ func TestDecodeFileHeader(t *testing.T) { return }) }(), - err: ErrNotAFitFile, + err: ErrNotFITFile, }, { name: "decode header invalid size", @@ -1187,7 +1184,7 @@ func TestDecodeFileHeader(t *testing.T) { return }) }(), - err: ErrNotAFitFile, + err: ErrNotFITFile, }, { name: "decode crc == 0x000", @@ -1274,7 +1271,7 @@ func TestDecodeMessageDefinition(t *testing.T) { r io.Reader opts []Option header byte - mesgDef proto.MessageDefinition + mesgDef *proto.MessageDefinition err error }{ { @@ -1297,8 +1294,11 @@ func TestDecodeMessageDefinition(t *testing.T) { opts: []Option{ WithMesgDefListener(fnMesgDefListener(func(mesgDef proto.MessageDefinition) {})), }, - header: proto.MesgDefinitionMask, - mesgDef: proto.CreateMessageDefinition(&fit.Messages[0]), // file_id + header: proto.MesgDefinitionMask, + mesgDef: func() *proto.MessageDefinition { + mesgDef, _ := proto.NewMessageDefinition(&fit.Messages[0]) // file_i, proto.LittleEndiand + return mesgDef + }(), }, { name: "decode read return io.EOF when retrieving init data", @@ -1406,7 +1406,7 @@ func TestDecodeMessageDefinition(t *testing.T) { if err != nil { return } - mesgDef := *dec.localMessageDefinitions[proto.MesgDefinitionMask&proto.LocalMesgNumMask] + mesgDef := dec.localMessageDefinitions[proto.MesgDefinitionMask&proto.LocalMesgNumMask] if len(mesgDef.DeveloperFieldDefinitions) == 0 { mesgDef.DeveloperFieldDefinitions = nil } @@ -1709,7 +1709,7 @@ func TestDecodeFields(t *testing.T) { }, }, } - mesgb, _ := mesg.MarshalBinary() + mesgb, _ := mesg.MarshalAppend(nil, proto.LittleEndian) mesgb = mesgb[1:] // splice mesg header cur := 0 return fnReader(func(b []byte) (n int, err error) { @@ -1752,7 +1752,7 @@ func TestDecodeFields(t *testing.T) { }, }, } - mesgb, _ := mesg.MarshalBinary() + mesgb, _ := mesg.MarshalAppend(nil, proto.LittleEndian) mesgb = mesgb[1:] // splice mesg header cur := 0 return fnReader(func(b []byte) (n int, err error) { @@ -1817,18 +1817,18 @@ func TestExpandComponents(t *testing.T) { }{ { name: "expand components single happy flow", - mesg: factory.CreateMesgOnly(mesgnum.Record).WithFields( + mesg: proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), - ), + }}, containingField: factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), components: factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).Components, nFieldAfterExpansion: 2, // 1 for speed, +1 expand field enhanced_speed }, { name: "expand components multiple happy flow", - mesg: factory.CreateMesgOnly(mesgnum.Event).WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventFrontGearChange)), - ), + }}, containingField: factory.CreateField(mesgnum.Event, fieldnum.EventData).WithValue(uint32(0x27010E08)), components: func() []proto.Component { subfields := factory.CreateField(mesgnum.Event, fieldnum.EventData).SubFields @@ -1843,9 +1843,9 @@ func TestExpandComponents(t *testing.T) { }, { name: "expand components run out bits for the last component", - mesg: factory.CreateMesgOnly(mesgnum.Event).WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventFrontGearChange)), - ), + }}, containingField: factory.CreateField(mesgnum.Event, fieldnum.EventData).WithValue(uint32(0x00010E08)), components: func() []proto.Component { subfields := factory.CreateField(mesgnum.Event, fieldnum.EventData).SubFields @@ -1860,27 +1860,27 @@ func TestExpandComponents(t *testing.T) { }, { name: "expand components containing field value mismatch", - mesg: factory.CreateMesgOnly(mesgnum.Record).WithFields( + mesg: proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue("invalid value"), - ), + }}, containingField: factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue("invalid value"), components: factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).Components, nFieldAfterExpansion: 1, }, { name: "expand components accumulate", - mesg: factory.CreateMesgOnly(mesgnum.Hr).WithFields( + mesg: proto.Message{Num: mesgnum.Hr, Fields: []proto.Field{ factory.CreateField(mesgnum.Hr, fieldnum.HrEventTimestamp).WithValue(uint8(10)), - ), + }}, containingField: factory.CreateField(mesgnum.Hr, fieldnum.HrEventTimestamp12).WithValue(uint8(10)), components: factory.CreateField(mesgnum.Hr, fieldnum.HrEventTimestamp12).Components, nFieldAfterExpansion: 2, }, { name: "expand components do not expand when containing field's value is invalid", - mesg: factory.CreateMesgOnly(mesgnum.Session).WithFields( + mesg: proto.Message{Num: mesgnum.Session, Fields: []proto.Field{ factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(basetype.Uint16Invalid)), - ), + }}, containingField: factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(basetype.Uint16Invalid)), components: factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).Components, nFieldAfterExpansion: 1, @@ -1903,7 +1903,7 @@ func TestExpandMutipleComponents(t *testing.T) { compressedSepeedDistanceField := factory.CreateField(mesgnum.Record, fieldnum.RecordCompressedSpeedDistance). WithValue([]byte{0, 4, 1}) - mesg := factory.CreateMesgOnly(mesgnum.Record).WithFields(compressedSepeedDistanceField) + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{compressedSepeedDistanceField}} dec := New(nil) dec.expandComponents(&mesg, &compressedSepeedDistanceField, compressedSepeedDistanceField.Components) @@ -2004,10 +2004,10 @@ func TestExpandMutipleComponentsDynamicField(t *testing.T) { }, ) - mesg := fac.CreateMesgOnly(customMesgNum).WithFields( + mesg := proto.Message{Num: customMesgNum, Fields: []proto.Field{ fac.CreateField(customMesgNum, 0).WithValue(uint8(10)), // event fac.CreateField(customMesgNum, 2).WithValue(uint32(10)), // compressed_data - ) + }} dec := New(nil, WithFactory(fac)) fieldToExpand := mesg.FieldByNum(2) @@ -2044,14 +2044,14 @@ func TestDecodeDeveloperFields(t *testing.T) { 0, }, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2070,14 +2070,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer fields missing fieldDescription with developer data index 1", r: fnReaderOK, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2096,14 +2096,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer fields missing field description number", r: fnReaderOK, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2122,14 +2122,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer fields missing field description number but unable to read acquired bytes", r: fnReaderErr, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2149,14 +2149,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer fields got io.EOF", r: fnReaderErr, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2176,14 +2176,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer field, devField def's size is zero, skip", r: fnReaderOK, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2208,14 +2208,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer field, devField def's size 1 < 4 size of uint32 ", r: fnReaderOK, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint32)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2245,14 +2245,14 @@ func TestDecodeDeveloperFields(t *testing.T) { name: "decode developer fields field description has invalid basetype", r: fnReaderOK, fieldDescription: mesgdef.NewFieldDescription( - kit.Ptr(factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + &proto.Message{Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(255)), - )), + }}, ), mesgDef: &proto.MessageDefinition{ Header: proto.MesgDefinitionMask, @@ -2584,6 +2584,7 @@ func TestReset(t *testing.T) { dec.Reset(buf, tc.opts...) if diff := cmp.Diff(dec, tc.dec, + cmp.AllowUnexported(Accumulator{}), cmp.AllowUnexported(options{}), cmp.AllowUnexported(Decoder{}), cmp.AllowUnexported(readBuffer{}), @@ -2645,8 +2646,11 @@ func BenchmarkDecodeMessageData(b *testing.B) { factory.CreateField(mesgnum.Record, fieldnum.RecordTemperature).WithValue(int8(32)), }, } - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgb, err := mesg.MarshalBinary() + mesgDef, err := proto.NewMessageDefinition(&mesg) + if err != nil { + b.Fatal(err) + } + mesgb, err := mesg.MarshalAppend(nil, proto.LittleEndian) if err != nil { b.Fatalf("marshal binary: %v", err) } @@ -2662,7 +2666,7 @@ func BenchmarkDecodeMessageData(b *testing.B) { }) dec := New(r, WithIgnoreChecksum(), WithNoComponentExpansion(), WithBroadcastOnly()) - dec.localMessageDefinitions[0] = &mesgDef + dec.localMessageDefinitions[0] = mesgDef b.StartTimer() for i := 0; i < b.N; i++ { diff --git a/decoder/raw.go b/decoder/raw.go index 511c917..421c1a8 100644 --- a/decoder/raw.go +++ b/decoder/raw.go @@ -107,7 +107,7 @@ func (d *RawDecoder) Decode(r io.Reader, fn func(flag RawFlag, b []byte) error) fileHeaderSize := d.BytesArray[0] if fileHeaderSize != 12 && fileHeaderSize != 14 { - return n, fmt.Errorf("file header's size [%d]: %w", fileHeaderSize, ErrNotAFitFile) + return n, fmt.Errorf("file header's size [%d]: %w", fileHeaderSize, ErrNotFITFile) } nr, err = io.ReadFull(r, d.BytesArray[1:fileHeaderSize]) @@ -117,7 +117,7 @@ func (d *RawDecoder) Decode(r io.Reader, fn func(flag RawFlag, b []byte) error) } if string(d.BytesArray[8:12]) != proto.DataTypeFIT { - return n, ErrNotAFitFile + return n, ErrNotFITFile } fileHeaderDataSize := binary.LittleEndian.Uint32(d.BytesArray[4:8]) diff --git a/decoder/raw_test.go b/decoder/raw_test.go index 744c261..ebd7ba0 100644 --- a/decoder/raw_test.go +++ b/decoder/raw_test.go @@ -114,7 +114,7 @@ func TestRawDecoderDecode(t *testing.T) { }) }(), fn: fnDecodeRawOK, - err: ErrNotAFitFile, + err: ErrNotFITFile, }, { name: "unexpected EOF when decode header", @@ -146,7 +146,7 @@ func TestRawDecoderDecode(t *testing.T) { }) }(), fn: fnDecodeRawOK, - err: ErrNotAFitFile, + err: ErrNotFITFile, }, { name: "fn FileHeader returns io.EOF", @@ -197,13 +197,13 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesgDef fields return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) cur := 0 @@ -221,19 +221,20 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesgDef n developer fields return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( - factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ).WithDeveloperFields( - proto.DeveloperField{ - DeveloperDataIndex: 0, - Num: 0, - Value: proto.Uint8(100), - }, - ) + mesg := proto.Message{Num: mesgnum.Record, + Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), + }, DeveloperFields: []proto.DeveloperField{ + { + DeveloperDataIndex: 0, + Num: 0, + Value: proto.Uint8(100), + }, + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) cur := 0 @@ -251,19 +252,19 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesgDef developer fields return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ).WithDeveloperFields( - proto.DeveloperField{ + }, DeveloperFields: []proto.DeveloperField{ + { DeveloperDataIndex: 0, Num: 0, Value: proto.Uint8(100), }, - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) cur := 0 @@ -281,19 +282,19 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesgDef fn return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ).WithDeveloperFields( - proto.DeveloperField{ + }, DeveloperFields: []proto.DeveloperField{ + { DeveloperDataIndex: 0, Num: 0, Value: proto.Uint8(100), }, - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) cur := 0 @@ -316,12 +317,12 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesg, mesgDef not found", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgb, _ := mesg.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgb, _ := mesg.MarshalAppend(nil, proto.LittleEndian) buf = append(buf, mesgb...) cur := 0 @@ -339,13 +340,13 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode mesg, read return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) buf = append(buf, mesgDefb[0]&proto.LocalMesgNumMask) @@ -384,15 +385,15 @@ func TestRawDecoderDecode(t *testing.T) { { name: "decode crc return io.EOF", r: func() io.Reader { - mesg := factory.CreateMesg(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), - ) + }} h := headerForTest() - buf, _ := h.MarshalBinary() - mesgDef := proto.CreateMessageDefinition(&mesg) - mesgDefb, _ := mesgDef.MarshalBinary() + buf, _ := h.MarshalAppend(nil) + mesgDef, _ := proto.NewMessageDefinition(&mesg) + mesgDefb, _ := mesgDef.MarshalAppend(nil) buf = append(buf, mesgDefb...) - mesgb, _ := mesg.MarshalBinary() + mesgb, _ := mesg.MarshalAppend(nil, proto.LittleEndian) buf = append(buf, mesgb...) binary.LittleEndian.PutUint32(buf[4:8], uint32(len(buf)-14)) @@ -469,13 +470,13 @@ func BenchmarkRawDecoderDecode(b *testing.B) { // But since it's big, it's should be good to benchmark. f, err := os.Open("../testdata/big_activity.fit") if err != nil { - panic(err) + b.Fatal(err) } defer f.Close() all, err := io.ReadAll(f) if err != nil { - panic(err) + b.Fatal(err) } buf := bytes.NewBuffer(all) diff --git a/docs/usage.md b/docs/usage.md index a6cd28c..4a919c2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -300,7 +300,7 @@ func main() { ### Decode Chained FIT Files -A single invocation of `Decode()` will process exactly one FIT sequence. To decode chained FIT files, wrap the decode process with a loop and use `dec.Next()` to check whether next sequence of bytes are still a valid FIT sequence. +A single invocation of `Decode()` will process exactly one FIT sequence. To decode a chained FIT file containing multiple FIT data, invoke Decode() or DecodeWithContext() method multiple times. For convenience, we can wrap it with the Next() method as follows (optional): ```go ... @@ -344,7 +344,7 @@ You can also use [PeekFileHeader()](#-Peek-FileHeader), [PeekFileId()](#Peek-Fil ### Peek FileHeader -We can verify whether the given file is a FIT file by checking the File Header (first 12-14 bytes). PeekFileHeader decodes only up to FileHeader (first 12-14 bytes) without decoding the whole reader. After this method is invoked, Decode picks up where this left then continue decoding next messages instead of starting from zero. This method is idempotent and can be invoked even after Decode has been invoked. +We can verify whether the given file is a FIT file by checking the File Header (first 12-14 bytes). PeekFileHeader decodes only up to FileHeader (first 12-14 bytes) without decoding the whole reader. If we choose to continue, Decode picks up where this left then continue decoding next messages instead of starting from zero. ```go package main @@ -849,35 +849,32 @@ func main() { now := time.Now() fit := proto.FIT{ Messages: []proto.Message{ - // Use factory.CreateMesg if performance is not your main concern, - // it's slightly slower as it allocates all of the message's fields. - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - fieldnum.FileIdManufacturer: typedef.ManufacturerBryton, - fieldnum.FileIdProduct: uint16(1901), // Bryton Rider 420 - fieldnum.FileIdProductName: "Bryton Rider 420", - }), - // For better performance, consider using factory.CreateMesgOnly - // or other alternatives listed below, as these only allocate the - // specified fields. However, you can not use WithFieldValues method. - factory.CreateMesgOnly(mesgnum.Activity).WithFields( - factory.CreateField(mesgnum.Activity, fieldnum.ActivityType).WithValue(typedef.ActivityManual.Byte()), - factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), - factory.CreateField(mesgnum.Activity, fieldnum.ActivityNumSessions).WithValue(uint16(1)), - ), - // Alternative #1: Directly compose like this, which is the same as above. - proto.Message{Num: mesgnum.Session}.WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity.Byte()), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerBryton.Uint16()), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProduct).WithValue(uint16(1901)), // Bryton Rider 420 + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("Bryton Rider 420"), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(100)), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(78)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(100)), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalDistance).WithValue(uint32(100)), factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(1000)), factory.CreateField(mesgnum.Session, fieldnum.SessionAvgCadence).WithValue(uint8(78)), factory.CreateField(mesgnum.Session, fieldnum.SessionAvgHeartRate).WithValue(uint8(100)), - ), - // Alternative #2: Compose like this, which is as performant as - // the two examples above. - proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ - {FieldBase: factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).FieldBase, Value: proto.Uint16(1000)}, - {FieldBase: factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).FieldBase, Value: proto.Uint8(78)}, - {FieldBase: factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).FieldBase, Value: proto.Uint8(100)}, + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityType).WithValue(typedef.ActivityManual.Byte()), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityLocalTimestamp).WithValue(datetime.ToUint32(now.Add(7 * time.Hour))), // GMT+7 + factory.CreateField(mesgnum.Activity, fieldnum.ActivityNumSessions).WithValue(uint16(1)), }}, }, } @@ -1147,8 +1144,10 @@ import ( "github.com/muktihari/fit/encoder" "github.com/muktihari/fit/factory" "github.com/muktihari/fit/kit/datetime" + "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 main() { @@ -1164,12 +1163,12 @@ func main() { } // Simplified example, writing only this mesg. - mesg := factory.CreateMesgOnly(mesgnum.FileId).WithFields( + mesg := proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity.Byte()), factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerDevelopment.Uint16()), factory.CreateField(mesgnum.FileId, fieldnum.FileIdProduct).WithValue(uint16(0)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(time.Now())), - ) + }} // Write per message, we can use this to write message as it arrives. // For example, message retrieved from decoder's Listener can be @@ -1180,8 +1179,7 @@ func main() { /* Write more messages */ - // This should be invoked for every sequence of FIT File - // (not every message) to finalize. + // After all messages have been written, invoke this to finalize. if err := streamEnc.SequenceCompleted(); err != nil { panic(err) } diff --git a/encoder/encoder.go b/encoder/encoder.go index 59acb0a..0d88056 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -34,9 +34,6 @@ const ( type headerOption byte const ( - littleEndian = 0 - bigEndian = 1 - // headerOptionNormal is the default header option. // This option has two sub-option to select from: // 1. LocalMessageTypeZero [Default] @@ -87,7 +84,7 @@ type options struct { func defaultOptions() options { return options{ - endianness: littleEndian, + endianness: proto.LittleEndian, headerOption: headerOptionNormal, writeBufferSize: defaultWriteBufferSize, } @@ -107,7 +104,7 @@ type Option func(o *options) // proto.V1, proto.V2, etc, which the validity is ensured. func WithProtocolVersion(protocolVersion proto.Version) Option { return func(o *options) { - if proto.Validate(byte(protocolVersion)) == nil { + if proto.Validate(protocolVersion) == nil { o.protocolVersion = protocolVersion } } @@ -124,7 +121,7 @@ func WithMessageValidator(validator MessageValidator) Option { // WithBigEndian directs the Encoder to encode values in Big-Endian bytes order (default: Little-Endian). func WithBigEndian() Option { - return func(o *options) { o.endianness = bigEndian } + return func(o *options) { o.endianness = proto.BigEndian } } // WithCompressedTimestampHeader directs the Encoder to compress timestamp in header to reduce file size. @@ -191,6 +188,10 @@ func New(w io.Writer, opts ...Option) *Encoder { protocolValidator: new(proto.Validator), localMesgNumLRU: new(lru), buf: make([]byte, 0, 1536), + mesgDef: proto.MessageDefinition{ + FieldDefinitions: make([]proto.FieldDefinition, 0, 255), + DeveloperFieldDefinitions: make([]proto.DeveloperFieldDefinition, 0, 255), + }, } e.Reset(w, opts...) return e @@ -314,11 +315,11 @@ func (e *Encoder) encodeFileHeader(header *proto.FileHeader) error { } if e.options.protocolVersion != 0 { // Override regardless the value in FileHeader. - header.ProtocolVersion = byte(e.options.protocolVersion) + header.ProtocolVersion = e.options.protocolVersion } else if header.ProtocolVersion == 0 { // Default when not specified in FileHeader. - header.ProtocolVersion = byte(proto.V1) + header.ProtocolVersion = proto.V1 } - e.protocolValidator.SetProtocolVersion(proto.Version(header.ProtocolVersion)) + e.protocolValidator.SetProtocolVersion(header.ProtocolVersion) header.DataType = proto.DataTypeFIT header.CRC = 0 // recalculated @@ -432,7 +433,6 @@ func (e *Encoder) encodeMessages(messages []proto.Message) error { // encodeMessage marshals and encodes message definition and its message into w. func (e *Encoder) encodeMessage(mesg *proto.Message) (err error) { mesg.Header = proto.MesgNormalHeaderMask - mesg.Architecture = e.options.endianness if err = e.options.messageValidator.Validate(mesg); err != nil { return fmt.Errorf("message validation failed: %w", err) @@ -463,12 +463,12 @@ func (e *Encoder) encodeMessage(mesg *proto.Message) (err error) { } } - proto.CreateMessageDefinitionTo(&e.mesgDef, mesg) - if err := e.protocolValidator.ValidateMessageDefinition(&e.mesgDef); err != nil { + mesgDef := e.newMessageDefinition(mesg) + if err := e.protocolValidator.ValidateMessageDefinition(mesgDef); err != nil { return err } - b, _ := e.mesgDef.MarshalAppend(e.buf[:0]) + b, _ := mesgDef.MarshalAppend(e.buf[:0]) localMesgNum, isNewMesgDef := e.localMesgNumLRU.Put(b) // This might alloc memory since we need to copy the item. if e.options.headerOption == headerOptionNormal { b[0] = (b[0] &^ proto.LocalMesgNumMask) | localMesgNum // Update the message definition header. @@ -486,7 +486,7 @@ func (e *Encoder) encodeMessage(mesg *proto.Message) (err error) { } // At this point, e.buf may grow. Re-assign e.buf in case slice has grown. - e.buf, err = mesg.MarshalAppend(e.buf[:0]) + e.buf, err = mesg.MarshalAppend(e.buf[:0], mesgDef.Architecture) if err != nil { return fmt.Errorf("marshal mesg failed: %w", err) } @@ -530,9 +530,40 @@ func (e *Encoder) compressTimestampIntoHeader(mesg *proto.Message) { mesg.RemoveFieldByNum(proto.FieldNumTimestamp) } +func (e *Encoder) newMessageDefinition(mesg *proto.Message) *proto.MessageDefinition { + e.mesgDef.Header = proto.MesgDefinitionMask + e.mesgDef.Reserved = 0 + e.mesgDef.Architecture = e.options.endianness + e.mesgDef.MesgNum = mesg.Num + e.mesgDef.FieldDefinitions = e.mesgDef.FieldDefinitions[:0] + e.mesgDef.DeveloperFieldDefinitions = e.mesgDef.DeveloperFieldDefinitions[:0] + + for i := range mesg.Fields { + e.mesgDef.FieldDefinitions = append(e.mesgDef.FieldDefinitions, proto.FieldDefinition{ + Num: mesg.Fields[i].Num, + Size: byte(proto.Sizeof(mesg.Fields[i].Value)), + BaseType: mesg.Fields[i].BaseType, + }) + } + + if len(mesg.DeveloperFields) == 0 { + return &e.mesgDef + } + + e.mesgDef.Header |= proto.DevDataMask + for i := range mesg.DeveloperFields { + e.mesgDef.DeveloperFieldDefinitions = append(e.mesgDef.DeveloperFieldDefinitions, proto.DeveloperFieldDefinition{ + Num: mesg.DeveloperFields[i].Num, + Size: byte(proto.Sizeof(mesg.DeveloperFields[i].Value)), + DeveloperDataIndex: mesg.DeveloperFields[i].DeveloperDataIndex, + }) + } + + return &e.mesgDef +} + func (e *Encoder) encodeCRC() error { - b := e.buf[:2] - binary.LittleEndian.PutUint16(b, e.crc16.Sum16()) + b := binary.LittleEndian.AppendUint16(e.buf[:0], e.crc16.Sum16()) n, err := e.w.Write(b) e.n += int64(n) diff --git a/encoder/encoder_bench_test.go b/encoder/encoder_bench_test.go index 7eec46d..0b2212a 100644 --- a/encoder/encoder_bench_test.go +++ b/encoder/encoder_bench_test.go @@ -35,77 +35,78 @@ var DiscardAt = discardAt{} func createFitForBenchmark(recodSize int) *proto.FIT { now := time.Now() - fit := new(proto.FIT) - fit.Messages = make([]proto.Message, 0, recodSize) - fit.Messages = append(fit.Messages, - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - fieldnum.FileIdManufacturer: typedef.ManufacturerBryton, - fieldnum.FileIdProductName: "1901", - fieldnum.FileIdNumber: uint16(0), - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - fieldnum.FileIdSerialNumber: uint32(5122), - }), - factory.CreateMesg(mesgnum.Sport).WithFieldValues(map[byte]any{ - fieldnum.SportSport: typedef.SportCycling, - fieldnum.SportSubSport: typedef.SubSportRoad, - }), - factory.CreateMesg(mesgnum.Activity).WithFieldValues(map[byte]any{ - fieldnum.ActivityTimestamp: datetime.ToUint32(now), - fieldnum.ActivityType: typedef.ActivityTypeCycling, - fieldnum.ActivityTotalTimerTime: uint32(30877.0 * 1000), - fieldnum.ActivityNumSessions: uint16(1), - fieldnum.ActivityEvent: typedef.EventActivity, - }), - factory.CreateMesg(mesgnum.Session).WithFieldValues(map[byte]any{ - fieldnum.SessionTimestamp: datetime.ToUint32(now), - fieldnum.SessionStartTime: datetime.ToUint32(now), - fieldnum.SessionTotalElapsedTime: uint32(30877.0 * 1000), - fieldnum.SessionTotalDistance: uint32(32172.05 * 100), - fieldnum.SessionSport: typedef.SportCycling, - fieldnum.SessionSubSport: typedef.SubSportRoad, - fieldnum.SessionTotalMovingTime: uint32(22079.0 * 1000), - fieldnum.SessionTotalCalories: uint16(12824), - fieldnum.SessionAvgSpeed: uint16(5.98 * 1000), - fieldnum.SessionMaxSpeed: uint16(13.05 * 1000), - fieldnum.SessionMaxAltitude: uint16((504.0 + 500) * 5), - fieldnum.SessionTotalAscent: uint16(909), - fieldnum.SessionTotalDescent: uint16(901), - fieldnum.SessionSwcLat: int32(0), - fieldnum.SessionSwcLong: int32(0), - fieldnum.SessionNecLat: int32(0), - fieldnum.SessionNecLong: int32(0), - }), - ) + fit := &proto.FIT{ + Messages: []proto.Message{ + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerBryton), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProduct).WithValue(uint16(1901)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("Rider 420"), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdNumber).WithValue(uint16(0)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdSerialNumber).WithValue(uint32(5122)), + }}, + {Num: mesgnum.Sport, Fields: []proto.Field{ + factory.CreateField(mesgnum.Sport, fieldnum.SportSport).WithValue(typedef.SportCycling), + factory.CreateField(mesgnum.Sport, fieldnum.SportSubSport).WithValue(typedef.SubSportRoad), + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityType).WithValue(typedef.ActivityTypeCycling), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTotalTimerTime).WithValue(uint32(30877.0 * 1000)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityNumSessions).WithValue(uint16(1)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityEvent).WithValue(typedef.EventActivity), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalElapsedTime).WithValue(uint32(30877.0 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalDistance).WithValue(uint32(32172.05 * 100)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSport).WithValue(typedef.SportCycling), + factory.CreateField(mesgnum.Session, fieldnum.SessionSubSport).WithValue(typedef.SubSportRoad), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalMovingTime).WithValue(uint32(22079.0 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalCalories).WithValue(uint16(12824)), + factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(5.98 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionMaxSpeed).WithValue(uint16(13.05 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionMaxAltitude).WithValue(uint16((504.0 + 500) * 5)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalAscent).WithValue(uint16(909)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalDescent).WithValue(uint16(901)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSwcLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSwcLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionNecLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionNecLong).WithValue(int32(0)), + }}, + }, + } for i := 0; i < recodSize-len(fit.Messages); i++ { now = now.Add(time.Second) // only time is moving forward if i%100 == 0 { // add event every 100 message - fit.Messages = append(fit.Messages, factory.CreateMesgOnly(mesgnum.Event).WithFields( + fit.Messages = append(fit.Messages, proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now)), factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStop)), - )) + }}) now = now.Add(10 * time.Second) // gap - fit.Messages = append(fit.Messages, factory.CreateMesgOnly(mesgnum.Event).WithFields( + fit.Messages = append(fit.Messages, proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now)), factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStart)), - )) + }}) now = now.Add(time.Second) // gap } - record := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - fieldnum.RecordPositionLat: int32(-90481372), - fieldnum.RecordPositionLong: int32(1323227263), - fieldnum.RecordSpeed: uint16(8.33 * 1000), - fieldnum.RecordDistance: uint32(405.81 * 100), - fieldnum.RecordHeartRate: uint8(110), - fieldnum.RecordCadence: uint8(85), - fieldnum.RecordAltitude: uint16((166.0 + 500.0) * 5.0), - fieldnum.RecordTemperature: int8(32), - }) + record := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(-90481372)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1323227263)), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(8.33 * 1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(405.81 * 100)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(110)), + factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(85)), + factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16((166.0 + 500.0) * 5.0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordTemperature).WithValue(int8(32)), + }} if i%200 == 0 { // assume every 200 record hr sensor is not sending any data record.RemoveFieldByNum(fieldnum.RecordHeartRate) diff --git a/encoder/encoder_test.go b/encoder/encoder_test.go index 08faeaf..ab184ac 100644 --- a/encoder/encoder_test.go +++ b/encoder/encoder_test.go @@ -43,26 +43,26 @@ func TestEncodeRealFiles(t *testing.T) { now := time.Date(2023, 9, 15, 6, 0, 0, 0, time.UTC) fit := &proto.FIT{ Messages: []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerBryton), factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("Bryton Active App"), - ), - factory.CreateMesgOnly(mesgnum.Activity).WithFields( + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ factory.CreateField(mesgnum.Activity, fieldnum.ActivityType).WithValue(typedef.ActivityTypeCycling), factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), factory.CreateField(mesgnum.Activity, fieldnum.ActivityNumSessions).WithValue(uint16(1)), - ), - factory.CreateMesgOnly(mesgnum.Session).WithFields( + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(1000)), factory.CreateField(mesgnum.Session, fieldnum.SessionAvgCadence).WithValue(uint8(78)), factory.CreateField(mesgnum.Session, fieldnum.SessionAvgHeartRate).WithValue(uint8(100)), - ), - factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(78)), factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(100)), - ), + }}, }, } @@ -242,7 +242,7 @@ func TestOptions(t *testing.T) { opts: nil, expected: options{ multipleLocalMessageType: 0, - endianness: 0, + endianness: proto.LittleEndian, messageValidator: NewMessageValidator(), writeBufferSize: defaultWriteBufferSize, }, @@ -258,7 +258,7 @@ func TestOptions(t *testing.T) { }, expected: options{ multipleLocalMessageType: 15, - endianness: 1, + endianness: proto.BigEndian, protocolVersion: proto.V2, messageValidator: fnValidateOK, headerOption: headerOptionNormal, @@ -275,7 +275,7 @@ func TestOptions(t *testing.T) { }, expected: options{ multipleLocalMessageType: 0, - endianness: 1, + endianness: proto.BigEndian, protocolVersion: proto.V2, messageValidator: fnValidateOK, headerOption: headerOptionCompressedTimestamp, @@ -398,9 +398,9 @@ func makeEncodeWithDirectUpdateStrategyTableTest() []encodeWithDirectUpdateTestC { name: "happy flow coverage", fit: &proto.FIT{Messages: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), - ), + }}, }}, w: mockWriterAt{fnWriteOK, fnWriteAtOK}, }, @@ -419,9 +419,9 @@ func makeEncodeWithDirectUpdateStrategyTableTest() []encodeWithDirectUpdateTestC { name: "encode crc error", fit: &proto.FIT{Messages: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), - ), + }}, }}, w: func() io.Writer { fnWrites := []io.Writer{fnWriteOK, fnWriteOK, fnWriteOK, fnWriteErr} @@ -441,9 +441,9 @@ func makeEncodeWithDirectUpdateStrategyTableTest() []encodeWithDirectUpdateTestC { name: "update error", fit: &proto.FIT{FileHeader: proto.FileHeader{Size: 14, DataSize: 100, DataType: proto.DataTypeFIT}, Messages: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), - ), + }}, }}, w: mockWriterAt{fnWriteOK, fnWriteAtErr}, err: io.EOF, @@ -510,9 +510,9 @@ func makeEncodeWithEarlyCheckStrategy() []encodeWithEarlyCheckStrategyTestCase { { name: "encode header error", fit: &proto.FIT{Messages: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(uint16(typedef.ManufacturerGarmin)), - ), + }}, }}, w: fnWriteErr, err: io.EOF, @@ -520,9 +520,9 @@ func makeEncodeWithEarlyCheckStrategy() []encodeWithEarlyCheckStrategyTestCase { { name: "encode messages error", fit: &proto.FIT{Messages: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(uint16(typedef.ManufacturerGarmin)), - ), + }}, }}, w: func() io.Writer { fnInstances := []io.Writer{fnWriteOK, fnWriteErr} @@ -614,7 +614,7 @@ func TestUpdateHeader(t *testing.T) { name: "writeSeeker using stub", header: proto.FileHeader{ Size: 12, - ProtocolVersion: byte(proto.V1), + ProtocolVersion: proto.V1, ProfileVersion: profile.Version, DataType: proto.DataTypeFIT, }, @@ -628,7 +628,7 @@ func TestUpdateHeader(t *testing.T) { expect: func() []byte { h := proto.FileHeader{ Size: 12, - ProtocolVersion: byte(proto.V1), + ProtocolVersion: proto.V1, ProfileVersion: profile.Version, DataType: proto.DataTypeFIT, DataSize: 2, // updated @@ -641,7 +641,7 @@ func TestUpdateHeader(t *testing.T) { name: "writerAt using stub", header: proto.FileHeader{ Size: 12, - ProtocolVersion: byte(proto.V1), + ProtocolVersion: proto.V1, ProfileVersion: profile.Version, DataType: proto.DataTypeFIT, }, @@ -651,7 +651,7 @@ func TestUpdateHeader(t *testing.T) { expect: func() []byte { h := proto.FileHeader{ Size: 12, - ProtocolVersion: byte(proto.V1), + ProtocolVersion: proto.V1, ProfileVersion: profile.Version, DataType: proto.DataTypeFIT, DataSize: 2, // updated @@ -830,7 +830,7 @@ func TestEncodeHeader(t *testing.T) { }, header: proto.FileHeader{ Size: 14, - ProtocolVersion: byte(proto.V1), + ProtocolVersion: proto.V1, ProfileVersion: 2135, DataSize: 136830, DataType: ".FIT", @@ -876,28 +876,28 @@ func TestEncodeMessage(t *testing.T) { }{ { name: "encode message with default header option happy flow", - mesg: factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - }), + mesg: proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + }}, w: fnWriteOK, }, { name: "encode message with big-endian", - mesg: factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - }), + mesg: proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + }}, w: fnWriteOK, opts: []Option{WithBigEndian()}, - endianness: bigEndian, + endianness: proto.BigEndian, }, { name: "encode message with header normal multiple local message type happy flow", opts: []Option{ WithNormalHeader(2), }, - mesg: factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - }), + mesg: proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + }}, w: fnWriteOK, }, { @@ -905,9 +905,9 @@ func TestEncodeMessage(t *testing.T) { opts: []Option{ WithCompressedTimestampHeader(), }, - mesg: factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - }), + mesg: proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + }}, w: fnWriteOK, }, { @@ -1008,8 +1008,8 @@ func TestEncodeMessage(t *testing.T) { t.Fatalf("message header should not contain Developer Data Flag") } - if tc.mesg.Architecture != tc.endianness { - t.Fatalf("expected endianness: %d, got: %d", tc.endianness, tc.mesg.Architecture) + if enc.mesgDef.Architecture != tc.endianness { + t.Fatalf("expected endianness: %d, got: %d", tc.endianness, enc.mesgDef.Architecture) } }) } @@ -1024,7 +1024,9 @@ func TestEncodeMessage(t *testing.T) { factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16((166.0 + 500.0) * 5.0)), }, } - expected := mesg.Clone() + expected := proto.Message{ + Fields: append(mesg.Fields[:0:0], mesg.Fields...), + } enc := New(io.Discard, WithCompressedTimestampHeader(), @@ -1053,17 +1055,17 @@ func TestEncodeMessage(t *testing.T) { func TestEncodeMessageWithMultipleLocalMessageType(t *testing.T) { now := time.Now() mesgs := []proto.Message{ - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(time.Second)), - fieldnum.RecordHeartRate: uint8(70), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(2 * time.Second)), - fieldnum.RecordSpeed: uint16(1000), - }), + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(time.Second))), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(70)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), + }}, } t.Run("multiple local mesg type", func(t *testing.T) { @@ -1072,7 +1074,7 @@ func TestEncodeMessageWithMultipleLocalMessageType(t *testing.T) { mesgs := append(mesgs[:0:0], mesgs...) for i := range mesgs { - mesgs[i] = mesgs[i].Clone() + mesgs[i].Fields = append(mesgs[i].Fields[:0:0], mesgs[i].Fields...) } buf := new(bytes.Buffer) @@ -1092,9 +1094,9 @@ func TestEncodeMessageWithMultipleLocalMessageType(t *testing.T) { } // add 4th mesg, header should be 0, reset. - mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - }) + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + }} buf.Reset() if err := enc.encodeMessage(&mesg); err != nil { t.Fatal(err) @@ -1120,10 +1122,10 @@ func makeEncodeMessagesTableTest() []encodeMessagesTestCase { name: "encode messages happy flow", mesgValidator: fnValidateOK, mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(uint16(typedef.ManufacturerGarmin)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdProduct).WithValue(uint16(typedef.GarminProductEdge1030)), - ), + }}, }, }, { @@ -1139,7 +1141,7 @@ func makeEncodeMessagesTableTest() []encodeMessagesTestCase { }, { name: "missing file_id mesg", - mesgs: []proto.Message{factory.CreateMesg(mesgnum.Record)}, + mesgs: []proto.Message{{Num: mesgnum.Record}}, err: ErrMissingFileId, }, } @@ -1183,22 +1185,22 @@ func TestCompressTimestampInHeader(t *testing.T) { { name: "compress timestamp in header happy flow", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdManufacturer: typedef.ManufacturerGarmin, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(time.Second)), // +1s - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(2 * time.Second)), // +2s - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(32 * time.Second)), // +32 rollover - }), + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerGarmin), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(time.Second))), // +1), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), // +2), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(32 * time.Second))), // +32 rollove), + }}, }, headers: []byte{ proto.MesgNormalHeaderMask, // file_id: has no timestamp @@ -1211,19 +1213,19 @@ func TestCompressTimestampInHeader(t *testing.T) { { name: "compress timestamp in header happy flow: roll over occurred exactly after 32 seconds", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdManufacturer: typedef.ManufacturerGarmin, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(32 * time.Second)), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now.Add(33 * time.Second)), - }), + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerGarmin), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(32 * time.Second))), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(33 * time.Second))), + }}, }, headers: []byte{ proto.MesgNormalHeaderMask, // file_id: has no timestamp @@ -1235,13 +1237,13 @@ func TestCompressTimestampInHeader(t *testing.T) { { name: "timestamp less than DateTimeMin", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdManufacturer: typedef.ManufacturerGarmin, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: uint32(1234), - }), + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerGarmin), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1234)), + }}, }, headers: []byte{ proto.MesgNormalHeaderMask, @@ -1251,13 +1253,13 @@ func TestCompressTimestampInHeader(t *testing.T) { { name: "timestamp wrong type not uint32 or typedef.DateTime", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdManufacturer: typedef.ManufacturerGarmin, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: typedef.DateTime(datetime.ToUint32(now)), - }), + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerGarmin), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(typedef.DateTime(datetime.ToUint32(now))), + }}, }, headers: []byte{ proto.MesgNormalHeaderMask, @@ -1267,13 +1269,13 @@ func TestCompressTimestampInHeader(t *testing.T) { { name: "timestamp wrong type not uint32 or typedef.DateTime", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdManufacturer: typedef.ManufacturerGarmin, - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - }), - factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: now, // time.Time{} - }), + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerGarmin), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(now), // time.Time{), + }}, }, headers: []byte{ proto.MesgNormalHeaderMask, @@ -1298,6 +1300,189 @@ func TestCompressTimestampInHeader(t *testing.T) { } } +func TestNewMessageDefinition(t *testing.T) { + tt := []struct { + name string + mesg *proto.Message + arch byte + mesgDef *proto.MessageDefinition + }{ + { + name: "fields only with non-array values", + mesg: &proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.FileIdType, BaseType: basetype.Enum}, Value: proto.Uint8(typedef.FileActivity.Byte())}, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask, + MesgNum: mesgnum.FileId, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.FileIdType, + Size: 1, + BaseType: basetype.Enum, + }, + }, + }, + }, + { + name: "fields only with mesg architecture big-endian", + mesg: func() *proto.Message { + mesg := &proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.FileIdType, BaseType: basetype.Enum}, Value: proto.Uint8(typedef.FileActivity.Byte())}, + }} + return mesg + }(), + arch: 1, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask, + Architecture: proto.BigEndian, + MesgNum: mesgnum.FileId, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.FileIdType, + Size: 1, + BaseType: basetype.Enum, + }, + }, + }, + }, + { + name: "fields only with string value", + mesg: &proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.FileIdProductName, BaseType: basetype.String}, Value: proto.String("FIT SDK Go")}, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask, + MesgNum: mesgnum.FileId, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.FileIdProductName, + Size: 1 * 11, // len("FIT SDK Go") == 10 + '0x00' + BaseType: basetype.String, + }, + }, + }, + }, + { + name: "fields only with array of byte", + mesg: &proto.Message{Num: mesgnum.UserProfile, Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: proto.SliceUint8([]byte{2, 9})}, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + }, + }, + + { + name: "developer fields", + mesg: &proto.Message{Num: mesgnum.UserProfile, + Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: proto.SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []proto.DeveloperField{ + {Num: 0, DeveloperDataIndex: 0, Value: proto.Uint8(1)}, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask | proto.DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ + { + Num: 0, Size: 1, DeveloperDataIndex: 0, + }, + }, + }, + }, + { + name: "developer fields with string value \"FIT SDK Go\", size should be 11", + mesg: &proto.Message{Num: mesgnum.UserProfile, + Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: proto.SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []proto.DeveloperField{ + { + Num: 0, DeveloperDataIndex: 0, Value: proto.String("FIT SDK Go"), + }, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask | proto.DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ + { + Num: 0, Size: 11, DeveloperDataIndex: 0, + }, + }, + }, + }, + { + name: "developer fields with value []uint16{1,2,3}, size should be 3*2 = 6", + mesg: &proto.Message{Num: mesgnum.UserProfile, + Fields: []proto.Field{ + {FieldBase: &proto.FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: proto.SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []proto.DeveloperField{ + {Num: 0, DeveloperDataIndex: 0, Value: proto.SliceUint16([]uint16{1, 2, 3})}, + }}, + mesgDef: &proto.MessageDefinition{ + Header: proto.MesgDefinitionMask | proto.DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []proto.FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ + { + Num: 0, Size: 6, DeveloperDataIndex: 0, + }, + }, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + enc := New(nil) + enc.options.endianness = tc.arch + mesgDef := enc.newMessageDefinition(tc.mesg) + if diff := cmp.Diff(mesgDef, tc.mesgDef, + cmp.Transformer("DeveloperFieldDefinitions", + func(devFields []proto.DeveloperFieldDefinition) []proto.DeveloperFieldDefinition { + if len(devFields) == 0 { + return nil + } + return devFields + }), + ); diff != "" { + t.Fatal(diff) + } + }) + } +} + // bufferAt wraps bytes.Buffer to enable WriteAt for faster encoding. type bufferAt struct{ *bytes.Buffer } @@ -1387,9 +1572,9 @@ func TestEncodeMessagesWithContext(t *testing.T) { cancel() mesgs := []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileActivity)), - ), + }}, } enc := New(nil, WithWriteBufferSize(0)) err := enc.encodeMessagesWithContext(ctx, mesgs) diff --git a/encoder/stream_test.go b/encoder/stream_test.go index 8ccef38..cf86e68 100644 --- a/encoder/stream_test.go +++ b/encoder/stream_test.go @@ -27,12 +27,12 @@ func TestStreamEncoderOneSequenceHappyFlow(t *testing.T) { t.Fatal(err) } - fileIdMesg := factory.CreateMesgOnly(mesgnum.FileId).WithFields( + fileIdMesg := proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(time.Now())), - ) - recordMesg := factory.CreateMesgOnly(mesgnum.Record).WithFields( + }} + recordMesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1000)), - ) + }} err = streamEnc.WriteMessage(&fileIdMesg) if err != nil { @@ -72,9 +72,9 @@ func TestStreamEncoderUnhappyFlow(t *testing.T) { enc := New(mockWriterAt{Writer: fnWriteErr}, WithWriteBufferSize(0)) streamEnc, _ := enc.StreamEncoder() - mesg := factory.CreateMesgOnly(mesgnum.FileId).WithFields( + mesg := proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(time.Now())), - ) + }} err := streamEnc.WriteMessage(&mesg) if !errors.Is(err, io.EOF) { t.Fatalf("expected err: %v, got: %v", io.EOF, err) @@ -155,9 +155,9 @@ func TestStreamEncoderWithoutWriteBuffer(t *testing.T) { t.Fatal(err) } - fileIdMesg := factory.CreateMesgOnly(mesgnum.FileId).WithFields( + fileIdMesg := proto.Message{Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(time.Now())), - ) + }} err = streamEnc.WriteMessage(&fileIdMesg) if err != nil { diff --git a/encoder/validator.go b/encoder/validator.go index a0c2427..88e233e 100644 --- a/encoder/validator.go +++ b/encoder/validator.go @@ -59,7 +59,8 @@ func defaultValidatorOptions() validatorOptions { // Factory defines a contract that any Factory containing these method can be used by the Encoder's Validator. type Factory interface { - // CreateField create new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. + // CreateField creates new field based on defined messages in the factory. + // If not found, it returns new field with "unknown" name. CreateField(mesgNum typedef.MesgNum, num byte) proto.Field } @@ -107,8 +108,6 @@ func (v *messageValidator) Reset() { } func (v *messageValidator) Validate(mesg *proto.Message) error { - mesg.Header = proto.MesgNormalHeaderMask // reset default - var valid int for i := range mesg.Fields { field := &mesg.Fields[i] diff --git a/encoder/validator_test.go b/encoder/validator_test.go index efab36d..91aa676 100644 --- a/encoder/validator_test.go +++ b/encoder/validator_test.go @@ -106,18 +106,18 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "valid message with developer fields happy flow", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Heart Rate", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(fieldnum.RecordHeartRate), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint8), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -135,7 +135,7 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "mesg contain expanded field", mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.Record).WithFields( + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), func() proto.Field { field := factory.CreateField(mesgnum.Record, fieldnum.RecordEnhancedSpeed) @@ -143,33 +143,33 @@ func TestMessageValidatorValidate(t *testing.T) { field.Value = proto.Uint32(1000) return field }(), - ), + }}, }, }, { name: "mesg contain field with scaled value", mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.Record).WithFields( + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue((float64(37304) / 5) - 500), // 6960.8m - ), + }}, }, }, { name: "mesg contain field value type not align", mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.Record).WithFields( + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint32(1000)), // should be uint16 - ), + }}, }, errs: []error{ErrValueTypeMismatch}, }, { name: "valid message with developer data index not found in previous message sequence", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - }), + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -184,10 +184,10 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "valid message with field description not found in previous message sequence", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -202,21 +202,21 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "invalid utf-8 string", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("\xbd"), - ), + }}, }, errs: []error{ErrInvalidUTF8String}, }, { name: "invalid utf-8 []string", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("valid utf-8 string"), - ), - factory.CreateMesg(mesgnum.SegmentFile).WithFields( + }}, + {Num: mesgnum.SegmentFile, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentFile, fieldnum.SegmentFileLeaderActivityIdString).WithValue([]string{"valid utf-8", "\xbd"}), // valid and invalid string in array - ), + }}, }, errs: []error{nil, ErrInvalidUTF8String}, }, @@ -248,18 +248,18 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "n developer fields exceed allowed", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Heart Rate", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(fieldnum.RecordHeartRate), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint8), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), + }}, { Num: mesgnum.Record, Fields: []proto.Field{ @@ -282,9 +282,9 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "field value size exceed max allowed", mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue(strings.Repeat("a", 256)), - ), + }}, }, errs: []error{ErrExceedMaxAllowed}, }, @@ -308,31 +308,31 @@ func TestMessageValidatorValidate(t *testing.T) { return mesgValidator }(), mesgs: []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithDeveloperFields( - proto.DeveloperField{ + {Num: mesgnum.FileId, DeveloperFields: []proto.DeveloperField{ + { DeveloperDataIndex: 0, Num: 1, Value: proto.String(strings.Repeat("a", 256)), }, - ), + }}, }, errs: []error{ErrExceedMaxAllowed}, }, { name: "valid message with developer fields invalid value", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Heart Rate", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(fieldnum.RecordHeartRate), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint8), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -350,20 +350,20 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "mesg contain developer field value scaled", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Custom Distance", - fieldnum.FieldDescriptionNativeMesgNum: uint16(basetype.Uint16Invalid), - fieldnum.FieldDescriptionNativeFieldNum: uint8(basetype.Uint8Invalid), - fieldnum.FieldDescriptionScale: uint8(100), - fieldnum.FieldDescriptionOffset: int8(0), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint16), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Custom Distance"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(basetype.Uint16Invalid)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(basetype.Uint8Invalid)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionScale).WithValue(uint8(100)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionOffset).WithValue(int8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint16)), + }}, { Num: mesgnum.Record, Fields: []proto.Field{ @@ -382,18 +382,18 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "mesg contain developer field with native value scaled", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Altitude", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(fieldnum.RecordAltitude), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint16), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Altitude"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordAltitude)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint16)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -411,18 +411,18 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "mesg contain developer field with unknown native", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "??", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(255), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint16), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("??"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(255)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint16)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -462,18 +462,18 @@ func TestMessageValidatorValidate(t *testing.T) { { name: "valid message with developer fields has invalid value", mesgs: []proto.Message{ - factory.CreateMesg(mesgnum.DeveloperDataId).WithFieldValues(map[byte]any{ - fieldnum.DeveloperDataIdDeveloperDataIndex: uint8(0), - fieldnum.DeveloperDataIdApplicationId: []byte{0, 1, 2, 3}, - }), - factory.CreateMesg(mesgnum.FieldDescription).WithFieldValues(map[byte]any{ - fieldnum.FieldDescriptionDeveloperDataIndex: uint8(0), - fieldnum.FieldDescriptionFieldDefinitionNumber: uint8(0), - fieldnum.FieldDescriptionFieldName: "Heart Rate", - fieldnum.FieldDescriptionNativeMesgNum: uint16(mesgnum.Record), - fieldnum.FieldDescriptionNativeFieldNum: uint8(fieldnum.RecordHeartRate), - fieldnum.FieldDescriptionFitBaseTypeId: uint8(basetype.Uint8), - }), + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdApplicationId).WithValue([]byte{0, 1, 2, 3}), + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue("Heart Rate"), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), + factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), + }}, { Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(time.Now())), @@ -522,7 +522,7 @@ func TestMessageValidatorValidate(t *testing.T) { func BenchmarkValidate(b *testing.B) { b.StopTimer() mesgValidator := NewMessageValidator() - mesg := factory.CreateMesgOnly(mesgnum.Record).WithFields( + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16(10000)), func() proto.Field { @@ -543,7 +543,7 @@ func BenchmarkValidate(b *testing.B) { field.Value = proto.Uint32(1000) return field }(), - ) + }} b.StartTimer() for i := 0; i < b.N; i++ { diff --git a/factory/exported_gen.go b/factory/exported_gen.go index 89dc5ab..7cd2478 100755 --- a/factory/exported_gen.go +++ b/factory/exported_gen.go @@ -19,7 +19,7 @@ func StandardFactory() *Factory { return std } // CreateMesg creates new message based on defined messages in the factory. If not found, it returns proto.Message{Num: num}. // // This will create a shallow copy of the Fields, so changing any value declared in Field's FieldBase is prohibited -// (except in case of unknown field). If you want a deep copy of the mesg, use mesg.Clone(). +// (except in case of unknown field). // // NOTE: This method is not used by either the Decoder or the Encoder, and the data will only be populated once upon the first invocation. // Unless you need most of the returned fields, it's recommended create an empty proto.Message{Num: num} then fill only the necessary fields @@ -28,15 +28,10 @@ func CreateMesg(num typedef.MesgNum) proto.Message { return std.CreateMesg(num) } -// CreateMesgOnly is a syntax sugar for creating proto.Message{Num: num}. -func CreateMesgOnly(num typedef.MesgNum) proto.Message { - return std.CreateMesgOnly(num) -} - // CreateField creates new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. // // The returned field contains a pointer reference to FieldBase defined in the factory, so changing any value -// declared in FieldBase is prohibited (except in the case of unknown field). If you want a deep copy, use field.Clone(). +// declared in FieldBase is prohibited (except in the case of unknown field). func CreateField(mesgNum typedef.MesgNum, num byte) proto.Field { return std.CreateField(mesgNum, num) } diff --git a/factory/factory_gen.go b/factory/factory_gen.go index a6cb597..1cff314 100755 --- a/factory/factory_gen.go +++ b/factory/factory_gen.go @@ -51,7 +51,7 @@ var ( // cache for CreateMesg method // CreateMesg creates new message based on defined messages in the factory. If not found, it returns proto.Message{Num: num}. // // This will create a shallow copy of the Fields, so changing any value declared in Field's FieldBase is prohibited -// (except in case of unknown field). If you want a deep copy of the mesg, use mesg.Clone(). +// (except in case of unknown field). // // NOTE: This method is not used by either the Decoder or the Encoder, and the data will only be populated once upon the first invocation. // Unless you need most of the returned fields, it's recommended create an empty proto.Message{Num: num} then fill only the necessary fields @@ -118,15 +118,10 @@ func (f *Factory) CreateMesg(num typedef.MesgNum) proto.Message { return mesg } -// CreateMesgOnly is a syntax sugar for creating proto.Message{Num: num}. -func (f *Factory) CreateMesgOnly(num typedef.MesgNum) proto.Message { - return proto.Message{Num: num} -} - // CreateField creates new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. // // The returned field contains a pointer reference to FieldBase defined in the factory, so changing any value -// declared in FieldBase is prohibited (except in the case of unknown field). If you want a deep copy, use field.Clone(). +// declared in FieldBase is prohibited (except in the case of unknown field). func (f *Factory) CreateField(mesgNum typedef.MesgNum, num byte) proto.Field { if mesgNum < 410 && mesgs[mesgNum] != nil { fieldBase := mesgs[mesgNum][num] diff --git a/fuzz_test.go b/fuzz_test.go index 160c9e8..7b1ff89 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -134,9 +134,11 @@ func FuzzDecodeEncodeRoundTrip(f *testing.F) { return } - if diff := cmp.Diff(sanitizeOutput(in), sanitizeOutput(encoded)); diff != "" { - fitdump("in", t, in) - fitdump("encoded", t, encoded) + sanitizedIn := sanitizeOutput(in) + sanitizedEncoded := sanitizeOutput(encoded) + if diff := cmp.Diff(sanitizedIn, sanitizedEncoded); diff != "" { + fitdump("in", t, sanitizedIn) + fitdump("encoded", t, sanitizedEncoded) t.Fatal(diff) } }) @@ -236,6 +238,7 @@ func sanitizeOutput(in []byte) []byte { case decoder.RawFlagMesgDef: // the encoder used for test always use localMesgNum 0 b[0] = proto.MesgDefinitionMask + b[1] = 0 // Reserved case decoder.RawFlagMesgData: // the encoder used for test always use localMesgNum 0 b[0] = 0 diff --git a/internal/cmd/benchfit/go.mod b/internal/cmd/benchfit/go.mod index 8bfc7de..d1da9d2 100644 --- a/internal/cmd/benchfit/go.mod +++ b/internal/cmd/benchfit/go.mod @@ -17,9 +17,9 @@ require ( golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect honnef.co/go/tools v0.4.2 // indirect mvdan.cc/gofumpt v0.4.0 // indirect diff --git a/internal/cmd/benchfit/go.sum b/internal/cmd/benchfit/go.sum index 9dfc5f1..0ea21db 100644 --- a/internal/cmd/benchfit/go.sum +++ b/internal/cmd/benchfit/go.sum @@ -69,8 +69,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -97,8 +97,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/internal/cmd/fitgen/factory/factory.tmpl b/internal/cmd/fitgen/factory/factory.tmpl index a87b8f2..4c8f9ab 100644 --- a/internal/cmd/fitgen/factory/factory.tmpl +++ b/internal/cmd/fitgen/factory/factory.tmpl @@ -120,11 +120,6 @@ func (f *Factory) CreateMesg(num typedef.MesgNum) proto.Message { return mesg } -{{ template "create_mesg_only_doc" -}} -func (f *Factory) CreateMesgOnly(num typedef.MesgNum) proto.Message { - return proto.Message{Num: num} -} - {{ template "create_field_doc" -}} func (f *Factory) CreateField(mesgNum typedef.MesgNum, num byte) proto.Field { if mesgNum < {{ .LenMesgs }} && mesgs[mesgNum] != nil { @@ -191,11 +186,6 @@ func CreateMesg(num typedef.MesgNum) proto.Message { return std.CreateMesg(num) } -{{- template "create_mesg_only_doc" -}} -func CreateMesgOnly(num typedef.MesgNum) proto.Message{ - return std.CreateMesgOnly(num) -} - {{ template "create_field_doc" -}} func CreateField(mesgNum typedef.MesgNum, num byte) proto.Field { return std.CreateField(mesgNum, num) @@ -213,22 +203,18 @@ func RegisterMesg(mesg proto.Message) error { // CreateMesg creates new message based on defined messages in the factory. If not found, it returns proto.Message{Num: num}. // // This will create a shallow copy of the Fields, so changing any value declared in Field's FieldBase is prohibited -// (except in case of unknown field). If you want a deep copy of the mesg, use mesg.Clone(). +// (except in case of unknown field). // // NOTE: This method is not used by either the Decoder or the Encoder, and the data will only be populated once upon the first invocation. // Unless you need most of the returned fields, it's recommended create an empty proto.Message{Num: num} then fill only the necessary fields // using CreateField method. {{ end }} -{{ define "create_mesg_only_doc" }} -// CreateMesgOnly is a syntax sugar for creating proto.Message{Num: num}. -{{ end }} - {{ define "create_field_doc" }} // CreateField creates new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. // // The returned field contains a pointer reference to FieldBase defined in the factory, so changing any value -// declared in FieldBase is prohibited (except in the case of unknown field). If you want a deep copy, use field.Clone(). +// declared in FieldBase is prohibited (except in the case of unknown field). {{ end }} {{ define "register_mesg_doc" }} diff --git a/internal/cmd/testgen/big_activity.go b/internal/cmd/testgen/big_activity.go index 7475b93..212cacc 100644 --- a/internal/cmd/testgen/big_activity.go +++ b/internal/cmd/testgen/big_activity.go @@ -29,63 +29,65 @@ func createBigActivityFile(ctx context.Context) error { defer f.Close() now := datetime.ToTime(uint32(1062766519)) - fit := new(proto.FIT) - fit.Messages = make([]proto.Message, 0, RecordSize) - fit.Messages = append(fit.Messages, - factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ - fieldnum.FileIdType: typedef.FileActivity, - fieldnum.FileIdManufacturer: typedef.ManufacturerBryton, - fieldnum.FileIdProductName: "1901", - fieldnum.FileIdNumber: uint16(0), - fieldnum.FileIdTimeCreated: datetime.ToUint32(now), - fieldnum.FileIdSerialNumber: uint32(5122), - }), - factory.CreateMesg(mesgnum.Sport).WithFieldValues(map[byte]any{ - fieldnum.SportSport: typedef.SportCycling, - fieldnum.SportSubSport: typedef.SubSportRoad, - }), - factory.CreateMesg(mesgnum.Activity).WithFieldValues(map[byte]any{ - fieldnum.ActivityTimestamp: datetime.ToUint32(now), - fieldnum.ActivityType: typedef.ActivityTypeCycling, - fieldnum.ActivityTotalTimerTime: uint32(30877.0 * 1000), - fieldnum.ActivityNumSessions: uint16(1), - fieldnum.ActivityEvent: typedef.EventActivity, - }), - factory.CreateMesg(mesgnum.Session).WithFieldValues(map[byte]any{ - fieldnum.SessionTimestamp: datetime.ToUint32(now), - fieldnum.SessionStartTime: datetime.ToUint32(now), - fieldnum.SessionTotalElapsedTime: uint32(30877.0 * 1000), - fieldnum.SessionTotalDistance: uint32(32172.05 * 100), - fieldnum.SessionSport: typedef.SportCycling, - fieldnum.SessionSubSport: typedef.SubSportRoad, - fieldnum.SessionTotalMovingTime: uint32(22079.0 * 1000), - fieldnum.SessionTotalCalories: uint16(12824), - fieldnum.SessionAvgSpeed: uint16(5.98 * 1000), - fieldnum.SessionMaxSpeed: uint16(13.05 * 1000), - fieldnum.SessionMaxAltitude: uint16((504.0 + 500) * 5), - fieldnum.SessionTotalAscent: uint16(909), - fieldnum.SessionTotalDescent: uint16(901), - fieldnum.SessionSwcLat: int32(0), - fieldnum.SessionSwcLong: int32(0), - fieldnum.SessionNecLat: int32(0), - fieldnum.SessionNecLong: int32(0), - }), - ) + fit := &proto.FIT{ + Messages: []proto.Message{ + {Num: mesgnum.FileId, Fields: []proto.Field{ + factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerBryton), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProduct).WithValue(uint16(1901)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("Rider 420"), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdNumber).WithValue(uint16(0)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.FileId, fieldnum.FileIdSerialNumber).WithValue(uint32(5122)), + }}, + {Num: mesgnum.Sport, Fields: []proto.Field{ + factory.CreateField(mesgnum.Sport, fieldnum.SportSport).WithValue(typedef.SportCycling), + factory.CreateField(mesgnum.Sport, fieldnum.SportSubSport).WithValue(typedef.SubSportRoad), + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityType).WithValue(typedef.ActivityTypeCycling), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTotalTimerTime).WithValue(uint32(30877.0 * 1000)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityNumSessions).WithValue(uint16(1)), + factory.CreateField(mesgnum.Activity, fieldnum.ActivityEvent).WithValue(typedef.EventActivity), + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ + factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Session, fieldnum.SessionStartTime).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalElapsedTime).WithValue(uint32(30877.0 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalDistance).WithValue(uint32(32172.05 * 100)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSport).WithValue(typedef.SportCycling), + factory.CreateField(mesgnum.Session, fieldnum.SessionSubSport).WithValue(typedef.SubSportRoad), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalMovingTime).WithValue(uint32(22079.0 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalCalories).WithValue(uint16(12824)), + factory.CreateField(mesgnum.Session, fieldnum.SessionAvgSpeed).WithValue(uint16(5.98 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionMaxSpeed).WithValue(uint16(13.05 * 1000)), + factory.CreateField(mesgnum.Session, fieldnum.SessionMaxAltitude).WithValue(uint16((504.0 + 500) * 5)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalAscent).WithValue(uint16(909)), + factory.CreateField(mesgnum.Session, fieldnum.SessionTotalDescent).WithValue(uint16(901)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSwcLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionSwcLong).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionNecLat).WithValue(int32(0)), + factory.CreateField(mesgnum.Session, fieldnum.SessionNecLong).WithValue(int32(0)), + }}, + }, + } n := RecordSize - len(fit.Messages) for i := 0; i < n; i++ { now = now.Add(time.Second) // only time is moving forward - fit.Messages = append(fit.Messages, factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - fieldnum.RecordPositionLat: int32(-90481372), - fieldnum.RecordPositionLong: int32(1323227263), - fieldnum.RecordSpeed: uint16(8.33 * 1000), - fieldnum.RecordDistance: uint32(405.81 * 100), - fieldnum.RecordHeartRate: uint8(110), - fieldnum.RecordCadence: uint8(85), - fieldnum.RecordAltitude: uint16((166.0 + 500.0) * 5.0), - fieldnum.RecordTemperature: int8(32), - })) + record := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(-90481372)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1323227263)), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(8.33 * 1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(405.81 * 100)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(110)), + factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(85)), + factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16((166.0 + 500.0) * 5.0)), + factory.CreateField(mesgnum.Record, fieldnum.RecordTemperature).WithValue(int8(32)), + }} + fit.Messages = append(fit.Messages, record) } enc := encoder.New(f) diff --git a/internal/cmd/testgen/main.go b/internal/cmd/testgen/main.go index 6ba4b28..641615d 100644 --- a/internal/cmd/testgen/main.go +++ b/internal/cmd/testgen/main.go @@ -61,26 +61,23 @@ func createValidFitOnlyContainFileId(ctx context.Context) error { type Uint16 uint16 now := datetime.ToTime(uint32(1062766519)) - fit := new(proto.FIT).WithMessages( - factory.CreateMesgOnly(typedef.MesgNumFileId).WithFields( + fit := &proto.FIT{Messages: []proto.Message{ + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("something ss"), factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerBryton), factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), - ), - factory.CreateMesgOnly(typedef.MesgNumActivity).WithFields( - factory.CreateField(typedef.MesgNumActivity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), - ), - factory.CreateMesg(typedef.MesgNumRecord).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(now), - fieldnum.RecordHeartRate: uint8(112), - fieldnum.RecordCadence: uint8(80), - // fieldnum.RecordAltitude: float64(150), // input scaled value - fieldnum.RecordAltitude: Uint16((150 + 500) * 5), // input scaled value - // fieldnum.RecordAltitude: uint16((150 + 500) * 5), // input scaled value - // fieldnum.RecordAltitude: "something", // input scaled value - }), - ) + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ + factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(now)), + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(112)), + factory.CreateField(mesgnum.Record, fieldnum.RecordCadence).WithValue(uint8(80)), + factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(Uint16((150 + 500) * 5)), + }}, + }} enc := encoder.New(f) if err := enc.EncodeWithContext(ctx, fit); err != nil { diff --git a/internal/kit/kit.go b/internal/kit/kit.go deleted file mode 100644 index 1b56f1e..0000000 --- a/internal/kit/kit.go +++ /dev/null @@ -1,8 +0,0 @@ -// 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 kit - -// Ptr returns new pointer of v -func Ptr[T any](v T) *T { return &v } diff --git a/internal/kit/kit_test.go b/internal/kit/kit_test.go deleted file mode 100644 index 0fe4a81..0000000 --- a/internal/kit/kit_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// 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 kit_test - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/muktihari/fit/internal/kit" -) - -func TestPtr(t *testing.T) { - val := float64(10) - ptr := kit.Ptr(val) - - if diff := cmp.Diff(val, *ptr); diff != "" { - t.Fatal(diff) - } -} diff --git a/profile/basetype/basetype.go b/profile/basetype/basetype.go index 90d1d96..14000ec 100644 --- a/profile/basetype/basetype.go +++ b/profile/basetype/basetype.go @@ -9,13 +9,26 @@ import ( "strconv" ) -// BaseTypeNumMask used to get the index/order of the constants (start from 0, Enum). -// Example: (Sint16 & BaseTypeNumMask) -> 3. -const BaseTypeNumMask = 0x1F - // BaseType is the base of all types used in FIT. +// +// Bits layout: +// - 7 : Endian Ability (0: for single byte data; 1: if base type has endianness) +// - 5–6: Reserved +// - 0–4: Base Type Number type BaseType byte +const ( + // BaseTypeNumMask is used to get the Base Type Number. + // + // Example: (Sint16 & BaseTypeNumMask) -> 3. + BaseTypeNumMask = 0b00011111 + + // EndianAbilityMask is used to get the Endian Ability. + // + // Example: (Sint32 & EndianAbilityMask == EndianAbilityMask) -> true + EndianAbilityMask = 0b10000000 +) + const ( Enum BaseType = 0x00 Sint8 BaseType = 0x01 // 2’s complement format @@ -97,29 +110,6 @@ func FromString(s string) BaseType { return 255 } -// List returns all constants. -func List() []BaseType { - return []BaseType{ - Enum, - Sint8, - Uint8, - Sint16, - Uint16, - Sint32, - Uint32, - String, - Float32, - Float64, - Uint8z, - Uint16z, - Uint32z, - Byte, - Sint64, - Uint64, - Uint64z, - } -} - // String returns string representation of t. func (t BaseType) String() string { switch t { @@ -186,6 +176,31 @@ func (t BaseType) Size() byte { return sizes[t] // PERF: use array to optimize speed since this method is frequently used. } +// Valid checks whether BaseType is valid or not. +func (t BaseType) Valid() bool { + switch t { + case Enum, + Sint8, + Uint8, + Sint16, + Uint16, + Sint32, + Uint32, + String, + Float32, + Float64, + Uint8z, + Uint16z, + Uint32z, + Byte, + Sint64, + Uint64, + Uint64z: + return true + } + return false +} + // GoType returns go equivalent type in string. func (t BaseType) GoType() string { switch t { @@ -219,63 +234,6 @@ func (t BaseType) GoType() string { return "invalid(" + strconv.Itoa(int(t)) + ")" } -// EndianAbility return whether t have endianness. -func (t BaseType) EndianAbility() byte { - switch t { - case Enum, Byte, Uint8, Uint8z: - return 0 - case Sint8: - return 0 - case Sint16: - return 1 - case Uint16, Uint16z: - return 1 - case Sint32: - return 1 - case Uint32, Uint32z: - return 1 - case String: - return 0 - case Float32: - return 1 - case Float64: - return 1 - case Sint64: - return 1 - case Uint64, Uint64z: - return 1 - } - return 0 -} - -func (t BaseType) IsInteger() bool { - switch t { - case Enum, Byte, Uint8, Uint8z: - return true - case Sint8: - return true - case Sint16: - return true - case Uint16, Uint16z: - return true - case Sint32: - return true - case Uint32, Uint32z: - return true - case String: - return false - case Float32: - return false - case Float64: - return false - case Sint64: - return true - case Uint64, Uint64z: - return true - } - return false -} - // Invalid returns invalid value of t. e.g. Byte is 255 (its highest value). func (t BaseType) Invalid() any { switch t { @@ -317,10 +275,10 @@ func (t BaseType) Invalid() any { return "invalid" } -// Valid checks whether BaseType is valid or not. -func (t BaseType) Valid() bool { - switch t { - case Enum, +// List returns all constants. +func List() []BaseType { + return []BaseType{ + Enum, Sint8, Uint8, Sint16, @@ -336,8 +294,6 @@ func (t BaseType) Valid() bool { Byte, Sint64, Uint64, - Uint64z: - return true + Uint64z, } - return false } diff --git a/profile/basetype/basetype_test.go b/profile/basetype/basetype_test.go index e44d3d3..1c9ac49 100644 --- a/profile/basetype/basetype_test.go +++ b/profile/basetype/basetype_test.go @@ -156,74 +156,6 @@ func TestGoType(t *testing.T) { } } -func TestEndianAbility(t *testing.T) { - tt := []struct { - baseType basetype.BaseType - endianess byte - }{ - {baseType: basetype.Sint8, endianess: 0}, - {baseType: basetype.Enum, endianess: 0}, - {baseType: basetype.Byte, endianess: 0}, - {baseType: basetype.Uint8, endianess: 0}, - {baseType: basetype.Uint8z, endianess: 0}, - {baseType: basetype.String, endianess: 0}, - {baseType: basetype.Sint16, endianess: 1}, - {baseType: basetype.Uint16, endianess: 1}, - {baseType: basetype.Uint16z, endianess: 1}, - {baseType: basetype.Sint32, endianess: 1}, - {baseType: basetype.Uint32, endianess: 1}, - {baseType: basetype.Uint32z, endianess: 1}, - {baseType: basetype.Float32, endianess: 1}, - {baseType: basetype.Sint64, endianess: 1}, - {baseType: basetype.Uint64, endianess: 1}, - {baseType: basetype.Uint64z, endianess: 1}, - {baseType: basetype.Float64, endianess: 1}, - {baseType: 255, endianess: 0}, - } - for _, tc := range tt { - t.Run(tc.baseType.String(), func(t *testing.T) { - endianess := tc.baseType.EndianAbility() - if endianess != tc.endianess { - t.Fatalf("expected: %d, got: %d", tc.endianess, endianess) - } - }) - } -} - -func TestIsInteger(t *testing.T) { - tt := []struct { - baseType basetype.BaseType - isInteger bool - }{ - {baseType: basetype.Sint8, isInteger: true}, - {baseType: basetype.Enum, isInteger: true}, - {baseType: basetype.Byte, isInteger: true}, - {baseType: basetype.Uint8, isInteger: true}, - {baseType: basetype.Uint8z, isInteger: true}, - {baseType: basetype.String, isInteger: false}, - {baseType: basetype.Sint16, isInteger: true}, - {baseType: basetype.Uint16, isInteger: true}, - {baseType: basetype.Uint16z, isInteger: true}, - {baseType: basetype.Sint32, isInteger: true}, - {baseType: basetype.Uint32, isInteger: true}, - {baseType: basetype.Uint32z, isInteger: true}, - {baseType: basetype.Float32, isInteger: false}, - {baseType: basetype.Sint64, isInteger: true}, - {baseType: basetype.Uint64, isInteger: true}, - {baseType: basetype.Uint64z, isInteger: true}, - {baseType: basetype.Float64, isInteger: false}, - {baseType: 255, isInteger: false}, - } - for _, tc := range tt { - t.Run(tc.baseType.String(), func(t *testing.T) { - isInteger := tc.baseType.IsInteger() - if isInteger != tc.isInteger { - t.Fatalf("expected: %t, got: %t", tc.isInteger, isInteger) - } - }) - } -} - func TestInvalid(t *testing.T) { tt := []struct { baseType basetype.BaseType diff --git a/profile/filedef/activity_summary_test.go b/profile/filedef/activity_summary_test.go index e98b47d..77d61b4 100644 --- a/profile/filedef/activity_summary_test.go +++ b/profile/filedef/activity_summary_test.go @@ -19,32 +19,32 @@ import ( func newActivitySummaryMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Lap).WithFields( + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(datetime.ToUint32(now)), - ), - factory.CreateMesgOnly(mesgnum.Session).WithFields( + }}, + {Num: mesgnum.Session, Fields: []proto.Field{ factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Activity).WithFields( + }}, + {Num: mesgnum.Activity, Fields: []proto.Field{ factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/activity_test.go b/profile/filedef/activity_test.go index bc48b82..17d5ed7 100644 --- a/profile/filedef/activity_test.go +++ b/profile/filedef/activity_test.go @@ -60,93 +60,93 @@ func newActivityMessageForTest(now time.Time) []proto.Message { func newActivityMessagesWithExpectedOrder(now time.Time) (mesgs []proto.Message, ordered []proto.Message) { mesgs = []proto.Message{ - 0: factory.CreateMesgOnly(mesgnum.FileId).WithFields( + 0: {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileActivity)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), - ), - 1: factory.CreateMesgOnly(mesgnum.DeveloperDataId).WithFields( + }}, + 1: {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - 2: factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + 2: {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldDefinitionNumber).WithValue(uint8(0)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFieldName).WithValue([]string{"Heart Rate"}), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeMesgNum).WithValue(uint16(mesgnum.Record)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionNativeFieldNum).WithValue(uint8(fieldnum.RecordHeartRate)), factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionFitBaseTypeId).WithValue(uint8(basetype.Uint8)), - ), - 3: factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + }}, + 3: {Num: mesgnum.DeviceInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoManufacturer).WithValue(uint16(typedef.ManufacturerGarmin)), - ), - 4: factory.CreateMesgOnly(mesgnum.UserProfile).WithFields( + }}, + 4: {Num: mesgnum.UserProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileFriendlyName).WithValue("Mary Jane"), factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(21)), - ), - 5: factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + 5: {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStart)), - ), - 6: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 6: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 7: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 7: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 8: factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + 8: {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 9: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 9: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 10: factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + 10: {Num: mesgnum.Event, Fields: []proto.Field{ // Intentionally using same timestamp as last message. factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now)), - ), - 11: factory.CreateMesgOnly(mesgnum.Lap).WithFields( + }}, + 11: {Num: mesgnum.Lap, Fields: []proto.Field{ factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 12: factory.CreateMesgOnly(mesgnum.Session).WithFields( + }}, + 12: {Num: mesgnum.Session, Fields: []proto.Field{ factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 13: factory.CreateMesgOnly(mesgnum.Activity).WithFields( + }}, + 13: {Num: mesgnum.Activity, Fields: []proto.Field{ factory.CreateField(mesgnum.Activity, fieldnum.ActivityTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unordered optional Messages - 14: factory.CreateMesgOnly(mesgnum.Length).WithFields( + 14: {Num: mesgnum.Length, Fields: []proto.Field{ factory.CreateField(mesgnum.Length, fieldnum.LengthAvgSpeed).WithValue(uint16(1000)), - ), - 15: factory.CreateMesgOnly(mesgnum.SegmentLap).WithFields( + }}, + 15: {Num: mesgnum.SegmentLap, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentLap, fieldnum.SegmentLapAvgCadence).WithValue(uint8(100)), - ), - 16: factory.CreateMesgOnly(mesgnum.ZonesTarget).WithFields( + }}, + 16: {Num: mesgnum.ZonesTarget, Fields: []proto.Field{ factory.CreateField(mesgnum.ZonesTarget, fieldnum.ZonesTargetMaxHeartRate).WithValue(uint8(190)), - ), - 17: factory.CreateMesgOnly(mesgnum.Workout).WithFields( + }}, + 17: {Num: mesgnum.Workout, Fields: []proto.Field{ factory.CreateField(mesgnum.Workout, fieldnum.WorkoutSport).WithValue(uint8(typedef.SportCycling)), - ), - 18: factory.CreateMesgOnly(mesgnum.WorkoutStep).WithFields( + }}, + 18: {Num: mesgnum.WorkoutStep, Fields: []proto.Field{ factory.CreateField(mesgnum.WorkoutStep, fieldnum.WorkoutStepIntensity).WithValue(uint8(typedef.IntensityActive)), - ), - 19: factory.CreateMesgOnly(mesgnum.Hr).WithFields( + }}, + 19: {Num: mesgnum.Hr, Fields: []proto.Field{ factory.CreateField(mesgnum.Hr, fieldnum.HrTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 20: factory.CreateMesgOnly(mesgnum.Hrv).WithFields( + }}, + 20: {Num: mesgnum.Hrv, Fields: []proto.Field{ factory.CreateField(mesgnum.Hrv, fieldnum.HrvTime).WithValue([]uint16{uint16(1000)}), - ), + }}, // Unrelated messages - 21: factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + 21: {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Special case: // 1. CoursePoint's Timestamp Num is 1 // 2. Set's Timestamp Num is 254 - 22: factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + 22: {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - 23: factory.CreateMesgOnly(mesgnum.Set).WithFields( + }}, + 23: {Num: mesgnum.Set, Fields: []proto.Field{ factory.CreateField(mesgnum.Set, fieldnum.SetTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } ordered = []proto.Message{ diff --git a/profile/filedef/blood_pressure_test.go b/profile/filedef/blood_pressure_test.go index 59ebfba..f5e359e 100644 --- a/profile/filedef/blood_pressure_test.go +++ b/profile/filedef/blood_pressure_test.go @@ -19,35 +19,35 @@ import ( func newBloodPressureMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.UserProfile).WithFields( + }}, + {Num: mesgnum.UserProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(27)), - ), - factory.CreateMesgOnly(mesgnum.BloodPressure).WithFields( + }}, + {Num: mesgnum.BloodPressure, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeviceInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/course_test.go b/profile/filedef/course_test.go index 9b7fe5c..480f8df 100644 --- a/profile/filedef/course_test.go +++ b/profile/filedef/course_test.go @@ -19,50 +19,50 @@ import ( func newCourseMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileCourse)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), - ), - factory.CreateMesgOnly(mesgnum.DeveloperDataId).WithFields( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Course).WithFields( + }}, + {Num: mesgnum.Course, Fields: []proto.Field{ factory.CreateField(mesgnum.Course, fieldnum.CourseSport).WithValue(uint8(typedef.SportRunning)), - ), - factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStart)), - ), - factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Lap).WithFields( + }}, + {Num: mesgnum.Lap, Fields: []proto.Field{ factory.CreateField(mesgnum.Lap, fieldnum.LapTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unordered optional Messages - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/device_test.go b/profile/filedef/device_test.go index a318212..a33ddb5 100644 --- a/profile/filedef/device_test.go +++ b/profile/filedef/device_test.go @@ -19,42 +19,42 @@ import ( func newDeviceMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Software).WithFields( + }}, + {Num: mesgnum.Software, Fields: []proto.Field{ factory.CreateField(mesgnum.Software, fieldnum.SoftwareMessageIndex).WithValue(uint16(typedef.MessageIndexReserved)), - ), - factory.CreateMesgOnly(mesgnum.Capabilities).WithFields( + }}, + {Num: mesgnum.Capabilities, Fields: []proto.Field{ factory.CreateField(mesgnum.Capabilities, fieldnum.CapabilitiesSports).WithValue([]uint8{ uint8(typedef.SportBits0Basketball), uint8(typedef.SportBits1AmericanFootball), uint8(typedef.SportBits2Paddling), }), - ), - factory.CreateMesgOnly(mesgnum.FileCapabilities).WithFields( + }}, + {Num: mesgnum.FileCapabilities, Fields: []proto.Field{ factory.CreateField(mesgnum.FileCapabilities, fieldnum.FileCapabilitiesType).WithValue(uint8(typedef.FileActivity)), - ), - factory.CreateMesgOnly(mesgnum.MesgCapabilities).WithFields( + }}, + {Num: mesgnum.MesgCapabilities, Fields: []proto.Field{ factory.CreateField(mesgnum.MesgCapabilities, fieldnum.MesgCapabilitiesFile).WithValue(uint8(typedef.FileActivity)), - ), - factory.CreateMesgOnly(mesgnum.FieldCapabilities).WithFields( + }}, + {Num: mesgnum.FieldCapabilities, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldCapabilities, fieldnum.FieldCapabilitiesFile).WithValue(uint8(typedef.FileActivity)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/filedef_test.go b/profile/filedef/filedef_test.go index eb4f1fc..47d7c5e 100644 --- a/profile/filedef/filedef_test.go +++ b/profile/filedef/filedef_test.go @@ -42,36 +42,36 @@ func TestSortMessagesByTimestamp(t *testing.T) { // 1. CoursePoint's Timestamp Num is 1 // 2. Set's Timestamp Num is 254 messages := []proto.Message{ - 0: factory.CreateMesgOnly(mesgnum.FileId).WithFields( + 0: {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdManufacturer).WithValue(typedef.ManufacturerDevelopment.Uint16()), - ), - 1: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 1: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now)), - ), - 2: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 2: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), - ), - 3: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 3: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(1 * time.Second))), - ), - 4: factory.CreateMesgOnly(mesgnum.Event).WithFields( + }}, + 4: {Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), - ), - 5: factory.CreateMesgOnly(mesgnum.Session).WithFields( + }}, + 5: {Num: mesgnum.Session, Fields: []proto.Field{ factory.CreateField(mesgnum.Session, fieldnum.SessionTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), - ), - 6: factory.CreateMesgOnly(mesgnum.UserProfile).WithFields( + }}, + 6: {Num: mesgnum.UserProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileFriendlyName).WithValue("muktihari"), - ), - 7: factory.CreateMesgOnly(mesgnum.Set).WithFields( + }}, + 7: {Num: mesgnum.Set, Fields: []proto.Field{ factory.CreateField(mesgnum.Set, fieldnum.SetTimestamp).WithValue(datetime.ToUint32(now.Add(4 * time.Second))), - ), - 8: factory.CreateMesgOnly(mesgnum.Record).WithFields( + }}, + 8: {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(now.Add(2 * time.Second))), - ), - 9: factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + 9: {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(now.Add(3 * time.Second))), - ), + }}, } expected := []proto.Message{ diff --git a/profile/filedef/goals_test.go b/profile/filedef/goals_test.go index b7af4d4..8698507 100644 --- a/profile/filedef/goals_test.go +++ b/profile/filedef/goals_test.go @@ -19,26 +19,26 @@ import ( func newGoalsMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Goal).WithFields( + }}, + {Num: mesgnum.Goal, Fields: []proto.Field{ factory.CreateField(mesgnum.Goal, fieldnum.GoalSport).WithValue(uint8(typedef.SportSoccer)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/listener_test.go b/profile/filedef/listener_test.go index 6b616b9..9065cb3 100644 --- a/profile/filedef/listener_test.go +++ b/profile/filedef/listener_test.go @@ -169,17 +169,17 @@ func TestListenerForSingleFitFile(t *testing.T) { func() table { mesgs := newActivityMessageForTest(now) mesgs = append(mesgs, - factory.CreateMesgOnly(mesgnum.Record). - WithFields( + proto.Message{Num: mesgnum.Record, + Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ). - WithDeveloperFields( - proto.DeveloperField{ + }, + DeveloperFields: []proto.DeveloperField{ + { DeveloperDataIndex: 0, Num: 0, Value: proto.Uint8(100), }, - ), + }}, ) return table{ name: "default listener for activity containing developer fields", diff --git a/profile/filedef/monitoring_ab_test.go b/profile/filedef/monitoring_ab_test.go index 2f43c52..f4cfd6c 100644 --- a/profile/filedef/monitoring_ab_test.go +++ b/profile/filedef/monitoring_ab_test.go @@ -19,39 +19,39 @@ import ( func newMonitoringAMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.MonitoringInfo).WithFields( + }}, + {Num: mesgnum.MonitoringInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.MonitoringInfo, fieldnum.MonitoringInfoActivityType).WithValue([]uint8{ uint8(typedef.ActivityTypeCycling), uint8(typedef.ActivityTypeRunning), }), - ), - factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + }}, + {Num: mesgnum.Monitoring, Fields: []proto.Field{ factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringActivityType).WithValue(uint8(typedef.ActivityTypeCycling)), - ), - factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + }}, + {Num: mesgnum.Monitoring, Fields: []proto.Field{ factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringActivityType).WithValue(uint8(typedef.ActivityTypeRunning)), - ), - factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + }}, + {Num: mesgnum.DeviceInfo, Fields: []proto.Field{ 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( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/monitoring_daily_test.go b/profile/filedef/monitoring_daily_test.go index 42f49a4..01301f9 100644 --- a/profile/filedef/monitoring_daily_test.go +++ b/profile/filedef/monitoring_daily_test.go @@ -19,32 +19,32 @@ import ( func newMonitoringDailyMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.MonitoringInfo).WithFields( + }}, + {Num: mesgnum.MonitoringInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.MonitoringInfo, fieldnum.MonitoringInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.Monitoring).WithFields( + }}, + {Num: mesgnum.Monitoring, Fields: []proto.Field{ factory.CreateField(mesgnum.Monitoring, fieldnum.MonitoringIntensity).WithValue(uint8(typedef.IntensityActive)), - ), - factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + }}, + {Num: mesgnum.DeviceInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/schedules_test.go b/profile/filedef/schedules_test.go index 0367751..e757b8f 100644 --- a/profile/filedef/schedules_test.go +++ b/profile/filedef/schedules_test.go @@ -19,26 +19,26 @@ import ( func newSchedulesMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Schedule).WithFields( + }}, + {Num: mesgnum.Schedule, Fields: []proto.Field{ factory.CreateField(mesgnum.Schedule, fieldnum.ScheduleCompleted).WithValue(true), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/segment_list_test.go b/profile/filedef/segment_list_test.go index d292d68..15712bf 100644 --- a/profile/filedef/segment_list_test.go +++ b/profile/filedef/segment_list_test.go @@ -19,29 +19,29 @@ import ( func newSegmentListMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FileCreator).WithFields( + }}, + {Num: mesgnum.FileCreator, Fields: []proto.Field{ factory.CreateField(mesgnum.FileCreator, fieldnum.FileCreatorSoftwareVersion).WithValue(uint16(1)), - ), - factory.CreateMesgOnly(mesgnum.SegmentFile).WithFields( + }}, + {Num: mesgnum.SegmentFile, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentFile, fieldnum.SegmentFileEnabled).WithValue(true), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/segment_test.go b/profile/filedef/segment_test.go index 9a2e9a0..954d824 100644 --- a/profile/filedef/segment_test.go +++ b/profile/filedef/segment_test.go @@ -19,35 +19,35 @@ import ( func newSegmentMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.SegmentId).WithFields( + }}, + {Num: mesgnum.SegmentId, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentId, fieldnum.SegmentIdEnabled).WithValue(true), - ), - factory.CreateMesgOnly(mesgnum.SegmentLeaderboardEntry).WithFields( + }}, + {Num: mesgnum.SegmentLeaderboardEntry, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentLeaderboardEntry, fieldnum.SegmentLeaderboardEntryName).WithValue("entry test"), - ), - factory.CreateMesgOnly(mesgnum.SegmentLap).WithFields( + }}, + {Num: mesgnum.SegmentLap, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentLap, fieldnum.SegmentLapName).WithValue("lap test"), - ), - factory.CreateMesgOnly(mesgnum.SegmentPoint).WithFields( + }}, + {Num: mesgnum.SegmentPoint, Fields: []proto.Field{ factory.CreateField(mesgnum.SegmentPoint, fieldnum.SegmentPointAltitude).WithValue(uint16(10000)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/settings_test.go b/profile/filedef/settings_test.go index 039d5d8..7ef2d33 100644 --- a/profile/filedef/settings_test.go +++ b/profile/filedef/settings_test.go @@ -19,38 +19,38 @@ import ( func newSettingsMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.UserProfile).WithFields( + }}, + {Num: mesgnum.UserProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(29)), - ), - factory.CreateMesgOnly(mesgnum.HrmProfile).WithFields( + }}, + {Num: mesgnum.HrmProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.HrmProfile, fieldnum.HrmProfileEnabled).WithValue(true), - ), - factory.CreateMesgOnly(mesgnum.SdmProfile).WithFields( + }}, + {Num: mesgnum.SdmProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.SdmProfile, fieldnum.SdmProfileEnabled).WithValue(true), - ), - factory.CreateMesgOnly(mesgnum.BikeProfile).WithFields( + }}, + {Num: mesgnum.BikeProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.BikeProfile, fieldnum.BikeProfileEnabled).WithValue(true), - ), - factory.CreateMesgOnly(mesgnum.DeviceSettings).WithFields( + }}, + {Num: mesgnum.DeviceSettings, Fields: []proto.Field{ factory.CreateField(mesgnum.DeviceSettings, fieldnum.DeviceSettingsBacklightMode).WithValue(uint8(typedef.BacklightModeAutoBrightness)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/sport_test.go b/profile/filedef/sport_test.go index b9daceb..0893000 100644 --- a/profile/filedef/sport_test.go +++ b/profile/filedef/sport_test.go @@ -19,44 +19,44 @@ import ( func newSportMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.ZonesTarget).WithFields( + }}, + {Num: mesgnum.ZonesTarget, Fields: []proto.Field{ factory.CreateField(mesgnum.ZonesTarget, fieldnum.ZonesTargetMaxHeartRate).WithValue(uint8(190)), - ), - factory.CreateMesgOnly(mesgnum.Sport).WithFields( + }}, + {Num: mesgnum.Sport, Fields: []proto.Field{ factory.CreateField(mesgnum.Sport, fieldnum.SportSport).WithValue(uint8(typedef.SportAmericanFootball)), - ), - factory.CreateMesgOnly(mesgnum.HrZone).WithFields( + }}, + {Num: mesgnum.HrZone, Fields: []proto.Field{ factory.CreateField(mesgnum.HrZone, fieldnum.HrZoneHighBpm).WithValue(uint8(177)), - ), - factory.CreateMesgOnly(mesgnum.PowerZone).WithFields( + }}, + {Num: mesgnum.PowerZone, Fields: []proto.Field{ factory.CreateField(mesgnum.PowerZone, fieldnum.PowerZoneHighValue).WithValue(uint16(200)), - ), - factory.CreateMesgOnly(mesgnum.MetZone).WithFields( + }}, + {Num: mesgnum.MetZone, Fields: []proto.Field{ factory.CreateField(mesgnum.MetZone, fieldnum.MetZoneHighBpm).WithValue(uint8(178)), - ), - factory.CreateMesgOnly(mesgnum.SpeedZone).WithFields( + }}, + {Num: mesgnum.SpeedZone, Fields: []proto.Field{ factory.CreateField(mesgnum.SpeedZone, fieldnum.SpeedZoneHighValue).WithValue(uint16(10000)), - ), - factory.CreateMesgOnly(mesgnum.CadenceZone).WithFields( + }}, + {Num: mesgnum.CadenceZone, Fields: []proto.Field{ factory.CreateField(mesgnum.CadenceZone, fieldnum.CadenceZoneHighValue).WithValue(uint8(100)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/totals_test.go b/profile/filedef/totals_test.go index f44518f..309974e 100644 --- a/profile/filedef/totals_test.go +++ b/profile/filedef/totals_test.go @@ -19,26 +19,26 @@ import ( func newTotalsMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Totals).WithFields( + }}, + {Num: mesgnum.Totals, Fields: []proto.Field{ factory.CreateField(mesgnum.Totals, fieldnum.TotalsSport).WithValue(uint8(typedef.SportSoccer)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/weight_test.go b/profile/filedef/weight_test.go index 694674e..5a1adfd 100644 --- a/profile/filedef/weight_test.go +++ b/profile/filedef/weight_test.go @@ -19,32 +19,32 @@ import ( func newWeightMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ 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( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.UserProfile).WithFields( + }}, + {Num: mesgnum.UserProfile, Fields: []proto.Field{ factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileAge).WithValue(uint8(27)), - ), - factory.CreateMesgOnly(mesgnum.WeightScale).WithFields( + }}, + {Num: mesgnum.WeightScale, Fields: []proto.Field{ factory.CreateField(mesgnum.WeightScale, fieldnum.WeightScaleBmi).WithValue(uint16(1000)), - ), - factory.CreateMesgOnly(mesgnum.DeviceInfo).WithFields( + }}, + {Num: mesgnum.DeviceInfo, Fields: []proto.Field{ factory.CreateField(mesgnum.DeviceInfo, fieldnum.DeviceInfoTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + }}, + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/filedef/workout_test.go b/profile/filedef/workout_test.go index c50c619..5721005 100644 --- a/profile/filedef/workout_test.go +++ b/profile/filedef/workout_test.go @@ -19,32 +19,32 @@ import ( func newWorkoutMessageForTest(now time.Time) []proto.Message { return []proto.Message{ - factory.CreateMesgOnly(mesgnum.FileId).WithFields( + {Num: mesgnum.FileId, Fields: []proto.Field{ factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(uint8(typedef.FileWorkout)), factory.CreateField(mesgnum.FileId, fieldnum.FileIdTimeCreated).WithValue(datetime.ToUint32(now)), - ), - factory.CreateMesgOnly(mesgnum.DeveloperDataId).WithFields( + }}, + {Num: mesgnum.DeveloperDataId, Fields: []proto.Field{ factory.CreateField(mesgnum.DeveloperDataId, fieldnum.DeveloperDataIdDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.FieldDescription).WithFields( + }}, + {Num: mesgnum.FieldDescription, Fields: []proto.Field{ factory.CreateField(mesgnum.FieldDescription, fieldnum.FieldDescriptionDeveloperDataIndex).WithValue(uint8(0)), - ), - factory.CreateMesgOnly(mesgnum.Workout).WithFields( + }}, + {Num: mesgnum.Workout, Fields: []proto.Field{ factory.CreateField(mesgnum.Workout, fieldnum.WorkoutSport).WithValue(uint8(typedef.SportSwimming)), - ), - factory.CreateMesgOnly(mesgnum.WorkoutStep).WithFields( + }}, + {Num: mesgnum.WorkoutStep, Fields: []proto.Field{ factory.CreateField(mesgnum.WorkoutStep, fieldnum.WorkoutStepEquipment).WithValue(uint8(typedef.WorkoutEquipmentSwimFins)), - ), - factory.CreateMesgOnly(mesgnum.WorkoutStep).WithFields( + }}, + {Num: mesgnum.WorkoutStep, Fields: []proto.Field{ factory.CreateField(mesgnum.WorkoutStep, fieldnum.WorkoutStepEquipment).WithValue(uint8(typedef.WorkoutEquipmentSwimSnorkel)), - ), + }}, // Unrelated messages - factory.CreateMesgOnly(mesgnum.CoursePoint).WithFields( + {Num: mesgnum.CoursePoint, Fields: []proto.Field{ factory.CreateField(mesgnum.CoursePoint, fieldnum.CoursePointTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), - factory.CreateMesgOnly(mesgnum.BarometerData).WithFields( + }}, + {Num: mesgnum.BarometerData, Fields: []proto.Field{ factory.CreateField(mesgnum.BarometerData, fieldnum.BarometerDataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), - ), + }}, } } diff --git a/profile/mesgdef/mesgdef.go b/profile/mesgdef/mesgdef.go index 4b1cd29..a285c81 100644 --- a/profile/mesgdef/mesgdef.go +++ b/profile/mesgdef/mesgdef.go @@ -12,7 +12,8 @@ import ( // Factory defines a contract that any Factory containing these method can be used by mesgdef's structs. type Factory interface { - // CreateField create new field based on defined messages in the factory. If not found, it returns new field with "unknown" name. + // CreateField creates new field based on defined messages in the factory. + // If not found, it returns new field with "unknown" name. CreateField(mesgNum typedef.MesgNum, num byte) proto.Field } diff --git a/profile/mesgdef/mesgdef_test.go b/profile/mesgdef/mesgdef_test.go index 481c693..45e29a4 100644 --- a/profile/mesgdef/mesgdef_test.go +++ b/profile/mesgdef/mesgdef_test.go @@ -13,6 +13,7 @@ import ( "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 TestDefaultOptions(t *testing.T) { @@ -34,9 +35,13 @@ func TestUnsafeCast(t *testing.T) { typedef.AttitudeValidityHwFail, typedef.AttitudeValiditySolutionCoasting, } - mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.AviationAttitudeValidity: attitudeValidities, - }) + mesg := factory.CreateMesg(mesgnum.Record) + for i := range mesg.Fields { + if mesg.Fields[i].Num == fieldnum.AviationAttitudeValidity { + mesg.Fields[i].Value = proto.SliceUint16(attitudeValidities) + break + } + } aviationAttitude := NewAviationAttitude(&mesg) newMesg := aviationAttitude.ToMesg(nil) diff --git a/profile/mesgdef/record_gen_test.go b/profile/mesgdef/record_gen_test.go index 3c9ad4a..336bb63 100644 --- a/profile/mesgdef/record_gen_test.go +++ b/profile/mesgdef/record_gen_test.go @@ -28,9 +28,13 @@ func BenchmarkNewRecord(b *testing.B) { } func BenchmarkRecordToMesg(b *testing.B) { - mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordTimestamp: datetime.ToUint32(time.Now()), - }) + mesg := factory.CreateMesg(mesgnum.Record) + for i := range mesg.Fields { + if mesg.Fields[i].Num == fieldnum.RecordTimestamp { + mesg.Fields[i].Value = proto.Uint32(datetime.ToUint32(time.Now())) + break + } + } record := NewRecord(&mesg) b.ResetTimer() diff --git a/proto/proto.go b/proto/proto.go index b48e1e4..941570e 100644 --- a/proto/proto.go +++ b/proto/proto.go @@ -5,6 +5,8 @@ package proto import ( + "fmt" + "github.com/muktihari/fit/profile" "github.com/muktihari/fit/profile/basetype" "github.com/muktihari/fit/profile/typedef" @@ -27,6 +29,9 @@ const ( // header is 1 byte -> 0bxxxxxxxx CompressedBitShift = 5 // Used for right-shifting the 5 least significant bits (lsb) of compressed time. + LittleEndian = 0 + BigEndian = 1 + DefaultFileHeaderSize byte = 14 // The preferred size is 14 DataTypeFIT string = ".FIT" // FIT is a constant string ".FIT" @@ -44,63 +49,6 @@ const ( // header is 1 byte -> 0bxxxxxxxx FieldNumTimestamp = 253 ) -// LocalMesgNum extracts LocalMesgNum from message header. -func LocalMesgNum(header byte) byte { - if (header & MesgCompressedHeaderMask) == MesgCompressedHeaderMask { - return (header & CompressedLocalMesgNumMask) >> CompressedBitShift - } - return header & LocalMesgNumMask -} - -// CreateMessageDefinition creates new MessageDefinition base on given Message. -// It will panic if mesg is nil. And mesg must be validated first, for instance -// if field.Value's size is more than 255 bytes, overflow will occurs. -func CreateMessageDefinition(mesg *Message) (mesgDef MessageDefinition) { - CreateMessageDefinitionTo(&mesgDef, mesg) - return -} - -// CreateMessageDefinitionTo create MessageDefinition base on given Message and put it at target object to avoid allocation. -// It will panic if either target or mesg is nil. And mesg must be validated first, for instance -// if field.Value's size is more than 255 bytes, overflow will occurs. -func CreateMessageDefinitionTo(target *MessageDefinition, mesg *Message) { - target.Header = MesgDefinitionMask - target.Reserved = mesg.Reserved - target.Architecture = mesg.Architecture - target.MesgNum = mesg.Num - - target.FieldDefinitions = target.FieldDefinitions[:0] - if cap(target.FieldDefinitions) < len(mesg.Fields) { - target.FieldDefinitions = make([]FieldDefinition, 0, len(mesg.Fields)) - } - - for i := range mesg.Fields { - target.FieldDefinitions = append(target.FieldDefinitions, FieldDefinition{ - Num: mesg.Fields[i].Num, - Size: byte(Sizeof(mesg.Fields[i].Value)), - BaseType: mesg.Fields[i].BaseType, - }) - } - - if len(mesg.DeveloperFields) == 0 { - return - } - - target.Header |= DevDataMask - - target.DeveloperFieldDefinitions = target.DeveloperFieldDefinitions[:0] - if cap(target.DeveloperFieldDefinitions) < len(mesg.DeveloperFields) { - target.DeveloperFieldDefinitions = make([]DeveloperFieldDefinition, 0, len(mesg.DeveloperFields)) - } - for i := range mesg.DeveloperFields { - target.DeveloperFieldDefinitions = append(target.DeveloperFieldDefinitions, DeveloperFieldDefinition{ - Num: mesg.DeveloperFields[i].Num, - Size: byte(Sizeof(mesg.DeveloperFields[i].Value)), - DeveloperDataIndex: mesg.DeveloperFields[i].DeveloperDataIndex, - }) - } -} - // FIT represents a structure for FIT Files. type FIT struct { FileHeader FileHeader // File Header contains either 12 or 14 bytes @@ -108,20 +56,14 @@ type FIT struct { CRC uint16 // Cyclic Redundancy Check 16-bit value to ensure the integrity of the messages. } -// WithMessages set Messages and return the pointer to the FIT. -func (f *FIT) WithMessages(messages ...Message) *FIT { - f.Messages = messages - return f -} - // FileHeader is a FIT's FileHeader with either 12 bytes size without CRC or a 14 bytes size with CRC, while 14 bytes size is the preferred size. type FileHeader struct { - Size byte // File header size either 12 (legacy) or 14. - ProtocolVersion byte // The FIT Protocol version which is being used to encode the FIT file. - ProfileVersion uint16 // The FIT Profile Version (associated with data defined in Global FIT Profile). - DataSize uint32 // The size of the messages in bytes (this field will be automatically updated by the encoder) - DataType string // ".FIT" (a string constant) - CRC uint16 // Cyclic Redundancy Check 16-bit value to ensure the integrity of the file header. (this field will be automatically updated by the encoder) + Size byte // File header size either 12 (legacy) or 14. + ProtocolVersion Version // The FIT Protocol version which is being used to encode the FIT file. + ProfileVersion uint16 // The FIT Profile Version (associated with data defined in Global FIT Profile). + DataSize uint32 // The size of the messages in bytes (this field will be automatically updated by the encoder) + DataType string // ".FIT" (a string constant) + CRC uint16 // Cyclic Redundancy Check 16-bit value to ensure the integrity of the file header. (this field will be automatically updated by the encoder) } // MessageDefinition is the definition of the upcoming data messages. @@ -134,13 +76,6 @@ type MessageDefinition struct { DeveloperFieldDefinitions []DeveloperFieldDefinition // List of the developer field definition (only if Developer Data Flag is set in Header) } -// Clone clones MessageDefinition -func (m MessageDefinition) Clone() MessageDefinition { - m.FieldDefinitions = append(m.FieldDefinitions[:0:0], m.FieldDefinitions...) - m.DeveloperFieldDefinitions = append(m.DeveloperFieldDefinitions[:0:0], m.DeveloperFieldDefinitions...) - return m -} - // FieldDefinition is the definition of the upcoming field within the message's structure. type FieldDefinition struct { Num byte // The field definition number @@ -159,40 +94,10 @@ type DeveloperFieldDefinition struct { // 3 bits type Message struct { Header byte // Message Header serves to distinguish whether the message is a Normal Data or a Compressed Timestamp Data. Unlike MessageDefinition, Message's Header should not contain Developer Data Flag. Num typedef.MesgNum // Global Message Number defined in Global FIT Profile, except number within range 0xFF00 - 0xFFFE are manufacturer specific number. - Reserved byte // Currently undetermined; the default value is 0. - Architecture byte // Architecture type / Endianness. Fields []Field // List of Field DeveloperFields []DeveloperField // List of DeveloperField } -// WithFields puts the provided fields into the message's fields. -func (m Message) WithFields(fields ...Field) Message { - m.Fields = fields - return m -} - -// WithFieldValues assigns the values of the targeted fields with the given map, -// where map[byte]any represents the field numbers and their respective values. -// If the Message does not have a corresponding field number match in the Fields, no value will be assigned or added. -func (m Message) WithFieldValues(fieldNumValues map[byte]any) Message { - for i := range m.Fields { - value, ok := fieldNumValues[m.Fields[i].Num] - if !ok { - continue - } - if value != nil { // only accept non-nil value. - m.Fields[i].Value = Any(value) - } - } - return m -} - -// WithFields puts the provided fields into the message's fields. -func (m Message) WithDeveloperFields(developerFields ...DeveloperField) Message { - m.DeveloperFields = developerFields - return m -} - // FieldByNum returns a pointer to the Field in a Message, if not found return nil. func (m *Message) FieldByNum(num byte) *Field { for i := range m.Fields { @@ -223,19 +128,6 @@ func (m *Message) RemoveFieldByNum(num byte) { } } -// Clone clones Message. -func (m Message) Clone() Message { - m.Fields = append(m.Fields[:0:0], m.Fields...) - for i := range m.Fields { - m.Fields[i] = m.Fields[i].Clone() - } - m.DeveloperFields = append(m.DeveloperFields[:0:0], m.DeveloperFields...) - for i := range m.DeveloperFields { - m.DeveloperFields[i] = m.DeveloperFields[i].Clone() - } - return m -} - // FieldBase acts as a fundamental representation of a field as defined in the Global FIT Profile. // The value of this representation should not be altered, except in the case of an unknown field. type FieldBase struct { @@ -315,23 +207,6 @@ func convertToInt64(val Value) (int64, bool) { return 0, false } -// Clone clones Field -func (f Field) Clone() Field { - if f.FieldBase == nil { - return f - } - - fieldBase := *f.FieldBase // also include FieldBase, clone is meant to be a deep copy - fieldBase.Components = append(fieldBase.Components[:0:0], fieldBase.Components...) - fieldBase.SubFields = append(fieldBase.SubFields[:0:0], fieldBase.SubFields...) - for i := range fieldBase.SubFields { - fieldBase.SubFields[i] = fieldBase.SubFields[i].Clone() - } - f.FieldBase = &fieldBase - - return f -} - // DeveloperField is a way to add custom data fields to existing messages. Developer Data Fields can be added // to any message at runtime by providing a self-describing FieldDefinition messages prior to that message. // The combination of the DeveloperDataIndex and FieldDefinitionNumber create a unique id for each FieldDescription. @@ -346,11 +221,6 @@ type DeveloperField struct { Value Value } -// Clone clones DeveloperField -func (f DeveloperField) Clone() DeveloperField { - return f -} - // Component is a way of compressing one or more fields into a bit field expressed in a single containing field. // The component can be expanded as a main Field in a Message or to update the value of the destination main Field. type Component struct { @@ -372,13 +242,6 @@ type SubField struct { Components []Component } -// Clone clones SubField -func (s SubField) Clone() SubField { - s.Components = append(s.Components[:0:0], s.Components...) - s.Maps = append(s.Maps[:0:0], s.Maps...) - return s -} - // SubFieldMap is the mapping between SubField and the corresponding main Field in a Message. // When any Field in a Message has Field.Num == RefFieldNum and Field.Value == RefFieldValue, then the SubField containing // this mapping can be interpreted as the main Field's properties (name, scale, type etc.) @@ -386,3 +249,70 @@ type SubFieldMap struct { RefFieldNum byte RefFieldValue int64 } + +// LocalMesgNum extracts LocalMesgNum from message header. +func LocalMesgNum(header byte) byte { + if (header & MesgCompressedHeaderMask) == MesgCompressedHeaderMask { + return (header & CompressedLocalMesgNumMask) >> CompressedBitShift + } + return header & LocalMesgNumMask +} + +const ( + errNilMesg = errorString("mesg is nil") + errValueSizeExceed255 = errorString("value's size exceed 255") +) + +// NewMessageDefinition returns a new MessageDefinition based on the given Message or an error if one occurs. +// This will set Reserved and Architecture with 0 value. It's up to the caller to change the returning value. +// +// This serves as a testing helper and is for documentation purposes only. +func NewMessageDefinition(mesg *Message) (*MessageDefinition, error) { + if mesg == nil { + return nil, errNilMesg + } + + const maxValueSize = 255 + + mesgDef := &MessageDefinition{ + Header: MesgDefinitionMask, + Reserved: 0, + Architecture: LittleEndian, + MesgNum: mesg.Num, + FieldDefinitions: make([]FieldDefinition, 0, len(mesg.Fields)), + } + + for i := range mesg.Fields { + size := Sizeof(mesg.Fields[i].Value) + if size > maxValueSize { + return nil, fmt.Errorf("Fields[%d].Value's size should be <= %d: %w", + i, maxValueSize, errValueSizeExceed255) + } + mesgDef.FieldDefinitions = append(mesgDef.FieldDefinitions, FieldDefinition{ + Num: mesg.Fields[i].Num, + Size: byte(size), + BaseType: mesg.Fields[i].BaseType, + }) + } + + if len(mesg.DeveloperFields) == 0 { + return mesgDef, nil + } + + mesgDef.DeveloperFieldDefinitions = make([]DeveloperFieldDefinition, 0, len(mesg.DeveloperFields)) + mesgDef.Header |= DevDataMask + for i := range mesg.DeveloperFields { + size := Sizeof(mesg.DeveloperFields[i].Value) + if size > maxValueSize { + return nil, fmt.Errorf("Fields[%d].Value's size should be <= %d: %w", + i, maxValueSize, errValueSizeExceed255) + } + mesgDef.DeveloperFieldDefinitions = append(mesgDef.DeveloperFieldDefinitions, DeveloperFieldDefinition{ + Num: mesg.DeveloperFields[i].Num, + Size: byte(size), + DeveloperDataIndex: mesg.DeveloperFields[i].DeveloperDataIndex, + }) + } + + return mesgDef, nil +} diff --git a/proto/proto_internal_test.go b/proto/proto_internal_test.go index ab1db00..c3e187d 100644 --- a/proto/proto_internal_test.go +++ b/proto/proto_internal_test.go @@ -5,10 +5,194 @@ package proto import ( + "errors" "fmt" "testing" + + "github.com/google/go-cmp/cmp" + "github.com/muktihari/fit/profile/basetype" + "github.com/muktihari/fit/profile/typedef" + "github.com/muktihari/fit/profile/untyped/fieldnum" + "github.com/muktihari/fit/profile/untyped/mesgnum" ) +func TestNewMessageDefinition(t *testing.T) { + tt := []struct { + name string + mesg *Message + mesgDef *MessageDefinition + err error + }{ + {name: "nil mesg", err: errNilMesg}, + { + name: "field value exceed max 255", + mesg: &Message{Num: mesgnum.FileId, Fields: []Field{ + { + FieldBase: &FieldBase{Num: fieldnum.FileIdProductName, BaseType: basetype.String}, + Value: String(string(make([]byte, 256))), + }, + }}, + err: errValueSizeExceed255, + }, + { + name: "developerField value exceed max 255", + mesg: &Message{Num: mesgnum.FileId, Fields: []Field{}, + DeveloperFields: []DeveloperField{ + {Value: String(string(make([]byte, 256)))}, + }, + }, + err: errValueSizeExceed255, + }, + { + name: "fields only with non-array values", + mesg: &Message{Num: mesgnum.FileId, Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.FileIdType, BaseType: basetype.Enum}, Value: Uint8(typedef.FileActivity.Byte())}, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask, + MesgNum: mesgnum.FileId, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.FileIdType, + Size: 1, + BaseType: basetype.Enum, + }, + }, + }, + }, + { + name: "fields only with string value", + mesg: &Message{Num: mesgnum.FileId, Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.FileIdProductName, BaseType: basetype.String}, Value: String("FIT SDK Go")}, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask, + MesgNum: mesgnum.FileId, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.FileIdProductName, + Size: 1 * 11, // len("FIT SDK Go") == 10 + '0x00' + BaseType: basetype.String, + }, + }, + }, + }, + { + name: "fields only with array of byte", + mesg: &Message{Num: mesgnum.UserProfile, Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: SliceUint8([]byte{2, 9})}, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + }, + }, + + { + name: "developer fields", + mesg: &Message{Num: mesgnum.UserProfile, + Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []DeveloperField{ + {Num: 0, DeveloperDataIndex: 0, Value: Uint8(1)}, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask | DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []DeveloperFieldDefinition{ + { + Num: 0, Size: 1, DeveloperDataIndex: 0, + }, + }, + }, + }, + { + name: "developer fields with string value \"FIT SDK Go\", size should be 11", + mesg: &Message{Num: mesgnum.UserProfile, + Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []DeveloperField{ + { + Num: 0, DeveloperDataIndex: 0, Value: String("FIT SDK Go"), + }, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask | DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []DeveloperFieldDefinition{ + { + Num: 0, Size: 11, DeveloperDataIndex: 0, + }, + }, + }, + }, + { + name: "developer fields with value []uint16{1,2,3}, size should be 3*2 = 6", + mesg: &Message{Num: mesgnum.UserProfile, + Fields: []Field{ + {FieldBase: &FieldBase{Num: fieldnum.UserProfileGlobalId, BaseType: basetype.Byte}, Value: SliceUint8([]byte{2, 9})}, + }, + DeveloperFields: []DeveloperField{ + {Num: 0, DeveloperDataIndex: 0, Value: SliceUint16([]uint16{1, 2, 3})}, + }}, + mesgDef: &MessageDefinition{ + Header: MesgDefinitionMask | DevDataMask, + MesgNum: mesgnum.UserProfile, + FieldDefinitions: []FieldDefinition{ + { + Num: fieldnum.UserProfileGlobalId, + Size: 2, + BaseType: basetype.Byte, + }, + }, + DeveloperFieldDefinitions: []DeveloperFieldDefinition{ + { + Num: 0, Size: 6, DeveloperDataIndex: 0, + }, + }, + }, + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { + mesgDef, err := NewMessageDefinition(tc.mesg) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if err != nil { + return + } + if diff := cmp.Diff(mesgDef, tc.mesgDef); diff != "" { + t.Fatal(diff) + } + }) + } +} + func TestIsValueEqualTo(t *testing.T) { tt := []struct { field Field diff --git a/proto/proto_marshal.go b/proto/proto_marshal.go index cf70eda..5ff4905 100644 --- a/proto/proto_marshal.go +++ b/proto/proto_marshal.go @@ -5,44 +5,15 @@ package proto import ( - "encoding" "encoding/binary" "fmt" - "sync" ) -const littleEndian = 0 - // Marshaler should only do one thing: marshaling to its bytes representation, any validation should be done outside. -// Header + ((max n Fields) * (n value)) + ((max n DeveloperFields) * (n value)) -const MaxBytesPerMessage = 1 + (255*255)*2 - -// Header + Reserved + Architecture + MesgNum (2 bytes) + n Fields + (Max n Fields * 3) + n DevFields + (Max n DevFields * 3). -const MaxBytesPerMessageDefinition = 5 + 1 + (255 * 3) + 1 + (255 * 3) - -var pool = sync.Pool{New: func() any { return new([MaxBytesPerMessage]byte) }} - -var ( - _ encoding.BinaryMarshaler = (*FileHeader)(nil) - _ encoding.BinaryMarshaler = (*MessageDefinition)(nil) - _ encoding.BinaryMarshaler = (*Message)(nil) -) - -// MarshalBinary returns the FIT format encoding of FileHeader and nil error. -func (h FileHeader) MarshalBinary() ([]byte, error) { - arr := pool.Get().(*[MaxBytesPerMessage]byte) - defer pool.Put(arr) - b := arr[:0] - - b, _ = h.MarshalAppend(b) - - return append([]byte{}, b...), nil -} - // MarshalAppend appends the FIT format encoding of FileHeader to b, returning the result. -func (h FileHeader) MarshalAppend(b []byte) ([]byte, error) { - b = append(b, h.Size, h.ProtocolVersion) +func (h *FileHeader) MarshalAppend(b []byte) ([]byte, error) { + b = append(b, h.Size, byte(h.ProtocolVersion)) b = binary.LittleEndian.AppendUint16(b, h.ProfileVersion) b = binary.LittleEndian.AppendUint32(b, h.DataSize) b = append(b, h.DataType[:4]...) @@ -52,24 +23,13 @@ func (h FileHeader) MarshalAppend(b []byte) ([]byte, error) { return b, nil } -// MarshalBinary returns the FIT format encoding of MessageDefinition and nil error. -func (m MessageDefinition) MarshalBinary() ([]byte, error) { - arr := pool.Get().(*[MaxBytesPerMessage]byte) - defer pool.Put(arr) - b := arr[:0] - - b, _ = m.MarshalAppend(b) - - return append([]byte{}, b...), nil -} - // MarshalAppend appends the FIT format encoding of MessageDefinition to b, returning the result. -func (m MessageDefinition) MarshalAppend(b []byte) ([]byte, error) { +func (m *MessageDefinition) MarshalAppend(b []byte) ([]byte, error) { b = append(b, m.Header) b = append(b, m.Reserved) b = append(b, m.Architecture) - if m.Architecture == littleEndian { + if m.Architecture == LittleEndian { b = binary.LittleEndian.AppendUint16(b, uint16(m.MesgNum)) } else { b = binary.BigEndian.AppendUint16(b, uint16(m.MesgNum)) @@ -98,27 +58,13 @@ func (m MessageDefinition) MarshalAppend(b []byte) ([]byte, error) { return b, nil } -// MarshalBinary returns the FIT format encoding of Message and any error encountered during marshal. -func (m Message) MarshalBinary() ([]byte, error) { - arr := pool.Get().(*[MaxBytesPerMessage]byte) - defer pool.Put(arr) - b := arr[:0] - - b, err := m.MarshalAppend(b) - if err != nil { - return nil, err - } - - return append([]byte{}, b...), nil -} - // MarshalAppend appends the FIT format encoding of Message to b, returning the result. -func (m Message) MarshalAppend(b []byte) ([]byte, error) { +func (m *Message) MarshalAppend(b []byte, arch byte) ([]byte, error) { b = append(b, m.Header) var err error for i := range m.Fields { - b, err = m.Fields[i].Value.MarshalAppend(b, m.Architecture) + b, err = m.Fields[i].Value.MarshalAppend(b, arch) if err != nil { return nil, fmt.Errorf("field: [num: %d, value: %v]: %w", m.Fields[i].Num, m.Fields[i].Value.Any(), err) @@ -126,7 +72,7 @@ func (m Message) MarshalAppend(b []byte) ([]byte, error) { } for i := range m.DeveloperFields { - b, err = m.DeveloperFields[i].Value.MarshalAppend(b, m.Architecture) + b, err = m.DeveloperFields[i].Value.MarshalAppend(b, arch) if err != nil { return nil, fmt.Errorf("developer field: [num: %d, value: %v]: %w", m.DeveloperFields[i].Num, m.DeveloperFields[i].Value.Any(), err) diff --git a/proto/proto_marshal_test.go b/proto/proto_marshal_test.go index 3fc7dc5..5142603 100644 --- a/proto/proto_marshal_test.go +++ b/proto/proto_marshal_test.go @@ -64,7 +64,7 @@ func TestHeaderMarshaler(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - b, err := tc.fileHeader.MarshalBinary() + b, err := tc.fileHeader.MarshalAppend(nil) if !errors.Is(err, tc.err) { t.Fatalf("expected err: %v, got: %v", tc.err, err) } @@ -148,7 +148,7 @@ func TestMessageDefinitionMarshaler(t *testing.T) { for i, tc := range tt { t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { - b, _ := tc.mesgdef.MarshalBinary() + b, _ := tc.mesgdef.MarshalAppend(nil) if diff := cmp.Diff(b, tc.b); diff != "" { t.Fatal(diff) } @@ -243,7 +243,7 @@ func TestMessageMarshaler(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - b, err := tc.mesg.MarshalBinary() + b, err := tc.mesg.MarshalAppend(nil, proto.LittleEndian) if !errors.Is(err, tc.err) { t.Fatalf("expected err: %v, got: %v", tc.err, err) } @@ -254,7 +254,7 @@ func TestMessageMarshaler(t *testing.T) { } } -func BenchmarkHeaderMarshalBinary(b *testing.B) { +func BenchmarkFileHeaderMarshalAppend(b *testing.B) { b.StopTimer() header := proto.FileHeader{ Size: 14, @@ -267,76 +267,58 @@ func BenchmarkHeaderMarshalBinary(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = header.MarshalBinary() - } -} - -func BenchmarkHeaderMarshalAppend(b *testing.B) { - b.StopTimer() - header := proto.FileHeader{ - Size: 14, - ProtocolVersion: 32, - ProfileVersion: 2132, - DataSize: 642262, - DataType: ".FIT", - CRC: 12856, - } - arr := [proto.MaxBytesPerMessageDefinition]byte{} - b.StartTimer() - - for i := 0; i < b.N; i++ { - _, _ = header.MarshalAppend(arr[:0]) - } -} - -func BenchmarkMessageDefinitionMarshalBinary(b *testing.B) { - b.StopTimer() - mesg := factory.CreateMesg(mesgnum.Record) - mesgDef := proto.CreateMessageDefinition(&mesg) - b.StartTimer() - - for i := 0; i < b.N; i++ { - _, _ = mesgDef.MarshalBinary() + _, _ = header.MarshalAppend(make([]byte, 0, 14)) } } func BenchmarkMessageDefinitionMarshalAppend(b *testing.B) { b.StopTimer() - mesg := factory.CreateMesg(mesgnum.Record) - mesgDef := proto.CreateMessageDefinition(&mesg) - arr := [proto.MaxBytesPerMessageDefinition]byte{} - b.StartTimer() - - for i := 0; i < b.N; i++ { - _, _ = mesgDef.MarshalAppend(arr[:0]) + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(70)), + factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16(300*5 - 500)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPower).WithValue(uint16(300)), + }} + mesgDef, err := proto.NewMessageDefinition(&mesg) + if err != nil { + b.Fatal(err) } -} - -func BenchmarkMessageMarshalBinary(b *testing.B) { - b.StopTimer() - mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordPositionLat: proto.Int32(1000), - fieldnum.RecordPositionLong: proto.Int32(1000), - fieldnum.RecordSpeed: proto.Uint16(1000), - }) + buf := make([]byte, 6+len(mesg.Fields)*3+len(mesg.DeveloperFields)*3) b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = mesg.MarshalBinary() + _, _ = mesgDef.MarshalAppend(buf[:0]) } } func BenchmarkMessageMarshalAppend(b *testing.B) { b.StopTimer() - mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ - fieldnum.RecordPositionLat: proto.Int32(1000), - fieldnum.RecordPositionLong: proto.Int32(1000), - fieldnum.RecordSpeed: proto.Uint16(1000), - }) - arr := [proto.MaxBytesPerMessage]byte{} + mesg := proto.Message{Num: mesgnum.Record, Fields: []proto.Field{ + factory.CreateField(mesgnum.Record, fieldnum.RecordTimestamp).WithValue(uint32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordDistance).WithValue(uint32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLat).WithValue(int32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPositionLong).WithValue(int32(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed).WithValue(uint16(1000)), + factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate).WithValue(uint8(70)), + factory.CreateField(mesgnum.Record, fieldnum.RecordAltitude).WithValue(uint16(300*5 - 500)), + factory.CreateField(mesgnum.Record, fieldnum.RecordPower).WithValue(uint16(300)), + }} + + var size = 1 + for i := range mesg.Fields { + size += proto.Sizeof(mesg.Fields[i].Value) + } + buf := make([]byte, size) b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = mesg.MarshalAppend(arr[:0]) + _, err := mesg.MarshalAppend(buf[:0], proto.LittleEndian) + if err != nil { + b.Fatal(err) + } } } diff --git a/proto/proto_test.go b/proto/proto_test.go index 7c77826..d50da93 100644 --- a/proto/proto_test.go +++ b/proto/proto_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/muktihari/fit/factory" - "github.com/muktihari/fit/profile/basetype" "github.com/muktihari/fit/profile/typedef" "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/profile/untyped/mesgnum" @@ -50,23 +49,23 @@ func TestFitWithMessages(t *testing.T) { { name: "withMessages", messages: []proto.Message{ - factory.CreateMesg(mesgnum.Record).WithFields( + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed), factory.CreateField(mesgnum.Record, fieldnum.RecordCadence), factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate), - ), - factory.CreateMesg(mesgnum.Record).WithFields( + }}, + {Num: mesgnum.Record, Fields: []proto.Field{ factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed), factory.CreateField(mesgnum.Record, fieldnum.RecordCadence), factory.CreateField(mesgnum.Record, fieldnum.RecordHeartRate), - ), + }}, }, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - fit := new(proto.FIT).WithMessages(tc.messages...) + fit := &proto.FIT{Messages: tc.messages} if diff := cmp.Diff(fit.Messages, tc.messages, cmp.Transformer("Value", func(v proto.Value) any { return v.Any() @@ -78,55 +77,6 @@ func TestFitWithMessages(t *testing.T) { } } -func TestMessageDefinitionClone(t *testing.T) { - mesgDef := proto.MessageDefinition{ - FieldDefinitions: []proto.FieldDefinition{ - {Num: fieldnum.RecordCadence, Size: 1, BaseType: basetype.Uint8}, - {Num: fieldnum.RecordHeartRate, Size: 1, BaseType: basetype.Uint8}, - }, - DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ - {Num: 0, DeveloperDataIndex: 0, Size: 1}, - }, - } - - cloned := mesgDef.Clone() - cloned.FieldDefinitions[0].Num = 100 - cloned.DeveloperFieldDefinitions[0].Num = 100 - - if diff := cmp.Diff(mesgDef, cloned); diff == "" { - t.Fatalf("expected deep cloned, but some data still being referenced.") - } -} - -func TestMessageWithFieldValues(t *testing.T) { - tt := []struct { - name string - fieldValues map[byte]any - }{ - { - name: "withFieldValues", - fieldValues: map[byte]any{ - fieldnum.RecordSpeed: uint16(1000), - fieldnum.RecordCadence: uint16(100), - }, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - mesg := factory.CreateMesg(mesgnum.Record) - mesg.WithFieldValues(tc.fieldValues) - for i := range mesg.Fields { - if value, ok := tc.fieldValues[mesg.Fields[i].Num]; ok { - if mesg.Fields[i].Value.Any() != value { - t.Errorf("expected %T(%v), got: %T(%v)", value, value, mesg.Fields[i].Value, mesg.Fields[i].Value) - } - } - } - }) - } -} - func TestMessageFieldByNum(t *testing.T) { sharedField := factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(typedef.EventTypeStart) @@ -138,17 +88,17 @@ func TestMessageFieldByNum(t *testing.T) { }{ { name: "FieldByNum found", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ sharedField, - ), + }}, fieldNum: fieldnum.EventEventType, field: &sharedField, }, { name: "FieldByNum not found", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ sharedField, - ), + }}, fieldNum: fieldnum.EventData, field: nil, }, @@ -177,17 +127,17 @@ func TestMessageFieldValueByNum(t *testing.T) { }{ { name: "FieldValueByNum found", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(typedef.EventTypeStart), - ), + }}, fieldNum: fieldnum.EventEventType, value: proto.Uint8(uint8(typedef.EventTypeStart)), }, { name: "FieldValueByNum not found", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(typedef.EventTypeStart), - ), + }}, fieldNum: fieldnum.EventData, value: proto.Value{}, }, @@ -213,18 +163,18 @@ func TestMessageRemoveFieldByNum(t *testing.T) { }{ { name: "remove existing field", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(typedef.EventTypeStart), - ), + }}, fieldNum: fieldnum.EventEventType, field: nil, size: 0, }, { name: "remove field that is not exist", - mesg: proto.Message{Num: mesgnum.Event}.WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(typedef.EventTypeStart), - ), + }}, fieldNum: fieldnum.EventData, field: nil, size: 1, @@ -245,32 +195,6 @@ func TestMessageRemoveFieldByNum(t *testing.T) { } } -func TestMessageClone(t *testing.T) { - mesg := factory.CreateMesg(mesgnum.Session).WithFieldValues(map[byte]any{ - fieldnum.SessionAvgAltitude: proto.Uint16(1000), - fieldnum.SessionAvgSpeed: proto.Uint16(1000), - }).WithDeveloperFields( - proto.DeveloperField{ - Num: 0, - DeveloperDataIndex: 0, - Value: proto.Uint8(1), - }, - proto.DeveloperField{}, - ) - - cloned := mesg.Clone() - cloned.Fields[0].Num = 100 - cloned.DeveloperFields[0].Num = 100 - - if diff := cmp.Diff(mesg, cloned, - cmp.Transformer("Value", func(v proto.Value) any { - return v.Any() - }), - ); diff == "" { - t.Fatalf("expected deep cloned, but some data still being referenced.") - } -} - func TestFieldSubFieldSubtitution(t *testing.T) { tt := []struct { name string @@ -281,26 +205,26 @@ func TestFieldSubFieldSubtitution(t *testing.T) { }{ { name: "SubFieldSubtitution ok, main field can be interpreted.", - mesg: factory.CreateMesg(mesgnum.Event).WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(10)), - ), + }}, field: factory.CreateField(mesgnum.Event, fieldnum.EventData), subfieldName: "course_point_index", ok: true, }, { name: "SubFieldSubtitution not ok, can't interpret main field.", - mesg: factory.CreateMesg(mesgnum.Event).WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(100)), - ), + }}, field: factory.CreateField(mesgnum.Event, fieldnum.EventData), ok: false, }, { name: "SubFieldSubtitution field reference not found", - mesg: factory.CreateMesg(mesgnum.Event).WithFields( + mesg: proto.Message{Num: mesgnum.Event, Fields: []proto.Field{ factory.CreateField(mesgnum.Event, fieldnum.EventActivityType).WithValue(uint8(10)), - ), + }}, field: factory.CreateField(mesgnum.Event, fieldnum.EventData), ok: false, }, @@ -321,202 +245,3 @@ func TestFieldSubFieldSubtitution(t *testing.T) { }) } } - -func TestFieldClone(t *testing.T) { - field := factory.CreateField(mesgnum.Record, fieldnum.RecordSpeed) - - cloned := field.Clone() - cloned.Components[0].Scale = 777 - - if diff := cmp.Diff(field, cloned, - cmp.Transformer("Value", func(v proto.Value) any { - return v.Any() - }), - ); diff == "" { - t.Fatalf("expected deep cloned, but some data still being referenced.") - } - - field = proto.Field{} - cloned = field.Clone() - - if diff := cmp.Diff(field, cloned, - cmp.Transformer("Value", func(v proto.Value) any { - return v.Any() - }), - ); diff != "" { - t.Fatalf("should not changed") - } -} - -func TestCreateMessageDefinition(t *testing.T) { - tt := []struct { - name string - mesg proto.Message - mesgDef proto.MessageDefinition - }{ - { - name: "fields only with non-array values", - mesg: proto.Message{Num: mesgnum.FileId}.WithFields( - factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask, - MesgNum: mesgnum.FileId, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.FileIdType, - Size: 1, - BaseType: basetype.Enum, - }, - }, - }, - }, - { - name: "fields only with mesg architecture big-endian", - mesg: func() proto.Message { - mesg := proto.Message{Num: mesgnum.FileId}.WithFields( - factory.CreateField(mesgnum.FileId, fieldnum.FileIdType).WithValue(typedef.FileActivity), - ) - mesg.Architecture = 1 // big-endian - return mesg - }(), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask, - Architecture: 1, // big-endian - MesgNum: mesgnum.FileId, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.FileIdType, - Size: 1, - BaseType: basetype.Enum, - }, - }, - }, - }, - { - name: "fields only with string value", - mesg: proto.Message{Num: mesgnum.FileId}.WithFields( - factory.CreateField(mesgnum.FileId, fieldnum.FileIdProductName).WithValue("FIT SDK Go"), - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask, - MesgNum: mesgnum.FileId, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.FileIdProductName, - Size: 1 * 11, // len("FIT SDK Go") == 10 + '0x00' - BaseType: basetype.String, - }, - }, - }, - }, - { - name: "fields only with array of byte", - mesg: proto.Message{Num: mesgnum.UserProfile}.WithFields( - factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileGlobalId).WithValue([]byte{2, 9}), - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask, - MesgNum: mesgnum.UserProfile, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.UserProfileGlobalId, - Size: 2, - BaseType: basetype.Byte, - }, - }, - }, - }, - - { - name: "developer fields", - mesg: proto.Message{Num: mesgnum.UserProfile}. - WithFields( - factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileGlobalId).WithValue([]byte{byte(2), byte(9)})). - WithDeveloperFields( - proto.DeveloperField{ - Num: 0, DeveloperDataIndex: 0, Value: proto.Uint8(1), - }, - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask | proto.DevDataMask, - MesgNum: mesgnum.UserProfile, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.UserProfileGlobalId, - Size: 2, - BaseType: basetype.Byte, - }, - }, - DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ - { - Num: 0, Size: 1, DeveloperDataIndex: 0, - }, - }, - }, - }, - { - name: "developer fields with string value \"FIT SDK Go\", size should be 11", - mesg: proto.Message{Num: mesgnum.UserProfile}. - WithFields( - factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileGlobalId).WithValue([]byte{byte(2), byte(9)})). - WithDeveloperFields( - proto.DeveloperField{ - Num: 0, DeveloperDataIndex: 0, Value: proto.String("FIT SDK Go"), - }, - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask | proto.DevDataMask, - MesgNum: mesgnum.UserProfile, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.UserProfileGlobalId, - Size: 2, - BaseType: basetype.Byte, - }, - }, - DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ - { - Num: 0, Size: 11, DeveloperDataIndex: 0, - }, - }, - }, - }, - { - name: "developer fields with value []uint16{1,2,3}, size should be 3*2 = 6", - mesg: proto.Message{Num: mesgnum.UserProfile}. - WithFields( - factory.CreateField(mesgnum.UserProfile, fieldnum.UserProfileGlobalId).WithValue([]byte{byte(2), byte(9)})). - WithDeveloperFields( - proto.DeveloperField{ - Num: 0, DeveloperDataIndex: 0, Value: proto.SliceUint16([]uint16{1, 2, 3}), - }, - ), - mesgDef: proto.MessageDefinition{ - Header: proto.MesgDefinitionMask | proto.DevDataMask, - MesgNum: mesgnum.UserProfile, - FieldDefinitions: []proto.FieldDefinition{ - { - Num: fieldnum.UserProfileGlobalId, - Size: 2, - BaseType: basetype.Byte, - }, - }, - DeveloperFieldDefinitions: []proto.DeveloperFieldDefinition{ - { - Num: 0, Size: 6, DeveloperDataIndex: 0, - }, - }, - }, - }, - } - - for i, tc := range tt { - t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { - mesgDef := proto.CreateMessageDefinition(&tc.mesg) - if diff := cmp.Diff(mesgDef, tc.mesgDef); diff != "" { - t.Fatal(diff) - } - }) - } -} diff --git a/proto/value_marshal.go b/proto/value_marshal.go index 47a877a..1f87f07 100644 --- a/proto/value_marshal.go +++ b/proto/value_marshal.go @@ -50,7 +50,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { b = append(b, v.SliceUint8()...) return b, nil case TypeInt16: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint16(b, uint16(v.num)) } else { b = binary.BigEndian.AppendUint16(b, uint16(v.num)) @@ -58,7 +58,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceInt16: vals := v.SliceInt16() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint16(b, uint16(vals[i])) } @@ -69,7 +69,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeUint16: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint16(b, uint16(v.num)) } else { b = binary.BigEndian.AppendUint16(b, uint16(v.num)) @@ -77,7 +77,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceUint16: vals := v.SliceUint16() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint16(b, vals[i]) } @@ -88,7 +88,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeInt32: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint32(b, uint32(v.num)) } else { b = binary.BigEndian.AppendUint32(b, uint32(v.num)) @@ -96,7 +96,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceInt32: vals := v.SliceInt32() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint32(b, uint32(vals[i])) } @@ -107,7 +107,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeUint32: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint32(b, uint32(v.num)) } else { b = binary.BigEndian.AppendUint32(b, uint32(v.num)) @@ -115,7 +115,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceUint32: vals := v.SliceUint32() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint32(b, vals[i]) } @@ -126,7 +126,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeInt64: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint64(b, uint64(v.num)) } else { b = binary.BigEndian.AppendUint64(b, uint64(v.num)) @@ -134,7 +134,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceInt64: vals := v.SliceInt64() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint64(b, uint64(vals[i])) } @@ -145,7 +145,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeUint64: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint64(b, v.num) } else { b = binary.BigEndian.AppendUint64(b, v.num) @@ -153,7 +153,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceUint64: vals := v.SliceUint64() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint64(b, vals[i]) } @@ -164,7 +164,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeFloat32: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint32(b, uint32(v.num)) } else { b = binary.BigEndian.AppendUint32(b, uint32(v.num)) @@ -172,7 +172,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceFloat32: vals := v.SliceFloat32() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint32(b, math.Float32bits(vals[i])) } @@ -183,7 +183,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { } return b, nil case TypeFloat64: - if arch == littleEndian { + if arch == LittleEndian { b = binary.LittleEndian.AppendUint64(b, v.num) } else { b = binary.BigEndian.AppendUint64(b, v.num) @@ -191,7 +191,7 @@ func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { return b, nil case TypeSliceFloat64: vals := v.SliceFloat64() - if arch == littleEndian { + if arch == LittleEndian { for i := range vals { b = binary.LittleEndian.AppendUint64(b, math.Float64bits(vals[i])) } diff --git a/proto/value_marshal_test.go b/proto/value_marshal_test.go index 4b2fa9e..81493ca 100644 --- a/proto/value_marshal_test.go +++ b/proto/value_marshal_test.go @@ -14,70 +14,63 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/muktihari/fit/internal/kit" ) func TestValueMarshalAppend(t *testing.T) { tt := []struct { - b *[]byte value Value err error }{ - {b: kit.Ptr([]byte{}), value: Bool(false)}, - {b: kit.Ptr([]byte{}), value: Bool(true)}, - {b: kit.Ptr([]byte{}), value: Int8(-19)}, - {b: kit.Ptr([]byte{}), value: Uint8(129)}, - {b: kit.Ptr([]byte{}), value: Int16(1429)}, - {b: kit.Ptr([]byte{}), value: Int16(-429)}, - {b: kit.Ptr([]byte{}), value: Uint16(9929)}, - {b: kit.Ptr([]byte{}), value: Int32(819293429)}, - {b: kit.Ptr([]byte{}), value: Int32(-8979123)}, - {b: kit.Ptr([]byte{}), value: Uint32(9929)}, - {b: kit.Ptr([]byte{}), value: Int64(819293429)}, - {b: kit.Ptr([]byte{}), value: Int64(-8979123)}, - {b: kit.Ptr([]byte{}), value: Uint64(9929)}, - {b: kit.Ptr([]byte{}), value: Float32(819293429.192321)}, - {b: kit.Ptr([]byte{}), value: Float32(-8979123.546734)}, - {b: kit.Ptr([]byte{}), value: Float64(8192934298908979.192321)}, - {b: kit.Ptr([]byte{}), value: Float64(-897912398989898.546734)}, - {b: kit.Ptr([]byte{}), value: String("FIT SDK")}, - {b: kit.Ptr([]byte{}), value: String("")}, - {b: kit.Ptr([]byte{}), value: SliceBool([]bool{true, false})}, - {b: kit.Ptr([]byte{}), value: SliceUint8([]byte{1, 2})}, - {b: kit.Ptr([]byte{}), value: SliceUint8([]uint8{1, 2})}, - {b: kit.Ptr([]byte{}), value: SliceInt8([]int8{-19})}, - {b: kit.Ptr([]byte{}), value: SliceUint8([]uint8{129})}, - {b: kit.Ptr([]byte{}), value: SliceInt16([]int16{1429})}, - {b: kit.Ptr([]byte{}), value: SliceInt16([]int16{-429})}, - {b: kit.Ptr([]byte{}), value: SliceUint16([]uint16{9929})}, - {b: kit.Ptr([]byte{}), value: SliceInt32([]int32{819293429})}, - {b: kit.Ptr([]byte{}), value: SliceInt32([]int32{-8979123})}, - {b: kit.Ptr([]byte{}), value: SliceUint32([]uint32{9929})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{"supported"})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{""})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{"\x00"})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{"\x00", "\x00"})}, - {b: kit.Ptr([]byte{}), value: SliceString([]string{string([]byte{'\x00'})})}, - {b: kit.Ptr([]byte{}), value: SliceInt64([]int64{819293429})}, - {b: kit.Ptr([]byte{}), value: SliceInt64([]int64{-8979123})}, - {b: kit.Ptr([]byte{}), value: SliceUint64([]uint64{9929})}, - {b: kit.Ptr([]byte{}), value: SliceFloat32([]float32{819293429.192321})}, - {b: kit.Ptr([]byte{}), value: SliceFloat32([]float32{-8979123.546734})}, - {b: kit.Ptr([]byte{}), value: SliceFloat64([]float64{8192934298908979.192321})}, - {b: kit.Ptr([]byte{}), value: SliceFloat64([]float64{-897912398989898.546734})}, - {b: kit.Ptr([]byte{}), value: Value{}, err: ErrTypeNotSupported}, + {value: Bool(false)}, + {value: Bool(true)}, + {value: Int8(-19)}, + {value: Uint8(129)}, + {value: Int16(1429)}, + {value: Int16(-429)}, + {value: Uint16(9929)}, + {value: Int32(819293429)}, + {value: Int32(-8979123)}, + {value: Uint32(9929)}, + {value: Int64(819293429)}, + {value: Int64(-8979123)}, + {value: Uint64(9929)}, + {value: Float32(819293429.192321)}, + {value: Float32(-8979123.546734)}, + {value: Float64(8192934298908979.192321)}, + {value: Float64(-897912398989898.546734)}, + {value: String("FIT SDK")}, + {value: String("")}, + {value: SliceBool([]bool{true, false})}, + {value: SliceUint8([]byte{1, 2})}, + {value: SliceUint8([]uint8{1, 2})}, + {value: SliceInt8([]int8{-19})}, + {value: SliceUint8([]uint8{129})}, + {value: SliceInt16([]int16{1429})}, + {value: SliceInt16([]int16{-429})}, + {value: SliceUint16([]uint16{9929})}, + {value: SliceInt32([]int32{819293429})}, + {value: SliceInt32([]int32{-8979123})}, + {value: SliceUint32([]uint32{9929})}, + {value: SliceString([]string{"supported"})}, + {value: SliceString([]string{})}, + {value: SliceString([]string{""})}, + {value: SliceString([]string{"\x00"})}, + {value: SliceString([]string{"\x00", "\x00"})}, + {value: SliceString([]string{string([]byte{'\x00'})})}, + {value: SliceInt64([]int64{819293429})}, + {value: SliceInt64([]int64{-8979123})}, + {value: SliceUint64([]uint64{9929})}, + {value: SliceFloat32([]float32{819293429.192321})}, + {value: SliceFloat32([]float32{-8979123.546734})}, + {value: SliceFloat64([]float64{8192934298908979.192321})}, + {value: SliceFloat64([]float64{-897912398989898.546734})}, + {value: Value{}, err: ErrTypeNotSupported}, } for i, tc := range tt { for arch := byte(0); arch <= 1; arch++ { t.Run(fmt.Sprintf("[%d] %T(%v))", i, tc.value.Any(), tc.value.Any()), func(t *testing.T) { - arr := pool.Get().(*[MaxBytesPerMessage]byte) - defer pool.Put(arr) - b := arr[:0] - - var err error - *tc.b, err = tc.value.MarshalAppend(b, arch) + b, err := tc.value.MarshalAppend(nil, arch) if !errors.Is(err, tc.err) { t.Fatalf("expected err: %v, got: %v", tc.err, err) } @@ -90,12 +83,12 @@ func TestValueMarshalAppend(t *testing.T) { t.Fatalf("marshalWithReflectionForTest: %v", err) } - if len(*tc.b) == 0 && len(buf.Bytes()) == 0 { + if len(b) == 0 && len(buf.Bytes()) == 0 { return } - if diff := cmp.Diff(*tc.b, buf.Bytes()); diff != "" { - fmt.Printf("value: %v, b: %v, buf: %v\n", tc.value.Any(), *tc.b, buf.Bytes()) + if diff := cmp.Diff(b, buf.Bytes()); diff != "" { + fmt.Printf("value: %v, b: %v, buf: %v\n", tc.value.Any(), b, buf.Bytes()) t.Fatal(diff) } @@ -162,6 +155,6 @@ func BenchmarkValueMarshalAppend(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = value.MarshalAppend(buf, littleEndian) + _, _ = value.MarshalAppend(buf, LittleEndian) } } diff --git a/proto/value_unmarshal.go b/proto/value_unmarshal.go index ca0d31e..0cc5e17 100755 --- a/proto/value_unmarshal.go +++ b/proto/value_unmarshal.go @@ -49,7 +49,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 2 vals := make([]int16, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, int16(binary.LittleEndian.Uint16(b[:n]))) } @@ -60,7 +60,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceInt16(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Int16(int16(binary.LittleEndian.Uint16(b))), nil } return Int16(int16(binary.BigEndian.Uint16(b))), nil @@ -68,7 +68,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 2 vals := make([]uint16, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, binary.LittleEndian.Uint16(b[:n])) } @@ -79,7 +79,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceUint16(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Uint16(binary.LittleEndian.Uint16(b)), nil } return Uint16(binary.BigEndian.Uint16(b)), nil @@ -87,7 +87,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 4 vals := make([]int32, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, int32(binary.LittleEndian.Uint32(b[:n]))) } @@ -98,7 +98,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceInt32(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Int32(int32(binary.LittleEndian.Uint32(b))), nil } return Int32(int32(binary.BigEndian.Uint32(b))), nil @@ -106,7 +106,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 4 vals := make([]uint32, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, binary.LittleEndian.Uint32(b[:n])) } @@ -117,7 +117,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceUint32(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Uint32(binary.LittleEndian.Uint32(b)), nil } return Uint32(binary.BigEndian.Uint32(b)), nil @@ -125,7 +125,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 8 vals := make([]int64, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, int64(binary.LittleEndian.Uint64(b[:n]))) } @@ -136,7 +136,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceInt64(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Int64(int64(binary.LittleEndian.Uint64(b))), nil } return Int64(int64(binary.BigEndian.Uint64(b))), nil @@ -144,7 +144,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 8 vals := make([]uint64, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, binary.LittleEndian.Uint64(b[:n])) } @@ -155,7 +155,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceUint64(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Uint64(binary.LittleEndian.Uint64(b)), nil } return Uint64(binary.BigEndian.Uint64(b)), nil @@ -163,7 +163,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 4 vals := make([]float32, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, math.Float32frombits(binary.LittleEndian.Uint32(b[:n]))) } @@ -174,7 +174,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceFloat32(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Float32(math.Float32frombits(binary.LittleEndian.Uint32(b))), nil } return Float32(math.Float32frombits(binary.BigEndian.Uint32(b))), nil @@ -182,7 +182,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType if isArray { const n = 8 vals := make([]float64, 0, len(b)/n) - if arch == littleEndian { + if arch == LittleEndian { for ; len(b) >= n; b = b[n:] { vals = append(vals, math.Float64frombits(binary.LittleEndian.Uint64(b[:n]))) } @@ -193,7 +193,7 @@ func UnmarshalValue(b []byte, arch byte, baseType basetype.BaseType, profileType } return SliceFloat64(vals), nil } - if arch == littleEndian { + if arch == LittleEndian { return Float64(math.Float64frombits(binary.LittleEndian.Uint64(b))), nil } return Float64(math.Float64frombits(binary.BigEndian.Uint64(b))), nil diff --git a/proto/version.go b/proto/version.go index 6ff10b7..9f7dea2 100644 --- a/proto/version.go +++ b/proto/version.go @@ -29,19 +29,19 @@ func CreateVersion(major, minor byte) (Version, bool) { } // Validate checks whether given version is a valid version. -func Validate(version byte) error { - if VersionMajor(version) > VersionMajor(byte(Vmax)) { +func Validate(version Version) error { + if VersionMajor(version) > VersionMajor(Vmax) { return ErrProtocolVersionNotSupported } return nil } // VersionMajor returns major value of given version -func VersionMajor(version byte) byte { - return version >> MajorVersionShift +func VersionMajor(version Version) byte { + return byte(version >> MajorVersionShift) } // VersionMinor returns minor value of given version -func VersionMinor(version byte) byte { - return version & MinorVersionMask +func VersionMinor(version Version) byte { + return byte(version & MinorVersionMask) } diff --git a/proto/version_test.go b/proto/version_test.go index ae2e83e..9180ed3 100644 --- a/proto/version_test.go +++ b/proto/version_test.go @@ -52,7 +52,7 @@ func TestCreateVersion(t *testing.T) { func TestValidateVersion(t *testing.T) { tt := []struct { - version byte + version proto.Version err error }{ {version: 32, err: nil}, From 2a825d263af98ebf7fc2ce7b937fc817cb5fc3e0 Mon Sep 17 00:00:00 2001 From: Mukti Date: Thu, 12 Sep 2024 14:39:50 +0700 Subject: [PATCH 10/12] feat: filedef add new messages in Activity File (#419) --- profile/filedef/activity.go | 57 +++++++++++++++++++++++--------- profile/filedef/activity_test.go | 42 +++++++++++++++-------- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/profile/filedef/activity.go b/profile/filedef/activity.go index 67bd7fa..aedb703 100644 --- a/profile/filedef/activity.go +++ b/profile/filedef/activity.go @@ -30,16 +30,20 @@ type Activity struct { Records []*mesgdef.Record // required fields: timestamp // Optional Messages - UserProfile *mesgdef.UserProfile - DeviceInfos []*mesgdef.DeviceInfo // required fields: timestamp - Events []*mesgdef.Event - Lengths []*mesgdef.Length // required fields: timestamp, event, event_type - SegmentLaps []*mesgdef.SegmentLap - ZonesTargets []*mesgdef.ZonesTarget - Workouts []*mesgdef.Workout - WorkoutSteps []*mesgdef.WorkoutStep - HRs []*mesgdef.Hr - HRVs []*mesgdef.Hrv // required fields: time + UserProfile *mesgdef.UserProfile + DeviceInfos []*mesgdef.DeviceInfo // required fields: timestamp + Events []*mesgdef.Event + Lengths []*mesgdef.Length // required fields: timestamp, event, event_type + SegmentLaps []*mesgdef.SegmentLap + ZonesTargets []*mesgdef.ZonesTarget + Workouts []*mesgdef.Workout + WorkoutSteps []*mesgdef.WorkoutStep + HRs []*mesgdef.Hr + HRVs []*mesgdef.Hrv // required fields: time + GpsMetadatas []*mesgdef.GpsMetadata + TimeInZones []*mesgdef.TimeInZone + Splits []*mesgdef.Split + SplitSummaries []*mesgdef.SplitSummary // entries must be unique within each split_type // Messages not related to Activity UnrelatedMessages []proto.Message @@ -73,10 +77,10 @@ func (f *Activity) Add(mesg proto.Message) { f.Laps = append(f.Laps, mesgdef.NewLap(&mesg)) case mesgnum.Record: f.Records = append(f.Records, mesgdef.NewRecord(&mesg)) - case mesgnum.DeviceInfo: - f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg)) case mesgnum.UserProfile: f.UserProfile = mesgdef.NewUserProfile(&mesg) + case mesgnum.DeviceInfo: + f.DeviceInfos = append(f.DeviceInfos, mesgdef.NewDeviceInfo(&mesg)) case mesgnum.Event: f.Events = append(f.Events, mesgdef.NewEvent(&mesg)) case mesgnum.Length: @@ -93,6 +97,14 @@ func (f *Activity) Add(mesg proto.Message) { f.HRs = append(f.HRs, mesgdef.NewHr(&mesg)) case mesgnum.Hrv: f.HRVs = append(f.HRVs, mesgdef.NewHrv(&mesg)) + case mesgnum.GpsMetadata: + f.GpsMetadatas = append(f.GpsMetadatas, mesgdef.NewGpsMetadata(&mesg)) + case mesgnum.TimeInZone: + f.TimeInZones = append(f.TimeInZones, mesgdef.NewTimeInZone(&mesg)) + case mesgnum.Split: + f.Splits = append(f.Splits, mesgdef.NewSplit(&mesg)) + case mesgnum.SplitSummary: + f.SplitSummaries = append(f.SplitSummaries, mesgdef.NewSplitSummary(&mesg)) default: mesg.Fields = append(mesg.Fields[:0:0], mesg.Fields...) f.UnrelatedMessages = append(f.UnrelatedMessages, mesg) @@ -103,10 +115,11 @@ func (f *Activity) Add(mesg proto.Message) { func (f *Activity) ToFIT(options *mesgdef.Options) proto.FIT { var size = 3 // non slice fields - size += len(f.Sessions) + len(f.Laps) + len(f.Records) + len(f.DeviceInfos) + - len(f.Events) + len(f.Lengths) + len(f.SegmentLaps) + len(f.ZonesTargets) + - len(f.Workouts) + len(f.WorkoutSteps) + len(f.HRs) + len(f.HRVs) + - len(f.DeveloperDataIds) + len(f.FieldDescriptions) + len(f.UnrelatedMessages) + size += len(f.DeveloperDataIds) + len(f.FieldDescriptions) + len(f.Sessions) + + len(f.Laps) + len(f.Records) + len(f.DeviceInfos) + len(f.Events) + + len(f.Lengths) + len(f.SegmentLaps) + len(f.ZonesTargets) + len(f.Workouts) + + len(f.WorkoutSteps) + len(f.HRs) + len(f.HRVs) + len(f.GpsMetadatas) + + len(f.TimeInZones) + len(f.Splits) + len(f.SplitSummaries) + len(f.UnrelatedMessages) fit := proto.FIT{ Messages: make([]proto.Message, 0, size), @@ -164,6 +177,18 @@ func (f *Activity) ToFIT(options *mesgdef.Options) proto.FIT { for i := range f.HRVs { fit.Messages = append(fit.Messages, f.HRVs[i].ToMesg(options)) } + for i := range f.GpsMetadatas { + fit.Messages = append(fit.Messages, f.GpsMetadatas[i].ToMesg(options)) + } + for i := range f.TimeInZones { + fit.Messages = append(fit.Messages, f.TimeInZones[i].ToMesg(options)) + } + for i := range f.Splits { + fit.Messages = append(fit.Messages, f.Splits[i].ToMesg(options)) + } + for i := range f.SplitSummaries { + fit.Messages = append(fit.Messages, f.SplitSummaries[i].ToMesg(options)) + } fit.Messages = append(fit.Messages, f.UnrelatedMessages...) diff --git a/profile/filedef/activity_test.go b/profile/filedef/activity_test.go index 17d5ed7..ace500e 100644 --- a/profile/filedef/activity_test.go +++ b/profile/filedef/activity_test.go @@ -147,6 +147,18 @@ func newActivityMessagesWithExpectedOrder(now time.Time) (mesgs []proto.Message, 23: {Num: mesgnum.Set, Fields: []proto.Field{ factory.CreateField(mesgnum.Set, fieldnum.SetTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), }}, + 24: {Num: mesgnum.GpsMetadata, Fields: []proto.Field{ + factory.CreateField(mesgnum.GpsMetadata, fieldnum.GpsMetadataTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), + }}, + 25: {Num: mesgnum.TimeInZone, Fields: []proto.Field{ + factory.CreateField(mesgnum.TimeInZone, fieldnum.TimeInZoneTimestamp).WithValue(datetime.ToUint32(incrementSecond(&now))), + }}, + 26: {Num: mesgnum.Split, Fields: []proto.Field{ + factory.CreateField(mesgnum.Split, fieldnum.SplitTotalDistance).WithValue(uint32(10000)), + }}, + 27: {Num: mesgnum.SplitSummary, Fields: []proto.Field{ + factory.CreateField(mesgnum.SplitSummary, fieldnum.SplitSummarySplitType).WithValue(typedef.SplitTypeAscentSplit.Byte()), + }}, } ordered = []proto.Message{ @@ -161,19 +173,23 @@ func newActivityMessagesWithExpectedOrder(now time.Time) (mesgs []proto.Message, 8: mesgs[17], 9: mesgs[18], 10: mesgs[20], - 11: mesgs[5], - 12: mesgs[6], - 13: mesgs[7], - 14: mesgs[8], - 15: mesgs[9], - 16: mesgs[10], - 17: mesgs[11], - 18: mesgs[12], - 19: mesgs[13], - 20: mesgs[19], - 21: mesgs[21], - 22: mesgs[22], - 23: mesgs[23], + 11: mesgs[26], + 12: mesgs[27], + 13: mesgs[5], + 14: mesgs[6], + 15: mesgs[7], + 16: mesgs[8], + 17: mesgs[9], + 18: mesgs[10], + 19: mesgs[11], + 20: mesgs[12], + 21: mesgs[13], + 22: mesgs[19], + 23: mesgs[21], + 24: mesgs[22], + 25: mesgs[23], + 26: mesgs[24], + 27: mesgs[25], } return } From 42bdc4d94b44b99ded594e86c9831f1814bab20a Mon Sep 17 00:00:00 2001 From: Mukti Date: Thu, 12 Sep 2024 16:20:00 +0700 Subject: [PATCH 11/12] refactor: set minimal go version to v1.21 (#420) * set minimal go version to go v1.21 * drop golang.org/x/exp dependency * add unit test for strutil.ToTitle --- cmd/fitactivity/combiner/combiner.go | 3 +- cmd/fitactivity/combiner/combiner_test.go | 3 +- go.mod | 3 +- go.sum | 4 +-- internal/cmd/benchfit/go.mod | 2 +- internal/cmd/fitgen/pkg/flagutil/flagutil.go | 2 +- .../cmd/fitgen/pkg/strutil/strutil_test.go | 36 +++++++++++++++++++ .../cmd/fitgen/profile/mesgdef/builder.go | 3 +- .../profile/untyped/fieldnum/builder.go | 3 +- kit/scaleoffset/scaleoffset.go | 3 +- profile/filedef/activity_test.go | 3 +- profile/filedef/filedef.go | 3 +- 12 files changed, 54 insertions(+), 14 deletions(-) diff --git a/cmd/fitactivity/combiner/combiner.go b/cmd/fitactivity/combiner/combiner.go index e088a0a..388f682 100644 --- a/cmd/fitactivity/combiner/combiner.go +++ b/cmd/fitactivity/combiner/combiner.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "slices" + "github.com/muktihari/fit/cmd/fitactivity/aggregator" "github.com/muktihari/fit/factory" "github.com/muktihari/fit/kit/datetime" @@ -17,7 +19,6 @@ import ( "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/profile/untyped/mesgnum" "github.com/muktihari/fit/proto" - "golang.org/x/exp/slices" ) type errorString string diff --git a/cmd/fitactivity/combiner/combiner_test.go b/cmd/fitactivity/combiner/combiner_test.go index 4baec1a..dc0c4cd 100644 --- a/cmd/fitactivity/combiner/combiner_test.go +++ b/cmd/fitactivity/combiner/combiner_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "slices" + "github.com/google/go-cmp/cmp" "github.com/muktihari/fit/factory" "github.com/muktihari/fit/kit/datetime" @@ -19,7 +21,6 @@ import ( "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 TestCombine(t *testing.T) { diff --git a/go.mod b/go.mod index 543ccbf..571fdce 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,12 @@ module github.com/muktihari/fit -go 1.20 +go 1.21 require ( github.com/client9/misspell v0.3.4 github.com/google/go-cmp v0.6.0 github.com/muktihari/carto v0.1.1 github.com/thedatashed/xlsxreader v1.2.8 - golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc golang.org/x/text v0.18.0 ) diff --git a/go.sum b/go.sum index bda533f..3bd691e 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/muktihari/carto v0.1.1 h1:2onp1tC7uYLBQy5WuIJyP5A6oQq3duRkcO3kf6+hzR0= @@ -14,8 +15,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/thedatashed/xlsxreader v1.2.8 h1:8aGbkXIPEThQbA8KzUZqIa4v4oqFrJFKLQ36vWePI5U= github.com/thedatashed/xlsxreader v1.2.8/go.mod h1:wZyb/2xF1+rkZ2ujhC72tuuOWBY574QvcXHFls+5AXc= -golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= -golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/benchfit/go.mod b/internal/cmd/benchfit/go.mod index d1da9d2..08d68b3 100644 --- a/internal/cmd/benchfit/go.mod +++ b/internal/cmd/benchfit/go.mod @@ -1,6 +1,6 @@ module benchfit -go 1.20 +go 1.21 require ( github.com/muktihari/fit v0.0.0 diff --git a/internal/cmd/fitgen/pkg/flagutil/flagutil.go b/internal/cmd/fitgen/pkg/flagutil/flagutil.go index e127cb4..3098050 100644 --- a/internal/cmd/fitgen/pkg/flagutil/flagutil.go +++ b/internal/cmd/fitgen/pkg/flagutil/flagutil.go @@ -10,7 +10,7 @@ import ( "os" "strings" - "golang.org/x/exp/slices" + "slices" ) func Usage() { diff --git a/internal/cmd/fitgen/pkg/strutil/strutil_test.go b/internal/cmd/fitgen/pkg/strutil/strutil_test.go index 62d51ba..2f4c9d5 100644 --- a/internal/cmd/fitgen/pkg/strutil/strutil_test.go +++ b/internal/cmd/fitgen/pkg/strutil/strutil_test.go @@ -5,11 +5,47 @@ package strutil_test import ( + "fmt" "testing" "github.com/muktihari/fit/internal/cmd/fitgen/pkg/strutil" ) +func BenchmarkToTitle(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = strutil.ToTitle("avg_speed") + } +} + +func TestToTitle(t *testing.T) { + tt := []struct { + input string + output string + }{ + { + input: "avg_speed", + output: "AvgSpeed", + }, + { + input: "avg speed", + output: "AvgSpeed", + }, + { + input: "timestamp_32k", + output: "Timestamp32K", + }, + } + + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.input), func(t *testing.T) { + out := strutil.ToTitle(tc.input) + if out != tc.output { + t.Fatalf("expected: %q, got: %q", tc.output, out) + } + }) + } +} + func TestTrimRepeatedChar(t *testing.T) { tt := []struct { s string diff --git a/internal/cmd/fitgen/profile/mesgdef/builder.go b/internal/cmd/fitgen/profile/mesgdef/builder.go index 40eb50f..e617f0f 100644 --- a/internal/cmd/fitgen/profile/mesgdef/builder.go +++ b/internal/cmd/fitgen/profile/mesgdef/builder.go @@ -12,12 +12,13 @@ import ( "strings" "text/template" + "slices" + "github.com/muktihari/fit/internal/cmd/fitgen/generator" "github.com/muktihari/fit/internal/cmd/fitgen/lookup" "github.com/muktihari/fit/internal/cmd/fitgen/parser" "github.com/muktihari/fit/internal/cmd/fitgen/pkg/strutil" "github.com/muktihari/fit/profile/basetype" - "golang.org/x/exp/slices" ) type Builder struct { diff --git a/internal/cmd/fitgen/profile/untyped/fieldnum/builder.go b/internal/cmd/fitgen/profile/untyped/fieldnum/builder.go index b2064a9..0479050 100644 --- a/internal/cmd/fitgen/profile/untyped/fieldnum/builder.go +++ b/internal/cmd/fitgen/profile/untyped/fieldnum/builder.go @@ -12,12 +12,13 @@ import ( "strings" "text/template" + "slices" + "github.com/muktihari/fit/internal/cmd/fitgen/generator" "github.com/muktihari/fit/internal/cmd/fitgen/lookup" "github.com/muktihari/fit/internal/cmd/fitgen/parser" "github.com/muktihari/fit/internal/cmd/fitgen/pkg/strutil" "github.com/muktihari/fit/internal/cmd/fitgen/shared" - "golang.org/x/exp/slices" ) type Builder struct { diff --git a/kit/scaleoffset/scaleoffset.go b/kit/scaleoffset/scaleoffset.go index f83bea1..227dea3 100644 --- a/kit/scaleoffset/scaleoffset.go +++ b/kit/scaleoffset/scaleoffset.go @@ -7,11 +7,10 @@ package scaleoffset import ( "github.com/muktihari/fit/profile/basetype" "github.com/muktihari/fit/proto" - "golang.org/x/exp/constraints" ) type Numeric interface { - constraints.Integer | constraints.Float + ~int8 | ~int16 | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 } // Apply applies scale and offset on value. diff --git a/profile/filedef/activity_test.go b/profile/filedef/activity_test.go index ace500e..033fd04 100644 --- a/profile/filedef/activity_test.go +++ b/profile/filedef/activity_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "slices" + "github.com/google/go-cmp/cmp" "github.com/muktihari/fit/factory" "github.com/muktihari/fit/kit/datetime" @@ -18,7 +20,6 @@ import ( "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 sortFields(mesgs []proto.Message) { diff --git a/profile/filedef/filedef.go b/profile/filedef/filedef.go index b21b066..297161f 100644 --- a/profile/filedef/filedef.go +++ b/profile/filedef/filedef.go @@ -5,11 +5,12 @@ package filedef import ( + "slices" + "github.com/muktihari/fit/profile/mesgdef" "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/profile/untyped/mesgnum" "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 From 7d1dc9ddcced99c8f9a7aa28b16f079841627a30 Mon Sep 17 00:00:00 2001 From: Mukti Date: Thu, 12 Sep 2024 16:57:12 +0700 Subject: [PATCH 12/12] chore: add missing copyright header (#421) --- cmd/fitactivity/concealer/conceal_test.go | 4 ++++ cmd/fitactivity/opener/opener_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/cmd/fitactivity/concealer/conceal_test.go b/cmd/fitactivity/concealer/conceal_test.go index bb3c9f5..9853f7b 100644 --- a/cmd/fitactivity/concealer/conceal_test.go +++ b/cmd/fitactivity/concealer/conceal_test.go @@ -1,3 +1,7 @@ +// Copyright 2024 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 concealer import ( diff --git a/cmd/fitactivity/opener/opener_test.go b/cmd/fitactivity/opener/opener_test.go index 3958b31..782e466 100644 --- a/cmd/fitactivity/opener/opener_test.go +++ b/cmd/fitactivity/opener/opener_test.go @@ -1,3 +1,7 @@ +// Copyright 2024 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 opener import (