Skip to content

Commit

Permalink
Boot logo changer
Browse files Browse the repository at this point in the history
  • Loading branch information
Biaogo committed Jan 25, 2025
1 parent f8900aa commit 2151397
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 2 deletions.
109 changes: 108 additions & 1 deletion python/legion_linux/legion_linux/legion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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('<I', image_checksum))
tmp_lbldesp, _ = self._backup_file(LBLDESP_FILE, timestamp)
with open(tmp_lbldesp, "r+b") as f:
f.seek(4)
f.write(b'\x01')
self._replace_efi_file(LBLDVC_FILE, tmp_lbldvc)
self._replace_efi_file(LBLDESP_FILE, tmp_lbldesp)
self.boot_dir = "/boot"
self.logo_dir = "/EFI/Lenovo/Logo"
os.remove(tmp_lbldvc)
os.remove(tmp_lbldesp)
log.info("Boot logo has been enabled successfully in EFIVars.")
full_logo_dir = os.path.join(self.boot_dir, self.logo_dir.lstrip("/"))
if os.path.exists(full_logo_dir):
for filename in os.listdir(full_logo_dir):
file_path = os.path.join(full_logo_dir, filename)
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
os.makedirs(full_logo_dir, exist_ok=True)
dest_filename = f"mylogo_{img_w}x{img_h}.{img_fmt}"
dest_path = os.path.join(full_logo_dir, dest_filename)
self._run_sudo(["cp", image_path, dest_path])
log.info(f"Image copied to {dest_path}")

def restore_boot_logo(self):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
tmp_lbldesp, _ = self._backup_file(LBLDESP_FILE, timestamp)
with open(tmp_lbldesp, "r+b") as f:
f.seek(4)
f.write(b'\x00')

self._replace_efi_file(LBLDESP_FILE, tmp_lbldesp)
os.remove(tmp_lbldesp)
log.info("Boot logo has been restored in EFIVars.")

@staticmethod
def is_root_user():
return is_root_user()
Expand Down
34 changes: 34 additions & 0 deletions python/legion_linux/legion_linux/legion_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,42 @@ def create_argparser()->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()

Expand Down
40 changes: 39 additions & 1 deletion python/legion_linux/legion_linux/legion_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__) + "/..")
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 2151397

Please sign in to comment.