-
-
Notifications
You must be signed in to change notification settings - Fork 277
-
I know a lot use the themes. I went with the Python script method of interacting, and leveraged ChatGPT some to help me get it working just right for me. Basically, I want a clean, simple interface for monitoring my RAM, GPU, and CPU usage. In my case, I have two GPUs (my 4090 could only drive 2x120hz monitors, so I have a second one driving other 4K monitors. So, I created something to help me watch how my games run. I also run AI inference on my computer, sometimes, and I have to watch my vRAM closely when I do.
If you want to tweak this, you can do it pretty easily. Since I have two GPUs, I had to use part off the name to identify which one I wanted, like 4090 or 1030. If you have different cards, you can just modify that part.
Also, the setup of the device (5 inch Turing in my case) is in the beginning of the script. You might need to tweak it some to get it working.
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 2
Replies: 19 comments 10 replies
-
stats5.py
import time import logging import os import sys import clr import ctypes import re from datetime import datetime from library.lcd.lcd_comm import Orientation from library.lcd.lcd_comm_rev_c import LcdCommRevC # For 5′′ monitor logging.basicConfig( level=logging.DEBUG, format='%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S', force=True ) # ====== Display Settings ====== # In landscape mode, physical resolution is 800x480. DISPLAY_WIDTH = 480 DISPLAY_HEIGHT = 800 FONT_PATH = "res/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf" FONT_SIZE = 20 BG_COLOR = (0, 0, 0) # Solid black background CPU_COLOR = (255, 0, 0) # Red for CPU (and RAM) GPU_COLOR = (0, 128, 0) # Dark green for GPU stats TEXT_COLOR = (255, 255, 255) # White text UPDATE_INTERVAL = .5 # seconds BACKGROUND_IMG = "background.png" # Bar vertical offset (in pixels) to align bars with text. BAR_OFFSET = 5 # ====== Setup LibreHardwareMonitor ====== clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'LibreHardwareMonitorLib.dll')) clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'HidSharp.dll')) from LibreHardwareMonitor import Hardware handle = Hardware.Computer() handle.IsCpuEnabled = True handle.IsGpuEnabled = True handle.IsMemoryEnabled = True handle.IsMotherboardEnabled = True handle.IsControllerEnabled = True handle.IsNetworkEnabled = True handle.IsStorageEnabled = True handle.IsPsuEnabled = True handle.Open() # ====== Sensor Query Functions ====== def get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name=None): for hw in hw_list: if hw.HardwareType == hw_type: if hw_name and (hw_name.lower() not in hw.Name.lower()): continue hw.Update() for sensor in hw.Sensors: if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name: return sensor.Value for subhw in hw.SubHardware: subhw.Update() for sensor in subhw.Sensors: if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name: return sensor.Value return None def get_hardware_name(hw_list, hw_type, skip_first=False): skipped = False for hw in hw_list: if hw.HardwareType == hw_type: if skip_first and not skipped: skipped = True continue return hw.Name return None def truncate_first_word(name_str): parts = name_str.split() if len(parts) > 1: return " ".join(parts[1:]) return name_str # Store CPU name globally. def initialize_hardware_names(): global CPU_NAME hw_list = handle.Hardware cpu_full_name = get_hardware_name(hw_list, Hardware.HardwareType.Cpu) or "Unknown CPU" CPU_NAME = truncate_first_word(cpu_full_name) # Get GPU stats for a given filter (e.g. "4090" or "1030") def get_gpu_stats(hw_list, filter_str): stats = {} for hw in hw_list: if hw.HardwareType == Hardware.HardwareType.GpuNvidia and filter_str.lower() in hw.Name.lower(): stats["name"] = hw.Name stats["util"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Load", "GPU Core", hw_name=filter_str) or 0.0 stats["temp"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Temperature", "GPU Core", hw_name=filter_str) or 0.0 stats["clock"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Clock", "GPU Core", hw_name=filter_str) or 0.0 stats["mem_used"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Used", hw_name=filter_str) or 0.0 stats["mem_total"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Total", hw_name=filter_str) or 1.0 stats["mem_percent"] = (stats["mem_used"] / stats["mem_total"]) * 100 return stats return None # Draw a GPU section starting at (x,y) for a given GPU's stats. def draw_gpu_section(lcd, x, y, stats): # Fix GPU progress bar's right edge at x=780. bar_width = 300 bar_x = 780 - bar_width # Header: display GPU name. lcd.DisplayText(stats["name"], x=x, y=y, font=FONT_PATH, font_size=20, font_color=GPU_COLOR, background_color=BG_COLOR) y += 20 # Utilization text: pad to 3 characters. util_str = f"Util: {int(stats['util']):3d}%" lcd.DisplayText(util_str, x=x, y=y, font=FONT_PATH, font_size=16, font_color=GPU_COLOR, background_color=BG_COLOR) y += 20 # Utilization progress bar. lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20, min_value=0, max_value=100, value=int(stats["util"]), bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR) y += 30 # Temperature and clock on one line. temp_freq_str = f"Temp: {int(stats['temp']):2d}°C Freq: {int(stats['clock']):4d}MHz" lcd.DisplayText(temp_freq_str, x=x, y=y, font=FONT_PATH, font_size=16, font_color=GPU_COLOR, background_color=BG_COLOR) y += 20 # Memory usage text: pad to 5 digits. mem_str = f"Mem: {int(stats['mem_used']):5d}MB/{int(stats['mem_total']):5d}MB" lcd.DisplayText(mem_str, x=x, y=y, font=FONT_PATH, font_size=16, font_color=GPU_COLOR, background_color=BG_COLOR) y += 20 # Memory usage progress bar. lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20, min_value=0, max_value=100, value=int(stats["mem_percent"]), bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR) y += 30 return y def get_sorted_core_loads(hw_list): core_loads = [] for hw in hw_list: if hw.HardwareType == Hardware.HardwareType.Cpu: hw.Update() for sensor in hw.Sensors: if str(sensor.SensorType) == "Load" and "Core" in sensor.Name: m = re.search(r'#(\d+)', sensor.Name) core_index = int(m.group(1)) if m else 99 core_loads.append((core_index, sensor.Name, sensor.Value)) for subhw in hw.SubHardware: subhw.Update() for sensor in subhw.Sensors: if str(sensor.SensorType) == "Load" and "Core" in sensor.Name: m = re.search(r'#(\d+)', sensor.Name) core_index = int(m.group(1)) if m else 99 core_loads.append((core_index, sensor.Name, sensor.Value)) core_loads.sort(key=lambda x: x[0]) return core_loads def initialize_display(): lcd = LcdCommRevC( com_port="AUTO", display_width=DISPLAY_WIDTH, display_height=DISPLAY_HEIGHT ) lcd.Reset() lcd.InitializeComm() lcd.SetBrightness(50) lcd.SetOrientation(Orientation.LANDSCAPE) logging.debug("Displaying initial background...") lcd.DisplayBitmap(BACKGROUND_IMG) logging.debug("Initial background displayed.") return lcd def draw_static_text(lcd): # Left side: CPU header and CPU name. lcd.DisplayText("CPU Stats", x=10, y=10, font=FONT_PATH, font_size=22, font_color=TEXT_COLOR, background_color=BG_COLOR) lcd.DisplayText(CPU_NAME, x=10, y=40, font=FONT_PATH, font_size=20, font_color=CPU_COLOR, background_color=BG_COLOR) # Right side: GPU Stats header. lcd.DisplayText("GPU Stats", x=420, y=10, font=FONT_PATH, font_size=22, font_color=TEXT_COLOR, background_color=BG_COLOR) def draw_dynamic_stats(lcd): hw_list = handle.Hardware # --- CPU Stats (Left Side) --- cpu_load = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Load", "CPU Total") or 0.0 cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Core (Tctl/Tdie)") if cpu_temp is None: cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Package") or 0.0 cpu_freq = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Clock", "Core #1") or 0.0 y_cpu = 70 # Total percentage: pad to 3 digits. lcd.DisplayText(f"Total: {int(cpu_load):3d}%", x=10, y=y_cpu, font=FONT_PATH, font_size=20, font_color=CPU_COLOR, background_color=BG_COLOR) cpu_bar_width = 170 cpu_bar_x = 320 - cpu_bar_width # = 180. lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=20, min_value=0, max_value=100, value=int(cpu_load), bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR) y_cpu += 30 lcd.DisplayText(f"Temp: {int(cpu_temp):2d}°C Freq: {int(cpu_freq):4d}MHz", x=10, y=y_cpu, font=FONT_PATH, font_size=20, font_color=CPU_COLOR, background_color=BG_COLOR) y_cpu += 30 core_loads = get_sorted_core_loads(hw_list) for core_index, sensor_name, load in core_loads: core_label = f"Core {core_index}:" if core_index != 99 else "Core (top):" lcd.DisplayText(core_label, x=10, y=y_cpu, font=FONT_PATH, font_size=18, font_color=CPU_COLOR, background_color=BG_COLOR) lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=15, min_value=0, max_value=100, value=int(load), bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR) lcd.DisplayText(f"{int(load):3d}%", x=330, y=y_cpu, font=FONT_PATH, font_size=18, font_color=CPU_COLOR, background_color=BG_COLOR) y_cpu += 20 # --- RAM Stats (Left Side, below CPU) --- mem_used = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Used") or 0.0 mem_avail = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Available") or 0.0 mem_total = mem_used + mem_avail mem_pct = (mem_used / mem_total) * 100 if mem_total > 0 else 0 y_ram = y_cpu + 20 lcd.DisplayText("RAM Stats", x=10, y=y_ram, font=FONT_PATH, font_size=22, font_color=TEXT_COLOR, background_color=BG_COLOR) y_ram += 30 # Convert values from GB to MB. mem_used_mb = int(round(mem_used * 1024)) mem_total_mb = int(round(mem_total * 1024)) # System RAM values: pad to 6 characters. lcd.DisplayText(f"{mem_used_mb:6d}MB / {mem_total_mb:6d}MB", x=10, y=y_ram, font=FONT_PATH, font_size=20, font_color=CPU_COLOR, background_color=BG_COLOR) ram_bar_width = 140 ram_bar_x = 420 - ram_bar_width # = 280. lcd.DisplayProgressBar(x=ram_bar_x, y=y_ram + BAR_OFFSET, width=ram_bar_width, height=20, min_value=0, max_value=100, value=int(mem_pct), bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR) # --- GPU Stats (Right Side) --- gpu_x = 420 # left margin for GPU section. gpu_stats_4090 = get_gpu_stats(hw_list, "4090") if gpu_stats_4090 is not None: y_gpu1 = 40 y_gpu1 = draw_gpu_section(lcd, gpu_x, y_gpu1, gpu_stats_4090) gpu_stats_1030 = get_gpu_stats(hw_list, "1030") if gpu_stats_1030 is not None: y_gpu2 = 180 # vertical gap. y_gpu2 = draw_gpu_section(lcd, gpu_x, y_gpu2, gpu_stats_1030) # --- Uptime and Clock (Centered at Bottom) --- now = datetime.now() clock_str = now.strftime("%a %m/%d/%Y %I:%M:%S %p") uptime_str = get_uptime_str() lcd.DisplayText(uptime_str, x=400, y=440, font=FONT_PATH, font_size=20, font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt") lcd.DisplayText(clock_str, x=400, y=460, font=FONT_PATH, font_size=20, font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt") def get_uptime_str(): uptime_ms = ctypes.windll.kernel32.GetTickCount64() uptime_sec = uptime_ms // 1000 days = uptime_sec // 86400 hours = (uptime_sec % 86400) // 3600 minutes = (uptime_sec % 3600) // 60 seconds = uptime_sec % 60 return f"Uptime: {days}d {hours:02d}:{minutes:02d}:{seconds:02d}" def main(): initialize_hardware_names() lcd = initialize_display() draw_static_text(lcd) while True: draw_dynamic_stats(lcd) time.sleep(UPDATE_INTERVAL) if __name__ == "__main__": try: main() finally: handle.Close()
Beta Was this translation helpful? Give feedback.
All reactions
-
Oh, you will need this file in the root of your directory as well... it is just a black image called background.png
Beta Was this translation helpful? Give feedback.
All reactions
-
Thank you @majormer nice to see something a little different than themes with System Monitor!
When I first started this project, there was no System Monitor & themes but only the basic library to display graphical elements on screen. I'm glad to see people are still using this today!
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
Hey @majormer This looks really awesome, and it's almost exactly what I was hoping for. I'm still pretty new to coding, so some of this is a bit confusing for me. I can probably use ChatGPT to tweak things a little if needed, but I was wondering if you could help me figure out how to use a custom Python theme instead of the regular .yaml files. I've already downloaded the repository and was able to run the configure.yaml file and load themes from there without any problems. What I'm stuck on is how to get my screen to load up using one of these custom .py themes instead of the .yaml ones. Any guidance you could offer would be super helpful!
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Ahhhhhh
That makes so much sense
Thank you do much man.
Really appreciate the reply.
Beta Was this translation helpful? Give feedback.
All reactions
-
I modified your script for my own 3.5" screen and added code to detect AMD GPUs:
import time
import logging
import os
import sys
import clr
import ctypes
import re
from datetime import datetime
from library.lcd.lcd_comm import Orientation
from library.lcd.lcd_comm_rev_a import LcdCommRevA # For 3.5′′ monitor
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
force=True
)
# ====== Display Settings ======
# In landscape mode, physical resolution is 800x480.
DISPLAY_WIDTH = 320
DISPLAY_HEIGHT = 480
FONT_PATH = "res/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf"
FONT_SIZE = 5
BG_COLOR = (0, 0, 0) # Solid black background
CPU_COLOR = (255, 0, 0) # Red for CPU (and RAM)
GPU_COLOR = (0, 128, 0) # Dark green for GPU stats
TEXT_COLOR = (255, 255, 255) # White text
UPDATE_INTERVAL = .5 # seconds
BACKGROUND_IMG = "background.png"
# Bar vertical offset (in pixels) to align bars with text.
BAR_OFFSET = 1
# ====== Setup LibreHardwareMonitor ======
clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'LibreHardwareMonitorLib.dll'))
clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'HidSharp.dll'))
from LibreHardwareMonitor import Hardware
handle = Hardware.Computer()
handle.IsCpuEnabled = True
handle.IsGpuEnabled = True
handle.IsMemoryEnabled = True
handle.IsMotherboardEnabled = True
handle.IsControllerEnabled = True
handle.IsNetworkEnabled = True
handle.IsStorageEnabled = True
handle.IsPsuEnabled = True
handle.Open()
# ====== Sensor Query Functions ======
def get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name=None):
for hw in hw_list:
if hw.HardwareType == hw_type:
if hw_name and (hw_name.lower() not in hw.Name.lower()):
continue
hw.Update()
for sensor in hw.Sensors:
if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name:
return sensor.Value
for subhw in hw.SubHardware:
subhw.Update()
for sensor in subhw.Sensors:
if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name:
return sensor.Value
return None
def get_hardware_name(hw_list, hw_type, skip_first=False):
skipped = False
for hw in hw_list:
if hw.HardwareType == hw_type:
if skip_first and not skipped:
skipped = True
continue
return hw.Name
return None
def truncate_first_word(name_str):
parts = name_str.split()
if len(parts) > 1:
return " ".join(parts[1:])
return name_str
# Store CPU name globally.
def initialize_hardware_names():
global CPU_NAME
hw_list = handle.Hardware
cpu_full_name = get_hardware_name(hw_list, Hardware.HardwareType.Cpu) or "Unknown CPU"
CPU_NAME = truncate_first_word(cpu_full_name)
# Get GPU stats for a given filter (e.g. "amd" or "Nvidia")
def get_gpu_stats(hw_list, filter_str):
stats = {}
for hw in hw_list:
if hw.HardwareType == Hardware.HardwareType.GpuNvidia and filter_str.lower() in hw.Name.lower():
stats["name"] = hw.Name
stats["util"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Load", "GPU Core", hw_name=filter_str) or 0.0
stats["temp"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Temperature", "GPU Core", hw_name=filter_str) or 0.0
stats["clock"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Clock", "GPU Core", hw_name=filter_str) or 0.0
stats["mem_used"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Used", hw_name=filter_str) or 0.0
stats["mem_total"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Total", hw_name=filter_str) or 1.0
stats["mem_percent"] = (stats["mem_used"] / stats["mem_total"]) * 100
return stats
elif hw.HardwareType == Hardware.HardwareType.GpuAmd and filter_str.lower() in hw.Name.lower():
stats["name"] = hw.Name
stats["util"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuAmd, "Load", "GPU Core", hw_name=filter_str) or 0.0
stats["temp"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuAmd, "Temperature", "GPU Core", hw_name=filter_str) or 0.0
stats["clock"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuAmd, "Clock", "GPU Core", hw_name=filter_str) or 0.0
stats["mem_used"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuAmd, "SmallData", "GPU Memory Used", hw_name=filter_str) or 0.0
stats["mem_total"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuAmd, "SmallData", "GPU Memory Total", hw_name=filter_str) or 1.0
stats["mem_percent"] = (stats["mem_used"] / stats["mem_total"]) * 100
return stats
return None
# Draw a GPU section starting at (x,y) for a given GPU's stats.
def draw_gpu_section(lcd, x, y, stats):
# Fix GPU progress bar's right edge at x=780.
bar_width = 150
bar_x = 360 - bar_width
# Header: display GPU name.
lcd.DisplayText(stats["name"], x=x, y=y, font=FONT_PATH, font_size=12,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Utilization text: pad to 3 characters.
util_str = f"Util: {int(stats['util']):3d}%"
lcd.DisplayText(util_str, x=x, y=y, font=FONT_PATH, font_size=10,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Utilization progress bar.
lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20,
min_value=0, max_value=100, value=int(stats["util"]),
bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y += 20
# Temperature and clock on one line.
temp_freq_str = f"Temp: {int(stats['temp']):2d}°C Freq: {int(stats['clock']):4d}MHz"
lcd.DisplayText(temp_freq_str, x=x, y=y, font=FONT_PATH, font_size=10,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Memory usage text: pad to 5 digits.
mem_str = f"Mem: {int(stats['mem_used']):5d}MB/{int(stats['mem_total']):5d}MB"
lcd.DisplayText(mem_str, x=x, y=y, font=FONT_PATH, font_size=10,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Memory usage progress bar.
lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20,
min_value=0, max_value=100, value=int(stats["mem_percent"]),
bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y += 10
return y
def get_sorted_core_loads(hw_list):
core_loads = []
for hw in hw_list:
if hw.HardwareType == Hardware.HardwareType.Cpu:
hw.Update()
for sensor in hw.Sensors:
if str(sensor.SensorType) == "Load" and "Core" in sensor.Name:
m = re.search(r'#(\d+)', sensor.Name)
core_index = int(m.group(1)) if m else 99
core_loads.append((core_index, sensor.Name, sensor.Value))
for subhw in hw.SubHardware:
subhw.Update()
for sensor in subhw.Sensors:
if str(sensor.SensorType) == "Load" and "Core" in sensor.Name:
m = re.search(r'#(\d+)', sensor.Name)
core_index = int(m.group(1)) if m else 99
core_loads.append((core_index, sensor.Name, sensor.Value))
core_loads.sort(key=lambda x: x[0])
return core_loads
def initialize_display():
lcd = LcdCommRevA(
com_port="AUTO",
display_width=DISPLAY_WIDTH,
display_height=DISPLAY_HEIGHT
)
lcd.Reset()
lcd.InitializeComm()
lcd.SetBrightness(50)
lcd.SetOrientation(Orientation.LANDSCAPE)
logging.debug("Displaying initial background...")
lcd.DisplayBitmap(BACKGROUND_IMG)
logging.debug("Initial background displayed.")
return lcd
def draw_static_text(lcd):
# Left side: CPU header and CPU name.
lcd.DisplayText("CPU Stats", x=1, y=1, font=FONT_PATH, font_size=12,
font_color=TEXT_COLOR, background_color=BG_COLOR)
lcd.DisplayText(CPU_NAME, x=1, y=15, font=FONT_PATH, font_size=10,
font_color=CPU_COLOR, background_color=BG_COLOR)
# Right side: GPU Stats header.
lcd.DisplayText("GPU Stats", x=220, y=1, font=FONT_PATH, font_size=12,
font_color=TEXT_COLOR, background_color=BG_COLOR)
def draw_dynamic_stats(lcd):
hw_list = handle.Hardware
# --- CPU Stats (Left Side) ---
cpu_load = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Load", "CPU Total") or 0.0
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Core (Tctl/Tdie)")
if cpu_temp is None:
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Package") or 0.0
cpu_freq = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Clock", "Core #1") or 0.0
y_cpu = 35
# Total percentage: pad to 3 digits.
lcd.DisplayText(f"Total: {int(cpu_load):3d}%", x=5, y=y_cpu, font=FONT_PATH, font_size=10,
font_color=CPU_COLOR, background_color=BG_COLOR)
cpu_bar_width = 70
cpu_bar_x = 120 - cpu_bar_width # = 180.
lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=5,
min_value=0, max_value=100, value=int(cpu_load),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y_cpu += 15
lcd.DisplayText(f"Temp: {int(cpu_temp):2d}°C Freq: {int(cpu_freq):4d}MHz", x=5, y=y_cpu,
font=FONT_PATH, font_size=10, font_color=CPU_COLOR, background_color=BG_COLOR)
y_cpu += 15
core_loads = get_sorted_core_loads(hw_list)
for core_index, sensor_name, load in core_loads:
core_label = f"Core {core_index}:" if core_index != 99 else "Core (top):"
lcd.DisplayText(core_label, x=10, y=y_cpu, font=FONT_PATH, font_size=8,
font_color=CPU_COLOR, background_color=BG_COLOR)
lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=5,
min_value=0, max_value=100, value=int(load),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
lcd.DisplayText(f"{int(load):3d}%", x=120, y=y_cpu, font=FONT_PATH, font_size=10,
font_color=CPU_COLOR, background_color=BG_COLOR)
y_cpu += 10
# --- RAM Stats (Left Side, below CPU) ---
mem_used = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Used") or 0.0
mem_avail = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Available") or 0.0
mem_total = mem_used + mem_avail
mem_pct = (mem_used / mem_total) * 100 if mem_total > 0 else 0
y_ram = y_cpu + 10
lcd.DisplayText("RAM Stats", x=10, y=y_ram, font=FONT_PATH, font_size=12,
font_color=TEXT_COLOR, background_color=BG_COLOR)
y_ram += 15
# Convert values from GB to MB.
mem_used_mb = int(round(mem_used * 1024))
mem_total_mb = int(round(mem_total * 1024))
# System RAM values: pad to 6 characters.
lcd.DisplayText(f"{mem_used_mb:6d}MB / {mem_total_mb:6d}MB", x=1, y=y_ram, font=FONT_PATH, font_size=10,
font_color=CPU_COLOR, background_color=BG_COLOR)
ram_bar_width = 100
ram_bar_x = 10 - ram_bar_width # = 280.
lcd.DisplayProgressBar(x=10, y=y_ram + 15, width=ram_bar_width, height=8,
min_value=0, max_value=100, value=int(mem_pct),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
# --- GPU Stats (Right Side) ---
gpu_x = 220 # left margin for GPU section.
gpu_stats_amd = get_gpu_stats(hw_list, "AMD")
if gpu_stats_amd is not None:
y_gpu1 = 20
y_gpu1 = draw_gpu_section(lcd, gpu_x, y_gpu1, gpu_stats_amd)
gpu_stats_Nvidia = get_gpu_stats(hw_list, "NVIDIA")
if gpu_stats_Nvidia is not None:
y_gpu2 = 145 # vertical gap.
y_gpu2 = draw_gpu_section(lcd, gpu_x, y_gpu2, gpu_stats_Nvidia)
# --- Uptime and Clock (Centered at Bottom) ---
now = datetime.now()
clock_str = now.strftime("%a %m/%d/%Y %I:%M:%S %p")
uptime_str = get_uptime_str()
lcd.DisplayText(uptime_str, x=400, y=280, font=FONT_PATH, font_size=10,
font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt")
lcd.DisplayText(clock_str, x=400, y=300, font=FONT_PATH, font_size=10,
font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt")
def get_uptime_str():
uptime_ms = ctypes.windll.kernel32.GetTickCount64()
uptime_sec = uptime_ms // 1000
days = uptime_sec // 86400
hours = (uptime_sec % 86400) // 3600
minutes = (uptime_sec % 3600) // 60
seconds = uptime_sec % 60
return f"Uptime: {days}d {hours:02d}:{minutes:02d}:{seconds:02d}"
def main():
initialize_hardware_names()
lcd = initialize_display()
draw_static_text(lcd)
while True:
draw_dynamic_stats(lcd)
time.sleep(UPDATE_INTERVAL)
if __name__ == "__main__":
try:
main()
finally:
handle.Close()
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
Here you go! I still need to fix some spacing and alignment issues:
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
Thank you!
Beta Was this translation helpful? Give feedback.
All reactions
-
Thanks! I tweaked it a bit to fit my use case — feel free to use it if you like!
sample
import time import logging import os import sys import clr import ctypes import re import math import queue import threading from datetime import datetime from library import config from library.lcd.lcd_comm import Orientation from library.lcd.lcd_comm_rev_a import LcdCommRevA # For 3.5′′ monitor from concurrent.futures import ThreadPoolExecutor # ====== Logging Configuration ====== logging.basicConfig( level=logging.DEBUG, format='%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S', force=True ) # ====== Display Settings ====== DISPLAY_WIDTH = 320 DISPLAY_HEIGHT = 480 FONT_PATH = "jetbrains-mono/JetBrainsMono-Bold.ttf" FONT_SIZE = 7 BG_COLOR = (0, 0, 0) # Solid black background CPU_COLOR = (192, 0, 0) # Red for CPU (and RAM) GPU_COLOR = (0, 128, 0) # Dark green for GPU stats TEXT_COLOR = (255, 255, 255) # White text UPDATE_INTERVAL = .1 # seconds BACKGROUND_IMG = "black.png" BAR_OFFSET = 3 # Bar vertical offset to align bars with text. # ====== Layout Constants ====== CPU_SECTION_Y = 35 GPU_SECTION_X = 220 RAM_SECTION_Y = 240 CLOCK_POSITION = (310, 290) # ====== LibreHardwareMonitor Setup ====== clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'LibreHardwareMonitorLib.dll')) clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'HidSharp.dll')) from LibreHardwareMonitor import Hardware handle = Hardware.Computer() for hw_type in ["Cpu", "Gpu", "Memory", "Motherboard", "Controller", "Network", "Storage", "Psu"]: setattr(handle, f"Is{hw_type}Enabled", True) handle.Open() # ====== Helper Functions ====== def get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name=None): """Retrieve a specific sensor value from hardware.""" for hw in hw_list: if hw.HardwareType == hw_type and (not hw_name or hw_name.lower() in hw.Name.lower()): for sensor in hw.Sensors: if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name: return sensor.Value return None def safe_get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name=None, default=0.0): """Safely retrieve a sensor value with exception handling.""" try: return get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name) or default except Exception as e: logging.error(f"Error retrieving {sensor_name} ({sensor_type}) for {hw_name or hw_type}: {e}") return default def update_sensor_value(hw_list): """Update sensor values for specific hardware types.""" for hw in hw_list: if hw.HardwareType in [Hardware.HardwareType.Cpu, Hardware.HardwareType.GpuNvidia, Hardware.HardwareType.GpuAmd, Hardware.HardwareType.Memory]: hw.Update() for subhw in hw.SubHardware: subhw.Update() def get_hardware_name(hw_list, hw_type, skip_first=False): """Retrieve the name of a specific hardware type.""" skipped = False for hw in hw_list: if hw.HardwareType == hw_type: if skip_first and not skipped: skipped = True continue return hw.Name return None def truncate_first_word(name_str): """Remove the first word from a string.""" parts = name_str.split() return " ".join(parts[1:]) if len(parts) > 1 else name_str def initialize_hardware_names(): """Initialize global hardware names.""" global CPU_NAME hw_list = handle.Hardware cpu_full_name = get_hardware_name(hw_list, Hardware.HardwareType.Cpu) or "Unknown CPU" CPU_NAME = truncate_first_word(cpu_full_name) def get_sorted_core_loads(hw_list): """Retrieve and sort CPU core loads.""" core_loads = [] for hw in hw_list: if hw.HardwareType == Hardware.HardwareType.Cpu: for sensor in hw.Sensors: if str(sensor.SensorType) == "Load" and "Core" in sensor.Name: core_index = int(re.search(r'#(\d+)', sensor.Name).group(1)) if re.search(r'#(\d+)', sensor.Name) else 99 core_loads.append((core_index, sensor.Name, sensor.Value)) core_loads.sort(key=lambda x: x[0]) return core_loads def get_gpu_stats(hw_list, filter_str): """Retrieve GPU stats for a specific filter (e.g., 'AMD' or 'NVIDIA').""" for hw in hw_list: if hw.HardwareType in [Hardware.HardwareType.GpuNvidia, Hardware.HardwareType.GpuAmd] and filter_str.lower() in hw.Name.lower(): util = safe_get_sensor_value(hw_list, hw.HardwareType, "Load", "GPU Core", hw_name=filter_str) temp = safe_get_sensor_value(hw_list, hw.HardwareType, "Temperature", "GPU Core", hw_name=filter_str) clock = safe_get_sensor_value(hw_list, hw.HardwareType, "Clock", "GPU Core", hw_name=filter_str) power = safe_get_sensor_value(hw_list, hw.HardwareType, "Power", "GPU Package", hw_name=filter_str) mem_percent = safe_get_sensor_value(hw_list, hw.HardwareType, "Load", "GPU Memory", hw_name=filter_str) return { "name": hw.Name, "util": util, "temp": temp, "clock": clock, "power": power, "mem_percent": mem_percent } return None def initialize_display(update_queue): """Initialize the LCD display.""" lcd = LcdCommRevA( com_port="AUTO", display_width=DISPLAY_WIDTH, display_height=DISPLAY_HEIGHT, update_queue=update_queue ) lcd.Reset() lcd.InitializeComm() lcd.SetBrightness(50) lcd.SetOrientation(Orientation.LANDSCAPE) lcd.DisplayBitmap(BACKGROUND_IMG) return lcd def draw_static_text(lcd): """Draw static text on the display.""" lcd.DisplayText("CPU Stats", x=1, y=1, font=FONT_PATH, font_size=12, font_color=TEXT_COLOR, background_color=BG_COLOR) lcd.DisplayText(CPU_NAME, x=1, y=15, font=FONT_PATH, font_size=10, font_color=CPU_COLOR, background_color=BG_COLOR) lcd.DisplayText("GPU Stats", x=GPU_SECTION_X, y=1, font=FONT_PATH, font_size=12, font_color=TEXT_COLOR, background_color=BG_COLOR) lcd.DisplayText("RAM Stats", x=1, y=RAM_SECTION_Y, font=FONT_PATH, font_size=12, font_color=TEXT_COLOR, background_color=BG_COLOR) def draw_dynamic_stats(lcd): """Draw dynamic stats on the display.""" threads = [] hw_list = handle.Hardware try: update_sensor_value(hw_list) except Exception as e: logging.error(f"Error updating hardware sensors: {e}") # CPU Stats threads.extend(draw_cpu_stats(lcd, hw_list)) # CPU Core Loads threads.extend(draw_cpu_core_load_stats(lcd, hw_list)) # GPU Stats gpu_stats_amd = get_gpu_stats(hw_list, "AMD") if gpu_stats_amd: threads.extend(draw_gpu_section(lcd, GPU_SECTION_X, 20, gpu_stats_amd)) gpu_stats_nvidia = get_gpu_stats(hw_list, "NVIDIA") if gpu_stats_nvidia: threads.extend(draw_gpu_section(lcd, GPU_SECTION_X, 145, gpu_stats_nvidia)) # RAM Stats threads.extend(draw_ram_stats(lcd, hw_list)) # Clock threads.append(threading.Thread(target=draw_clock, args=(lcd,))) return threads def draw_cpu_stats(lcd, hw_list): """Draw CPU stats on the display.""" threads = [] y_cpu = CPU_SECTION_Y cpu_load = safe_get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Load", "CPU Total") cpu_temp = safe_get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Core (Tctl/Tdie)") cpu_freq = safe_get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Clock", "Core #1") if math.isnan(cpu_freq): cpu_freq = 0.0 threads.append(threading.Thread(target=lcd.DisplayText, args=(f"Total: {int(cpu_load):3d}% Freq: {int(cpu_freq):4d}MHz",), kwargs={"x": 1, "y": y_cpu, "font": FONT_PATH, "font_size": 10, "font_color": CPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayText, args=(f"{int(cpu_temp):2d}°C",), kwargs={"x": 155, "y": y_cpu, "font": FONT_PATH, "font_size": 20, "font_color": CPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayProgressBar, kwargs={"x": 1, "y": y_cpu + 10 + BAR_OFFSET, "width": 150, "height": 10, "min_value": 0, "max_value": 100, "value": int(cpu_load), "bar_color": CPU_COLOR, "bar_outline": True, "background_color": BG_COLOR})) return threads def draw_cpu_core_load_stats(lcd, hw_list): """Draw CPU core loads on the display.""" threads = [] y_cpu_detail = 50 core_loads = get_sorted_core_loads(hw_list) for core_index, _, load in core_loads: y = y_cpu_detail + core_index * 10 if core_index != 99 else y_cpu_detail + 10 * len(core_loads) threads.append(threading.Thread(target=lcd.DisplayText, args=(f"Core {core_index}:" if core_index != 99 else "Core (top):",), kwargs={"x": 5, "y": y, "font": FONT_PATH, "font_size": 9, "font_color": CPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayProgressBar, kwargs={"x": 125 - 60, "y": y + BAR_OFFSET + 1, "width": 60, "height": 7, "min_value": 0, "max_value": 100, "value": int(load), "bar_color": CPU_COLOR, "bar_outline": True, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayText, args=(f"{int(load):3d}%",), kwargs={"x": 125 + 4, "y": y, "font": FONT_PATH, "font_size": 9, "font_color": CPU_COLOR, "background_color": BG_COLOR})) return threads def draw_ram_stats(lcd, hw_list): """Draw RAM stats on the display.""" threads = [] mem_used = safe_get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Used") mem_avail = safe_get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Available") mem_total = mem_used + mem_avail mem_pct = (mem_used / mem_total) * 100 if mem_total > 0 else 0 y_ram = RAM_SECTION_Y + 15 threads.append(threading.Thread(target=lcd.DisplayText, args=(f"{int(mem_used * 1024):6d}MB / {int(mem_total * 1024):6d}MB",), kwargs={"x": 1, "y": y_ram, "font": FONT_PATH, "font_size": 10, "font_color": CPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayProgressBar, kwargs={"x": 1, "y": y_ram + 10 + BAR_OFFSET, "width": 150, "height": 10, "min_value": 0, "max_value": 100, "value": int(mem_pct), "bar_color": CPU_COLOR, "bar_outline": True, "background_color": BG_COLOR})) return threads def draw_gpu_section(lcd, x, y, stats): """Draw GPU stats section.""" threads = [] bar_width = 160 bar_x = 380 - bar_width threads.append(threading.Thread(target=lcd.DisplayText, args=(stats["name"],), kwargs={"x": x, "y": y, "font": FONT_PATH, "font_size": 12, "font_color": GPU_COLOR, "background_color": BG_COLOR})) y += 20 threads.append(threading.Thread(target=lcd.DisplayText, kwargs={"text": f"Util: {int(stats['util']):3d}%", "x": x, "y": y, "font": FONT_PATH, "font_size": 10, "font_color": GPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayText, kwargs={"text": f"{int(stats['temp']):3d}°C", "x": bar_x + bar_width + 10, "y": y, "font": FONT_PATH, "font_size": 30, "font_color": GPU_COLOR, "background_color": BG_COLOR})) y += 10 threads.append(threading.Thread(target=lcd.DisplayProgressBar, kwargs={"x": bar_x, "y": y + BAR_OFFSET, "width": bar_width, "height": 20, "min_value": 0, "max_value": 100, "value": int(stats["util"]), "bar_color": GPU_COLOR, "bar_outline": True, "background_color": BG_COLOR})) y += 30 threads.append(threading.Thread(target=lcd.DisplayText, kwargs={"text": f"Freq: {int(stats['clock']):4d}MHz", "x": x, "y": y, "font": FONT_PATH, "font_size": 10, "font_color": GPU_COLOR, "background_color": BG_COLOR})) y += 20 threads.append(threading.Thread(target=lcd.DisplayText, kwargs={"text": "Mem", "x": x, "y": y, "font": FONT_PATH, "font_size": 10, "font_color": GPU_COLOR, "background_color": BG_COLOR})) threads.append(threading.Thread(target=lcd.DisplayText, kwargs={"text": f"{int(stats['power']):3d}W", "x": x + bar_width + 10, "y": y, "font": FONT_PATH, "font_size": 30, "font_color": GPU_COLOR, "background_color": BG_COLOR})) y += 10 threads.append(threading.Thread(target=lcd.DisplayProgressBar, kwargs={"x": bar_x, "y": y + BAR_OFFSET, "width": bar_width, "height": 20, "min_value": 0, "max_value": 100, "value": int(stats["mem_percent"]), "bar_color": GPU_COLOR, "bar_outline": True, "background_color": BG_COLOR})) return threads def draw_clock(lcd): """Draw the current time on the display.""" now = datetime.now() clock_str = now.strftime("%m/%d/%Y %H:%M:%S") lcd.DisplayText(clock_str, x=CLOCK_POSITION[0], y=CLOCK_POSITION[1], font=FONT_PATH, font_size=30, font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt") def main(): initialize_hardware_names() update_queue = queue.Queue() lcd = initialize_display(update_queue) stop_event = threading.Event() with ThreadPoolExecutor(max_workers=16) as executor: draw_static_text(lcd) def queue_handler(): while not stop_event.is_set(): try: f, args = update_queue.get(timeout=0.05) # Reduced timeout for faster response to stop_event if f: f(*args) except queue.Empty: continue except Exception as e: logging.error(f"Queue handler error: {e}") queue_thread = threading.Thread(target=queue_handler, daemon=True) queue_thread.start() def update_display(): while not stop_event.is_set(): try: start_time = time.time() threads = draw_dynamic_stats(lcd) # Wait for the queue to be empty before executing threads while not update_queue.empty() and not stop_event.is_set(): time.sleep(UPDATE_INTERVAL / 10) # Small delay to avoid busy-waiting # Execute threads using the shared thread pool futures = [executor.submit(thread.run) for thread in threads] for future in futures: try: future.result() except Exception as e: logging.error(f"Thread execution error: {e}") elapsed_time = time.time() - start_time time.sleep(max(0, UPDATE_INTERVAL - elapsed_time)) except Exception as e: logging.error(f"Display update error: {e}") display_thread = threading.Thread(target=update_display, daemon=True) display_thread.start() try: while not stop_event.is_set(): time.sleep(1) except KeyboardInterrupt: logging.info("Shutting down...") finally: stop_event.set() queue_thread.join() display_thread.join() executor.shutdown(wait=True) if __name__ == "__main__": try: main() except Exception as e: logging.critical(f"Critical error in main: {e}") finally: handle.Close()
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1 -
❤️ 1
-
Very cool, looks great!
Beta Was this translation helpful? Give feedback.
All reactions
-
I get this error in the terminal: [CRITICAL] Critical error in main: cannot open resource.
Beta Was this translation helpful? Give feedback.
All reactions
-
Love it. Love seeing people inspired to create their own data-driven displays. Themes are cool looking, but power users want to squeeze every last bit of screen space to get the data they want! Well done!
Beta Was this translation helpful? Give feedback.
All reactions
-
This looks like something I've wanted/have been passively working on for a while in my free time but I'm trying to use data from a different machine on my local network via either the Glances' API or maybe Zabbix. But this has definitely inspired me and given me some ideas.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yeah. I was toying with monitoring data from my home lab. Zabbix was one data source I played with. I also created one that connected to Home Assistant for data from my home and even my 3D printer, including an image of my current print.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Nice. I've made the most progress with the Glances API just because it's a basic python API I'm already familiar with and I just got started with Zabbix pretty recently and I still don't have everything configured correctly. I may look into it more if I can get the hosts to actually show up correctly.
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
I have created an update to this that I am testing before sharing. The CPU and GPU queries are run asynchronously. The GPU query was taking like 1 to 2 seconds, so I spun that off and let the main loop just check if there is an update from the GPU process. The result is multiple refreshes a second now, allowing the processor updates to happen faster. Runs MUCH better!
Beta Was this translation helpful? Give feedback.
All reactions
-
Here is my version with the updated asynchronous stat reading. The result is much higher refresh rates of the screen. In this case, the GPU details are pretty slow, so we have a separate thread that handles that lookup and updates the main thread when the result comes back. That way, the refresh isn't waiting for the GPU reads each time. I went ahead and got the CPU lookup into a separate thread as well, but didn't get that much performance increase... perhaps someone else might see more?
Once again, I use a 5 inch turing screen for mine. If you have tweaked yours to use different GPU/s or use a 3.5" turing (or other supported screens), you might want to just steal the logic from this.
Anyway, feel free to modify or enhance your own versions using this as a template! Cheers!
import time
import os
import sys
import clr
import ctypes
import re
from datetime import datetime
from library.lcd.lcd_comm import Orientation
from library.lcd.lcd_comm_rev_c import LcdCommRevC # For 5′′ monitor
# ====== Display Settings ======
# In landscape mode, physical resolution is 800x480.
DISPLAY_WIDTH = 480
DISPLAY_HEIGHT = 800
FONT_PATH = "res/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf"
FONT_SIZE = 20
BG_COLOR = (0, 0, 0) # Solid black background
CPU_COLOR = (255, 0, 0) # Red for CPU (and RAM)
GPU_COLOR = (0, 128, 0) # Dark green for GPU stats
TEXT_COLOR = (255, 255, 255) # White text
UPDATE_INTERVAL = 0.2 # seconds
BACKGROUND_IMG = "background.png"
# Bar vertical offset (in pixels) to align bars with text.
BAR_OFFSET = 5
# ====== Setup LibreHardwareMonitor ======
clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'LibreHardwareMonitorLib.dll'))
clr.AddReference(os.path.join(os.getcwd(), 'external', 'LibreHardwareMonitor', 'HidSharp.dll'))
from LibreHardwareMonitor import Hardware
handle = Hardware.Computer()
handle.IsCpuEnabled = True
handle.IsGpuEnabled = True
handle.IsMemoryEnabled = True
handle.IsMotherboardEnabled = True
handle.IsControllerEnabled = True
handle.IsNetworkEnabled = True
handle.IsStorageEnabled = True
handle.IsPsuEnabled = True
handle.Open()
# ====== Sensor Query Functions ======
def get_sensor_value(hw_list, hw_type, sensor_type, sensor_name, hw_name=None):
for hw in hw_list:
if hw.HardwareType == hw_type:
if hw_name and (hw_name.lower() not in hw.Name.lower()):
continue
hw.Update()
for sensor in hw.Sensors:
if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name:
return sensor.Value
for subhw in hw.SubHardware:
subhw.Update()
for sensor in subhw.Sensors:
if str(sensor.SensorType) == sensor_type and sensor.Name == sensor_name:
return sensor.Value
return None
def get_hardware_name(hw_list, hw_type, skip_first=False):
skipped = False
for hw in hw_list:
if hw.HardwareType == hw_type:
if skip_first and not skipped:
skipped = True
continue
return hw.Name
return None
def truncate_first_word(name_str):
parts = name_str.split()
if len(parts) > 1:
return " ".join(parts[1:])
return name_str
# Store CPU name globally.
def initialize_hardware_names():
global CPU_NAME
hw_list = handle.Hardware
cpu_full_name = get_hardware_name(hw_list, Hardware.HardwareType.Cpu) or "Unknown CPU"
CPU_NAME = truncate_first_word(cpu_full_name)
# Get GPU stats for a given filter (e.g. "4090" or "4080")
def get_gpu_stats(hw_list, filter_str):
stats = {}
for hw in hw_list:
if hw.HardwareType == Hardware.HardwareType.GpuNvidia and filter_str.lower() in hw.Name.lower():
stats["name"] = hw.Name
stats["util"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Load", "GPU Core", hw_name=filter_str) or 0.0
stats["temp"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Temperature", "GPU Core", hw_name=filter_str) or 0.0
stats["clock"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "Clock", "GPU Core", hw_name=filter_str) or 0.0
stats["mem_used"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Used", hw_name=filter_str) or 0.0
stats["mem_total"] = get_sensor_value(hw_list, Hardware.HardwareType.GpuNvidia, "SmallData", "GPU Memory Total", hw_name=filter_str) or 1.0
stats["mem_percent"] = (stats["mem_used"] / stats["mem_total"]) * 100
return stats
return None
# Draw a GPU section starting at (x,y) for a given GPU's stats.
def draw_gpu_section(lcd, x, y, stats):
# Fix GPU progress bar's right edge at x=780.
bar_width = 300
bar_x = 780 - bar_width
# Header: display GPU name.
lcd.DisplayText(stats["name"], x=x, y=y, font=FONT_PATH, font_size=20,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Utilization text: pad to 3 characters.
util_str = f"Util: {int(stats['util']):3d}%"
lcd.DisplayText(util_str, x=x, y=y, font=FONT_PATH, font_size=16,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Utilization progress bar.
lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20,
min_value=0, max_value=100, value=int(stats["util"]),
bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y += 30
# Temperature and clock on one line.
temp_freq_str = f"Temp: {int(stats['temp']):2d}°C Freq: {int(stats['clock']):4d}MHz"
lcd.DisplayText(temp_freq_str, x=x, y=y, font=FONT_PATH, font_size=16,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Memory usage text: pad to 5 digits.
mem_str = f"Mem: {int(stats['mem_used']):5d}MB/{int(stats['mem_total']):5d}MB"
lcd.DisplayText(mem_str, x=x, y=y, font=FONT_PATH, font_size=16,
font_color=GPU_COLOR, background_color=BG_COLOR)
y += 20
# Memory usage progress bar.
lcd.DisplayProgressBar(x=bar_x, y=y + BAR_OFFSET, width=bar_width, height=20,
min_value=0, max_value=100, value=int(stats["mem_percent"]),
bar_color=GPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y += 30
return y
def get_sorted_core_loads(hw_list):
"""
Collects all per-core CPU load sensors, supporting CPUs with >8 cores, multiple CCDs, and non-contiguous numbering.
Returns a sorted list of (core_index, sensor_name, value) for all detected cores.
"""
core_loads = []
for hw in hw_list:
if hw.HardwareType == Hardware.HardwareType.Cpu:
hw.Update()
# Collect sensors from main hardware
for sensor in hw.Sensors:
if str(sensor.SensorType) == "Load" and re.search(r'Core #\d+', sensor.Name):
m = re.search(r'#(\d+)', sensor.Name)
if m:
core_index = int(m.group(1))
core_loads.append((core_index, sensor.Name, sensor.Value))
# Collect sensors from subhardware (for multi-CCD CPUs)
for subhw in getattr(hw, 'SubHardware', []):
subhw.Update()
for sensor in subhw.Sensors:
if str(sensor.SensorType) == "Load" and re.search(r'Core #\d+', sensor.Name):
m = re.search(r'#(\d+)', sensor.Name)
if m:
core_index = int(m.group(1))
core_loads.append((core_index, sensor.Name, sensor.Value))
# Remove duplicate core indices (in case of overlapping sensors)
seen = set()
unique_core_loads = []
for core in sorted(core_loads, key=lambda x: x[0]):
if core[0] not in seen:
unique_core_loads.append(core)
seen.add(core[0])
return unique_core_loads
def initialize_display():
lcd = LcdCommRevC(
com_port="AUTO",
display_width=DISPLAY_WIDTH,
display_height=DISPLAY_HEIGHT
)
lcd.Reset()
lcd.InitializeComm()
lcd.SetBrightness(50)
lcd.SetOrientation(Orientation.LANDSCAPE)
# logging.debug("Displaying initial background...")
lcd.DisplayBitmap(BACKGROUND_IMG)
# logging.debug("Initial background displayed.")
return lcd
def draw_static_text(lcd):
# Left side: CPU header and CPU name.
lcd.DisplayText("CPU Stats", x=10, y=10, font=FONT_PATH, font_size=22,
font_color=TEXT_COLOR, background_color=BG_COLOR)
lcd.DisplayText(CPU_NAME, x=10, y=40, font=FONT_PATH, font_size=20,
font_color=CPU_COLOR, background_color=BG_COLOR)
# Right side: GPU Stats header.
lcd.DisplayText("GPU Stats", x=420, y=10, font=FONT_PATH, font_size=22,
font_color=TEXT_COLOR, background_color=BG_COLOR)
import threading
# Global cache for all stats and lock for thread safety
STATS_CACHE = {
'cpu': None,
'cpu_cores': None,
'ram': None,
'gpu': {'4090': None, '4080': None}
}
STATS_LOCK = threading.Lock()
CPU_UPDATE_INTERVAL = 0.2 # seconds
RAM_UPDATE_INTERVAL = 0.2 # seconds
GPU_UPDATE_INTERVAL = 2.0 # seconds
# CPU stats updater
def cpu_stats_background_updater():
hw_list = handle.Hardware
while True:
cpu_load = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Load", "CPU Total") or 0.0
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Core (Tctl/Tdie)")
if cpu_temp is None:
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Package") or 0.0
cpu_freq = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Clock", "Core #1") or 0.0
cpu_cores = get_sorted_core_loads(hw_list)
with STATS_LOCK:
STATS_CACHE['cpu'] = {
'load': cpu_load,
'temp': cpu_temp,
'freq': cpu_freq
}
STATS_CACHE['cpu_cores'] = cpu_cores
time.sleep(CPU_UPDATE_INTERVAL)
# RAM stats updater
def ram_stats_background_updater():
hw_list = handle.Hardware
while True:
mem_used = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Used") or 0.0
mem_avail = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Available") or 0.0
mem_total = mem_used + mem_avail
mem_pct = (mem_used / mem_total) * 100 if mem_total > 0 else 0
with STATS_LOCK:
STATS_CACHE['ram'] = {
'used': mem_used,
'avail': mem_avail,
'total': mem_total,
'pct': mem_pct
}
time.sleep(RAM_UPDATE_INTERVAL)
# GPU stats updater (already present, just adapted)
def gpu_stats_background_updater():
hw_list = handle.Hardware
while True:
stats = {}
stats['4090'] = get_gpu_stats(hw_list, '4090')
stats['4080'] = get_gpu_stats(hw_list, '4080')
with STATS_LOCK:
STATS_CACHE['gpu'] = stats
# logging.info("[GPU Thread] Refreshed GPU stats from hardware.")
time.sleep(GPU_UPDATE_INTERVAL)
def draw_dynamic_stats(lcd):
section_start = time.time()
# --- CPU Stats (Left Side) ---
with STATS_LOCK:
cpu_stats = STATS_CACHE['cpu']
cpu_cores = STATS_CACHE['cpu_cores']
if cpu_stats is not None:
cpu_load = cpu_stats['load']
cpu_temp = cpu_stats['temp']
cpu_freq = cpu_stats['freq']
else:
cpu_load = cpu_temp = cpu_freq = 0.0
cpu_time = time.time() - section_start
# logging.info(f"CPU stats time: {cpu_time:.3f} seconds")
y_cpu = 70
# Total percentage: pad to 3 digits.
lcd.DisplayText(f"Total: {int(cpu_load):3d}%", x=10, y=y_cpu, font=FONT_PATH, font_size=20,
font_color=CPU_COLOR, background_color=BG_COLOR)
cpu_bar_width = 170
cpu_bar_x = 320 - cpu_bar_width # = 180.
lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=20,
min_value=0, max_value=100, value=int(cpu_load),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
y_cpu += 30
lcd.DisplayText(f"Temp: {int(cpu_temp):2d}°C Freq: {int(cpu_freq):4d}MHz", x=10, y=y_cpu,
font=FONT_PATH, font_size=20, font_color=CPU_COLOR, background_color=BG_COLOR)
y_cpu += 30
if cpu_cores is not None:
for core_index, sensor_name, load in cpu_cores:
core_label = f"Core {core_index}:" if core_index != 99 else "Core (top):"
lcd.DisplayText(core_label, x=10, y=y_cpu, font=FONT_PATH, font_size=18,
font_color=CPU_COLOR, background_color=BG_COLOR)
lcd.DisplayProgressBar(x=cpu_bar_x, y=y_cpu + BAR_OFFSET, width=cpu_bar_width, height=15,
min_value=0, max_value=100, value=int(load),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
lcd.DisplayText(f"{int(load):3d}%", x=330, y=y_cpu, font=FONT_PATH, font_size=18,
font_color=CPU_COLOR, background_color=BG_COLOR)
y_cpu += 20
cpu_draw_time = time.time() - section_start - cpu_time
# logging.info(f"CPU draw time: {cpu_draw_time:.3f} seconds")
# --- RAM Stats (Left Side, below CPU) ---
ram_section_start = time.time()
with STATS_LOCK:
ram_stats = STATS_CACHE['ram']
if ram_stats is not None:
mem_used = ram_stats['used']
mem_total = ram_stats['total']
mem_pct = ram_stats['pct']
else:
mem_used = mem_total = mem_pct = 0.0
ram_time = time.time() - ram_section_start
# logging.info(f"RAM stats time: {ram_time:.3f} seconds")
y_ram = y_cpu + 20
lcd.DisplayText("RAM Stats", x=10, y=y_ram, font=FONT_PATH, font_size=22,
font_color=TEXT_COLOR, background_color=BG_COLOR)
y_ram += 30
# Convert values from GB to MB.
mem_used_mb = int(round(mem_used * 1024))
mem_total_mb = int(round(mem_total * 1024))
# System RAM values: pad to 6 characters.
lcd.DisplayText(f"{mem_used_mb:6d}MB / {mem_total_mb:6d}MB", x=10, y=y_ram, font=FONT_PATH, font_size=20,
font_color=CPU_COLOR, background_color=BG_COLOR)
ram_bar_width = 140
ram_bar_x = 420 - ram_bar_width # = 280.
lcd.DisplayProgressBar(x=ram_bar_x, y=y_ram + BAR_OFFSET, width=ram_bar_width, height=20,
min_value=0, max_value=100, value=int(mem_pct),
bar_color=CPU_COLOR, bar_outline=True, background_color=BG_COLOR)
ram_draw_time = time.time() - ram_section_start - ram_time
# logging.info(f"RAM draw time: {ram_draw_time:.3f} seconds")
# --- GPU Stats (Right Side) ---
gpu_section_start = time.time()
gpu_x = 420 # left margin for GPU section.
with STATS_LOCK:
gpu_stats_4090 = STATS_CACHE['gpu'].get('4090')
gpu_stats_4080 = STATS_CACHE['gpu'].get('4080')
if gpu_stats_4090 is not None:
y_gpu1 = 40
y_gpu1 = draw_gpu_section(lcd, gpu_x, y_gpu1, gpu_stats_4090)
if gpu_stats_4080 is not None:
y_gpu2 = 180 # vertical gap.
y_gpu2 = draw_gpu_section(lcd, gpu_x, y_gpu2, gpu_stats_4080)
gpu_time = time.time() - gpu_section_start
# logging.info(f"GPU section time (query + draw): {gpu_time:.3f} seconds")
# --- Uptime and Clock (Centered at Bottom) ---
bottom_section_start = time.time()
now = datetime.now()
clock_str = now.strftime("%a %m/%d/%Y %I:%M:%S %p")
uptime_str = get_uptime_str()
lcd.DisplayText(uptime_str, x=400, y=440, font=FONT_PATH, font_size=20,
font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt")
lcd.DisplayText(clock_str, x=400, y=460, font=FONT_PATH, font_size=20,
font_color=TEXT_COLOR, background_color=BG_COLOR, anchor="mt")
bottom_time = time.time() - bottom_section_start
# logging.info(f"Bottom section (uptime/clock) time: {bottom_time:.3f} seconds")
def get_uptime_str():
uptime_ms = ctypes.windll.kernel32.GetTickCount64()
uptime_sec = uptime_ms // 1000
days = uptime_sec // 86400
hours = (uptime_sec % 86400) // 3600
minutes = (uptime_sec % 3600) // 60
seconds = uptime_sec % 60
return f"Uptime: {days}d {hours:02d}:{minutes:02d}:{seconds:02d}"
def debug_print_cpu_load_sensors(hw_list):
print("=== CPU Load Sensors Detected ===")
for hw in hw_list:
if hw.HardwareType == Hardware.HardwareType.Cpu:
print(f"CPU HW: {getattr(hw, 'Name', 'Unknown')}")
hw.Update()
for sensor in hw.Sensors:
if str(sensor.SensorType) == "Load":
print(f" Sensor: {sensor.Name} Value: {sensor.Value}")
for subhw in getattr(hw, 'SubHardware', []):
subhw.Update()
for sensor in subhw.Sensors:
if str(sensor.SensorType) == "Load":
print(f" SubHW Sensor: {sensor.Name} Value: {sensor.Value}")
def main():
initialize_hardware_names()
lcd = initialize_display()
draw_static_text(lcd)
# --- Synchronously gather all stats for the first frame ---
hw_list = handle.Hardware
debug_print_cpu_load_sensors(hw_list) # Diagnostic printout
# CPU
cpu_load = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Load", "CPU Total") or 0.0
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Core (Tctl/Tdie)")
if cpu_temp is None:
cpu_temp = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Temperature", "Package") or 0.0
cpu_freq = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Clock", "Core #1") or 0.0
cpu_cores = get_sorted_core_loads(hw_list)
# RAM
mem_used = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Used") or 0.0
mem_avail = get_sensor_value(hw_list, Hardware.HardwareType.Memory, "Data", "Memory Available") or 0.0
mem_total = mem_used + mem_avail
mem_pct = (mem_used / mem_total) * 100 if mem_total > 0 else 0
# GPU
gpu_stats = {
'4090': get_gpu_stats(hw_list, '4090'),
'4080': get_gpu_stats(hw_list, '4080')
}
# Populate cache synchronously
with STATS_LOCK:
STATS_CACHE['cpu'] = {'load': cpu_load, 'temp': cpu_temp, 'freq': cpu_freq}
STATS_CACHE['cpu_cores'] = cpu_cores
STATS_CACHE['ram'] = {'used': mem_used, 'avail': mem_avail, 'total': mem_total, 'pct': mem_pct}
STATS_CACHE['gpu'] = gpu_stats
# Draw the initial frame after all stats are available
draw_dynamic_stats(lcd)
# Start background threads for CPU, RAM, and GPU
cpu_thread = threading.Thread(target=cpu_stats_background_updater, daemon=True)
ram_thread = threading.Thread(target=ram_stats_background_updater, daemon=True)
gpu_thread = threading.Thread(target=gpu_stats_background_updater, daemon=True)
cpu_thread.start()
ram_thread.start()
gpu_thread.start()
UPDATE_INTERVAL = 0.1
while True:
start_time = time.time()
draw_dynamic_stats(lcd)
elapsed = time.time() - start_time
# logging.info(f"Loop iteration took {elapsed:.3f} seconds")
time.sleep(max(0, UPDATE_INTERVAL - elapsed))
if __name__ == "__main__":
try:
main()
finally:
handle.Close()
Beta Was this translation helpful? Give feedback.
All reactions
-
AMD CPU power sensor completely broken in LHM? I tried to add it like this:
cpu_power = get_sensor_value(hw_list, Hardware.HardwareType.Cpu, "Power", "Package") or 0.0
The sensor receives random values, but not correct ones. After some time I get a message in the terminal: cannot convert float infinity to integer. I can't even imagine why the sensor becomes float infinity.
Beta Was this translation helpful? Give feedback.
All reactions
-
20250425_091632.mp4
Beta Was this translation helpful? Give feedback.
All reactions
-
👀 1
-
This theme looks very detailed and cool !!
I'd like to try using it myself, but I'm a beginner and don't understand how to use the Python ver.
I've already installed Python 3.12.9, and I'm using a 3.5-inch monitor.
I'd appreciate it if you could write some hints when you have time...
Beta Was this translation helpful? Give feedback.
All reactions
-
Well, I think the key is to get the requirements and read warnings/errors if you get them. With regards to the 3.5" screen, there are others using versions of that above in the comments. Beyond that, I think error messages would be useful to help troubleshoot.
Keep in mind that the version I put code in for is the 5" screen, so it will likely need to be adjusted for 3.5". Different resolutions and all, and the layout is pretty particular about where things get placed.
Beta Was this translation helpful? Give feedback.
All reactions
-
I'm debating creating a version of this script that works for both 3.5" and 5" screens. I have both available, and imagine that I could use a try/catch to test for the size. Dunno. Could be interesting.
Alternatively, I was thinking about doing a double screen version of this, but I don't know if that is any better than having two different scripts... one for each screen.
Beta Was this translation helpful? Give feedback.
All reactions
-
I did a ton of enhancements to this. See this thread (in discussion as it discusses a new way to do themes:
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.