diff --git a/python/legion_linux/legion_linux/legion.py b/python/legion_linux/legion_linux/legion.py index 51c3969..1eef284 100755 --- a/python/legion_linux/legion_linux/legion.py +++ b/python/legion_linux/legion_linux/legion.py @@ -9,6 +9,11 @@ import logging import subprocess import yaml +import sys +import struct +import zlib +from datetime import datetime +from PIL import Image # import jsonrpyc # import inotify.adapters @@ -20,7 +25,8 @@ DEFAULT_CONFIG_DIR = "/etc/legion_linux" LEGION_SYS_BASEPATH = '/sys/module/legion_laptop/drivers/platform:legion/PNP0C09:00' IDEAPAD_SYS_BASEPATH = '/sys/bus/platform/drivers/ideapad_acpi/VPC2004:00' - +LBLDVC_FILE = "/sys/firmware/efi/efivars/LBLDVC-871455d1-5576-4fb8-9865-af0824463c9f" +LBLDESP_FILE = "/sys/firmware/efi/efivars/LBLDESP-871455d0-5576-4fb8-9865-af0824463b9e" def is_root_user(): return os.geteuid() == 0 @@ -1458,6 +1464,107 @@ def __init__(self, expect_hwmon=True, use_legion_cli_to_write=False, config_dir= self.settings_manager.add_feature(self.app_model.enable_gui_monitoring) self.settings_manager.add_feature(self.app_model.icon_color_mode) + def _run_sudo(self, cmd_args): + subprocess.run(["sudo"] + cmd_args, check=True) + + def _replace_efi_file(self, original_file, new_file): + self._run_sudo(["chattr", "-i", original_file]) + self._run_sudo(["cp", new_file, original_file]) + self._run_sudo(["chattr", "+i", original_file]) + + def _backup_file(self, file_path, timestamp): + base_name = os.path.basename(file_path) + tmp_path = os.path.join("/tmp", base_name) + shutil.copy(file_path, tmp_path) + + backup_path = os.path.join("/tmp", f"{base_name}_{timestamp}.bak") + shutil.copy(file_path, backup_path) + log.info(f"Backup of {base_name} created: {backup_path}") + return tmp_path, backup_path + + def _calculate_crc32(self, file_path, length=512): + with open(file_path, 'rb') as file: + data = file.read(length) + return zlib.crc32(data) & 0xFFFFFFFF + + def _read_file(self, file_path): + with open(file_path, 'rb') as f: + return f.read() + + def _check_image_dimensions_and_format(self, image_path, expected_width, expected_height): + with Image.open(image_path) as img: + img_width, img_height = img.size + img_format = img.format.lower() + if (expected_width != 0 and img_width != expected_width) or \ + (expected_height != 0 and img_height != expected_height): + raise ValueError( + f"Image dimensions do not match: expect {expected_width}x{expected_height}, " + f"got {img_width}x{img_height}." + ) + if img_format not in ['jpeg', 'png', 'bmp']: + raise ValueError( + f"Image format '{img_format.upper()}' is not supported (only JPG/PNG/BMP)." + ) + return img_width, img_height, img_format + + def get_boot_logo_status(self): + data = self._read_file(LBLDESP_FILE) + if len(data) < 13: + log.warning("LBLDESP data is unexpectedly short.") + return False, 0, 0 + + fifth_byte = data[4] + width = int.from_bytes(data[5:9], byteorder='little') + height = int.from_bytes(data[9:13], byteorder='little') + is_on = (fifth_byte == 0x01) + return is_on, width, height + + def enable_boot_logo(self, image_path): + is_on, expected_width, expected_height = self.get_boot_logo_status() + log.info(f"Current LBLDESP is ON={is_on}, required size={expected_width}x{expected_height}") + img_w, img_h, img_fmt = self._check_image_dimensions_and_format(image_path, expected_width, expected_height) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + tmp_lbldvc, _ = self._backup_file(LBLDVC_FILE, timestamp) + image_checksum = self._calculate_crc32(image_path, 512) + with open(tmp_lbldvc, "r+b") as f: + f.seek(8) + f.write(struct.pack('argparse.ArgumentParser: 'values', type=str, help='Value of feature', nargs='+') set_feature_cmd.set_defaults( func=set_feature) + + bootlogo_parser = subcommands.add_parser('boot-logo', help="Custom Boot Logo") + bootlogo_sub = bootlogo_parser.add_subparsers(dest='bootlogo_cmd') + enable_parser = bootlogo_sub.add_parser('enable', help='Set Boot Logo') + enable_parser.add_argument('image_path', type=str, help='Path to the image to be used') + enable_parser.set_defaults(func=lambda legion, **kw: boot_logo_enable(legion, **kw)) + restore_parser = bootlogo_sub.add_parser('restore', help='Restore modified boot logo') + restore_parser.set_defaults(func=lambda legion, **kw: boot_logo_restore(legion, **kw)) + status_parser = bootlogo_sub.add_parser('status', help='View status') + status_parser.set_defaults(func=lambda legion, **kw: boot_logo_status(legion, **kw)) + return parser, subcommands +def boot_logo_enable(legion: LegionModelFacade, image_path: str, **kwargs) -> int: + try: + legion.enable_boot_logo(image_path) + print("Boot Logo enabled.") + return 0 + except Exception as e: + print(f"Error enabling Boot Logo: {e}") + return 1 + +def boot_logo_restore(legion: LegionModelFacade, **kwargs) -> int: + try: + legion.restore_boot_logo() + print("Boot Logo restored.") + return 0 + except Exception as e: + print(f"Error restoring boot logo: {e}") + return 1 + +def boot_logo_status(legion: LegionModelFacade, **kwargs) -> int: + is_on, w, h = legion.get_boot_logo_status() + print(f"Current Boot Logo status: {'ON' if is_on else 'OFF'}; Required image dimensions: {w} x {h}") + return 0 + def main(): parser, subcommands = create_argparser() diff --git a/python/legion_linux/legion_linux/legion_gui.py b/python/legion_linux/legion_linux/legion_gui.py index c107fc7..b96c9e8 100755 --- a/python/legion_linux/legion_linux/legion_gui.py +++ b/python/legion_linux/legion_linux/legion_gui.py @@ -14,7 +14,7 @@ from PyQt6.QtGui import QAction, QGuiApplication from PyQt6.QtWidgets import QApplication, QMainWindow, QTabWidget, QWidget, QLabel, \ QVBoxLayout, QGridLayout, QLineEdit, QPushButton, QComboBox, QGroupBox, \ - QCheckBox, QSystemTrayIcon, QMenu, QScrollArea, QMessageBox, QSpinBox, QTextBrowser, QHBoxLayout + QCheckBox, QSystemTrayIcon, QMenu, QScrollArea, QMessageBox, QSpinBox, QTextBrowser, QHBoxLayout, QFileDialog # Make it possible to run without installation # pylint: disable=# pylint: disable=wrong-import-position sys.path.insert(0, os.path.dirname(__file__) + "/..") @@ -1187,6 +1187,44 @@ def init_ui(self): self.main_layout.addStretch() self.setLayout(self.main_layout) + self.bootlogo_group = QGroupBox("Boot Logo") + self.bootlogo_layout = QVBoxLayout() + self.bootlogo_group.setLayout(self.bootlogo_layout) + self.bootlogo_checkbox = QCheckBox("Enable Boot Logo") + self.bootlogo_checkbox.clicked.connect(self.on_bootlogo_toggled) + self.bootlogo_layout.addWidget(self.bootlogo_checkbox) + self.select_image_btn = QPushButton("Select Image") + self.select_image_btn.clicked.connect(self.on_select_image) + self.bootlogo_layout.addWidget(self.select_image_btn) + self.main_layout.addWidget(self.bootlogo_group) + self.update_bootlogo_view() + + def update_bootlogo_view(self): + is_on, w, h = self.controller.model.get_boot_logo_status() + self.bootlogo_checkbox.setChecked(is_on) + + def on_bootlogo_toggled(self): + if self.bootlogo_checkbox.isChecked(): + self.on_select_image() + else: + try: + self.controller.model.restore_boot_logo() + QMessageBox.information(self, "Success", "Boot Logo restored.") + except Exception as e: + QMessageBox.critical(self, "Error", f"Restore failed: {e}") + self.update_bootlogo_view() + + def on_select_image(self): + path, _ = QFileDialog.getOpenFileName(self, "Select Image", "", "Images (*.png *.jpg *.bmp)") + if path: + try: + self.controller.model.enable_boot_logo(path) + QMessageBox.information(self, "Success", f"Boot Logo enabled with {path}.") + except Exception as e: + QMessageBox.critical(self, "Error", f"Enable failed: {e}") + self.bootlogo_checkbox.setChecked(False) + self.update_bootlogo_view() + def init_power_ui(self): # pylint: disable=too-many-statements self.power_group = QGroupBox("Power Options")