diff --git a/gopolloplus.go b/gopolloplus.go index 014f093..5455d69 100644 --- a/gopolloplus.go +++ b/gopolloplus.go @@ -18,7 +18,10 @@ import ( ) func main() { - var cfg *apolloUtils.ApolloConfig + var ( + cfg *apolloUtils.ApolloConfig + history_file *os.File + ) standard_cfg, _ := homedir.Expand("~/.gopolloplus.ini") _, err := os.Stat(standard_cfg) @@ -63,11 +66,6 @@ func main() { data_flow := make(chan *apolloMonitor.ApolloData, 5) // Make a buffered chan just in case killWriter := make(chan bool) - hfile := apolloUtils.GetHistoryFile(cfg) - history_file, _ := os.OpenFile(hfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - defer history_file.Close() - apolloUtils.CSVHeader(history_file) - if err != nil { log.Fatal(err) } @@ -84,6 +82,10 @@ func main() { window.SetFixedSize(true) } + mainContainer := container.NewVBox() + dataContainer := container.NewVBox() + hfile := apolloUtils.GetHistoryFile(cfg) + // Define time things (clock and elapsed time) clockLabel := apolloUI.TimeCanvas("Time") val_elapsed := apolloUI.TimeCanvas("Elapsed") @@ -96,16 +98,6 @@ func main() { container.NewCenter(val_dist), ) - // Define buttons - button_quit := widget.NewButtonWithIcon("Quit", theme.CancelIcon(), func() { - killWriter <- true - monitor.Disconnect() - history_file.Close() - window.Close() - ui.Quit() - }) - - // Split canvas lshift := float32(10) split_title := &canvas.Text{Color: theme.TextColor(), Text: "Split Time", @@ -132,7 +124,7 @@ func main() { // SPM canvas lshift = (2*apolloUI.GraphWidth)+10 spm_title := &canvas.Text{Color: theme.TextColor(), - Text: "Strokes per minutes", + Text: "Strokes per minute", TextSize: apolloUI.TitleFontSize, TextStyle: fyne.TextStyle{Bold: true}} @@ -152,6 +144,23 @@ func main() { spm_title, spm_current, spm_curr_txt, spm_avg, spm_avg_txt, spm_max, spm_max_txt) + var ( + split_history = []int64{} + power_history = []int64{} + spm_history = []int64{} + duration time.Duration + ) + + button_history := &widget.Button{Text: "History", Icon: theme.FolderOpenIcon(),} + + // Define buttons + button_quit := widget.NewButtonWithIcon("Quit", theme.CancelIcon(), func() { + killWriter <- true + monitor.Disconnect() + history_file.Close() + window.Close() + ui.Quit() + }) button_reset := widget.NewButtonWithIcon("New Session", theme.DeleteIcon(), func() { log.Print("Resetting remote monitor") @@ -159,7 +168,9 @@ func main() { history_file.Close() // Prepare a new history file hfile = apolloUtils.GetHistoryFile(cfg) - history_file, _ = os.OpenFile(hfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + split_history = []int64{} + power_history = []int64{} + spm_history = []int64{} }) button_c2 := widget.NewButtonWithIcon("Send to log.C2", theme.MailForwardIcon(), func() { @@ -188,16 +199,19 @@ func main() { button_theme.SetChecked(true) } + button_history.OnTapped = func () {apolloUI.ToggleHistory(ui, cfg)} + button_history.Refresh() + containerButtons := container.NewAdaptiveGrid( - 4, - button_theme, button_reset, button_c2, button_quit, + 5, + button_theme, button_reset, button_history, button_c2, button_quit, ) - mainContainer := container.NewVBox( - containerButtons, - containerTimes, - containerGraphs, - ) + dataContainer.Add(containerTimes) + dataContainer.Add(containerGraphs) + + mainContainer.Add(containerButtons) + mainContainer.Add(dataContainer) window.SetContent(mainContainer) @@ -233,12 +247,6 @@ func main() { }() go func() { log.Print("Start chan reader") - var ( - split_history = []uint64{} - power_history = []uint64{} - spm_history = []uint64{} - duration time.Duration - ) exit := false for { select { @@ -261,6 +269,8 @@ func main() { go apolloUI.ResizeCanvas(spm_history, spm_current, spm_avg, spm_max, spm_curr_txt, spm_avg_txt, spm_max_txt, false) go func() { + history_file, _ = os.OpenFile(hfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + defer history_file.Close() history_file.Write([]byte(d.ToCSV())) }() default: diff --git a/pkg/apolloGraph/apolloGraph.go b/pkg/apolloGraph/apolloGraph.go new file mode 100644 index 0000000..912fa35 --- /dev/null +++ b/pkg/apolloGraph/apolloGraph.go @@ -0,0 +1,73 @@ +package apolloGraph + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "github.com/cjeanneret/gopolloplus/pkg/apolloMonitor" + "github.com/cjeanneret/gopolloplus/pkg/apolloUtils" + "image/color" + "path" +) + +const ( + pos_shift = 2 +) + +func PlotGraph(session string, ct *fyne.Container, cfg *apolloUtils.ApolloConfig, width, height int) { + fname := path.Join(cfg.HistoryDir, session) + data := apolloMonitor.LoadCSV(fname) + watt := []int64{} + split := []int64{} + for _, d := range data { + watt = append(watt, int64(d.Watt)) + split = append(split, int64(d.TimeTo500m)) + } + + _, w_max := apolloUtils.FindMinMax(watt) + _, s_max := apolloUtils.FindMinMax(split) + + w_scale := float32(1) + s_scale := float32(1) + + if int(w_max) > height { + w_scale = float32(w_max)*100/float32(height) + } + if int(s_max) > height { + s_scale = float32(s_max)*100/float32(height) + } + + w_color := color.RGBA{3, 169, 244, 125} + s_color := color.RGBA{210, 132, 69, 125} + + _ct := container.NewWithoutLayout() + _ct.Resize(fyne.Size{Width: float32(width), Height: float32(2*height)}) + + for i, _ := range watt { + if watt[i] != 0 && split[i] != 0 { + w_size := fyne.Size{Width: pos_shift, Height: w_scale*float32(watt[i])} + s_size := fyne.Size{Width: pos_shift, Height: s_scale*float32(split[i])} + + pos := fyne.Position{float32(i)*pos_shift*0.5, 100} + + w_rect := canvas.NewRectangle(w_color) + w_rect.Resize(w_size) + w_rect.Move(pos) + s_rect := canvas.NewRectangle(s_color) + s_rect.Resize(s_size) + s_rect.Move(pos) + _ct.Add(w_rect) + _ct.Add(s_rect) + } + } + + scroller := container.NewHScroll(_ct) + scroller.Resize(fyne.Size{Height: float32(height-100), Width: float32(width)}) + scroller.Move(fyne.Position{310, 0}) + + // Empty the existing container + if len(ct.Objects) == 3 { + ct.Remove(ct.Objects[2]) + } + ct.Add(scroller) +} diff --git a/pkg/apolloMonitor/types.go b/pkg/apolloMonitor/types.go index 4a4e653..4ae04ab 100644 --- a/pkg/apolloMonitor/types.go +++ b/pkg/apolloMonitor/types.go @@ -1,8 +1,9 @@ package apolloMonitor import ( - "fmt" + "encoding/csv" "encoding/json" + "fmt" "log" "os" "strconv" @@ -143,9 +144,10 @@ func (m *Monitor) Disconnect() { } type ApolloData struct { - TotalTime, Distance, TimeTo500m uint64 - SPM, Watt, CalPerH, Level uint64 - Timestamp, Raw string + TotalTime, Distance, TimeTo500m int64 + SPM, Watt, CalPerH, Level int64 + Timestamp int64 + Raw string } func (d *ApolloData) ToJSON() ([]byte, error) { @@ -158,24 +160,61 @@ func (d *ApolloData) ToCSV() (string) { d.SPM, d.Watt, d.CalPerH, d.Level, d.Raw) } +func LoadCSV(filename string) (data []*ApolloData) { + f, err := os.Open(filename) + if err != nil { + log.Fatalf("LoadCSV: %v", err) + } + defer f.Close() + lines, err := csv.NewReader(f).ReadAll() + if err != nil { + log.Fatalf("LoadCSV: %v", err) + } + + for _, line := range lines { + timestamp, _ := strconv.ParseInt(line[0], 10, 64) + totalTime, _ := strconv.ParseInt(line[1], 10, 64) + distance, _ := strconv.ParseInt(line[2], 10, 64) + split, _ := strconv.ParseInt(line[3], 10, 64) + spm, _ := strconv.ParseInt(line[4], 10, 64) + watt, _ := strconv.ParseInt(line[5], 10, 64) + calph, _ := strconv.ParseInt(line[6], 10, 64) + lvl, _ := strconv.ParseInt(line[7], 10, 64) + + data = append(data, &ApolloData{ + Timestamp: timestamp, + TotalTime: totalTime, + Distance: distance, + TimeTo500m: split, + SPM: spm, + Watt: watt, + CalPerH: calph, + Level: lvl, + Raw: line[8], + }) + } + return +} + func (m Monitor) ParseData() *ApolloData { - totalMinutes, _ := strconv.ParseUint(m.Data[3:5], 10, 64) - totalSeconds, _ := strconv.ParseUint(m.Data[5:7], 10, 64) - distance, _ := strconv.ParseUint(m.Data[7:12], 10, 64) - MinutesTo500m, _ := strconv.ParseUint(m.Data[13:15], 10, 64) - SecondsTo500m, _ := strconv.ParseUint(m.Data[15:17], 10, 64) - spm, _ := strconv.ParseUint(m.Data[17:20], 10, 64) - watt, _ := strconv.ParseUint(m.Data[20:23], 10, 64) - calph, _ := strconv.ParseUint(m.Data[23:27], 10, 64) - level, _ := strconv.ParseUint(m.Data[27:29], 10, 64) + t := time.Now() + + timestamp, _ := strconv.ParseInt(t.Format("20060102150405"), 10, 64) + totalMinutes, _ := strconv.ParseInt(m.Data[3:5], 10, 64) + totalSeconds, _ := strconv.ParseInt(m.Data[5:7], 10, 64) + distance, _ := strconv.ParseInt(m.Data[7:12], 10, 64) + MinutesTo500m, _ := strconv.ParseInt(m.Data[13:15], 10, 64) + SecondsTo500m, _ := strconv.ParseInt(m.Data[15:17], 10, 64) + spm, _ := strconv.ParseInt(m.Data[17:20], 10, 64) + watt, _ := strconv.ParseInt(m.Data[20:23], 10, 64) + calph, _ := strconv.ParseInt(m.Data[23:27], 10, 64) + level, _ := strconv.ParseInt(m.Data[27:29], 10, 64) totalTime := totalMinutes*60+totalSeconds timeTo500m := MinutesTo500m*60+SecondsTo500m - t := time.Now() - output := &ApolloData{ - Timestamp: t.Format("20060102150405"), + Timestamp: timestamp, TotalTime: totalTime, Distance: distance, TimeTo500m: timeTo500m, diff --git a/pkg/apolloUI/apolloUI.go b/pkg/apolloUI/apolloUI.go index 3260dc3..dcde961 100644 --- a/pkg/apolloUI/apolloUI.go +++ b/pkg/apolloUI/apolloUI.go @@ -4,17 +4,23 @@ import ( "fmt" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/cjeanneret/gopolloplus/pkg/apolloGraph" "github.com/cjeanneret/gopolloplus/pkg/apolloUtils" "image/color" + "io/ioutil" + "log" + "sort" "time" ) const ( BarHeight = 50 GraphWidth = 300 - TitleFontSize = 26 - ValueFontSize = 18 + TitleFontSize = 25 + ValueFontSize = 30 ) var ( ValueColor = color.RGBA{3, 169, 244, 255} @@ -22,11 +28,12 @@ var ( AVGColor = color.Gray{Y: 85} MaxColor = color.Gray{Y: 65} WhiteColor = color.Gray{Y: 255} + historyList = &widget.List{} ) func TimeCanvas(title string) (c *canvas.Text) { c = &canvas.Text{Color: theme.TextColor(), - TextSize: TitleFontSize, + TextSize: ValueFontSize, Text: title, TextStyle: fyne.TextStyle{Bold: true}} return @@ -42,13 +49,13 @@ func CreateCanvas(lshift, down float32, c color.Color) (rect *canvas.Rectangle, rect_txt = &canvas.Text{Color: ValueColor, TextSize: ValueFontSize, TextStyle: fyne.TextStyle{Bold: true}} - rect_txt.Move(fyne.Position{lshift+10, down+20}) + rect_txt.Move(fyne.Position{lshift+10, down+10}) return } -func ResizeCanvas(values []uint64, curr, avg, max *canvas.Rectangle, +func ResizeCanvas(values []int64, curr, avg, max *canvas.Rectangle, curr_txt, avg_txt, max_txt *canvas.Text, is_duration bool) { @@ -84,3 +91,54 @@ func ResizeCanvas(values []uint64, curr, avg, max *canvas.Rectangle, max_txt.Refresh() } + +func historyListing(cfg *apolloUtils.ApolloConfig, ct *fyne.Container) *widget.List { + files, err := ioutil.ReadDir(cfg.HistoryDir) + if err != nil { + log.Printf("historyListing: %v", err) + } + filenames := []string{} + for _, n := range files { + filenames = append(filenames, n.Name()) + } + sort.Sort(sort.Reverse(sort.StringSlice(filenames))) + + historyList = widget.NewList( + func() int { return len(filenames) }, + func() fyne.CanvasObject { return widget.NewLabel("Sessions") }, + func(i widget.ListItemID, o fyne.CanvasObject) { o.(*widget.Label).SetText(filenames[i]) }, + ) + + historyList.OnSelected = func(i widget.ListItemID) { showSession(files[i].Name(), cfg, ct) } + historyList.Resize(fyne.Size{Height: 500, Width: GraphWidth}) + + return historyList +} + +func showSession(f string, cfg *apolloUtils.ApolloConfig, ct *fyne.Container) { + apolloGraph.PlotGraph(f, ct, cfg, GraphWidth*2, 500) +} + +func ToggleHistory(ui fyne.App, cfg *apolloUtils.ApolloConfig) { + history := ui.NewWindow("History") + history.CenterOnScreen() + history.Resize(fyne.Size{Width:float32(GraphWidth*3 + 10), Height: 600}) + history.SetFixedSize(true) + + layout := container.NewWithoutLayout() + listing := historyListing(cfg, layout) + listing.Move(fyne.Position{5, 40}) + close_button := widget.NewButtonWithIcon("Close", theme.CancelIcon(), func() { + history.Close() + }) + + close_button.Resize(fyne.Size{Height: 35, Width: GraphWidth}) + close_button.Move(fyne.Position{5, 5}) + + layout.Add(close_button) + layout.Add(listing) + + history.SetContent(layout) + + history.Show() +} diff --git a/pkg/apolloUtils/apolloUtils.go b/pkg/apolloUtils/apolloUtils.go index a2cb431..6622dcd 100644 --- a/pkg/apolloUtils/apolloUtils.go +++ b/pkg/apolloUtils/apolloUtils.go @@ -6,7 +6,7 @@ import ( "time" ) -func FindMinMax(a []uint64) (min, max uint64) { +func FindMinMax(a []int64) (min, max int64) { min = a[0] max = a[0] for _, value := range a { @@ -20,7 +20,7 @@ func FindMinMax(a []uint64) (min, max uint64) { return } -func Average(a []uint64) (avg float64) { +func Average(a []int64) (avg float64) { total := 0.0 for _, value := range a{ total += float64(value)