Files

489 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import base64
import os
import json
import time
import re
from datetime import datetime
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QStackedWidget, QProgressBar)
from PyQt6.QtCore import Qt, QSize, QTimer
from PyQt6.QtGui import QIcon, QFont, QPixmap
from pages.status_page import StatusPage
from pages.control_page import ControlPage
from pages.setting_page import SettingPage
from utils.aio_print_api import AIOPrrintSystemAPI
from utils.auto_fan_status import AutoFanStatus
from utils.config_parse import ConfigParse
from utils.wifi_manager import WifiManager
from utils.gcode_viewer import GCodeViewerWidget
from utils.get_bootstrap_icon import get_colored_svg_uri
class SplashWidget(QWidget):
"""开屏启动动画,显示 Logo 与初始化进度"""
def __init__(self, fix_width=None, fix_height=None, parent=None):
self.gcode_viewer = parent.gcode_viewer
super().__init__(parent)
self.setStyleSheet("background-color: #222222;")
self.fix_width = fix_width
self.fix_height = fix_height
if self.fix_width is not None and self.fix_height is not None:
self.setFixedSize(self.fix_width,self.fix_height)
# 主布局
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setContentsMargins(40,40,40,40)
layout.setSpacing(20)
# ---- Logo ----
self.logo_label = QLabel()
script_dir = os.path.dirname(os.path.abspath(__file__))
logo_path = os.path.join(script_dir, "assets", "img", "logo.jpg")
if os.path.isfile(logo_path):
pixmap = QPixmap(logo_path)
scaled = pixmap.scaled(400, 400,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
self.logo_label.setPixmap(scaled)
else:
self.logo_label.setText("🖨️")
self.logo_label.setStyleSheet("font-size: 120px; color: #4CAF50;")
self.logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.logo_label)
# ---- 标题 ----
self.title_label = QLabel("Printer Screen Menu")
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title_label.setStyleSheet(
"color: #e0e0e0; font-size: 36px; font-weight: 700;"
"padding: 10px 0 5px 0;"
)
layout.addWidget(self.title_label)
# ---- 副标题 / 版本 ----
self.subtitle_label = QLabel("正在启动 ...")
self.subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.subtitle_label.setStyleSheet(
"color: #888888; font-size: 18px; font-weight: 400;"
)
layout.addWidget(self.subtitle_label)
# ---- 进度条 ----
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # 不确定模式(脉冲动画)
self.progress_bar.setFixedHeight(6)
self.progress_bar.setFixedWidth(500)
self.progress_bar.setTextVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
background-color: #333355;
border: none;
border-radius: 3px;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 3px;
}
""")
bar_container = QHBoxLayout()
bar_container.addStretch()
bar_container.addWidget(self.progress_bar)
bar_container.addStretch()
layout.addLayout(bar_container)
# ---- 进度文字 ----
self.progress_label = QLabel("正在加载配置...")
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.progress_label.setStyleSheet(
"color: #a0d8a0; font-size: 22px; font-weight: 500; padding: 10px;"
)
layout.addWidget(self.progress_label)
layout.addWidget(self.gcode_viewer)
def update_progress(self, text: str):
"""更新进度文字并立即刷新界面"""
self.progress_label.setText(text)
if text.find("界面") != -1:
self.subtitle_label.setText("请稍候 (界面加载时间较长) ...")
else:
self.subtitle_label.setText("请稍候 ...")
QApplication.processEvents()
class TopStacked(QWidget):
def __init__(self, fix_width=None, fix_height=None):
super().__init__()
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(0, 0, 0, 0)
self.root_layout.setSpacing(0)
self.fix_width = fix_width
self.fix_height = fix_height
if self.fix_width is not None and self.fix_height is not None:
self.setFixedSize(self.fix_width,self.fix_height)
self.root_stack = QStackedWidget()
self.root_stack.setStyleSheet("background-color: #222222;") # 深灰色主显示区
# !Important: 先初始化带OpenGL的东西防止 Could not queue DRM page flip on screen HDMI1 (Device or resource busy)
self.gcode_viewer = GCodeViewerWidget()
self.gcode_viewer.setUpdatesEnabled(False)
self.gcode_viewer.hide()
# 1. 模拟的启动画面
self.splash_widget = SplashWidget(fix_width=self.fix_width, fix_height=self.fix_height, parent=self)
self.root_stack.addWidget(self.splash_widget)
# 2. 真实主页面容器
self.main_content = MainWindow(fix_width=self.fix_width, fix_height=self.fix_height, parent=self)
self.root_stack.addWidget(self.main_content)
self.root_layout.addWidget(self.root_stack)
# self.root_layout.addWidget(self.root_container)
# 初始显示启动界面
# self.root_stack.setCurrentWidget(self.splash_widget)
self.root_stack.setCurrentIndex(0)
QTimer.singleShot(100, self.main_content.start_init)
# self.main_content.start_init()
# self.setLayout(self.root_layout)
class MainWindow(QWidget):
def __init__(self, fix_width=None, fix_height=None, parent=None):
super().__init__(parent)
self.fix_width = fix_width
self.fix_height = fix_height
if self.fix_width is not None and self.fix_height is not None:
self.setFixedSize(self.fix_width,self.fix_height)
self.splash_widget = parent.splash_widget
self.root_stack = parent.root_stack
self.gcode_viewer = parent.gcode_viewer
self._last_network_check = 0.0
self._is_network_connected = False
self._clock_has_synced = False
def start_init(self):
"""真正的初始化逻辑,使用定时器链式调用,避免卡死主线程"""
self.splash_widget.update_progress("正在加载配置...")
QTimer.singleShot(100, self._init_step1)
def _init_step1(self):
self.config_parser = ConfigParse()
self.config_parser.config_changed.connect(self._on_config_changed)
self.splash_widget.update_progress("正在初始化 API 客户端...")
QTimer.singleShot(600, self._init_step2)
def _init_step2(self):
self.api_client = AIOPrrintSystemAPI(
api_url=self.config_parser.api_url,
api_key=self.config_parser.api_key
)
self.splash_widget.update_progress("正在检测散热风扇...")
QTimer.singleShot(600, self._init_step3)
def _init_step3(self):
self.auto_fan_status = AutoFanStatus()
self.splash_widget.update_progress("正在检查网络连接...")
QTimer.singleShot(600, self._init_step4)
def _init_step4(self):
self.wifi_manager = WifiManager()
self.splash_widget.update_progress("正在初始化主框架...")
QTimer.singleShot(100, self._init_step5)
def _init_step5(self):
# 整体布局附加到 main_content 占位符
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# 顶部主显示区 (使用 QStackedWidget 切换不同页面)
self.stacked_widget = QStackedWidget()
self.stacked_widget.setStyleSheet("background-color: #555555;") # 深灰色主显示区
self.splash_widget.update_progress("正在加载状态界面...")
QTimer.singleShot(100, self._init_step5_status)
def _init_step5_status(self):
self.page_status = StatusPage(self.api_client, GcodeViewer=self.gcode_viewer)
self.stacked_widget.addWidget(self.page_status)
self.splash_widget.update_progress("正在加载控制界面...")
QTimer.singleShot(100, self._init_step5_control)
def _init_step5_control(self):
self.page_control = ControlPage(self.api_client)
self.stacked_widget.addWidget(self.page_control)
self.splash_widget.update_progress("正在加载设置界面...")
QTimer.singleShot(100, self._init_step5_settings)
def _init_step5_settings(self):
self.page_settings = SettingPage(self.api_client)
self.stacked_widget.addWidget(self.page_settings)
self.splash_widget.update_progress("正在组合界面...")
QTimer.singleShot(100, self._init_step5_finish)
def _init_step5_finish(self):
self.build_ui_bars()
# 定时刷新风扇状态显示
self._fan_timer = QTimer(self)
self._fan_timer.timeout.connect(self._update_top_bar)
self._fan_timer.start(1000)
self._update_top_bar() # 立即刷新横条数据
self.splash_widget.update_progress("启动完成!")
QTimer.singleShot(600, self._init_step6)
def _init_step6(self):
# 切换到真实主界面
self.root_stack.setCurrentIndex(1)
self.gcode_viewer.setUpdatesEnabled(True)
self.gcode_viewer.show()
def _check_network(self):
"""检查网络连接状态每30秒检测一次避免频繁调用"""
now = time.time()
if now - self._last_network_check < 30:
return self._is_network_connected
self._last_network_check = now
try:
status = self.wifi_manager.get_current_status()
self._is_network_connected = status.get("wpa_state") == "COMPLETED"
except Exception:
self._is_network_connected = False
return self._is_network_connected
def _on_config_changed(self, config_instance):
"""配置变化时更新 API 客户端等相关设置"""
self.api_client = AIOPrrintSystemAPI(
api_url=self.config_parser.api_url,
api_key=self.config_parser.api_key
)
def _signal_to_bars(self, signal_val):
"""将信号强度转换为条形图标字符串
支持 dBm负值如 -45和百分比0-100nmcli 格式)
"""
icon_name = "reception-0.svg"
if signal_val is None:
icon_name = "reception-0.svg"
elif signal_val < 0:
# dBm 格式
if signal_val >= -50:
icon_name = "reception-4.svg"
elif signal_val >= -60:
icon_name = "reception-2.svg"
elif signal_val >= -70:
icon_name = "reception-1.svg"
else:
icon_name = "reception-0.svg"
else:
# 百分比格式 (0-100)
if signal_val >= 75:
icon_name = "reception-4.svg"
elif signal_val >= 50:
icon_name = "reception-2.svg"
elif signal_val >= 25:
icon_name = "reception-1.svg"
else:
icon_name = "reception-0.svg"
# 为了能够在深色主题正确显示颜色,通过图片标签引入
colored_uri = get_colored_svg_uri(icon_name, "#a0d8a0", 40, 20, "0 -4 18 18")
return f"<img src='{colored_uri}'>"
def _update_top_bar(self):
"""更新风扇/网络状态横条显示"""
s = self.auto_fan_status
temp = f"{s.cpu_temp:.1f}°C" if s.is_auto_fan_service_running else "--.-°C"
speed_pct = min(s.fan_speed / 255 * 100, 100)
speed = f"{speed_pct:.1f}%" if s.is_auto_fan_service_running else "--.-%"
state = s.fan_state if s.is_auto_fan_service_running else "Auto Fan service is not running"
rpm = f"{s.fan_rpm:.1f} r/min" if s.is_auto_fan_service_running else "--.-- r/min"
# 根据运行状态改变颜色
if s.is_auto_fan_service_running:
if s.fan_state == "Stalled":
color = "#e86c60" # 风扇异常-红色
else:
color = "#a0d8a0" # 正常-浅绿色
else:
color = "#e8a060" # 异常-橙色
self._top_bar.setStyleSheet(
f"background-color: #2a2a2a; color: {color}; "
f"font-size: 18px; font-weight: 600; padding: 4px 16px;"
)
load_color = "#a0d8a0" if s.cpu_load < 1.0 else ("#e8a060" if s.cpu_load < 3.0 else "#e86c60")
cpu_load_str = f"<span style='color:{load_color}'>{s.cpu_load:.2f}</span><span>/4.0</span>"
icon_temp = get_colored_svg_uri("thermometer-half.svg", color, 20, 20, "-4 -2 18 18")
icon_fan = get_colored_svg_uri("fan.svg", color, 20, 20, "-2 -2 18 18")
icon_cpu = get_colored_svg_uri("cpu.svg", load_color, 20, 20, "-2 -2 18 18")
self._fan_label.setText(
f"<img src='{icon_temp}'> {temp} &nbsp; {state} &nbsp; "
f"<img src='{icon_fan}'> {speed} &nbsp; {rpm} &nbsp; "
f"<img src='{icon_cpu}'> {cpu_load_str}"
)
self._fan_label.setTextFormat(Qt.TextFormat.RichText)
# --- WiFi 状态指示 ---
is_connected = self._check_network()
if is_connected:
try:
status = self.wifi_manager.get_current_status()
raw_signal = status.get("signal_level")
signal_dbm = int(raw_signal) if raw_signal else None
except Exception:
signal_dbm = None
bars = self._signal_to_bars(signal_dbm)
self._wifi_label.setText(f"Signal: {bars}")
self._wifi_label.setTextFormat(Qt.TextFormat.RichText)
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
else:
self._wifi_label.setText("No Signal")
self._wifi_label.setStyleSheet("color: #e86c60; font-size: 18px; font-weight: 600;")
# --- 时钟(有网络时更新;断网后保留最后一次的时间) ---
now = datetime.now()
self._clock_label.setText(now.strftime("%H:%M:%S"))
if is_connected:
if not self._clock_has_synced:
self._clock_has_synced = True
self._clock_label.show()
else:
# 从未同步过则隐藏,否则保留上次时间
if self._clock_has_synced:
self._clock_label.show()
else:
self._clock_label.hide()
def build_ui_bars(self):
# 风扇状态横条
self._top_bar = QWidget()
self._top_bar.setFixedHeight(36)
self._top_bar.setStyleSheet(
"background-color: #2a2a2a; color: #a0d8a0; "
"font-size: 18px; font-weight: 600; padding: 4px 16px;"
)
top_layout = QHBoxLayout(self._top_bar)
top_layout.setContentsMargins(16, 0, 16, 0)
self._fan_label = QLabel("🌡 --°C -- 𖣘 --% -- r/min")
self._fan_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
top_layout.addWidget(self._fan_label)
top_layout.addStretch()
# WiFi 状态指示
init_wifi_uri = get_colored_svg_uri('reception-0.svg', '#a0d8a0', 40, 20, "0 -4 18 18")
self._wifi_label = QLabel(f"<img src='{init_wifi_uri}'> --")
self._wifi_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._wifi_label.setTextFormat(Qt.TextFormat.RichText)
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
top_layout.addWidget(self._wifi_label)
# 时钟标签(有网络时显示,获取过一次后断网也不隐藏)
self._clock_label = QLabel("--:--:--")
self._clock_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._clock_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
self._clock_label.hide()
top_layout.addWidget(self._clock_label)
# 底部按钮区
bottom_layout = QHBoxLayout()
bottom_layout.setContentsMargins(10, 10, 10, 10)
bottom_layout.setSpacing(20)
# 定义底部区域背景
bottom_widget = QWidget()
bottom_widget.setStyleSheet("background-color: #333333;")
bottom_widget.setLayout(bottom_layout)
bottom_widget.setFixedHeight(120) # 为触摸优化的按钮高度
# 创建触摸友好的按钮
self.btn_status = self.create_nav_button("状态", self.switchToStatus)
self.btn_control = self.create_nav_button("控制", self.switchToControl)
self.btn_settings = self.create_nav_button("设置", self.switchToSettings)
bottom_layout.addWidget(self.btn_status)
bottom_layout.addWidget(self.btn_control)
bottom_layout.addWidget(self.btn_settings)
# 将主显示区、风扇横条、底部按钮加入整体布局
self.main_layout.addWidget(self._top_bar)
self.main_layout.addWidget(self.stacked_widget)
self.main_layout.addWidget(bottom_widget)
self.setStyleSheet("background-color: #666666;") # 整体灰色背景
def create_nav_button(self, text, callback):
# 如果有图标,可以使用 btn.setIcon(QIcon("path/to/icon.png"))
btn = QPushButton(text)
# 为触摸屏优化:大字体,增加内边距
btn.setStyleSheet("""
QPushButton {
background-color: #444444;
color: white;
border: 2px solid #555555;
border-radius: 10px;
font-size: 32px;
padding: 10px;
}
QPushButton:pressed {
background-color: #222222;
border: 2px solid #4CAF50;
}
""")
btn.setSizePolicy(btn.sizePolicy().Policy.Expanding, btn.sizePolicy().Policy.Expanding)
btn.clicked.connect(callback)
return btn
def switchToStatus(self):
self.stacked_widget.setCurrentIndex(0)
def switchToControl(self):
self.stacked_widget.setCurrentIndex(1)
def switchToSettings(self):
self.stacked_widget.setCurrentIndex(2)
def main():
app = QApplication(sys.argv)
# 隐藏鼠标光标,针对触摸屏优化
app.setOverrideCursor(Qt.CursorShape.BlankCursor)
# # 唯一且纯粹的顶级窗口
# window = MainWindow()
# window.showFullScreen()
# # 待窗口在底层完全拿到 Plane 并开始渲染后,触发后续耗时初始化
# QTimer.singleShot(100, window.start_init)
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
screen = QApplication.primaryScreen()
fix_width = screen.size().width()
fix_height = screen.size().height()
window = TopStacked(fix_width,fix_height)
window.showFullScreen()
sys.exit(app.exec())
if __name__ == "__main__":
main()