From 42066e23f5169b6752a1b0db0632b86149901fbe Mon Sep 17 00:00:00 2001 From: Samarth Date: Wed, 4 Jan 2023 10:57:00 +0530 Subject: [PATCH 1/3] added battery info page --- cmd/root.go | 28 +++++- pkg/metrics/factory/options.go | 8 ++ pkg/metrics/factory/system_wide_metrics.go | 29 ++++++ pkg/metrics/general/battery_info.go | 111 +++++++++++++++++++++ pkg/sink/tui/general/init.go | 45 +++++++++ pkg/sink/tui/general/overall_graphs.go | 79 +++++++++++++++ 6 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 pkg/metrics/general/battery_info.go diff --git a/cmd/root.go b/cmd/root.go index 03d3210..fa130b8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,6 +31,7 @@ const ( defaultOverallRefreshRate = 1000 defaultConfigFileLocation = "" defaultCPUBehavior = false + defaultBatteryBehaviour = false ) var cfgFile string @@ -60,9 +61,16 @@ While using a TUI based command, press ? to get information about key bindings ( return err } - err = systemWideMetricScraper.Serve(factory.WithCPUInfoAs(rootCmd.cpuInfo)) - if err != nil && err != core.ErrCanceledByUser { - fmt.Printf("Error: %v\n", err) + if !rootCmd.batteryInfo { + err = systemWideMetricScraper.Serve(factory.WithCPUInfoAs(rootCmd.cpuInfo)) + if err != nil && err != core.ErrCanceledByUser { + fmt.Printf("Error: %v\n", err) + } + } else { + err = systemWideMetricScraper.Serve(factory.WithBatteryInfoAs(rootCmd.batteryInfo)) + if err != nil && err != core.ErrCanceledByUser { + fmt.Printf("Error: %v\n", err) + } } return nil @@ -72,6 +80,7 @@ While using a TUI based command, press ? to get information about key bindings ( type rootCommand struct { refreshRate uint64 cpuInfo bool + batteryInfo bool } func constructRootCommand(cmd *cobra.Command, args []string) (*rootCommand, error) { @@ -89,9 +98,15 @@ func constructRootCommand(cmd *cobra.Command, args []string) (*rootCommand, erro return nil, err } + batteryInfo, err := cmd.Flags().GetBool("battery") + if err != nil { + return nil, err + } + return &rootCommand{ refreshRate: refreshRate, cpuInfo: cpuInfo, + batteryInfo: batteryInfo, }, nil } @@ -124,6 +139,13 @@ func init() { defaultCPUBehavior, "Info about the CPU Load over all CPUs", ) + + rootCmd.Flags().BoolP( + "battery", + "b", + defaultBatteryBehaviour, + "All stats about the battery.", + ) } // initConfig reads in config file and ENV variables if set. diff --git a/pkg/metrics/factory/options.go b/pkg/metrics/factory/options.go index 83d22b2..473810d 100644 --- a/pkg/metrics/factory/options.go +++ b/pkg/metrics/factory/options.go @@ -34,3 +34,11 @@ func WithCPUInfoAs(cpuInfo bool) Option { swm.cpuInfo = cpuInfo } } + +// WithBatteryInfoAs sets the battery flag value for the RootCommand. +func WithBatteryInfoAs(batteryInfo bool) Option { + return func(ms MetricScraper) { + swm := ms.(*systemWideMetrics) + swm.batteryInfo = batteryInfo + } +} diff --git a/pkg/metrics/factory/system_wide_metrics.go b/pkg/metrics/factory/system_wide_metrics.go index 85b1e34..bf35f6a 100644 --- a/pkg/metrics/factory/system_wide_metrics.go +++ b/pkg/metrics/factory/system_wide_metrics.go @@ -27,6 +27,7 @@ import ( type systemWideMetrics struct { cpuInfo bool + batteryInfo bool sink core.Sink // defaults to TUI. refreshRate uint64 } @@ -41,6 +42,10 @@ func (swm *systemWideMetrics) Serve(opts ...Option) error { if swm.cpuInfo { return swm.serveCPUInfo() } + + if swm.batteryInfo { + return swm.serveBatteryInfo() + } return swm.serveGenericMetrics() } @@ -91,6 +96,30 @@ func (swm *systemWideMetrics) serveCPUInfo() error { return eg.Wait() } +// serveBatteryInfo serves battery stats such as the name of the manufacturer +// and other battery specific information. +func (swm *systemWideMetrics) serveBatteryInfo() error { + eg, ctx := errgroup.WithContext(context.Background()) + metricBus := make(chan general.BatteryData, 1) + + // start producing metrics. + eg.Go(func() error { + batteryInfo := general.NewBatteryInfo() + alteredRefreshRate := uint64(4 * swm.refreshRate / 5) + return general.GetBatteryInfo(ctx, batteryInfo, metricBus, alteredRefreshRate) + }) + + // start consuming metrics. + switch swm.sink { + case core.TUI: + eg.Go(func() error { + return overallGraph.RenderBatteryinfo(ctx, metricBus, swm.refreshRate) + }) + } + + return eg.Wait() +} + // SetSink sets the Sink for the produced metrics. func (swm *systemWideMetrics) SetSink(sink core.Sink) { swm.sink = sink diff --git a/pkg/metrics/general/battery_info.go b/pkg/metrics/general/battery_info.go new file mode 100644 index 0000000..99baccb --- /dev/null +++ b/pkg/metrics/general/battery_info.go @@ -0,0 +1,111 @@ +package general + +import ( + "bufio" + "context" + "os" + "reflect" + "strings" + + "github.com/pesos/grofer/pkg/utils" +) + +type BatteryInfo struct { + Manufacturer string `json:"manufacturer"` + Technology string `json:"technology"` + ModelName string `json:"model_name"` + SerialNumber string `json:"serial_number"` + Capacity string `json:"capacity"` + CycleCount string `json:"cycle_count"` + EnergyFullDesign string `json:"energy_full_design"` + EnergyFull string `json:"energy_full"` + EnergyNow string `json:"energy_now"` + PowerNow string `json:"power_now"` + Status string `json:"status"` + ChargeStartThreshold string `json:"charge_start_threshold"` + ChargeStopThreshold string `json:"charge_stop_threshold"` +} + +type BatteryData struct { + Battery [][]string +} + +// NewBatteryInfo is a constructor for the BatteryInfo type. +func NewBatteryInfo() *BatteryInfo { + return &BatteryInfo{} +} + +// GetBatteryInfo updates the BatteryInfo struct and serves the data to the data channel. +func GetBatteryInfo(ctx context.Context, batteryInfo *BatteryInfo, dataChannel chan BatteryData, refreshRate uint64) error { + return utils.TickUntilDone(ctx, refreshRate, func() error { + err := batteryInfo.UpdateBatteryInfo() + batteryData := batteryInfo.getBatteryData() + if err != nil { + return err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case dataChannel <- batteryData: + return nil + } + }) +} + +// UpdateBatteryInfo updates fields of the type BatteryInfo +func (c *BatteryInfo) UpdateBatteryInfo() error { + err := c.readBatteryInfo() + if err != nil { + return err + } + + return nil +} + +// ReadBatteryInfo reads files from /sys/class/power_supply/BAT0 +// and returns battery specific stats +func (c *BatteryInfo) readBatteryInfo() error { + val := reflect.ValueOf(c).Elem() + for i := 0; i < val.Type().NumField(); i++ { + fileName := val.Type().Field(i).Tag.Get("json") + file, err := os.Open("/sys/class/power_supply/BAT0/" + fileName) + if err != nil { + return err + } + defer file.Close() + reader := bufio.NewReader(file) + + // Read first line containing load values + data, err := reader.ReadBytes(byte('\n')) + if err != nil { + return err + } + vals := strings.Fields(string(data)) + val.FieldByName(val.Type().Field(i).Name).SetString(vals[0]) + } + return nil +} + +// getBatteryData structures all the battery stats into the Battery data struct. +func (c *BatteryInfo) getBatteryData() BatteryData { + var bData BatteryData = BatteryData{ + Battery: [][]string{ + {"Stats", "Info"}, + {"manufacturer", c.Manufacturer}, + {"technology", c.Technology}, + {"model name", c.ModelName}, + {"serial number", c.SerialNumber}, + {"capacity", c.Capacity}, + {"cycle count", c.CycleCount}, + {"energy full design", c.EnergyFullDesign}, + {"energy full", c.EnergyFull}, + {"energy now", c.EnergyNow}, + {"power now", c.PowerNow}, + {"status", c.Status}, + {"charge start threshold", c.ChargeStartThreshold}, + {"charge stop threshold", c.ChargeStopThreshold}, + }, + } + return bData +} diff --git a/pkg/sink/tui/general/init.go b/pkg/sink/tui/general/init.go index 454c302..25a95a4 100644 --- a/pkg/sink/tui/general/init.go +++ b/pkg/sink/tui/general/init.go @@ -54,6 +54,12 @@ type CPUPage struct { CPUTable *viz.Table } +// BatteryPage contains the UI widgets for the UI rendered by the grofer -b command +type BatteryPage struct { + Grid *ui.Grid + Battery *viz.Table +} + // NewPage returns a new page initialized from the MainPage struct func NewPage(numCores int) *MainPage { rxSparkLine := viz.NewSparkline() @@ -95,6 +101,16 @@ func NewCPUPage(numCores int) *CPUPage { return page } +// NewBatteryPage is a constructor for the BatteryPage type +func NewBatteryPage() *BatteryPage { + page := &BatteryPage{ + Grid: ui.NewGrid(), + Battery: viz.NewTable(), + } + page.init() + return page +} + // init initializes all ui elements for the ui rendered by the grofer command func (page *MainPage) init(numCores int) { if numCores > 8 { @@ -424,3 +440,32 @@ func (page *CPUPage) init(numCores int) { page.Grid.SetRect(0, 0, w, h) } + +func (page *BatteryPage) initBatteryTableWidget() { + page.Battery.Title = " Battery Info" + page.Battery.TitleStyle = ui.NewStyle(ui.ColorClear) + page.Battery.BorderStyle.Fg = ui.ColorCyan + page.Battery.HeaderStyle = ui.NewStyle(ui.ColorClear, ui.ColorClear, ui.ModifierBold) + page.Battery.ShowCursor = false + page.Battery.ColResizer = func() { + x := page.Battery.Inner.Dx() + page.Battery.ColWidths = []int{x / 2, x / 2} + } +} + +func (page *BatteryPage) initPageGrid() { + page.Grid = ui.NewGrid() + page.Grid.Set( + ui.NewCol( + 0.3, + ui.NewRow(0.3, page.Battery), + ), + ) + // w, h := ui.TerminalDimensions() + page.Grid.SetRect(0, 0, 200, 55) +} + +func (page *BatteryPage) init() { + page.initBatteryTableWidget() + page.initPageGrid() +} diff --git a/pkg/sink/tui/general/overall_graphs.go b/pkg/sink/tui/general/overall_graphs.go index 2f72514..a24a02e 100644 --- a/pkg/sink/tui/general/overall_graphs.go +++ b/pkg/sink/tui/general/overall_graphs.go @@ -425,3 +425,82 @@ func RenderCPUinfo(ctx context.Context, dataChannel chan *general.CPULoad, refre } } } + +// RenderBatteryinfo displays the Battery info page +func RenderBatteryinfo(ctx context.Context, dataChannel chan general.BatteryData, refreshRate uint64) error { + var on sync.Once + var help *misc.HelpMenu = misc.NewHelpMenu().ForCommand(misc.RootCommand) + + if err := ui.Init(); err != nil { + return fmt.Errorf("failed to initialize termui: %v", err) + } + defer ui.Close() + + page := NewBatteryPage() + + utilitySelected := core.None + + // Pause to pause updating data + pause := func() { + run = !run + } + + // Re render UI + updateUI := func() { + w, h := ui.TerminalDimensions() + page.Grid.SetRect(0, 0, 200, 55) + + ui.Clear() + + switch utilitySelected { + case core.Help: + help.Resize(w, h) + ui.Render(help) + default: + ui.Render(page.Grid) + } + } + + updateUI() + + uiEvents := ui.PollEvents() + t := time.NewTicker(time.Duration(refreshRate) * time.Millisecond) + tick := t.C + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case e := <-uiEvents: // For keyboard events + switch e.ID { + case "q", "": // q or Ctrl-C to quit + return core.ErrCanceledByUser + + case "": + updateUI() + + case "": + utilitySelected = core.None + + case "p": + pause() + + case "?": + utilitySelected = core.Help + } + updateUI() + case data := <-dataChannel: + if run { + header, rows := data.Battery[0], data.Battery[1:] + page.Battery.Header = header + page.Battery.Rows = rows + on.Do(updateUI) + } + case <-tick: + if utilitySelected != core.Help { + ui.Render(page.Grid) + } + } + } + +} From b941cc2feb5d43404ec04ec3e0a43f14685a14ee Mon Sep 17 00:00:00 2001 From: Samarth Date: Mon, 23 Jan 2023 14:37:16 +0530 Subject: [PATCH 2/3] handled battery file not found exception --- pkg/metrics/general/battery_info.go | 58 ++++++++++++++++++-------- pkg/sink/tui/general/init.go | 6 +-- pkg/sink/tui/general/overall_graphs.go | 29 ++++++++----- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/pkg/metrics/general/battery_info.go b/pkg/metrics/general/battery_info.go index 99baccb..3774b99 100644 --- a/pkg/metrics/general/battery_info.go +++ b/pkg/metrics/general/battery_info.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" + "github.com/pesos/grofer/pkg/core" "github.com/pesos/grofer/pkg/utils" ) @@ -27,7 +28,8 @@ type BatteryInfo struct { } type BatteryData struct { - Battery [][]string + FieldSet string + Battery [][]string } // NewBatteryInfo is a constructor for the BatteryInfo type. @@ -38,10 +40,21 @@ func NewBatteryInfo() *BatteryInfo { // GetBatteryInfo updates the BatteryInfo struct and serves the data to the data channel. func GetBatteryInfo(ctx context.Context, batteryInfo *BatteryInfo, dataChannel chan BatteryData, refreshRate uint64) error { return utils.TickUntilDone(ctx, refreshRate, func() error { + var batteryFound bool = true + var batteryData BatteryData = BatteryData{ + FieldSet: "NO BATTERY DATA", + } err := batteryInfo.UpdateBatteryInfo() - batteryData := batteryInfo.getBatteryData() if err != nil { - return err + if err == core.ErrBatteryNotFound { + batteryFound = false + } else { + return err + } + } + + if batteryFound { + batteryData = batteryInfo.getBatteryData() } select { @@ -66,23 +79,31 @@ func (c *BatteryInfo) UpdateBatteryInfo() error { // ReadBatteryInfo reads files from /sys/class/power_supply/BAT0 // and returns battery specific stats func (c *BatteryInfo) readBatteryInfo() error { - val := reflect.ValueOf(c).Elem() - for i := 0; i < val.Type().NumField(); i++ { - fileName := val.Type().Field(i).Tag.Get("json") - file, err := os.Open("/sys/class/power_supply/BAT0/" + fileName) - if err != nil { - return err - } - defer file.Close() - reader := bufio.NewReader(file) + _, err1 := os.Stat("/sys/class/power_supply/BAT0/manufacturer") + _, err2 := os.Stat("/sys/class/power_supply/BAT0/technology") + _, err3 := os.Stat("/sys/class/power_supply/BAT0/model_name") - // Read first line containing load values - data, err := reader.ReadBytes(byte('\n')) - if err != nil { - return err + if err1 == nil && err2 == nil && err3 == nil { + val := reflect.ValueOf(c).Elem() + for i := 0; i < val.Type().NumField(); i++ { + fileName := val.Type().Field(i).Tag.Get("json") + file, err := os.Open("/sys/class/power_supply/BAT0/" + fileName) + if err != nil { + return err + } + defer file.Close() + reader := bufio.NewReader(file) + + // Read first line containing load values + data, err := reader.ReadBytes(byte('\n')) + if err != nil { + return err + } + vals := strings.Fields(string(data)) + val.FieldByName(val.Type().Field(i).Name).SetString(vals[0]) } - vals := strings.Fields(string(data)) - val.FieldByName(val.Type().Field(i).Name).SetString(vals[0]) + } else if os.IsNotExist(err1) || os.IsNotExist(err2) || os.IsNotExist(err3) { + return core.ErrBatteryNotFound } return nil } @@ -90,6 +111,7 @@ func (c *BatteryInfo) readBatteryInfo() error { // getBatteryData structures all the battery stats into the Battery data struct. func (c *BatteryInfo) getBatteryData() BatteryData { var bData BatteryData = BatteryData{ + FieldSet: "BATTERY", Battery: [][]string{ {"Stats", "Info"}, {"manufacturer", c.Manufacturer}, diff --git a/pkg/sink/tui/general/init.go b/pkg/sink/tui/general/init.go index 25a95a4..2c222e6 100644 --- a/pkg/sink/tui/general/init.go +++ b/pkg/sink/tui/general/init.go @@ -442,7 +442,7 @@ func (page *CPUPage) init(numCores int) { } func (page *BatteryPage) initBatteryTableWidget() { - page.Battery.Title = " Battery Info" + page.Battery.Title = " Battery Stats Not Found " page.Battery.TitleStyle = ui.NewStyle(ui.ColorClear) page.Battery.BorderStyle.Fg = ui.ColorCyan page.Battery.HeaderStyle = ui.NewStyle(ui.ColorClear, ui.ColorClear, ui.ModifierBold) @@ -461,8 +461,8 @@ func (page *BatteryPage) initPageGrid() { ui.NewRow(0.3, page.Battery), ), ) - // w, h := ui.TerminalDimensions() - page.Grid.SetRect(0, 0, 200, 55) + w, h := ui.TerminalDimensions() + page.Grid.SetRect(0, 0, w, h) } func (page *BatteryPage) init() { diff --git a/pkg/sink/tui/general/overall_graphs.go b/pkg/sink/tui/general/overall_graphs.go index a24a02e..c88671c 100644 --- a/pkg/sink/tui/general/overall_graphs.go +++ b/pkg/sink/tui/general/overall_graphs.go @@ -429,8 +429,8 @@ func RenderCPUinfo(ctx context.Context, dataChannel chan *general.CPULoad, refre // RenderBatteryinfo displays the Battery info page func RenderBatteryinfo(ctx context.Context, dataChannel chan general.BatteryData, refreshRate uint64) error { var on sync.Once + var pageRender bool = false var help *misc.HelpMenu = misc.NewHelpMenu().ForCommand(misc.RootCommand) - if err := ui.Init(); err != nil { return fmt.Errorf("failed to initialize termui: %v", err) } @@ -447,13 +447,18 @@ func RenderBatteryinfo(ctx context.Context, dataChannel chan general.BatteryData // Re render UI updateUI := func() { - w, h := ui.TerminalDimensions() - page.Grid.SetRect(0, 0, 200, 55) + var w, h int + if !pageRender { + w, h = ui.TerminalDimensions() + } else { + w, h = 200, 55 + } + page.Grid.SetRect(0, 0, w, h) ui.Clear() - switch utilitySelected { case core.Help: + w, h = ui.TerminalDimensions() help.Resize(w, h) ui.Render(help) default: @@ -476,9 +481,6 @@ func RenderBatteryinfo(ctx context.Context, dataChannel chan general.BatteryData case "q", "": // q or Ctrl-C to quit return core.ErrCanceledByUser - case "": - updateUI() - case "": utilitySelected = core.None @@ -491,9 +493,16 @@ func RenderBatteryinfo(ctx context.Context, dataChannel chan general.BatteryData updateUI() case data := <-dataChannel: if run { - header, rows := data.Battery[0], data.Battery[1:] - page.Battery.Header = header - page.Battery.Rows = rows + switch data.FieldSet { + case "BATTERY": + page.Battery.Title = " Battery Info " + header, rows := data.Battery[0], data.Battery[1:] + page.Battery.Header = header + page.Battery.Rows = rows + pageRender = true + default: + pageRender = false + } on.Do(updateUI) } case <-tick: From e68318a51e4bd311730f4ecd397bf61f11a1ca46 Mon Sep 17 00:00:00 2001 From: Samarth Sudarshan <43909851+Samarth2898@users.noreply.github.com> Date: Tue, 24 Jan 2023 11:25:45 +0530 Subject: [PATCH 3/3] added liscence --- pkg/metrics/general/battery_info.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/metrics/general/battery_info.go b/pkg/metrics/general/battery_info.go index 3774b99..e4b45ce 100644 --- a/pkg/metrics/general/battery_info.go +++ b/pkg/metrics/general/battery_info.go @@ -1,3 +1,19 @@ +/* +Copyright © 2020 The PES Open Source Team pesos@pes.edu + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package general import (