Skip to content

Feature - plot graph #371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
100 changes: 100 additions & 0 deletions library/lcd/lcd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
from enum import IntEnum
from typing import Tuple

try:
from itertools import izip
except ImportError: # Python 3
izip = zip

import serial
from PIL import Image, ImageDraw, ImageFont

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

self.DisplayPILImage(bar_image, x, y)

def DisplayPlotGraph(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100,
autoscale: bool = False,
values: list[float] = [],
line_color: Tuple[int, int, int] = (0, 0, 0),
graph_axis: bool = True,
background_color: Tuple[int, int, int] = (255, 255, 255),
background_image: str = None):
# Generate a plot graph and display it
# Provide the background image path to display plot graph with transparent background

if isinstance(line_color, str):
line_color = tuple(map(int, line_color.split(', ')))

if isinstance(background_color, str):
background_color = tuple(map(int, background_color.split(', ')))

assert x <= self.get_width(), 'Progress bar X coordinate must be <= display width'
assert y <= self.get_height(), 'Progress bar Y coordinate must be <= display height'
assert x + width <= self.get_width(), 'Progress bar width exceeds display width'
assert y + height <= self.get_height(), 'Progress bar height exceeds display height'


if background_image is None:
# A bitmap is created with solid background
graph_image = Image.new('RGB', (width, height), background_color)
else:
# A bitmap is created from provided background image
graph_image = self.open_image(background_image)

# Crop bitmap to keep only the plot graph background
graph_image = graph_image.crop(box=(x, y, x + width, y + height))

#if autoscale is enabled, define new min/max value to "zoom" the graph
if autoscale:
trueMin = max_value
trueMax = min_value
for value in values:
if value >= 0:
if trueMin > value:
trueMin = value
if trueMax < value:
trueMax = value

if trueMin != max_value and trueMax != min_value:
min_value = max (trueMin-5, min_value)
max_value = min (trueMax+5, max_value)

step = width / len(values)
#pre compute yScale multiplier value
yScale = height / (max_value - min_value)

plotsX = []
plotsY = []
count = 0
for value in values:
if value >= 0:
# Don't let the set value exceed our min or max value, this is bad :)
if value < min_value:
value = min_value
elif max_value < value:
value = max_value

assert min_value <= value <= max_value, 'Plot point value shall be between min and max'

plotsX.append(count * step)
plotsY.append(height - (value - min_value) * yScale)

count += 1


# Draw plot graph
draw = ImageDraw.Draw(graph_image)
draw.line(list(izip(plotsX, plotsY)), fill=line_color, width=2)

if graph_axis:
# Draw axis
draw.line([0, height - 1, width - 1, height - 1], fill=line_color)
draw.line([0, 0, 0, height - 1], fill=line_color)

# Draw Legend
draw.line([0, 0, 1, 0], fill=line_color)
text = f"{int(max_value)}"
font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10)
left, top, right, bottom = font.getbbox(text)
draw.text((2, 0 - top), text,
font=font, fill=line_color)

text = f"{int(min_value)}"
font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10)
left, top, right, bottom = font.getbbox(text)
draw.text((width - 1 - right, height - 2 - bottom), text,
font=font, fill=line_color)

self.DisplayPILImage(graph_image, x, y)

def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int,
min_value: int = 0,
max_value: int = 100,
Expand Down
154 changes: 154 additions & 0 deletions library/sensors/sensors_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,86 @@
import platform
from abc import ABC, abstractmethod

import ctypes
import math
import os
import sys
from statistics import mean
from typing import Tuple

import clr # Clr is from pythonnet package. Do not install clr package
import psutil
from win32api import *

import library.sensors.sensors as sensors
from library.log import logger

# Import LibreHardwareMonitor dll to Python
lhm_dll = os.getcwd() + '\\external\\LibreHardwareMonitor\\LibreHardwareMonitorLib.dll'
# noinspection PyUnresolvedReferences
clr.AddReference(lhm_dll)
# noinspection PyUnresolvedReferences
clr.AddReference(os.getcwd() + '\\external\\LibreHardwareMonitor\\HidSharp.dll')
# noinspection PyUnresolvedReferences
from LibreHardwareMonitor import Hardware

# Import RTSSSharedMemoryNET dll to Python
clr.AddReference(os.getcwd() + '\\external\\RTSSSharedMemoryNET\\RTSSSharedMemoryNET.dll')
from RTSSSharedMemoryNET import OSD


File_information = GetFileVersionInfo(lhm_dll, "\\")

ms_file_version = File_information['FileVersionMS']
ls_file_version = File_information['FileVersionLS']

logger.debug("Found LibreHardwareMonitorLib %s" % ".".join([str(HIWORD(ms_file_version)), str(LOWORD(ms_file_version)),
str(HIWORD(ls_file_version)),
str(LOWORD(ls_file_version))]))

if ctypes.windll.shell32.IsUserAnAdmin() == 0:
logger.error(
"Program is not running as administrator. Please run with admin rights or choose another HW_SENSORS option in "
"config.yaml")
try:
sys.exit(0)
except:
os._exit(0)

handle = Hardware.Computer()
handle.IsCpuEnabled = True
handle.IsGpuEnabled = True
handle.IsMemoryEnabled = True
handle.IsMotherboardEnabled = True
handle.IsControllerEnabled = False
handle.IsNetworkEnabled = True
handle.IsStorageEnabled = True
handle.Open()
for hardware in handle.Hardware:
if hardware.HardwareType == Hardware.HardwareType.Cpu:
logger.info("Found CPU: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.Memory:
logger.info("Found Memory: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.GpuNvidia:
logger.info("Found Nvidia GPU: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.GpuAmd:
logger.info("Found AMD GPU: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.GpuIntel:
logger.info("Found Intel GPU: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.Storage:
logger.info("Found Storage: %s" % hardware.Name)
elif hardware.HardwareType == Hardware.HardwareType.Network:
logger.info("Found Network interface: %s" % hardware.Name)


def get_hw_and_update(hwtype: Hardware.HardwareType, name: str = None) -> Hardware.Hardware:
for hardware in handle.Hardware:
if hardware.HardwareType == hwtype:
if (name and hardware.Name == name) or not name:
hardware.Update()
return hardware
return None


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

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

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

def as_histo(self) -> list[float]:
pass


# Example for a custom data class that only has text values
class ExampleCustomTextOnlyData(CustomDataSource):
Expand All @@ -71,3 +159,69 @@ def as_numeric(self) -> float:
def as_string(self) -> str:
# If a custom data class only has text values, it won't be possible to display graph or radial bars
return "Python version: " + platform.python_version()

def as_histo(self) -> list[float]:
pass


class GpuNvidiaFanPercent(CustomDataSource):
def as_numeric(self) -> float:
gpu = get_hw_and_update(Hardware.HardwareType.GpuNvidia)
for sensor in gpu.Sensors:
if sensor.SensorType == Hardware.SensorType.Control:
return float(sensor.Value)
#return float(50)

logger.error("GPU Nvidia fan percent cannot be read")
return math.nan

def as_string(self) -> str:
return f'{int(self.as_numeric())}%'

def as_histo(self) -> list[float]:
pass

class CpuFanPercent(CustomDataSource):
def as_numeric(self) -> float:
mb = get_hw_and_update(Hardware.HardwareType.Motherboard)
for sh in mb.SubHardware:
sh.Update()
for sensor in sh.Sensors:
if sensor.SensorType == Hardware.SensorType.Control and "#2" in str(sensor.Name):
return float(sensor.Value)

logger.error("CPU fan percent cannot be read")
return math.nan

def as_string(self) -> str:
return f'{int(self.as_numeric())}%'

def as_histo(self) -> list[float]:
pass

class RTSSFps(CustomDataSource):

histo = [-1] * 100

def as_numeric(self) -> float:
appEntries = OSD.GetAppEntries()
for app in appEntries:
if app.InstantaneousFrames > 0:
return float(app.InstantaneousFrames)

return float(0)

def as_string(self) -> str:
return f'{int(self.as_numeric())}'

def as_histo(self) -> list[float]:
appEntries = OSD.GetAppEntries()
for app in appEntries:
if app.InstantaneousFrames > 0:
RTSSFps.histo.append(app.InstantaneousFrames)
RTSSFps.histo.pop(0)
return RTSSFps.histo

return RTSSFps.histo


Loading