Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add CPU fan selection for Python libraries on Linux #484

Merged
merged 11 commits into from
Jun 18, 2024
Merged
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
Loading