Skip to content

Commit

Permalink
Merge pull request #484 from mathoudebine/fix/473-gpu-and-fps-fan-error
Browse files Browse the repository at this point in the history
  • Loading branch information
mathoudebine authored Jun 18, 2024
2 parents e1d7d8a + 0cfaff7 commit 0759c80
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 42 deletions.
7 changes: 7 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ config:
ETH: "" # Ethernet Card
WLO: "" # Wi-Fi Card

# CPU fan
# For Linux/MacOS platforms, the CPU fan is amongst all fan sensors gathered from the motherboard chipset
# If value is AUTO the system monitor will try to auto-select the CPU fan
# If auto-detection fails, it might be necessary to manually indicate which fan is the CPU fan
# Value must be 'controller/fan' e.g. 'nct6798/fan2'. Use configuration wizard for help in selection
CPU_FAN: AUTO

display:
# Display revision:
# - A for Turing 3.5" and UsbPCMonitor 3.5"/5"
Expand Down
68 changes: 62 additions & 6 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import sv_ttk
from PIL import Image
from serial.tools.list_ports import comports
from tktooltip import ToolTip
except:
print(
"[ERROR] Python dependencies not installed. Please follow start guide: https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-how-to-start")
Expand All @@ -59,6 +60,8 @@
except:
os._exit(0)

from library.sensors.sensors_python import sensors_fans, is_cpu_fan

TURING_MODEL = "Turing Smart Screen"
USBPCMONITOR_MODEL = "UsbPCMonitor"
XUANFANG_MODEL = "XuanFang rev. B & flagship"
Expand Down Expand Up @@ -141,14 +144,28 @@ def get_net_if():
return if_list


def get_fans():
fan_list = list()
auto_detected_cpu_fan = "None"
for name, entries in sensors_fans().items():
for entry in entries:
fan_list.append("%s/%s (%d%% - %d RPM)" % (name, entry.label, entry.percent, entry.current))
if (is_cpu_fan(entry.label) or is_cpu_fan(name)) and auto_detected_cpu_fan == "None":
auto_detected_cpu_fan = "Auto-detected: %s/%s" % (name, entry.label)

fan_list.insert(0, auto_detected_cpu_fan) # Add manual entry on top if auto-detection succeeded
return fan_list


class TuringConfigWindow:
def __init__(self):
self.window = Tk()
self.window.title('Turing System Monitor configuration')
self.window.geometry("770x550")
self.window.geometry("770x570")
self.window.iconphoto(True, PhotoImage(file="res/icons/monitor-icon-17865/64.png"))
# When window gets focus again, reload theme preview in case it has been updated by theme editor
self.window.bind("<FocusIn>", self.on_theme_change)
self.window.after(0, self.on_fan_speed_update)

# Make TK look better with Sun Valley ttk theme
sv_ttk.set_theme("light")
Expand Down Expand Up @@ -224,18 +241,29 @@ def __init__(self):
self.wl_cb = ttk.Combobox(self.window, values=get_net_if(), state='readonly')
self.wl_cb.place(x=500, y=415, width=250)

# For Windows platform only
self.lhm_admin_warning = ttk.Label(self.window,
text="❌ Restart as admin. or select another Hardware monitoring",
foreground='#f00')
# For platform != Windows
self.cpu_fan_label = ttk.Label(self.window, text='CPU fan (?)')
self.cpu_fan_label.config(foreground="#a3a3ff", cursor="hand2")
self.cpu_fan_cb = ttk.Combobox(self.window, values=get_fans(), state='readonly')

self.tooltip = ToolTip(self.cpu_fan_label,
msg="If \"None\" is selected, CPU fan was not auto-detected.\n"
"Manually select your CPU fan from the list.\n\n"
"Fans missing from the list? Install lm-sensors package\n"
"and run 'sudo sensors-detect' command, then reboot.")

self.edit_theme_btn = ttk.Button(self.window, text="Edit theme", command=lambda: self.on_theme_editor_click())
self.edit_theme_btn.place(x=310, y=490, height=50, width=130)
self.edit_theme_btn.place(x=310, y=510, height=50, width=130)

self.save_btn = ttk.Button(self.window, text="Save settings", command=lambda: self.on_save_click())
self.save_btn.place(x=450, y=490, height=50, width=130)
self.save_btn.place(x=450, y=510, height=50, width=130)

self.save_run_btn = ttk.Button(self.window, text="Save and run", command=lambda: self.on_saverun_click())
self.save_run_btn.place(x=590, y=490, height=50, width=130)
self.save_run_btn.place(x=590, y=510, height=50, width=130)

self.config = None
self.load_config_values()
Expand All @@ -261,7 +289,8 @@ def load_theme_preview(self):
self.theme_author.config(text="Author: " + author_name)
if author_name.startswith("@"):
self.theme_author.config(foreground="#a3a3ff", cursor="hand2")
self.theme_author.bind("<Button-1>", lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:]))
self.theme_author.bind("<Button-1>",
lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:]))
else:
self.theme_author.config(foreground="#a3a3a3", cursor="")
self.theme_author.unbind("<Button-1>")
Expand Down Expand Up @@ -336,6 +365,14 @@ def load_config_values(self):
except:
self.brightness_slider.set(50)

try:
if self.config['config']['CPU_FAN'] == "AUTO":
self.cpu_fan_cb.current(0)
else:
self.cpu_fan_cb.set(self.config['config']['CPU_FAN'])
except:
self.cpu_fan_cb.current(0)

# Reload content on screen
self.on_model_change()
self.on_size_change()
Expand All @@ -358,6 +395,10 @@ def save_config_values(self):
self.config['config']['COM_PORT'] = "AUTO"
else:
self.config['config']['COM_PORT'] = self.com_cb.get()
if self.cpu_fan_cb.current() == 0:
self.config['config']['CPU_FAN'] = "AUTO"
else:
self.config['config']['CPU_FAN'] = self.cpu_fan_cb.get().split(' ')[0]
self.config['display']['REVISION'] = model_and_size_to_revision_map[(self.model_cb.get(), self.size_cb.get())]
self.config['display']['DISPLAY_REVERSE'] = [k for k, v in reverse_map.items() if v == self.orient_cb.get()][0]
self.config['display']['BRIGHTNESS'] = int(self.brightness_slider.get())
Expand Down Expand Up @@ -421,11 +462,18 @@ def on_hwlib_change(self, e=None):
import ctypes
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
if (hwlib == "LHM" or hwlib == "AUTO") and not is_admin:
self.lhm_admin_warning.place(x=320, y=455)
self.lhm_admin_warning.place(x=320, y=460)
self.save_run_btn.state(["disabled"])
else:
self.lhm_admin_warning.place_forget()
self.save_run_btn.state(["!disabled"])
else:
if hwlib == "PYTHON" or hwlib == "AUTO":
self.cpu_fan_label.place(x=320, y=460)
self.cpu_fan_cb.place(x=500, y=455, width=250)
else:
self.cpu_fan_label.place_forget()
self.cpu_fan_cb.place_forget()

def show_hide_brightness_warning(self, e=None):
if int(self.brightness_slider.get()) > 50 and self.model_cb.get() == TURING_MODEL and self.size_cb.get() == SIZE_3_5_INCH:
Expand All @@ -434,6 +482,14 @@ def show_hide_brightness_warning(self, e=None):
else:
self.brightness_warning_label.place_forget()

def on_fan_speed_update(self):
# Update fan speed periodically
prev_value = self.cpu_fan_cb.current() # Save currently selected index
self.cpu_fan_cb.config(values=get_fans())
if prev_value != -1:
self.cpu_fan_cb.current(prev_value) # Force select same index to refresh displayed value
self.window.after(500, self.on_fan_speed_update)


if __name__ == "__main__":
configurator = TuringConfigWindow()
Expand Down
2 changes: 1 addition & 1 deletion library/sensors/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def temperature() -> float:

@staticmethod
@abstractmethod
def fan_percent() -> float:
def fan_percent(fan_name: str = None) -> float:
pass


Expand Down
2 changes: 1 addition & 1 deletion library/sensors/sensors_librehardwaremonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def temperature() -> float:
return math.nan

@staticmethod
def fan_percent() -> float:
def fan_percent(fan_name: str = None) -> float:
mb = get_hw_and_update(Hardware.HardwareType.Motherboard)
try:
for sh in mb.SubHardware:
Expand Down
55 changes: 36 additions & 19 deletions library/sensors/sensors_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import math
import platform
import sys
from collections import namedtuple
from enum import IntEnum, auto
from typing import Tuple

Expand Down Expand Up @@ -58,8 +59,8 @@ class GpuType(IntEnum):


# Function inspired of psutil/psutil/_pslinux.py:sensors_fans()
# Adapted to get fan speed percentage instead of raw value
def sensors_fans_percent():
# Adapted to also get fan speed percentage instead of raw value
def sensors_fans():
"""Return hardware fans info (for CPU and other peripherals) as a
dict including hardware label and current speed.
Expand All @@ -69,7 +70,7 @@ def sensors_fans_percent():
only (old distros will probably use something else)
- lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
"""
from psutil._common import bcat, cat, sfan
from psutil._common import bcat, cat
import collections, glob, os

ret = collections.defaultdict(list)
Expand All @@ -82,19 +83,31 @@ def sensors_fans_percent():
basenames = sorted(set([x.split('_')[0] for x in basenames]))
for base in basenames:
try:
current = int(bcat(base + '_input'))
max = int(bcat(base + '_max'))
min = int(bcat(base + '_min'))
percent = int((current - min) / (max - min) * 100)
current_rpm = int(bcat(base + '_input'))
try:
max_rpm = int(bcat(base + '_max'))
except:
max_rpm = 1500 # Approximated: max fan speed is 1500 RPM
try:
min_rpm = int(bcat(base + '_min'))
except:
min_rpm = 0 # Approximated: min fan speed is 0 RPM
percent = int((current_rpm - min_rpm) / (max_rpm - min_rpm) * 100)
except (IOError, OSError) as err:
continue
unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip()
label = cat(base + '_label', fallback='').strip()
ret[unit_name].append(sfan(label, percent))
label = cat(base + '_label', fallback=os.path.basename(base)).strip()

custom_sfan = namedtuple('sfan', ['label', 'current', 'percent'])
ret[unit_name].append(custom_sfan(label, current_rpm, percent))

return dict(ret)


def is_cpu_fan(label: str) -> bool:
return ("cpu" in label.lower()) or ("proc" in label.lower())


class Cpu(sensors.Cpu):
@staticmethod
def percentage(interval: float) -> float:
Expand Down Expand Up @@ -140,14 +153,18 @@ def temperature() -> float:
return cpu_temp

@staticmethod
def fan_percent() -> float:
def fan_percent(fan_name: str = None) -> float:
try:
fans = sensors_fans_percent()
fans = sensors_fans()
if fans:
for name, entries in fans.items():
for entry in entries:
if "cpu" in (entry.label or name):
return entry.current
if fan_name is not None and fan_name == "%s/%s" % (name, entry.label):
# Manually selected fan
return entry.percent
elif is_cpu_fan(entry.label) or is_cpu_fan(name):
# Auto-detected fan
return entry.percent
except:
pass

Expand Down Expand Up @@ -255,12 +272,12 @@ def fps() -> int:
@staticmethod
def fan_percent() -> float:
try:
fans = sensors_fans_percent()
fans = sensors_fans()
if fans:
for name, entries in fans.items():
for entry in entries:
if "gpu" in (entry.label or name):
return entry.current
if "gpu" in (entry.label.lower() or name.lower()):
return entry.percent
except:
pass

Expand Down Expand Up @@ -336,12 +353,12 @@ def fps() -> int:
def fan_percent() -> float:
try:
# Try with psutil fans
fans = sensors_fans_percent()
fans = sensors_fans()
if fans:
for name, entries in fans.items():
for entry in entries:
if "gpu" in (entry.label or name):
return entry.current
if "gpu" in (entry.label.lower() or name.lower()):
return entry.percent

# Try with pyadl if psutil did not find GPU fan
if pyadl:
Expand Down
2 changes: 1 addition & 1 deletion library/sensors/sensors_stub_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def temperature() -> float:
return random.uniform(30, 90)

@staticmethod
def fan_percent() -> float:
def fan_percent(fan_name: str = None) -> float:
return random.uniform(0, 100)


Expand Down
2 changes: 1 addition & 1 deletion library/sensors/sensors_stub_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def temperature() -> float:
return TEMPERATURE_SENSOR_VALUE

@staticmethod
def fan_percent() -> float:
def fan_percent(fan_name: str = None) -> float:
return PERCENTAGE_SENSOR_VALUE


Expand Down
18 changes: 13 additions & 5 deletions library/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@

DEFAULT_HISTORY_SIZE = 10

ETH_CARD = config.CONFIG_DATA["config"]["ETH"]
WLO_CARD = config.CONFIG_DATA["config"]["WLO"]
HW_SENSORS = config.CONFIG_DATA["config"]["HW_SENSORS"]
ETH_CARD = config.CONFIG_DATA["config"].get("ETH", "")
WLO_CARD = config.CONFIG_DATA["config"].get("WLO", "")
HW_SENSORS = config.CONFIG_DATA["config"].get("HW_SENSORS", "AUTO")
CPU_FAN = config.CONFIG_DATA["config"].get("CPU_FAN", "AUTO")

if HW_SENSORS == "PYTHON":
if platform.system() == 'Windows':
Expand Down Expand Up @@ -319,7 +320,11 @@ def temperature(cls):

@classmethod
def fan_speed(cls):
fan_percent = sensors.Cpu.fan_percent()
if CPU_FAN != "AUTO":
fan_percent = sensors.Cpu.fan_percent(CPU_FAN)
else:
fan_percent = sensors.Cpu.fan_percent()

save_last_value(fan_percent, cls.last_values_cpu_fan_speed,
config.THEME_DATA['STATS']['CPU']['FAN_SPEED']['LINE_GRAPH'].get("HISTORY_SIZE",
DEFAULT_HISTORY_SIZE))
Expand All @@ -333,7 +338,10 @@ def fan_speed(cls):
fan_percent = 0
if cpu_fan_text_data['SHOW'] or cpu_fan_radial_data['SHOW'] or cpu_fan_graph_data[
'SHOW'] or cpu_fan_line_graph_data['SHOW']:
logger.warning("Your CPU Fan Speed is not supported yet")
if sys.platform == "win32":
logger.warning("Your CPU Fan sensor could not be auto-detected")
else:
logger.warning("Your CPU Fan sensor could not be auto-detected. Select it from Configuration UI.")
cpu_fan_text_data['SHOW'] = False
cpu_fan_radial_data['SHOW'] = False
cpu_fan_graph_data['SHOW'] = False
Expand Down
17 changes: 9 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Python packages requirements
Pillow~=10.3.0 # Image generation
pyserial~=3.5 # Serial link to communicate with the display
PyYAML~=6.0.1 # For themes files
psutil~=5.9.8 # CPU / disk / network metrics
pystray~=0.19.5 # Tray icon (all OS)
babel~=2.15.0 # Date/time formatting
ruamel.yaml~=0.18.6 # For configuration editor
sv-ttk~=2.6.0 # Tk Sun Valley theme for configuration editor
Pillow~=10.3.0 # Image generation
pyserial~=3.5 # Serial link to communicate with the display
PyYAML~=6.0.1 # For themes files
psutil~=5.9.8 # CPU / disk / network metrics
pystray~=0.19.5 # Tray icon (all OS)
babel~=2.15.0 # Date/time formatting
ruamel.yaml~=0.18.6 # For configuration editor
sv-ttk~=2.6.0 # Tk Sun Valley theme for configuration editor
tkinter-tooltip~=3.0.0 # Tooltips for configuration editor

# Efficient image serialization
numpy~=1.24.4; python_version < "3.9" # For Python 3.8 max.
Expand Down

0 comments on commit 0759c80

Please sign in to comment.