Skip to content

Commit 0759c80

Browse files
authored
Merge pull request #484 from mathoudebine/fix/473-gpu-and-fps-fan-error
2 parents e1d7d8a + 0cfaff7 commit 0759c80

9 files changed

+131
-42
lines changed

config.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ config:
2626
ETH: "" # Ethernet Card
2727
WLO: "" # Wi-Fi Card
2828

29+
# CPU fan
30+
# For Linux/MacOS platforms, the CPU fan is amongst all fan sensors gathered from the motherboard chipset
31+
# If value is AUTO the system monitor will try to auto-select the CPU fan
32+
# If auto-detection fails, it might be necessary to manually indicate which fan is the CPU fan
33+
# Value must be 'controller/fan' e.g. 'nct6798/fan2'. Use configuration wizard for help in selection
34+
CPU_FAN: AUTO
35+
2936
display:
3037
# Display revision:
3138
# - A for Turing 3.5" and UsbPCMonitor 3.5"/5"

configure.py

+62-6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import sv_ttk
5252
from PIL import Image
5353
from serial.tools.list_ports import comports
54+
from tktooltip import ToolTip
5455
except:
5556
print(
5657
"[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 @@
5960
except:
6061
os._exit(0)
6162

63+
from library.sensors.sensors_python import sensors_fans, is_cpu_fan
64+
6265
TURING_MODEL = "Turing Smart Screen"
6366
USBPCMONITOR_MODEL = "UsbPCMonitor"
6467
XUANFANG_MODEL = "XuanFang rev. B & flagship"
@@ -141,14 +144,28 @@ def get_net_if():
141144
return if_list
142145

143146

147+
def get_fans():
148+
fan_list = list()
149+
auto_detected_cpu_fan = "None"
150+
for name, entries in sensors_fans().items():
151+
for entry in entries:
152+
fan_list.append("%s/%s (%d%% - %d RPM)" % (name, entry.label, entry.percent, entry.current))
153+
if (is_cpu_fan(entry.label) or is_cpu_fan(name)) and auto_detected_cpu_fan == "None":
154+
auto_detected_cpu_fan = "Auto-detected: %s/%s" % (name, entry.label)
155+
156+
fan_list.insert(0, auto_detected_cpu_fan) # Add manual entry on top if auto-detection succeeded
157+
return fan_list
158+
159+
144160
class TuringConfigWindow:
145161
def __init__(self):
146162
self.window = Tk()
147163
self.window.title('Turing System Monitor configuration')
148-
self.window.geometry("770x550")
164+
self.window.geometry("770x570")
149165
self.window.iconphoto(True, PhotoImage(file="res/icons/monitor-icon-17865/64.png"))
150166
# When window gets focus again, reload theme preview in case it has been updated by theme editor
151167
self.window.bind("<FocusIn>", self.on_theme_change)
168+
self.window.after(0, self.on_fan_speed_update)
152169

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

244+
# For Windows platform only
227245
self.lhm_admin_warning = ttk.Label(self.window,
228246
text="❌ Restart as admin. or select another Hardware monitoring",
229247
foreground='#f00')
248+
# For platform != Windows
249+
self.cpu_fan_label = ttk.Label(self.window, text='CPU fan (?)')
250+
self.cpu_fan_label.config(foreground="#a3a3ff", cursor="hand2")
251+
self.cpu_fan_cb = ttk.Combobox(self.window, values=get_fans(), state='readonly')
252+
253+
self.tooltip = ToolTip(self.cpu_fan_label,
254+
msg="If \"None\" is selected, CPU fan was not auto-detected.\n"
255+
"Manually select your CPU fan from the list.\n\n"
256+
"Fans missing from the list? Install lm-sensors package\n"
257+
"and run 'sudo sensors-detect' command, then reboot.")
230258

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

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

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

240268
self.config = None
241269
self.load_config_values()
@@ -261,7 +289,8 @@ def load_theme_preview(self):
261289
self.theme_author.config(text="Author: " + author_name)
262290
if author_name.startswith("@"):
263291
self.theme_author.config(foreground="#a3a3ff", cursor="hand2")
264-
self.theme_author.bind("<Button-1>", lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:]))
292+
self.theme_author.bind("<Button-1>",
293+
lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:]))
265294
else:
266295
self.theme_author.config(foreground="#a3a3a3", cursor="")
267296
self.theme_author.unbind("<Button-1>")
@@ -336,6 +365,14 @@ def load_config_values(self):
336365
except:
337366
self.brightness_slider.set(50)
338367

368+
try:
369+
if self.config['config']['CPU_FAN'] == "AUTO":
370+
self.cpu_fan_cb.current(0)
371+
else:
372+
self.cpu_fan_cb.set(self.config['config']['CPU_FAN'])
373+
except:
374+
self.cpu_fan_cb.current(0)
375+
339376
# Reload content on screen
340377
self.on_model_change()
341378
self.on_size_change()
@@ -358,6 +395,10 @@ def save_config_values(self):
358395
self.config['config']['COM_PORT'] = "AUTO"
359396
else:
360397
self.config['config']['COM_PORT'] = self.com_cb.get()
398+
if self.cpu_fan_cb.current() == 0:
399+
self.config['config']['CPU_FAN'] = "AUTO"
400+
else:
401+
self.config['config']['CPU_FAN'] = self.cpu_fan_cb.get().split(' ')[0]
361402
self.config['display']['REVISION'] = model_and_size_to_revision_map[(self.model_cb.get(), self.size_cb.get())]
362403
self.config['display']['DISPLAY_REVERSE'] = [k for k, v in reverse_map.items() if v == self.orient_cb.get()][0]
363404
self.config['display']['BRIGHTNESS'] = int(self.brightness_slider.get())
@@ -421,11 +462,18 @@ def on_hwlib_change(self, e=None):
421462
import ctypes
422463
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
423464
if (hwlib == "LHM" or hwlib == "AUTO") and not is_admin:
424-
self.lhm_admin_warning.place(x=320, y=455)
465+
self.lhm_admin_warning.place(x=320, y=460)
425466
self.save_run_btn.state(["disabled"])
426467
else:
427468
self.lhm_admin_warning.place_forget()
428469
self.save_run_btn.state(["!disabled"])
470+
else:
471+
if hwlib == "PYTHON" or hwlib == "AUTO":
472+
self.cpu_fan_label.place(x=320, y=460)
473+
self.cpu_fan_cb.place(x=500, y=455, width=250)
474+
else:
475+
self.cpu_fan_label.place_forget()
476+
self.cpu_fan_cb.place_forget()
429477

430478
def show_hide_brightness_warning(self, e=None):
431479
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):
434482
else:
435483
self.brightness_warning_label.place_forget()
436484

485+
def on_fan_speed_update(self):
486+
# Update fan speed periodically
487+
prev_value = self.cpu_fan_cb.current() # Save currently selected index
488+
self.cpu_fan_cb.config(values=get_fans())
489+
if prev_value != -1:
490+
self.cpu_fan_cb.current(prev_value) # Force select same index to refresh displayed value
491+
self.window.after(500, self.on_fan_speed_update)
492+
437493

438494
if __name__ == "__main__":
439495
configurator = TuringConfigWindow()

library/sensors/sensors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def temperature() -> float:
4646

4747
@staticmethod
4848
@abstractmethod
49-
def fan_percent() -> float:
49+
def fan_percent(fan_name: str = None) -> float:
5050
pass
5151

5252

library/sensors/sensors_librehardwaremonitor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def temperature() -> float:
239239
return math.nan
240240

241241
@staticmethod
242-
def fan_percent() -> float:
242+
def fan_percent(fan_name: str = None) -> float:
243243
mb = get_hw_and_update(Hardware.HardwareType.Motherboard)
244244
try:
245245
for sh in mb.SubHardware:

library/sensors/sensors_python.py

+36-19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import math
2323
import platform
2424
import sys
25+
from collections import namedtuple
2526
from enum import IntEnum, auto
2627
from typing import Tuple
2728

@@ -58,8 +59,8 @@ class GpuType(IntEnum):
5859

5960

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

7576
ret = collections.defaultdict(list)
@@ -82,19 +83,31 @@ def sensors_fans_percent():
8283
basenames = sorted(set([x.split('_')[0] for x in basenames]))
8384
for base in basenames:
8485
try:
85-
current = int(bcat(base + '_input'))
86-
max = int(bcat(base + '_max'))
87-
min = int(bcat(base + '_min'))
88-
percent = int((current - min) / (max - min) * 100)
86+
current_rpm = int(bcat(base + '_input'))
87+
try:
88+
max_rpm = int(bcat(base + '_max'))
89+
except:
90+
max_rpm = 1500 # Approximated: max fan speed is 1500 RPM
91+
try:
92+
min_rpm = int(bcat(base + '_min'))
93+
except:
94+
min_rpm = 0 # Approximated: min fan speed is 0 RPM
95+
percent = int((current_rpm - min_rpm) / (max_rpm - min_rpm) * 100)
8996
except (IOError, OSError) as err:
9097
continue
9198
unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip()
92-
label = cat(base + '_label', fallback='').strip()
93-
ret[unit_name].append(sfan(label, percent))
99+
label = cat(base + '_label', fallback=os.path.basename(base)).strip()
100+
101+
custom_sfan = namedtuple('sfan', ['label', 'current', 'percent'])
102+
ret[unit_name].append(custom_sfan(label, current_rpm, percent))
94103

95104
return dict(ret)
96105

97106

107+
def is_cpu_fan(label: str) -> bool:
108+
return ("cpu" in label.lower()) or ("proc" in label.lower())
109+
110+
98111
class Cpu(sensors.Cpu):
99112
@staticmethod
100113
def percentage(interval: float) -> float:
@@ -140,14 +153,18 @@ def temperature() -> float:
140153
return cpu_temp
141154

142155
@staticmethod
143-
def fan_percent() -> float:
156+
def fan_percent(fan_name: str = None) -> float:
144157
try:
145-
fans = sensors_fans_percent()
158+
fans = sensors_fans()
146159
if fans:
147160
for name, entries in fans.items():
148161
for entry in entries:
149-
if "cpu" in (entry.label or name):
150-
return entry.current
162+
if fan_name is not None and fan_name == "%s/%s" % (name, entry.label):
163+
# Manually selected fan
164+
return entry.percent
165+
elif is_cpu_fan(entry.label) or is_cpu_fan(name):
166+
# Auto-detected fan
167+
return entry.percent
151168
except:
152169
pass
153170

@@ -255,12 +272,12 @@ def fps() -> int:
255272
@staticmethod
256273
def fan_percent() -> float:
257274
try:
258-
fans = sensors_fans_percent()
275+
fans = sensors_fans()
259276
if fans:
260277
for name, entries in fans.items():
261278
for entry in entries:
262-
if "gpu" in (entry.label or name):
263-
return entry.current
279+
if "gpu" in (entry.label.lower() or name.lower()):
280+
return entry.percent
264281
except:
265282
pass
266283

@@ -336,12 +353,12 @@ def fps() -> int:
336353
def fan_percent() -> float:
337354
try:
338355
# Try with psutil fans
339-
fans = sensors_fans_percent()
356+
fans = sensors_fans()
340357
if fans:
341358
for name, entries in fans.items():
342359
for entry in entries:
343-
if "gpu" in (entry.label or name):
344-
return entry.current
360+
if "gpu" in (entry.label.lower() or name.lower()):
361+
return entry.percent
345362

346363
# Try with pyadl if psutil did not find GPU fan
347364
if pyadl:

library/sensors/sensors_stub_random.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def temperature() -> float:
4343
return random.uniform(30, 90)
4444

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

4949

library/sensors/sensors_stub_static.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def temperature() -> float:
5656
return TEMPERATURE_SENSOR_VALUE
5757

5858
@staticmethod
59-
def fan_percent() -> float:
59+
def fan_percent(fan_name: str = None) -> float:
6060
return PERCENTAGE_SENSOR_VALUE
6161

6262

library/stats.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@
3737

3838
DEFAULT_HISTORY_SIZE = 10
3939

40-
ETH_CARD = config.CONFIG_DATA["config"]["ETH"]
41-
WLO_CARD = config.CONFIG_DATA["config"]["WLO"]
42-
HW_SENSORS = config.CONFIG_DATA["config"]["HW_SENSORS"]
40+
ETH_CARD = config.CONFIG_DATA["config"].get("ETH", "")
41+
WLO_CARD = config.CONFIG_DATA["config"].get("WLO", "")
42+
HW_SENSORS = config.CONFIG_DATA["config"].get("HW_SENSORS", "AUTO")
43+
CPU_FAN = config.CONFIG_DATA["config"].get("CPU_FAN", "AUTO")
4344

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

320321
@classmethod
321322
def fan_speed(cls):
322-
fan_percent = sensors.Cpu.fan_percent()
323+
if CPU_FAN != "AUTO":
324+
fan_percent = sensors.Cpu.fan_percent(CPU_FAN)
325+
else:
326+
fan_percent = sensors.Cpu.fan_percent()
327+
323328
save_last_value(fan_percent, cls.last_values_cpu_fan_speed,
324329
config.THEME_DATA['STATS']['CPU']['FAN_SPEED']['LINE_GRAPH'].get("HISTORY_SIZE",
325330
DEFAULT_HISTORY_SIZE))
@@ -333,7 +338,10 @@ def fan_speed(cls):
333338
fan_percent = 0
334339
if cpu_fan_text_data['SHOW'] or cpu_fan_radial_data['SHOW'] or cpu_fan_graph_data[
335340
'SHOW'] or cpu_fan_line_graph_data['SHOW']:
336-
logger.warning("Your CPU Fan Speed is not supported yet")
341+
if sys.platform == "win32":
342+
logger.warning("Your CPU Fan sensor could not be auto-detected")
343+
else:
344+
logger.warning("Your CPU Fan sensor could not be auto-detected. Select it from Configuration UI.")
337345
cpu_fan_text_data['SHOW'] = False
338346
cpu_fan_radial_data['SHOW'] = False
339347
cpu_fan_graph_data['SHOW'] = False

requirements.txt

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Python packages requirements
2-
Pillow~=10.3.0 # Image generation
3-
pyserial~=3.5 # Serial link to communicate with the display
4-
PyYAML~=6.0.1 # For themes files
5-
psutil~=5.9.8 # CPU / disk / network metrics
6-
pystray~=0.19.5 # Tray icon (all OS)
7-
babel~=2.15.0 # Date/time formatting
8-
ruamel.yaml~=0.18.6 # For configuration editor
9-
sv-ttk~=2.6.0 # Tk Sun Valley theme for configuration editor
2+
Pillow~=10.3.0 # Image generation
3+
pyserial~=3.5 # Serial link to communicate with the display
4+
PyYAML~=6.0.1 # For themes files
5+
psutil~=5.9.8 # CPU / disk / network metrics
6+
pystray~=0.19.5 # Tray icon (all OS)
7+
babel~=2.15.0 # Date/time formatting
8+
ruamel.yaml~=0.18.6 # For configuration editor
9+
sv-ttk~=2.6.0 # Tk Sun Valley theme for configuration editor
10+
tkinter-tooltip~=3.0.0 # Tooltips for configuration editor
1011

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

0 commit comments

Comments
 (0)