Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Simple Performance Script #664

majormer started this conversation in Themes
Feb 5, 2025 · 19 comments · 10 replies
Discussion options

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.

20250205_133131

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.

You must be logged in to vote

Replies: 19 comments 10 replies

Comment options

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()
You must be logged in to vote
0 replies
Comment options

background

Oh, you will need this file in the root of your directory as well... it is just a black image called background.png

You must be logged in to vote
0 replies
Comment options

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!

You must be logged in to vote
0 replies
Comment options

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!

You must be logged in to vote
0 replies
Comment options

The trick is have the script in the root of the repository, and run it with Python directly. You don't use the app or configuration. Instead, the python script can just use the files directly to access the screen. The screen size also matters, and needs to set up in the script.
...
On Thu, Apr 3, 2025, 8:28 PM SithLord_Sylar ***@***.***> wrote: Hey @majormer <https://github.com/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! — Reply to this email directly, view it on GitHub <#664 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AB2XMTKSP6PMEJS5JITO77T2XXN2JAVCNFSM6AAAAABWR3PEU6VHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTENZSGAYTCOI> . You are receiving this because you were mentioned.Message ID: <mathoudebine/turing-smart-screen-python/repo-discussions/664/comments/12720119 @github.com>
You must be logged in to vote
1 reply
Comment options

Ahhhhhh
That makes so much sense
Thank you do much man.
Really appreciate the reply.

Comment options

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()
You must be logged in to vote
0 replies
Comment options

Cool! Got a picture?
...
On Thu, Apr 10, 2025, 2:45 PM CanHasDIY ***@***.***> wrote: 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() — Reply to this email directly, view it on GitHub <#664 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AB2XMTMFB6D75ZH5O2HVT5L2Y3C7NAVCNFSM6AAAAABWR3PEU6VHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTENZZGU4DKMA> . You are receiving this because you were mentioned.Message ID: <mathoudebine/turing-smart-screen-python/repo-discussions/664/comments/12795850 @github.com>
You must be logged in to vote
1 reply
Comment options

Here you go! I still need to fix some spacing and alignment issues:

example

Comment options

Excellent! Well done!
...
On Thu, Apr 10, 2025, 3:03 PM CanHasDIY ***@***.***> wrote: Here you go! I still need to fix some spacing and alignment issues: example.png (view on web) <https://github.com/user-attachments/assets/1f154d13-21d3-4f07-8f88-41b299fb5d28> — Reply to this email directly, view it on GitHub <#664 (reply in thread)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AB2XMTPR4NMV7LT5YVFZCWL2Y3FA7AVCNFSM6AAAAABWR3PEU6VHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTENZZGU4TQNI> . You are receiving this because you were mentioned.Message ID: <mathoudebine/turing-smart-screen-python/repo-discussions/664/comments/12795985 @github.com>
You must be logged in to vote
1 reply
Comment options

Thank you!

Comment options

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()
You must be logged in to vote
2 replies
Comment options

Very cool, looks great!

Comment options

I get this error in the terminal: [CRITICAL] Critical error in main: cannot open resource.

Comment options

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!

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
2 replies
Comment options

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.

Comment options

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.

Comment options

Dunno. Might need to update Com port or something.
...
On Thu, Apr 24, 2025, 1:41 PM reactor333-debug ***@***.***> wrote: I get this error in the terminal: [CRITICAL] Critical error in main: cannot open resource. — Reply to this email directly, view it on GitHub <#664 (reply in thread)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AB2XMTP2U6YEBJVY4EJ5ZNT23EV7NAVCNFSM6AAAAABWR3PEU6VHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTEOJTHA4DQOA> . You are receiving this because you were mentioned.Message ID: <mathoudebine/turing-smart-screen-python/repo-discussions/664/comments/12938888 @github.com>
You must be logged in to vote
0 replies
Comment options

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!

You must be logged in to vote
0 replies
Comment options

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()
You must be logged in to vote
1 reply
Comment options

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.

Comment options

20250425_091632.mp4
You must be logged in to vote
2 replies
Comment options

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...

Comment options

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.

Comment options

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.

You must be logged in to vote
0 replies
Comment options

I did a ton of enhancements to this. See this thread (in discussion as it discusses a new way to do themes:

#764

You must be logged in to vote
0 replies
Comment options

here is my version
a little change
made with gpt

35.py

20250904_020113 1

You must be logged in to vote
0 replies
Comment options

So nice!
...
On Wed, Sep 3, 2025, 9:44 PM guhyeongjin ***@***.***> wrote: here is my version a little change made with gpt 35.py <https://github.com/user-attachments/files/22130030/35.py> 20250904_020113.1.jpg (view on web) <https://github.com/user-attachments/assets/db84e0bb-d886-421e-abdb-51d2b9d9e0cb> — Reply to this email directly, view it on GitHub <#664 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AB2XMTMP6XFXVQPHFKQLKV33Q6RSJAVCNFSM6AAAAABWR3PEU6VHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTIMZQGIZTQMQ> . You are receiving this because you were mentioned.Message ID: <mathoudebine/turing-smart-screen-python/repo-discussions/664/comments/14302382 @github.com>
You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /