From 1b77a3963278fbc02be33d2937db906a56882dd8 Mon Sep 17 00:00:00 2001 From: karminski Date: Sun, 8 Dec 2024 11:04:59 +0800 Subject: [PATCH] REWRITE by pyqt6 --- Makefile | 6 +- admin_manifest.xml | 15 -- fan-lord.spec | 18 +- file_version_info.txt | 27 ++ main.py | 585 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + supermicro-x-series.py | 534 ------------------------------------- 7 files changed, 623 insertions(+), 563 deletions(-) delete mode 100644 admin_manifest.xml create mode 100644 file_version_info.txt create mode 100644 main.py delete mode 100644 supermicro-x-series.py diff --git a/Makefile b/Makefile index 483670f..8b28947 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,8 @@ -.PHONY: install-requirements build-seprate build-bundle +.PHONY: install-requirements build-bundle all: install-requirements build-bundle install-requirements: pip install -r requirements.txt - -build-seprate: - python -m PyInstaller --windowed --uac-admin --add-binary "IPMICFG-Win.exe;." --onefile supermicro-x-series.py - build-bundle: python -m PyInstaller fan-lord.spec diff --git a/admin_manifest.xml b/admin_manifest.xml deleted file mode 100644 index 0e54cbf..0000000 --- a/admin_manifest.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - Fan Lord, modify the fan speed of Supermicro X-Series motherboard including X-9, X-10, X-11 - - - - - - - - diff --git a/fan-lord.spec b/fan-lord.spec index 5fb62db..66206ce 100644 --- a/fan-lord.spec +++ b/fan-lord.spec @@ -3,14 +3,13 @@ block_cipher = None a = Analysis( - ['supermicro-x-series.py'], + ['main.py'], pathex=[], - binaries=[ - ('IPMICFG-Win.exe', '.'), - ('pmdll.dll', '.') - ], + binaries=[], datas=[ - ('fan-lord.ico', '.') + ('IPMICFG-Win.exe', '.'), # 包含 IPMI 工具 + ('pmdll.dll', '.'), # 包含依赖 DLL + ('fan-lord.ico', '.') # 包含图标文件(如果有的话) ], hiddenimports=[], hookspath=[], @@ -32,17 +31,18 @@ exe = EXE( a.zipfiles, a.datas, [], - name='FanLord', + name='Fan-Lord', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, - console=False, + console=False, # 设置为 False 以隐藏控制台窗口 disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None, - icon='fan-lord.ico' + icon='fan-lord.ico', # 设置程序图标 + version='file_version_info.txt', # 版本信息文件 ) diff --git a/file_version_info.txt b/file_version_info.txt new file mode 100644 index 0000000..a84eedc --- /dev/null +++ b/file_version_info.txt @@ -0,0 +1,27 @@ +VSVersionInfo( + ffi=FixedFileInfo( + filevers=(0, 1, 2, 0), + prodvers=(0, 1, 2, 0), + mask=0x3f, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u'KCORES'), + StringStruct(u'FileDescription', u'Fan Lord for Supermicro X-Series'), + StringStruct(u'FileVersion', u'0.1.3'), + StringStruct(u'InternalName', u'Fan-Lord'), + StringStruct(u'LegalCopyright', u'Copyright (c) 2024 KCORES'), + StringStruct(u'OriginalFilename', u'Fan-Lord.exe'), + StringStruct(u'ProductName', u'Fan Lord'), + StringStruct(u'ProductVersion', u'0.1.3')]) + ]) + ] +) diff --git a/main.py b/main.py new file mode 100644 index 0000000..f9c9eff --- /dev/null +++ b/main.py @@ -0,0 +1,585 @@ +import sys +import os +import ctypes +import subprocess +from datetime import datetime +import locale +from PyQt6.QtWidgets import ( + QApplication, + QMainWindow, + QWidget, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QFrame, + QSlider, + QTextEdit, + QMenuBar, + QMenu, + QMessageBox, +) +from PyQt6.QtCore import Qt, QSize +from PyQt6.QtGui import QIcon, QColor, QAction, QActionGroup + +VERSION = "v0.1.3" + + +# Keep original helper functions +def is_admin(): + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except: + return False + + +def run_as_admin(): + try: + if not is_admin(): + ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, " ".join(sys.argv), None, 1 + ) + sys.exit() + except Exception as e: + print(f"Failed to get admin rights: {str(e)}") + sys.exit(1) + + +class CustomSlider(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + layout = QVBoxLayout(self) + + # Create progress bar display area + self.progress_bar = QFrame(self) + self.progress_bar.setFixedHeight(5) + self.progress_bar.setStyleSheet("background-color: #D3D3D3;") + + # Create slider + self.slider = QSlider(Qt.Orientation.Horizontal) + self.slider.setRange(0, 100) + self.slider.valueChanged.connect(self.update_progress) + self.slider.sliderReleased.connect(self.on_slider_released) + + layout.addWidget(self.progress_bar) + layout.addWidget(self.slider) + layout.setSpacing(5) + + # Add variable to store last value + self.last_value = 0 + + def update_progress(self, value): + threshold = 30 + if value < threshold: + color = "#D3D3D3" # Gray + else: + color = "#90EE90" # Light green + self.progress_bar.setStyleSheet(f"background-color: {color};") + + def value(self): + return self.slider.value() + + def on_slider_released(self): + """Triggered when slider is released""" + current_value = self.slider.value() + if current_value != self.last_value: + self.last_value = current_value + # Emit custom signal + if hasattr(self, "value_changed_on_release"): + self.value_changed_on_release(current_value) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + # Set application icon + icon_path = self.get_icon_path() + if icon_path: + self.setWindowIcon(QIcon(icon_path)) + # Initialize language configuration + self.init_languages() + # Initialize IPMI tool path + self.init_ipmi_tool() + self.init_ui() + + def get_icon_path(self): + """Get icon path""" + if getattr(sys, "frozen", False): + # If running as executable + base_path = sys._MEIPASS + else: + # If running as Python script + base_path = os.path.dirname(os.path.abspath(__file__)) + + icon_path = os.path.join(base_path, "fan-lord.ico") + return icon_path if os.path.exists(icon_path) else None + + def init_languages(self): + """Initialize language configuration""" + self.languages = { + "中文": { + "window_title": "Fan Lord for Supermicro X-Series", + "preset_modes": "预设模式", + "silent_mode": "静音模式", + "performance_mode": "性能模式", + "full_speed_mode": "全速模式", + "manual_control": "手动控制", + "cpu_fan_speed": "CPU风扇转速", + "peripheral_fan_speed": "外设风扇转速", + "warning_text": "注意:如果数值小于30%,BMC可能会自动重置风扇转速为全速", + "reset_auto": "重置为自动控制", + "status_info": "状态信息", + "created_by": "Created by: ", + "this_is_a": " | This is a ", + "project": " opensource project", + "language_menu": "语言", + "execute_command": "执行命令", + "command_success": "命令执行成功!", + "command_failed": "命令执行失败:", + "command_error": "执行出错:", + }, + "English": { + "window_title": "Fan Lord for Supermicro X-Series", + "preset_modes": "Preset Modes", + "silent_mode": "Silent Mode", + "performance_mode": "Performance Mode", + "full_speed_mode": "Full Speed Mode", + "manual_control": "Manual Control", + "cpu_fan_speed": "CPU Fan Speed", + "peripheral_fan_speed": "Peripheral Fan Speed", + "warning_text": "Note: If the value is less than 30%, BMC may automatically reset fan speed to full speed", + "reset_auto": "Reset to Auto Control", + "status_info": "Status Information", + "created_by": "Created by: ", + "this_is_a": " | This is a ", + "project": " opensource project", + "language_menu": "Language", + "execute_command": "Execute command", + "command_success": "Command executed successfully!", + "command_failed": "Command execution failed:", + "command_error": "Error executing command:", + }, + "日本語": { + "window_title": "Fan Lord for Supermicro X-Series", + "preset_modes": "プリセットモード", + "silent_mode": "サイレントモード", + "performance_mode": "パフォーマンスモード", + "full_speed_mode": "フルスピードモード", + "manual_control": "手動制御", + "cpu_fan_speed": "CPUファン速度", + "peripheral_fan_speed": "周辺機器ファン速度", + "warning_text": "注意:値が30%未満の場合、BMCが自動的にファン速度をフルスピードにリセットする可能性があります", + "reset_auto": "自動制御にリセット", + "status_info": "ステータス情報", + "created_by": "作成者: ", + "this_is_a": " | これは ", + "project": " オープンソースプロジェクトです", + "language_menu": "言語", + "execute_command": "コマンドを実行", + "command_success": "コマンドが正常に実行されました!", + "command_failed": "コマンド実行に失敗しました:", + "command_error": "コマンドの実行中にエラーが発生しました:", + }, + } + self.current_language = self.get_system_language() + + def get_system_language(self): + """Get system language""" + try: + system_lang = locale.getdefaultlocale()[0] + lang_mapping = { + "zh": "中文", + "ja": "日本語", + "en": "English", + } + system_lang_prefix = system_lang.split("_")[0].lower() + return lang_mapping.get(system_lang_prefix, "English") + except: + return "English" + + def change_language(self, language): + """Switch interface language""" + try: + self.current_language = language + self.update_texts() + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to change language: {str(e)}") + + def update_texts(self): + """Update all interface texts""" + lang = self.languages[self.current_language] + + # Update window title + self.setWindowTitle(lang["window_title"]) + + # Update menu bar - use class attributes directly + self.language_menu.setTitle(lang["language_menu"]) + + # Update preset modes area + preset_frame = self.findChild(QFrame, "preset_frame") + if preset_frame: + preset_frame.findChild(QLabel, "preset_title").setText(lang["preset_modes"]) + preset_frame.findChild(QPushButton, "silent_btn").setText( + lang["silent_mode"] + ) + preset_frame.findChild(QPushButton, "performance_btn").setText( + lang["performance_mode"] + ) + preset_frame.findChild(QPushButton, "full_speed_btn").setText( + lang["full_speed_mode"] + ) + + # Update manual control area + manual_frame = self.findChild(QFrame, "manual_frame") + if manual_frame: + manual_frame.findChild(QLabel, "manual_title").setText( + lang["manual_control"] + ) + manual_frame.findChild(QLabel, "cpu_label").setText(lang["cpu_fan_speed"]) + manual_frame.findChild(QLabel, "peripheral_label").setText( + lang["peripheral_fan_speed"] + ) + manual_frame.findChild(QLabel, "warning_label").setText( + lang["warning_text"] + ) + manual_frame.findChild(QPushButton, "reset_btn").setText(lang["reset_auto"]) + + # Update status information area + status_frame = self.findChild(QFrame, "status_frame") + if status_frame: + status_frame.findChild(QLabel, "status_title").setText(lang["status_info"]) + + def init_ipmi_tool(self): + """Initialize IPMI tool path""" + if getattr(sys, "frozen", False): + # If running as executable + base_path = sys._MEIPASS + else: + # If running as Python script + base_path = os.path.dirname(os.path.abspath(__file__)) + + self.ipmi_exe = os.path.join(base_path, "IPMICFG-Win.exe") + + # Check if IPMI tool exists + if not os.path.exists(self.ipmi_exe): + QMessageBox.critical( + self, "Error", f"IPMICFG-Win.exe not found at: {self.ipmi_exe}" + ) + sys.exit(1) + + def execute_command(self, command): + """Execute IPMI command and update status""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.update_status( + f"[{current_time}] Execute command: {command}\nCommand executed successfully!\n", + "success", + ) + else: + self.update_status( + f"[{current_time}] Execute command: {command}\nCommand execution failed:\n{result.stderr}\n", + "error", + ) + except Exception as e: + self.update_status( + f"[{current_time}] Execute command: {command}\nError executing command:\n{str(e)}\n", + "error", + ) + + def update_status(self, message, status_type): + """Update status information display""" + color = "red" if status_type == "error" else "green" + self.status_text.setTextColor(QColor(color)) + self.status_text.append(message) + # Scroll to bottom + self.status_text.verticalScrollBar().setValue( + self.status_text.verticalScrollBar().maximum() + ) + + def init_ui(self): + # Set window basic properties + self.setWindowTitle("Fan Lord for Supermicro X-Series") + self.setMinimumSize(800, 600) + + # Create central widget + central_widget = QWidget() + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # Create menu bar + self.create_menu_bar() + + # Create preset modes area + self.create_preset_modes() + + # Create manual control area + self.create_manual_control() + + # Create status information area + self.create_status_area() + + # Create footer + self.create_footer() + + def create_menu_bar(self): + menubar = self.menuBar() + + # Create language menu and save as class attribute + self.language_menu = QMenu( + self.languages[self.current_language]["language_menu"], self + ) + self.language_menu.setObjectName("language_menu") + menubar.addMenu(self.language_menu) + + # Add language options + languages = ["中文", "English", "日本語"] + language_group = QActionGroup(self) + for lang in languages: + action = QAction(lang, self, checkable=True) + language_group.addAction(action) + self.language_menu.addAction(action) + if lang == self.current_language: # Set default based on system language + action.setChecked(True) + action.triggered.connect(lambda checked, l=lang: self.change_language(l)) + + def create_preset_modes(self): + # Create preset modes frame + preset_frame = QFrame() + preset_frame.setObjectName("preset_frame") + preset_frame.setFrameStyle(QFrame.Shape.Box | QFrame.Shadow.Sunken) + preset_layout = QVBoxLayout(preset_frame) + + # Title + title = QLabel(self.languages[self.current_language]["preset_modes"]) + title.setObjectName("preset_title") + title.setStyleSheet("font-weight: bold;") + preset_layout.addWidget(title) + + # Button container + button_layout = QHBoxLayout() + + # Create preset mode buttons and set object names + silent_btn = QPushButton(self.languages[self.current_language]["silent_mode"]) + silent_btn.setObjectName("silent_btn") + performance_btn = QPushButton( + self.languages[self.current_language]["performance_mode"] + ) + performance_btn.setObjectName("performance_btn") + full_speed_btn = QPushButton( + self.languages[self.current_language]["full_speed_mode"] + ) + full_speed_btn.setObjectName("full_speed_btn") + + # Connect signals + silent_btn.clicked.connect(self.silent_mode) + performance_btn.clicked.connect(self.performance_mode) + full_speed_btn.clicked.connect(self.full_speed_mode) + + # Add buttons to layout + button_layout.addWidget(silent_btn) + button_layout.addWidget(performance_btn) + button_layout.addWidget(full_speed_btn) + button_layout.addStretch() + + preset_layout.addLayout(button_layout) + self.centralWidget().layout().addWidget(preset_frame) + + def create_manual_control(self): + # Create manual control frame + manual_frame = QFrame() + manual_frame.setObjectName("manual_frame") + manual_frame.setFrameStyle(QFrame.Shape.Box | QFrame.Shadow.Sunken) + manual_layout = QVBoxLayout(manual_frame) + + # Title + title = QLabel(self.languages[self.current_language]["manual_control"]) + title.setObjectName("manual_title") + title.setStyleSheet("font-weight: bold;") + manual_layout.addWidget(title) + + # CPU fan control + cpu_control_layout = QHBoxLayout() + cpu_label = QLabel(self.languages[self.current_language]["cpu_fan_speed"]) + cpu_label.setObjectName("cpu_label") + self.cpu_percentage = QLabel("0%") # Add percentage label + cpu_control_layout.addWidget(cpu_label) + cpu_control_layout.addWidget(self.cpu_percentage) + cpu_control_layout.addStretch() + + self.cpu_slider = CustomSlider() + self.cpu_slider.value_changed_on_release = self.on_cpu_slider_release + self.cpu_slider.slider.valueChanged.connect( + lambda value: self.cpu_percentage.setText(f"{value}%") + ) + + # Peripheral fan control + peripheral_control_layout = QHBoxLayout() + peripheral_label = QLabel( + self.languages[self.current_language]["peripheral_fan_speed"] + ) + peripheral_label.setObjectName("peripheral_label") + self.peripheral_percentage = QLabel("0%") # Add percentage label + peripheral_control_layout.addWidget(peripheral_label) + peripheral_control_layout.addWidget(self.peripheral_percentage) + peripheral_control_layout.addStretch() + + self.peripheral_slider = CustomSlider() + self.peripheral_slider.value_changed_on_release = ( + self.on_peripheral_slider_release + ) + self.peripheral_slider.slider.valueChanged.connect( + lambda value: self.peripheral_percentage.setText(f"{value}%") + ) + + # Warning text + warning_label = QLabel(self.languages[self.current_language]["warning_text"]) + warning_label.setObjectName("warning_label") + warning_label.setStyleSheet("color: red;") + warning_label.setWordWrap(True) + + # Reset button + reset_btn = QPushButton(self.languages[self.current_language]["reset_auto"]) + reset_btn.setObjectName("reset_btn") + reset_btn.clicked.connect(self.reset_fan_control) + + # Add components to layout + manual_layout.addLayout(cpu_control_layout) + manual_layout.addWidget(self.cpu_slider) + manual_layout.addLayout(peripheral_control_layout) + manual_layout.addWidget(self.peripheral_slider) + manual_layout.addWidget(warning_label) + manual_layout.addWidget(reset_btn) + + self.centralWidget().layout().addWidget(manual_frame) + + def create_status_area(self): + # Create status information frame + status_frame = QFrame() + status_frame.setObjectName("status_frame") + status_frame.setFrameStyle(QFrame.Shape.Box | QFrame.Shadow.Sunken) + status_layout = QVBoxLayout(status_frame) + + # Title + title = QLabel(self.languages[self.current_language]["status_info"]) + title.setObjectName("status_title") + title.setStyleSheet("font-weight: bold;") + status_layout.addWidget(title) + + # Status text box + self.status_text = QTextEdit() + self.status_text.setReadOnly(True) + status_layout.addWidget(self.status_text) + + self.centralWidget().layout().addWidget(status_frame) + + def create_footer(self): + footer_frame = QFrame() + footer_layout = QHBoxLayout(footer_frame) + + # Author information + author_label = QLabel("Created by: ") + author_link = QLabel('karminski') + author_link.setOpenExternalLinks(True) + + # Project information + project_label = QLabel(" | This is a ") + project_link = QLabel('KCORES') + project_link.setOpenExternalLinks(True) + project_suffix = QLabel(" opensource project") + + # Version information + version_link = QLabel( + f'{VERSION}' + ) + version_link.setOpenExternalLinks(True) + + # Add components to layout + footer_layout.addWidget(author_label) + footer_layout.addWidget(author_link) + footer_layout.addWidget(project_label) + footer_layout.addWidget(project_link) + footer_layout.addWidget(project_suffix) + footer_layout.addStretch() + footer_layout.addWidget(version_link) + + self.centralWidget().layout().addWidget(footer_frame) + + # Implement control function slots + def silent_mode(self): + """Silent mode: Set CPU and peripheral fans to 40% speed""" + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x28') + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x28') + # Update slider positions + self.cpu_slider.slider.setValue(40) + self.peripheral_slider.slider.setValue(40) + + def performance_mode(self): + """Performance mode: CPU fan 50%, peripheral fan 100%""" + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x32') + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x64') + # Update slider positions + self.cpu_slider.slider.setValue(50) + self.peripheral_slider.slider.setValue(100) + + def full_speed_mode(self): + """Full speed mode: All fans 100%""" + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x64') + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x64') + # Update slider positions + self.cpu_slider.slider.setValue(100) + self.peripheral_slider.slider.setValue(100) + + def on_cpu_slider_release(self, value): + """CPU fan speed control - only triggered when slider is released""" + cpu_value = format(value, "x").zfill(2) + self.execute_command( + f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x{cpu_value}' + ) + + def on_peripheral_slider_release(self, value): + """Peripheral fan speed control - only triggered when slider is released""" + peripheral_value = format(value, "x").zfill(2) + self.execute_command( + f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x{peripheral_value}' + ) + + def reset_fan_control(self): + """Reset to automatic control mode""" + self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x45 0x01 0x01') + # Reset slider positions + self.cpu_slider.slider.setValue(0) + self.peripheral_slider.slider.setValue(0) + + +if __name__ == "__main__": + # Check administrator privileges + try: + if not is_admin(): + # Request administrator privileges and restart program + ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, " ".join(sys.argv), None, 1 + ) + sys.exit() + + # If already has administrator privileges, start program normally + app = QApplication(sys.argv) + # Set application icon (shown in start menu and taskbar) + icon_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "fan-lord.ico" + ) + if os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + window = MainWindow() + window.show() + sys.exit(app.exec()) + except Exception as e: + # If failed to get administrator privileges, show error message + if QApplication.instance() is None: + app = QApplication(sys.argv) + QMessageBox.critical( + None, "Error", f"Failed to get administrator privileges:\n{str(e)}" + ) + sys.exit(1) diff --git a/requirements.txt b/requirements.txt index 6f219be..192e61c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyinstaller>=6.0.0 pywin32>=223 +PyQt6>=6.6.0 diff --git a/supermicro-x-series.py b/supermicro-x-series.py deleted file mode 100644 index 88b0bd9..0000000 --- a/supermicro-x-series.py +++ /dev/null @@ -1,534 +0,0 @@ -import tkinter as tk -from tkinter import messagebox -import subprocess -import os -import sys -import ctypes -from datetime import datetime -import locale - -# 在文件顶部添加版本常量 -VERSION = "v0.1.2" - - -def is_admin(): - try: - return ctypes.windll.shell32.IsUserAnAdmin() - except: - return False - - -def run_as_admin(): - try: - if not is_admin(): - # Restart program with admin privileges - ctypes.windll.shell32.ShellExecuteW( - None, "runas", sys.executable, " ".join(sys.argv), None, 1 - ) - sys.exit() - except Exception as e: - messagebox.showerror("Error", f"Failed to get admin rights:\n{str(e)}") - sys.exit(1) - - -def get_resource_path(relative_path): - """Get absolute path to resource file""" - try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS - except Exception: - # If not packaged, use current folder - base_path = os.path.abspath(os.path.dirname(__file__)) - - return os.path.join(base_path, relative_path) - - -def get_system_language(): - """Get system language and match it with available languages""" - try: - system_lang = locale.getdefaultlocale()[0] - - # Map system language codes to our available languages - lang_mapping = { - "zh": "中文", # Chinese - "ja": "日本語", # Japanese - "en": "English", # English - } - - # Get the first two letters of language code (e.g., 'zh_CN' -> 'zh') - system_lang_prefix = system_lang.split("_")[0].lower() - - # Return matched language or default to English - return lang_mapping.get(system_lang_prefix, "English") - except: - return "English" - - -class CustomScale(tk.Frame): - def __init__(self, master, **kwargs): - super().__init__(master) - self.canvas = tk.Canvas(self, height=20, highlightthickness=0) - self.canvas.pack(fill="x", pady=(0, 5)) - self.scale = tk.Scale(self, **kwargs) - self.scale.pack(fill="x") - - self.update_background() - self.scale.configure(command=self.update_background) - - def update_background(self, *args): - self.canvas.delete("all") - width = self.canvas.winfo_width() - if width == 0: # Width may be 0 during window initialization - self.canvas.after(10, self.update_background) - return - - value = self.scale.get() - threshold = 30 - threshold_x = width * (threshold / 100) - current_x = width * (value / 100) - - # Draw gray part (0-30) - if threshold_x > 0: - self.canvas.create_rectangle( - 0, 0, min(threshold_x, current_x), 5, fill="#D3D3D3", outline="" - ) - - # Draw green part (30-100) - if current_x > threshold_x: - self.canvas.create_rectangle( - threshold_x, 0, current_x, 5, fill="#90EE90", outline="" - ) - - def bind(self, sequence=None, func=None, add=None): - self.scale.bind(sequence, func, add) - - def get(self): - return self.scale.get() - - -class IPMIGui: - def __init__(self, root): - self.root = root - - # Add language configuration - self.languages = { - "中文": { - "window_title": "Fan Lord for Supermicro X-Series", - "preset_modes": "预设模式", - "silent_mode": "静音模式", - "performance_mode": "性能模式", - "full_speed_mode": "全速模式", - "manual_control": "手动控制", - "cpu_fan_speed": "CPU风扇转速", - "peripheral_fan_speed": "外设风扇转速", - "warning_text": "注意:如果数值小于30%,BMC可能会自动重置风扇转速为全速", - "reset_auto": "重置为自动控制", - "status_info": "状态信息", - "created_by": "Created by: ", - "this_is_a": " | This is a ", - "project": " opensource project", - "language_menu": "语言", - "execute_command": "执行命令", - "command_success": "命令执行成功!", - "command_failed": "命令执行失败:", - "command_error": "执行命令时出错:", - }, - "English": { - "window_title": "Fan Lord for Supermicro X-Series", - "preset_modes": "Preset Modes", - "silent_mode": "Silent Mode", - "performance_mode": "Performance Mode", - "full_speed_mode": "Full Speed Mode", - "manual_control": "Manual Control", - "cpu_fan_speed": "CPU Fan Speed", - "peripheral_fan_speed": "Peripheral Fan Speed", - "warning_text": "Note: If the value is less than 30%, BMC may automatically reset fan speed to full speed", - "reset_auto": "Reset to Auto Control", - "status_info": "Status Information", - "created_by": "Created by: ", - "this_is_a": " | This is a ", - "project": " opensource project", - "language_menu": "Language", - "execute_command": "Execute command", - "command_success": "Command executed successfully!", - "command_failed": "Command execution failed:", - "command_error": "Error executing command:", - }, - "日本語": { - "window_title": "Fan Lord for Supermicro X-Series", - "preset_modes": "プリセットモード", - "silent_mode": "サイレントモード", - "performance_mode": "パフォーマンスモード", - "full_speed_mode": "フルスピードモード", - "manual_control": "手動制御", - "cpu_fan_speed": "CPUファン速度", - "peripheral_fan_speed": "周辺機器ファン速度", - "warning_text": "注意:値が30%未満の場合、BMCが自動的にファン速度をフルスピードにリセットする可能性があります", - "reset_auto": "自動制御にリセット", - "status_info": "ステータス情報", - "created_by": "作成者: ", - "this_is_a": " | これは ", - "project": " オープンソースプロジェクトです", - "language_menu": "言語", - "execute_command": "コマンドを実行", - "command_success": "コマンドが正常に実行されました!", - "command_failed": "コマンドの実行に失敗しました:", - "command_error": "コマンドの実行中にエラーが発生しました:", - }, - } - - # Initialize language based on system settings - self.current_language = get_system_language() - # Create a class variable to store StringVar - self.language_var = tk.StringVar(value=self.current_language) - - # Create menu bar - self.menubar = tk.Menu(root) - self.root.config(menu=self.menubar) - - # Create language menu - self.language_menu = tk.Menu(self.menubar, tearoff=0) - self.menubar.add_cascade(label="语言", menu=self.language_menu) - - # Add language options - for lang in self.languages.keys(): - self.language_menu.add_radiobutton( - label=lang, - variable=self.language_var, # Use class variable - value=lang, # Set option value - command=lambda l=lang: self.change_language(l), - ) - - # Set window icon - if getattr(sys, "frozen", False): - # If running as packaged executable - icon_path = os.path.join(sys._MEIPASS, "fan-lord.ico") - else: - # If running as Python script - icon_path = "fan-lord.ico" - - self.root.iconbitmap(icon_path) - - # Get executable directory - if getattr(sys, "frozen", False): - # If running as packaged executable - self.current_dir = os.path.dirname(sys.executable) - else: - # If running as Python script - self.current_dir = os.path.dirname(os.path.abspath(__file__)) - - # Modify this part - # Use get_resource_path to get IPMI tool path - self.ipmi_exe = get_resource_path("IPMICFG-Win.exe") - - # Add debug info - print(f"IPMI exe path: {self.ipmi_exe}") - print(f"File exists: {os.path.exists(self.ipmi_exe)}") - - # Check if IPMI tool exists - if not os.path.exists(self.ipmi_exe): - messagebox.showerror( - "Error", f"IPMICFG-Win.exe not found at: {self.ipmi_exe}" - ) - root.destroy() - return - - # Set window size and position - window_width = 800 - window_height = 600 - screen_width = root.winfo_screenwidth() - screen_height = root.winfo_screenheight() - center_x = int(screen_width / 2 - window_width / 2) - center_y = int(screen_height / 2 - window_height / 2) - self.root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}") - - # Create mode selection frame - mode_frame = tk.LabelFrame(root, text="预设模式", padx=10, pady=5) - mode_frame.pack(fill="x", padx=10, pady=5) - - tk.Button(mode_frame, text="静音模式", command=self.silent_mode).pack( - side=tk.LEFT, padx=5 - ) - tk.Button(mode_frame, text="性能模式", command=self.performance_mode).pack( - side=tk.LEFT, padx=5 - ) - tk.Button(mode_frame, text="全速模式", command=self.full_speed_mode).pack( - side=tk.LEFT, padx=5 - ) - - # Create manual control frame - manual_frame = tk.LabelFrame(root, text="手动控制", padx=10, pady=5) - manual_frame.pack(fill="x", padx=10, pady=5) - - # CPU fan control - tk.Label(manual_frame, text="CPU风扇转速").pack() - self.cpu_scale = CustomScale( - manual_frame, - from_=0, - to=100, - orient=tk.HORIZONTAL, - ) - self.cpu_scale.bind("", self.on_cpu_scale_release) - self.cpu_scale.pack(fill="x") - - # Peripheral fan control - tk.Label(manual_frame, text="外设风扇转速").pack() - self.peripheral_scale = CustomScale( - manual_frame, - from_=0, - to=100, - orient=tk.HORIZONTAL, - ) - self.peripheral_scale.bind( - "", self.on_peripheral_scale_release - ) - self.peripheral_scale.pack(fill="x") - - # Add warning label - warning_label = tk.Label( - manual_frame, - text="注意:如果数值小于30%,BMC可能会自动重置风扇转速为全速", - fg="red", - wraplength=600, # Text auto wrap width - ) - warning_label.pack(pady=5) - - # Add reset button - tk.Button( - manual_frame, text="重置为自动控制", command=self.reset_fan_control - ).pack(pady=5) - - # Create status display area - status_frame = tk.LabelFrame(root, text="状态信息", padx=10, pady=5) - status_frame.pack(fill="both", expand=True, padx=10, pady=5) - - # Add scrollbar - scrollbar = tk.Scrollbar(status_frame) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.status_text = tk.Text( - status_frame, height=10, wrap=tk.WORD, yscrollcommand=scrollbar.set - ) - self.status_text.pack(fill="both", expand=True) - scrollbar.config(command=self.status_text.yview) - - # Add credits frame - credits_frame = tk.Frame(root) - credits_frame.pack(fill="x", padx=10, pady=5) - - # Add version label with link (修改) - version_label = tk.Label( - credits_frame, text=f"{VERSION}", fg="blue", cursor="hand2" - ) - version_label.pack(side=tk.RIGHT) - version_label.bind( - "", lambda e: self.open_url("https://github.com/KCORES/fan-lord") - ) - - project_suffix = tk.Label(credits_frame, text=" opensource project") - project_suffix.pack(side=tk.RIGHT) - - project_link = tk.Label(credits_frame, text="KCORES", fg="blue", cursor="hand2") - project_link.pack(side=tk.RIGHT) - project_link.bind( - "", lambda e: self.open_url("https://github.com/kcores") - ) - - project_label = tk.Label(credits_frame, text=" | This is a ") - project_label.pack(side=tk.RIGHT) - - author_link = tk.Label( - credits_frame, text="karminski", fg="blue", cursor="hand2" - ) - author_link.pack(side=tk.RIGHT) - author_link.bind( - "", lambda e: self.open_url("https://github.com/karminski") - ) - - author_label = tk.Label(credits_frame, text="Created by: ") - author_label.pack(side=tk.RIGHT) - - # Update all text to current language - self.update_texts() - - def update_texts(self): - """Update all interface text to currently selected language""" - lang = self.languages[self.current_language] - - # Update window title - self.root.title(lang["window_title"]) - - # Update menu bar text - self.menubar.entryconfigure(1, label=lang["language_menu"]) - - # Update frames and their components - for widget in self.root.winfo_children(): - if isinstance(widget, tk.LabelFrame): - # Update LabelFrame titles - if ( - "预设模式" in widget.cget("text") - or "Preset" in widget.cget("text") - or "プリセット" in widget.cget("text") - ): - widget.configure(text=lang["preset_modes"]) - elif ( - "手动控制" in widget.cget("text") - or "Manual" in widget.cget("text") - or "手動" in widget.cget("text") - ): - widget.configure(text=lang["manual_control"]) - elif ( - "状态信息" in widget.cget("text") - or "Status" in widget.cget("text") - or "ステータス" in widget.cget("text") - ): - widget.configure(text=lang["status_info"]) - - # Update components inside frames - for child in widget.winfo_children(): - # Update button text - if isinstance(child, tk.Button): - if any( - x in child.cget("text") - for x in ["静音模式", "Silent", "サイレント"] - ): - child.configure(text=lang["silent_mode"]) - elif any( - x in child.cget("text") - for x in ["性能模式", "Performance", "パフォーマンス"] - ): - child.configure(text=lang["performance_mode"]) - elif any( - x in child.cget("text") - for x in ["全速模式", "Full Speed", "フルスピード"] - ): - child.configure(text=lang["full_speed_mode"]) - elif any( - x in child.cget("text") - for x in ["重置为自动控制", "Reset", "自動制御"] - ): - child.configure(text=lang["reset_auto"]) - - # Update label text - elif isinstance(child, tk.Label): - if any( - x in child.cget("text") - for x in ["CPU风扇转速", "CPU Fan", "CPUファン"] - ): - child.configure(text=lang["cpu_fan_speed"]) - elif any( - x in child.cget("text") - for x in ["外设风扇转速", "Peripheral Fan", "周辺機器"] - ): - child.configure(text=lang["peripheral_fan_speed"]) - elif ( - "注意" in child.cget("text") - or "Note" in child.cget("text") - or "注意" in child.cget("text") - ): - child.configure(text=lang["warning_text"]) - - # Recursively process nested components - if hasattr(child, "winfo_children"): - for grandchild in child.winfo_children(): - if isinstance(grandchild, tk.Label): - if any( - x in grandchild.cget("text") - for x in ["CPU风扇转速", "CPU Fan", "CPUファン"] - ): - grandchild.configure(text=lang["cpu_fan_speed"]) - elif any( - x in grandchild.cget("text") - for x in [ - "外设风扇转速", - "Peripheral Fan", - "周辺機器", - ] - ): - grandchild.configure( - text=lang["peripheral_fan_speed"] - ) - - def change_language(self, new_language): - """Switch interface language""" - self.current_language = new_language - self.update_texts() - - def execute_command(self, command): - current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - lang = self.languages[self.current_language] - - try: - result = subprocess.run(command, shell=True, capture_output=True, text=True) - if result.returncode == 0: - self.update_status( - f"[{current_time}] {lang['execute_command']}: {command} {lang['command_success']}\n", - "success", - ) - else: - self.update_status( - f"[{current_time}] {lang['execute_command']}: {command} {lang['command_failed']}\n{result.stderr}\n", - "error", - ) - except Exception as e: - self.update_status( - f"[{current_time}] {lang['execute_command']}: {command} {lang['command_error']}\n{str(e)}\n", - "error", - ) - - def update_status(self, message, status_type): - if status_type == "error": - tag = "error" - color = "red" - else: - tag = "success" - color = "green" - - self.status_text.tag_config(tag, foreground=color) - self.status_text.insert(tk.END, message, tag) - self.status_text.see(tk.END) # Auto scroll to latest content - self.root.update() - - def silent_mode(self): - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x28') - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x28') - - def performance_mode(self): - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x32') - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x64') - - def full_speed_mode(self): - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x64') - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x64') - - def on_cpu_scale_release(self, event): - value = self.cpu_scale.get() - cpu_value = format(int(value), "x").zfill(2) - self.execute_command( - f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x00 0x{cpu_value}' - ) - - def on_peripheral_scale_release(self, event): - value = self.peripheral_scale.get() - peripheral_value = format(int(value), "x").zfill(2) - self.execute_command( - f'"{self.ipmi_exe}" -raw 0x30 0x70 0x66 0x01 0x01 0x{peripheral_value}' - ) - - def reset_fan_control(self): - self.execute_command(f'"{self.ipmi_exe}" -raw 0x30 0x45 0x01 0x01') - - # Add method to open URL - def open_url(self, url): - import webbrowser - - webbrowser.open(url) - - -if __name__ == "__main__": - # Check admin privileges - run_as_admin() - - root = tk.Tk() - app = IPMIGui(root) - root.mainloop()