Skip to content

Commit 4468e79

Browse files
authored
Merge pull request #371 from hicwic/feature/plot-graph
2 parents 14a0db9 + 8153eb8 commit 4468e79

File tree

9 files changed

+589
-2
lines changed

9 files changed

+589
-2
lines changed
Binary file not shown.

library/lcd/lcd_comm.py

+100
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
from enum import IntEnum
2828
from typing import Tuple
2929

30+
try:
31+
from itertools import izip
32+
except ImportError: # Python 3
33+
izip = zip
34+
3035
import serial
3136
from PIL import Image, ImageDraw, ImageFont
3237

@@ -321,6 +326,101 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value:
321326

322327
self.DisplayPILImage(bar_image, x, y)
323328

329+
def DisplayPlotGraph(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100,
330+
autoscale: bool = False,
331+
values: list[float] = [],
332+
line_color: Tuple[int, int, int] = (0, 0, 0),
333+
graph_axis: bool = True,
334+
background_color: Tuple[int, int, int] = (255, 255, 255),
335+
background_image: str = None):
336+
# Generate a plot graph and display it
337+
# Provide the background image path to display plot graph with transparent background
338+
339+
if isinstance(line_color, str):
340+
line_color = tuple(map(int, line_color.split(', ')))
341+
342+
if isinstance(background_color, str):
343+
background_color = tuple(map(int, background_color.split(', ')))
344+
345+
assert x <= self.get_width(), 'Progress bar X coordinate must be <= display width'
346+
assert y <= self.get_height(), 'Progress bar Y coordinate must be <= display height'
347+
assert x + width <= self.get_width(), 'Progress bar width exceeds display width'
348+
assert y + height <= self.get_height(), 'Progress bar height exceeds display height'
349+
350+
351+
if background_image is None:
352+
# A bitmap is created with solid background
353+
graph_image = Image.new('RGB', (width, height), background_color)
354+
else:
355+
# A bitmap is created from provided background image
356+
graph_image = self.open_image(background_image)
357+
358+
# Crop bitmap to keep only the plot graph background
359+
graph_image = graph_image.crop(box=(x, y, x + width, y + height))
360+
361+
#if autoscale is enabled, define new min/max value to "zoom" the graph
362+
if autoscale:
363+
trueMin = max_value
364+
trueMax = min_value
365+
for value in values:
366+
if value >= 0:
367+
if trueMin > value:
368+
trueMin = value
369+
if trueMax < value:
370+
trueMax = value
371+
372+
if trueMin != max_value and trueMax != min_value:
373+
min_value = max (trueMin-5, min_value)
374+
max_value = min (trueMax+5, max_value)
375+
376+
step = width / len(values)
377+
#pre compute yScale multiplier value
378+
yScale = height / (max_value - min_value)
379+
380+
plotsX = []
381+
plotsY = []
382+
count = 0
383+
for value in values:
384+
if value >= 0:
385+
# Don't let the set value exceed our min or max value, this is bad :)
386+
if value < min_value:
387+
value = min_value
388+
elif max_value < value:
389+
value = max_value
390+
391+
assert min_value <= value <= max_value, 'Plot point value shall be between min and max'
392+
393+
plotsX.append(count * step)
394+
plotsY.append(height - (value - min_value) * yScale)
395+
396+
count += 1
397+
398+
399+
# Draw plot graph
400+
draw = ImageDraw.Draw(graph_image)
401+
draw.line(list(izip(plotsX, plotsY)), fill=line_color, width=2)
402+
403+
if graph_axis:
404+
# Draw axis
405+
draw.line([0, height - 1, width - 1, height - 1], fill=line_color)
406+
draw.line([0, 0, 0, height - 1], fill=line_color)
407+
408+
# Draw Legend
409+
draw.line([0, 0, 1, 0], fill=line_color)
410+
text = f"{int(max_value)}"
411+
font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10)
412+
left, top, right, bottom = font.getbbox(text)
413+
draw.text((2, 0 - top), text,
414+
font=font, fill=line_color)
415+
416+
text = f"{int(min_value)}"
417+
font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10)
418+
left, top, right, bottom = font.getbbox(text)
419+
draw.text((width - 1 - right, height - 2 - bottom), text,
420+
font=font, fill=line_color)
421+
422+
self.DisplayPILImage(graph_image, x, y)
423+
324424
def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int,
325425
min_value: int = 0,
326426
max_value: int = 100,

library/sensors/sensors_custom.py

+154
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,86 @@
2323
import platform
2424
from abc import ABC, abstractmethod
2525

26+
import ctypes
27+
import math
28+
import os
29+
import sys
30+
from statistics import mean
31+
from typing import Tuple
32+
33+
import clr # Clr is from pythonnet package. Do not install clr package
34+
import psutil
35+
from win32api import *
36+
37+
import library.sensors.sensors as sensors
38+
from library.log import logger
39+
40+
# Import LibreHardwareMonitor dll to Python
41+
lhm_dll = os.getcwd() + '\\external\\LibreHardwareMonitor\\LibreHardwareMonitorLib.dll'
42+
# noinspection PyUnresolvedReferences
43+
clr.AddReference(lhm_dll)
44+
# noinspection PyUnresolvedReferences
45+
clr.AddReference(os.getcwd() + '\\external\\LibreHardwareMonitor\\HidSharp.dll')
46+
# noinspection PyUnresolvedReferences
47+
from LibreHardwareMonitor import Hardware
48+
49+
# Import RTSSSharedMemoryNET dll to Python
50+
clr.AddReference(os.getcwd() + '\\external\\RTSSSharedMemoryNET\\RTSSSharedMemoryNET.dll')
51+
from RTSSSharedMemoryNET import OSD
52+
53+
54+
File_information = GetFileVersionInfo(lhm_dll, "\\")
55+
56+
ms_file_version = File_information['FileVersionMS']
57+
ls_file_version = File_information['FileVersionLS']
58+
59+
logger.debug("Found LibreHardwareMonitorLib %s" % ".".join([str(HIWORD(ms_file_version)), str(LOWORD(ms_file_version)),
60+
str(HIWORD(ls_file_version)),
61+
str(LOWORD(ls_file_version))]))
62+
63+
if ctypes.windll.shell32.IsUserAnAdmin() == 0:
64+
logger.error(
65+
"Program is not running as administrator. Please run with admin rights or choose another HW_SENSORS option in "
66+
"config.yaml")
67+
try:
68+
sys.exit(0)
69+
except:
70+
os._exit(0)
71+
72+
handle = Hardware.Computer()
73+
handle.IsCpuEnabled = True
74+
handle.IsGpuEnabled = True
75+
handle.IsMemoryEnabled = True
76+
handle.IsMotherboardEnabled = True
77+
handle.IsControllerEnabled = False
78+
handle.IsNetworkEnabled = True
79+
handle.IsStorageEnabled = True
80+
handle.Open()
81+
for hardware in handle.Hardware:
82+
if hardware.HardwareType == Hardware.HardwareType.Cpu:
83+
logger.info("Found CPU: %s" % hardware.Name)
84+
elif hardware.HardwareType == Hardware.HardwareType.Memory:
85+
logger.info("Found Memory: %s" % hardware.Name)
86+
elif hardware.HardwareType == Hardware.HardwareType.GpuNvidia:
87+
logger.info("Found Nvidia GPU: %s" % hardware.Name)
88+
elif hardware.HardwareType == Hardware.HardwareType.GpuAmd:
89+
logger.info("Found AMD GPU: %s" % hardware.Name)
90+
elif hardware.HardwareType == Hardware.HardwareType.GpuIntel:
91+
logger.info("Found Intel GPU: %s" % hardware.Name)
92+
elif hardware.HardwareType == Hardware.HardwareType.Storage:
93+
logger.info("Found Storage: %s" % hardware.Name)
94+
elif hardware.HardwareType == Hardware.HardwareType.Network:
95+
logger.info("Found Network interface: %s" % hardware.Name)
96+
97+
98+
def get_hw_and_update(hwtype: Hardware.HardwareType, name: str = None) -> Hardware.Hardware:
99+
for hardware in handle.Hardware:
100+
if hardware.HardwareType == hwtype:
101+
if (name and hardware.Name == name) or not name:
102+
hardware.Update()
103+
return hardware
104+
return None
105+
26106

27107
# Custom data classes must be implemented in this file, inherit the CustomDataSource and implement its 2 methods
28108
class CustomDataSource(ABC):
@@ -40,6 +120,11 @@ def as_string(self) -> str:
40120
# If this function is empty, the numeric value will be used as string without formatting
41121
pass
42122

123+
@abstractmethod
124+
def as_histo(self) -> list[float]:
125+
# List of numeric values will be used for plot graph
126+
# If there is no histo values, keep this function empty
127+
pass
43128

44129
# Example for a custom data class that has numeric and text values
45130
class ExampleCustomNumericData(CustomDataSource):
@@ -61,6 +146,9 @@ def as_string(self) -> str:
61146
# --> return f'{self.as_numeric():>4}%'
62147
# Otherwise, part of the previous value can stay displayed ("ghosting") after a refresh
63148

149+
def as_histo(self) -> list[float]:
150+
pass
151+
64152

65153
# Example for a custom data class that only has text values
66154
class ExampleCustomTextOnlyData(CustomDataSource):
@@ -71,3 +159,69 @@ def as_numeric(self) -> float:
71159
def as_string(self) -> str:
72160
# If a custom data class only has text values, it won't be possible to display graph or radial bars
73161
return "Python version: " + platform.python_version()
162+
163+
def as_histo(self) -> list[float]:
164+
pass
165+
166+
167+
class GpuNvidiaFanPercent(CustomDataSource):
168+
def as_numeric(self) -> float:
169+
gpu = get_hw_and_update(Hardware.HardwareType.GpuNvidia)
170+
for sensor in gpu.Sensors:
171+
if sensor.SensorType == Hardware.SensorType.Control:
172+
return float(sensor.Value)
173+
#return float(50)
174+
175+
logger.error("GPU Nvidia fan percent cannot be read")
176+
return math.nan
177+
178+
def as_string(self) -> str:
179+
return f'{int(self.as_numeric())}%'
180+
181+
def as_histo(self) -> list[float]:
182+
pass
183+
184+
class CpuFanPercent(CustomDataSource):
185+
def as_numeric(self) -> float:
186+
mb = get_hw_and_update(Hardware.HardwareType.Motherboard)
187+
for sh in mb.SubHardware:
188+
sh.Update()
189+
for sensor in sh.Sensors:
190+
if sensor.SensorType == Hardware.SensorType.Control and "#2" in str(sensor.Name):
191+
return float(sensor.Value)
192+
193+
logger.error("CPU fan percent cannot be read")
194+
return math.nan
195+
196+
def as_string(self) -> str:
197+
return f'{int(self.as_numeric())}%'
198+
199+
def as_histo(self) -> list[float]:
200+
pass
201+
202+
class RTSSFps(CustomDataSource):
203+
204+
histo = [-1] * 100
205+
206+
def as_numeric(self) -> float:
207+
appEntries = OSD.GetAppEntries()
208+
for app in appEntries:
209+
if app.InstantaneousFrames > 0:
210+
return float(app.InstantaneousFrames)
211+
212+
return float(0)
213+
214+
def as_string(self) -> str:
215+
return f'{int(self.as_numeric())}'
216+
217+
def as_histo(self) -> list[float]:
218+
appEntries = OSD.GetAppEntries()
219+
for app in appEntries:
220+
if app.InstantaneousFrames > 0:
221+
RTSSFps.histo.append(app.InstantaneousFrames)
222+
RTSSFps.histo.pop(0)
223+
return RTSSFps.histo
224+
225+
return RTSSFps.histo
226+
227+

0 commit comments

Comments
 (0)