Skip to content

Commit

Permalink
feat!: filedef listener now support multiple common file types (#44)
Browse files Browse the repository at this point in the history
- now listener can accept multiple common file types during decoding chained FIT file
- update usage.md documentation related to common file type
  • Loading branch information
muktihari authored Dec 12, 2023
1 parent de4874f commit 5a311cc
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 104 deletions.
95 changes: 55 additions & 40 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ If you are uncertain if it's a chained fit file. Create a loop and use dec.Next(
if err != nil {
return err
}
// do something with fit variable
}

...
Expand All @@ -80,14 +81,13 @@ If you are uncertain if it's a chained fit file. Create a loop and use dec.Next(

Decode to Common File Types enables us to interact with FIT files through common file types such as Activity Files, Course Files, Workout Files, and more, which group protocol messages based on specific purposes.

_Note: Currently only 3 common file types are defined: Activity, Course & Workout_
_Note: Currently only 3 common file types are defined: Activity, Course & Workout, but you can create your own file types using our building block and register it in the listener as an option using WithFileSets()_

```go
package main

import (
"bufio"
"context"
"fmt"
"os"

Expand All @@ -102,49 +102,56 @@ func main() {
}
defer f.Close()

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

dec := decoder.New(bufio.NewReader(f),
decoder.WithMesgListener(al), // Add activity listener to the decoder
decoder.WithBroadcastOnly(), // Direct the decoder to only broadcast the messages without retaining them.
)

_, err = dec.Decode()
if err != nil {
panic(err)
}

// The resulting Activity File can be retrieved after decoding process completed.
activity := al.File()

fmt.Printf("File Type: %s\n", activity.FileId.Type)
fmt.Printf("Sessions count: %d\n", len(activity.Sessions))
fmt.Printf("Laps count: %d\n", len(activity.Laps))
fmt.Printf("Records count: %d\n", len(activity.Records))

i := 100
fmt.Printf("\nSample value of record[%d]:\n", i)
fmt.Printf(" Lat: %v semicircles\n", activity.Records[i].PositionLat)
fmt.Printf(" Long: %v semicircles\n", activity.Records[i].PositionLong)
fmt.Printf(" Speed: %g m/s\n", float64(activity.Records[i].Speed)/1000)
fmt.Printf(" HeartRate: %v bpm\n", activity.Records[i].HeartRate)
fmt.Printf(" Cadence: %v rpm\n", activity.Records[i].Cadence)
for dec.Next() {
_, err = dec.Decode()
if err != nil {
panic(err)
}

// Output:
// File Type: activity
// Sessions count: 1
// Laps count: 1
// Records count: 3601
//
// Sample value of record[100]:
// Lat: 0 semicircles
// Long: 10717 semicircles
// Speed: 1 m/s
// HeartRate: 126 bpm
// Cadence: 100 rpm
// The resulting File can be retrieved after decoding process completed.
switch file := al.File().(type) {
case *filedef.Course:
// do something if it's a course file
case *filedef.Workout:
// do something if it's a workout file
case *filedef.Activity:
fmt.Printf("File Type: %s\n", file.FileId.Type)
fmt.Printf("Sessions count: %d\n", len(file.Sessions))
fmt.Printf("Laps count: %d\n", len(file.Laps))
fmt.Printf("Records count: %d\n", len(file.Records))

i := 100
fmt.Printf("\nSample value of record[%d]:\n", i)
fmt.Printf(" Lat: %v semicircles\n", file.Records[i].PositionLat)
fmt.Printf(" Long: %v semicircles\n", file.Records[i].PositionLong)
fmt.Printf(" Speed: %g m/s\n", float64(file.Records[i].Speed)/1000)
fmt.Printf(" HeartRate: %v bpm\n", file.Records[i].HeartRate)
fmt.Printf(" Cadence: %v rpm\n", file.Records[i].Cadence)

// Output:
// File Type: activity
// Sessions count: 1
// Laps count: 1
// Records count: 3601
//
// Sample value of record[100]:
// Lat: 0 semicircles
// Long: 10717 semicircles
// Speed: 1 m/s
// HeartRate: 126 bpm
// Cadence: 100 rpm
}
}
}
```

Expand All @@ -157,7 +164,6 @@ package main

import (
"bufio"
"context"
"fmt"
"os"

Expand All @@ -173,15 +179,15 @@ func main() {
}
defer f.Close()

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

dec := decoder.New(bufio.NewReader(f),
decoder.WithMesgListener(al),
decoder.WithBroadcastOnly(),
)

fileId, err = dec.PeekFileId()
fileId, err := dec.PeekFileId()
if err != nil {
panic(err)
}
Expand All @@ -197,6 +203,15 @@ func main() {

// It's an Activity File, let's Decode it.
_, err = dec.Decode()
if err != nil {
panic(err)
}

activity := al.File().(*filedef.Activity)

fmt.Printf("Sessions count: %d\n", len(activity.Sessions))
fmt.Printf("Laps count: %d\n", len(activity.Laps))
fmt.Printf("Records count: %d\n", len(activity.Records))
// ...
}
```
Expand Down
11 changes: 3 additions & 8 deletions profile/filedef/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package filedef
import (
"github.com/muktihari/fit/factory"
"github.com/muktihari/fit/profile/mesgdef"
"github.com/muktihari/fit/profile/typedef"
"github.com/muktihari/fit/profile/untyped/mesgnum"
"github.com/muktihari/fit/proto"
)
Expand Down Expand Up @@ -39,17 +38,13 @@ type Course struct {

var _ File = &Course{}

func NewCourse(mesgs ...proto.Message) (f *Course, ok bool) {
f = &Course{}
func NewCourse(mesgs ...proto.Message) *Course {
f := &Course{}
for i := range mesgs {
f.Add(mesgs[i])
}

if f.FileId == nil || f.FileId.Type != typedef.FileCourse {
return
}

return f, true
return f
}

func (f *Course) Add(mesg proto.Message) {
Expand Down
9 changes: 9 additions & 0 deletions profile/filedef/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package filedef contains the implementation of known common file types and its listener
// to convert decoded FIT file into the desired common file type as soon as the message is decoded.
//
// TODO:
// Currently this package is still under experimental development, some functions or methods might change.
// The implementation in this package is generaly work even though we still have no proper tests.
// If you find any bug or you have an idea that you think it's better to implement it that way,
// please open an issue in github, any other form of feedback would be appreciated.
package filedef
131 changes: 83 additions & 48 deletions profile/filedef/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,79 @@
package filedef

import (
"github.com/muktihari/fit/kit/typeconv"
"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"
)

// Listener is Message Listener.
type Listener[F any, T FilePtr[F]] struct {
file F
options *options
mesgc chan proto.Message
done chan struct{}
type options struct {
channelBuffer uint
fileSets FileSets
}

func defaultOptions() *options {
return &options{
channelBuffer: 1000,
fileSets: PredefinedFileSet(),
}
}

// PredefinedFileSet is a list of default filesets used in listener, it's exported so user can append their own types and register it as an option.
func PredefinedFileSet() FileSets {
return FileSets{
typedef.FileActivity: func() File { return NewActivity() },
typedef.FileCourse: func() File { return NewCourse() },
typedef.FileWorkout: func() File { return NewWorkout() },
}
}

type Option interface{ apply(o *options) }

type fnApply func(o *options)

func (f fnApply) apply(o *options) { f(o) }

// WithChannelBuffer sets the size of buffered channel, default is 1000.
func WithChannelBuffer(size uint) Option {
return fnApply(func(o *options) {
if size > 0 {
o.channelBuffer = size
}
})
}

// FilePtr is a type constraint for pointer of File.
type FilePtr[T any] interface {
*T
File
// FileSets is a set of file type mapped to a function to create that File.
type FileSets = map[typedef.File]func() File

// WithFileSets sets what kind of file listener should listen to, when we encounter a file type that is not listed in fileset,
// that file type will be skipped. This will replace the default filesets registered in listener, if you intend to append your own
// file types, please call PredefinedFileSet() and add your file types.
func WithFileSets(fileSets FileSets) Option {
return fnApply(func(o *options) {
if o.fileSets != nil {
o.fileSets = fileSets
}
})
}

// NewListener creates mesg listener for given file T.
func NewListener[F any, T FilePtr[F]](opts ...Option) *Listener[F, T] {
// Listener is Message Listener.
type Listener struct {
options *options
activeFile File
mesgc chan proto.Message
done chan struct{}
}

// NewListener creates mesg listener.
func NewListener(opts ...Option) *Listener {
options := defaultOptions()
for _, opt := range opts {
opt.apply(options)
}

l := &Listener[F, T]{
file: *new(F),
l := &Listener{
options: options,
mesgc: make(chan proto.Message, options.channelBuffer),
done: make(chan struct{}),
Expand All @@ -41,60 +88,48 @@ func NewListener[F any, T FilePtr[F]](opts ...Option) *Listener[F, T] {
return l
}

func (l *Listener[F, T]) loop() {
func (l *Listener) loop() {
for mesg := range l.mesgc {
T(&l.file).Add(mesg)
if mesg.Num == mesgnum.FileId {
fileType := typeconv.ToByte[typedef.File](mesg.FieldValueByNum(fieldnum.FileIdType))
newFile, ok := l.options.fileSets[fileType]
if !ok {
continue
}
l.activeFile = newFile()
}
if l.activeFile == nil {
continue // No file is created since not defined in fileSets. Skip.
}
l.activeFile.Add(mesg)
}
close(l.done)
}

func (l *Listener[F, T]) OnMesg(mesg proto.Message) { l.mesgc <- mesg }
func (l *Listener) OnMesg(mesg proto.Message) { l.mesgc <- mesg }

// Close closes channel and wait until all messages is consumed.
func (l *Listener[F, T]) Close() {
func (l *Listener) Close() {
close(l.mesgc)
<-l.done
}

// File returns the resulting file after the a single decode process is completed. This will reset fields used by listener
// File returns the resulting file after the a single decode process is completed. If we the current decoded result is not listed
// in fileSets, nil will be returned, it's recommended to use switch type assertion to check. This will reset fields used by listener
// and the listener is ready to be used for next chained FIT file.
func (l *Listener[F, T]) File() T {
func (l *Listener) File() File {
l.Close()

file := l.file
file := l.activeFile
l.reset()

go l.loop()

return T(&file)
return file
}

func (l *Listener[F, T]) reset() {
l.file = *new(F)
func (l *Listener) reset() {
l.activeFile = nil
l.mesgc = make(chan proto.Message, l.options.channelBuffer)
l.done = make(chan struct{})
}

type options struct {
channelBuffer uint
}

func defaultOptions() *options {
return &options{
channelBuffer: 1000,
}
}

type Option interface{ apply(o *options) }

type fnApply func(o *options)

func (f fnApply) apply(o *options) { f(o) }

func WithChannelBuffer(size uint) Option {
return fnApply(func(o *options) {
if size > 0 {
o.channelBuffer = size
}
})
}
Loading

0 comments on commit 5a311cc

Please sign in to comment.