aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaster4385 <venkatesh@tablaster.dev>2025-01-13 14:29:08 +0530
committerBlaster4385 <venkatesh@tablaster.dev>2025-01-13 16:48:16 +0530
commitc068b6192ae1996a3c09fd73dfdee8adb36b43cc (patch)
tree6f311f6aba7039db8f80c2697def7ac377a9a9db
parentc9b29354af7819c294862b8942c28169a89778a8 (diff)
feat: added support for displaying cpu temperature and usage
-rw-r--r--README.md5
-rwxr-xr-xfrontend/package.json.md52
-rw-r--r--main.go112
-rw-r--r--modules/cpu.go159
-rw-r--r--modules/numbers.go150
5 files changed, 410 insertions, 18 deletions
diff --git a/README.md b/README.md
index f6f57a7..c761336 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/main.go b/main.go
index d73e056..213aed6 100644
--- a/main.go
+++ b/main.go
@@ -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
+}