diff options
author | Blaster4385 <venkatesh@tablaster.dev> | 2025-01-13 14:29:08 +0530 |
---|---|---|
committer | Blaster4385 <venkatesh@tablaster.dev> | 2025-01-13 16:48:16 +0530 |
commit | c068b6192ae1996a3c09fd73dfdee8adb36b43cc (patch) | |
tree | 6f311f6aba7039db8f80c2697def7ac377a9a9db | |
parent | c9b29354af7819c294862b8942c28169a89778a8 (diff) |
feat: added support for displaying cpu temperature and usage
-rw-r--r-- | README.md | 5 | ||||
-rwxr-xr-x | frontend/package.json.md5 | 2 | ||||
-rw-r--r-- | main.go | 112 | ||||
-rw-r--r-- | modules/cpu.go | 159 | ||||
-rw-r--r-- | modules/numbers.go | 150 |
5 files changed, 410 insertions, 18 deletions
@@ -1,6 +1,6 @@ # DeepCool Display Linux -This application is a replacement of the original DeepCool Windows application for the LP360 AIO cooler. I may add support for the entire LP series and any other new devices that use a similar pixel display. This currently only supports drawing custom patterns on the display. Support for displaying CPU temprature and usage will be added in future releases. +This application is a replacement of the original DeepCool Windows application for the LP360 AIO cooler. I may add support for the entire LP series and any other new devices that use a similar pixel display. This supports drawing custom patterns on the as well as displaying the CPU temperature and usage. Special thanks to [@Nortank12](https://github.com/Nortank12) for his work on [deepcool-digital-linux](https://github.com/Nortank12/deepcool-digital-linux). I would recommend checking out his app for additional functionality and support for other devices. Additionally, thanks to [@rohan09-raj](https://github.com/rohan09-raj) for figuring out the logic of the commands for creating the patterns. @@ -33,6 +33,9 @@ You can run the applications with or without providing any options. Running it w Options: -d, --daemon Run the application in daemon mode -f, --file Specify the CSV file containing the pattern data (This is required in daemon mode) + -t, --temperature Display the CPU temperature + -c, --celcius Display the CPU temperature in celcius + -u, --usage Display the CPU usage Commands: -h, --help Print help diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 9681b27..40af346 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -8108dc188578437a2319b82b16b2b045
\ No newline at end of file +26eb5e5b6b7d631743c25bacf45f3bd2
\ No newline at end of file @@ -2,6 +2,7 @@ package main import ( "context" + "deepcool-display-linux/modules" "embed" "flag" "fmt" @@ -9,6 +10,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "os" + "time" ) //go:embed all:frontend/dist @@ -16,7 +18,11 @@ var assets embed.FS var ( daemonFlag bool + err error filename string + tempFlag bool + celsiusFlag bool + usageFlag bool helpFlag bool versionFlag bool version = "0.1.0" @@ -32,6 +38,9 @@ Options: -h, --help Show this help message -d, --daemon Run in daemon mode -f, --file Specify CSV file path for pattern + -t, --temp Show CPU temperature + -c, --celsius Show CPU temperature in Celsius + -u, --usage Show CPU usage -v, --version Show the version of the app Modes: @@ -40,7 +49,7 @@ Modes: Example: %s 2. Daemon Mode: - Run with -d flag and specify a CSV file to load a pattern + Run with -d flag and specify an option. Example: %s -d -f pattern.csv For more information, visit: https://github.com/blaster4385/deepcool-display-linux @@ -52,6 +61,12 @@ func main() { flag.BoolVar(&daemonFlag, "d", false, "Run as daemon") flag.StringVar(&filename, "file", "", "CSV file") flag.StringVar(&filename, "f", "", "CSV file") + flag.BoolVar(&tempFlag, "temp", false, "Show CPU temperature") + flag.BoolVar(&tempFlag, "t", false, "Show CPU temperature") + flag.BoolVar(&celsiusFlag, "celsius", false, "Show CPU temperature in Celsius") + flag.BoolVar(&celsiusFlag, "c", false, "Show CPU temperature in Celsius") + flag.BoolVar(&usageFlag, "usage", false, "Show CPU usage") + flag.BoolVar(&usageFlag, "u", false, "Show CPU usage") flag.BoolVar(&helpFlag, "help", false, "Show help message") flag.BoolVar(&helpFlag, "h", false, "Show help message") flag.BoolVar(&versionFlag, "version", false, "Show app version") @@ -70,23 +85,88 @@ func main() { app := NewApp() if daemonFlag { - if filename == "" { - fmt.Println("Error: CSV file path is required in daemon mode") - fmt.Println("Use -h or --help for usage information") - os.Exit(1) - } - ctx := context.Background() app.startup(ctx) - grid, err := app.ParseCSV(filename) - if err != nil { - fmt.Printf("Error parsing CSV file: %v\n", err) - os.Exit(1) - } - err = app.SendPattern(grid) - if err != nil { - fmt.Printf("Error sending pattern: %v\n", err) - os.Exit(1) + if filename != "" { + grid, err := app.ParseCSV(filename) + if err != nil { + fmt.Printf("Error parsing CSV file: %v\n", err) + os.Exit(1) + } + err = app.SendPattern(grid) + if err != nil { + fmt.Printf("Error sending pattern: %v\n", err) + os.Exit(1) + } + } else if tempFlag { + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + var temp float64 + if celsiusFlag { + temp, err = modules.GetCPUTemperature(false) + grid, err := modules.CreateNumberGrid(int(temp), "celsius", 5) + if err != nil { + fmt.Printf("Error creating number grid: %v\n", err) + os.Exit(1) + } + err = app.SendPattern(grid) + if err != nil { + fmt.Printf("Error sending pattern: %v\n", err) + os.Exit(1) + } + } else { + temp, err = modules.GetCPUTemperature(true) + grid, err := modules.CreateNumberGrid(int(temp), "fahrenheit", 5) + if err != nil { + fmt.Printf("Error creating number grid: %v\n", err) + os.Exit(1) + } + err = app.SendPattern(grid) + } + } + } + }() + } else if usageFlag { + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + usage, err := modules.GetCPUUsage() + if err != nil { + fmt.Printf("Error getting CPU usage: %v\n", err) + os.Exit(1) + } + grid, err := modules.CreateNumberGrid(int(usage), "percent", 5) + if err != nil { + fmt.Printf("Error creating number grid: %v\n", err) + os.Exit(1) + } + err = app.SendPattern(grid) + if err != nil { + fmt.Printf("Error sending pattern: %v\n", err) + os.Exit(1) + } + } + } + }() } select {} } else { diff --git a/modules/cpu.go b/modules/cpu.go new file mode 100644 index 0000000..45f6e46 --- /dev/null +++ b/modules/cpu.go @@ -0,0 +1,159 @@ +package modules + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + "time" +) + +const ( + tempSensorCacheDuration = 24 * time.Hour + cpuUsageSampleInterval = 200 * time.Millisecond + tempMilliCelsiusDivisor = 1000.0 + fahrenheitConversion = 9.0 / 5.0 + fahrenheitBase = 32.0 +) + +type CPUUsage struct { + User int64 + Nice int64 + System int64 + Idle int64 + IOWait int64 + IRQ int64 + SoftIRQ int64 + Steal int64 +} + +var ( + cachedTemp float64 + lastTempUpdate time.Time + cachedTempSensor string + tempSensorCachedAt time.Time +) + +func GetCPUTemperature(fahrenheit bool) (float64, error) { + now := time.Now() + if now.Sub(lastTempUpdate) < time.Second { + return cachedTemp, nil + } + + tempSensorPath, err := findTempSensor() + if err != nil { + return 0, fmt.Errorf("finding temp sensor: %w", err) + } + + data, err := ioutil.ReadFile(tempSensorPath) + if err != nil { + return 0, fmt.Errorf("reading CPU temperature (%s): %w", tempSensorPath, err) + } + + tempMilliCelsius, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing CPU temperature: %w", err) + } + + tempCelsius := float64(tempMilliCelsius) / tempMilliCelsiusDivisor + if fahrenheit { + tempCelsius = (tempCelsius * fahrenheitConversion) + fahrenheitBase + } + + cachedTemp = tempCelsius + lastTempUpdate = now + return tempCelsius, nil +} + +func GetCPUUsage() (float64, error) { + prevUsage, err := readCPUUsage() + if err != nil { + return 0, fmt.Errorf("reading initial CPU usage: %w", err) + } + + time.Sleep(cpuUsageSampleInterval) + + currUsage, err := readCPUUsage() + if err != nil { + return 0, fmt.Errorf("reading current CPU usage: %w", err) + } + + usage := calculateCPUUsage(prevUsage, currUsage) + return usage, nil +} + +func findTempSensor() (string, error) { + if cachedTempSensor != "" && time.Since(tempSensorCachedAt) < tempSensorCacheDuration { + return cachedTempSensor, nil + } + + hwmonPath := "/sys/class/hwmon" + files, err := ioutil.ReadDir(hwmonPath) + if err != nil { + return "", fmt.Errorf("locating CPU temperature sensor directory: %w", err) + } + + for _, file := range files { + sensorPath := fmt.Sprintf("%s/%s", hwmonPath, file.Name()) + nameFilePath := fmt.Sprintf("%s/name", sensorPath) + nameData, err := ioutil.ReadFile(nameFilePath) + if err != nil { + continue + } + name := strings.TrimSpace(string(nameData)) + if name == "coretemp" || name == "k10temp" || name == "zenpower" { + cachedTempSensor = fmt.Sprintf("%s/temp1_input", sensorPath) + tempSensorCachedAt = time.Now() + return cachedTempSensor, nil + } + } + + return "", errors.New("appropriate CPU temperature sensor not found") +} + +func readCPUUsage() (CPUUsage, error) { + data, err := os.ReadFile("/proc/stat") + if err != nil { + return CPUUsage{}, fmt.Errorf("reading /proc/stat: %w", err) + } + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + fields := strings.Fields(line) + if fields[0] == "cpu" { + usage := CPUUsage{ + User: parseInt64(fields[1]), + Nice: parseInt64(fields[2]), + System: parseInt64(fields[3]), + Idle: parseInt64(fields[4]), + IOWait: parseInt64(fields[5]), + IRQ: parseInt64(fields[6]), + SoftIRQ: parseInt64(fields[7]), + } + return usage, nil + } + } + + return CPUUsage{}, errors.New("failed to parse CPU usage from /proc/stat") +} + +func calculateCPUUsage(prev, curr CPUUsage) float64 { + prevTotal := prev.User + prev.Nice + prev.System + prev.Idle + prev.IOWait + prev.IRQ + prev.SoftIRQ + currTotal := curr.User + curr.Nice + curr.System + curr.Idle + curr.IOWait + curr.IRQ + curr.SoftIRQ + + totalDiff := float64(currTotal - prevTotal) + idleDiff := float64(curr.Idle - prev.Idle) + + if totalDiff == 0 { + return 0 + } + + return (totalDiff - idleDiff) / totalDiff * 100 +} + +func parseInt64(s string) int64 { + n, _ := strconv.ParseInt(s, 10, 64) + return n +} diff --git a/modules/numbers.go b/modules/numbers.go new file mode 100644 index 0000000..2d19b9f --- /dev/null +++ b/modules/numbers.go @@ -0,0 +1,150 @@ +package modules + +import ( + "errors" +) + +type Pattern [][]bool + +var DigitPatterns = map[int]Pattern{ + 0: { + {true, true, true}, + {true, false, true}, + {true, false, true}, + {true, false, true}, + {true, true, true}, + }, + 1: { + {false, true, false}, + {true, true, false}, + {false, true, false}, + {false, true, false}, + {true, true, true}, + }, + 2: { + {true, true, true}, + {false, false, true}, + {true, true, true}, + {true, false, false}, + {true, true, true}, + }, + 3: { + {true, true, true}, + {false, false, true}, + {true, true, true}, + {false, false, true}, + {true, true, true}, + }, + 4: { + {true, false, true}, + {true, false, true}, + {true, true, true}, + {false, false, true}, + {false, false, true}, + }, + 5: { + {true, true, true}, + {true, false, false}, + {true, true, true}, + {false, false, true}, + {true, true, true}, + }, + 6: { + {true, true, true}, + {true, false, false}, + {true, true, true}, + {true, false, true}, + {true, true, true}, + }, + 7: { + {true, true, true}, + {false, false, true}, + {false, true, false}, + {false, true, false}, + {false, true, false}, + }, + 8: { + {true, true, true}, + {true, false, true}, + {true, true, true}, + {true, false, true}, + {true, true, true}, + }, + 9: { + {true, true, true}, + {true, false, true}, + {true, true, true}, + {false, false, true}, + {true, true, true}, + }, +} + +var SymbolPatterns = map[string]Pattern{ + "celsius": { + {true, false, false, false, false}, + {false, false, true, true, false}, + {false, true, false, false, false}, + {false, true, false, false, false}, + {false, false, true, true, false}, + }, + "fahrenheit": { + {true, false, true, true, false}, + {false, false, true, false, false}, + {false, false, true, true, false}, + {false, false, true, false, false}, + {false, false, true, false, false}, + }, + "percent": { + {false, false, false, false, false}, + {false, true, false, false, true}, + {false, false, false, true, false}, + {false, false, true, false, false}, + {false, true, false, false, true}, + }, +} + +func InsertPattern(grid [][]bool, pattern Pattern, row, col int) { + for i, rowPattern := range pattern { + for j, val := range rowPattern { + if row+i < len(grid) && col+j < len(grid[0]) { + grid[row+i][col+j] = val + } + } + } +} + +func CreateNumberGrid(value int, symbol string, row int) ([][]bool, error) { + if value < 0 || value >= 1000 { + return nil, errors.New("value must be between 0 and 999") + } + if _, ok := SymbolPatterns[symbol]; !ok { + return nil, errors.New("unsupported symbol") + } + + grid := make([][]bool, 14) + for i := range grid { + grid[i] = make([]bool, 14) + } + + var ( + digits []int + symbolCol int + ) + + if value < 100 { + digits = []int{value / 10, value % 10} + symbolCol = 9 + } else { + digits = []int{value / 100, (value % 100) / 10, value % 10} + symbolCol = 13 + } + + col := 1 + for _, digit := range digits { + InsertPattern(grid, DigitPatterns[digit], row, col) + col += 4 + } + InsertPattern(grid, SymbolPatterns[symbol], row, symbolCol) + + return grid, nil +} |