diff --git a/config.yaml b/config.yaml index 7e992f47..ef8c7d18 100644 --- a/config.yaml +++ b/config.yaml @@ -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" diff --git a/configure.py b/configure.py index 5cb3b688..a46387fe 100755 --- a/configure.py +++ b/configure.py @@ -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") @@ -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" @@ -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("", 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") @@ -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() @@ -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("", lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:])) + self.theme_author.bind("", + lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:])) else: self.theme_author.config(foreground="#a3a3a3", cursor="") self.theme_author.unbind("") @@ -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() @@ -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()) @@ -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: @@ -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() diff --git a/library/sensors/sensors.py b/library/sensors/sensors.py index adbc9bb7..a23766b9 100644 --- a/library/sensors/sensors.py +++ b/library/sensors/sensors.py @@ -46,7 +46,7 @@ def temperature() -> float: @staticmethod @abstractmethod - def fan_percent() -> float: + def fan_percent(fan_name: str = None) -> float: pass diff --git a/library/sensors/sensors_librehardwaremonitor.py b/library/sensors/sensors_librehardwaremonitor.py index 791cee3d..7f95df11 100644 --- a/library/sensors/sensors_librehardwaremonitor.py +++ b/library/sensors/sensors_librehardwaremonitor.py @@ -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: diff --git a/library/sensors/sensors_python.py b/library/sensors/sensors_python.py index a312baa3..cbcee89c 100644 --- a/library/sensors/sensors_python.py +++ b/library/sensors/sensors_python.py @@ -22,6 +22,7 @@ import math import platform import sys +from collections import namedtuple from enum import IntEnum, auto from typing import Tuple @@ -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. @@ -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) @@ -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: @@ -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 @@ -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 @@ -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: diff --git a/library/sensors/sensors_stub_random.py b/library/sensors/sensors_stub_random.py index 227d2f97..28a5f4b8 100644 --- a/library/sensors/sensors_stub_random.py +++ b/library/sensors/sensors_stub_random.py @@ -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) diff --git a/library/sensors/sensors_stub_static.py b/library/sensors/sensors_stub_static.py index 6ebc2d49..e5485df0 100644 --- a/library/sensors/sensors_stub_static.py +++ b/library/sensors/sensors_stub_static.py @@ -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 diff --git a/library/stats.py b/library/stats.py index 0617ff2e..e01157e9 100644 --- a/library/stats.py +++ b/library/stats.py @@ -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': @@ -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)) @@ -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 diff --git a/requirements.txt b/requirements.txt index fb78cefc..2dd9fbe4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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.