489 lines
19 KiB
Python
489 lines
19 KiB
Python
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-100,nmcli 格式)
|
||
"""
|
||
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} {state} "
|
||
f"<img src='{icon_fan}'> {speed} {rpm} "
|
||
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()
|