Files
AIO_3D_Print_Local_Screen/pages/status_page.py

393 lines
16 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 os
import json
import re
from PyQt6.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout,
QPushButton, QLabel, QFrame, QGraphicsView,
QGraphicsScene, QGraphicsPathItem, QProgressBar)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF, QSize
from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath, QFont, QLinearGradient, QBrush
from utils.config_parse import ConfigParse
import sys
import os
from utils.gcode_viewer import GCodeViewerWidget
# ── 状态主题色 ──────────────────────────────────────────
STATUS_COLORS = {
"Operational": "#4CAF50",
"Printing": "#2196F3",
"Paused": "#FF9800",
"Error": "#F44336",
"Offline": "#9E9E9E",
"Cancelling": "#FF5722",
"Finishing": "#8BC34A",
}
STATUS_LABELS = {
"Operational": "就绪",
"Printing": "打印中",
"Paused": "已暂停",
"Error": "错误",
"Offline": "离线",
"Cancelling": "取消中",
"Finishing": "整理中",
}
class CardFrame(QFrame):
"""统一的信息卡片样式"""
def __init__(self, title="", parent=None):
super().__init__(parent)
self.setStyleSheet("""
CardFrame {
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 8px;
}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 10, 12, 10)
layout.setSpacing(4)
if title:
title_lbl = QLabel(title)
title_lbl.setStyleSheet("color: #aaaaaa; font-size: 13px; font-weight: 500; border: none;")
layout.addWidget(title_lbl)
self.content_layout = layout
def add_row(self, label, value_widget):
row = QHBoxLayout()
row.setSpacing(8)
lbl = QLabel(label)
lbl.setStyleSheet("color: #cccccc; font-size: 15px; border: none;")
lbl.setFixedWidth(70)
row.addWidget(lbl)
row.addWidget(value_widget, 1)
self.content_layout.addLayout(row)
class TempGauge(QWidget):
"""温度计指示器"""
def __init__(self, label="Tool", temp_range=(0, 300), parent=None):
super().__init__(parent)
self.setFixedSize(160, 90)
self._label = label
self._actual = 0.0
self._target = 0.0
self._max_temp = temp_range[1]
self._min_temp = temp_range[0]
def set_value(self, actual, target, temp_range=None):
if temp_range is not None:
self._max_temp = temp_range[1]
self._min_temp = temp_range[0]
self._actual = actual
self._target = target
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
w, h = self.width(), self.height()
# 背景条
bar_x, bar_w = 60, 20
bar_y, bar_h = 10, 56
p.setPen(QPen(QColor("#555555"), 1))
p.setBrush(QBrush(QColor("#2a2a2a")))
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
# 填充柱(按温度比例,最高 300°C
ratio = min(max((self._actual - self._min_temp) / (self._max_temp - self._min_temp), 0), 1)
fill_h = int((bar_h - 4) * ratio)
if fill_h > 0:
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
grad.setColorAt(0, QColor("#f57c00"))
grad.setColorAt(1, QColor("#e53935") if self._actual > 200 else QColor("#ffb74d"))
p.setBrush(QBrush(grad))
p.setPen(Qt.PenStyle.NoPen)
p.drawRoundedRect(bar_x + 2, bar_y + bar_h - 2 - fill_h, bar_w - 4, fill_h, 3, 3)
# 目标值标记线
if self._target > 0:
tgt_y = bar_y + bar_h - int((bar_h - 4) * min((self._target - self._min_temp) / (self._max_temp - self._min_temp), 1))
p.setPen(QPen(QColor("#888888"), 2))
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
# 文字
font = QFont("sans-serif", 11, QFont.Weight.Bold)
p.setFont(font)
temp_to_hex_soft = lambda t: "#{:02x}{:02x}{:02x}".format(*((lambda x: ((int(255*(0.3+0.7*(x/0.5)**0.8)), int(180*(x/0.5)**0.9), 255)if x < 0.5 else(255, int(180*(1-((x-0.5)/0.5)**1.2)), 80)))(max(0, min(t - self._min_temp, self._max_temp - self._min_temp))/(self._max_temp - self._min_temp))))
p.setPen(QPen(QColor(temp_to_hex_soft(self._actual))))
p.drawText(0, int(bar_y + bar_h*(1-ratio)-12), w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._actual:>5.1f}°C")
if self._target > 0:
tgt_y = bar_y + bar_h - int((bar_h - 4) * min((self._target - self._min_temp) / (self._max_temp - self._min_temp), 1))
font2 = QFont("sans-serif", 10)
p.setFont(font2)
p.setPen(QPen(QColor("#888888")))
p.drawText(90, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._target:.1f}°C")
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
p.setFont(font3)
p.setPen(QPen(QColor("#aaaaaa")))
p.drawText(0, h - 20, w-20, 20, Qt.AlignmentFlag.AlignCenter, self._label)
# ── 状态页面 ────────────────────────────────────────────
class StatusPage(QWidget):
def __init__(self, api_client, GcodeViewer=None, parent=None):
super().__init__(parent)
self.api_client = api_client
self.gcode_viewer = GcodeViewer
self.file_name = "None"
self.progress = 0.0
self.filepos = 0
self.display_name = "None"
self.state = "Unknown"
self.print_time = 0
self.print_time_left = 0
self.tool_temp_actual = 0.0
self.tool_temp_target = 0.0
self.bed_temp_actual = 0.0
self.bed_temp_target = 0.0
self.config_parser = ConfigParse()
self.config_parser.config_changed.connect(self._on_config_changed)
self.gcode_dir = self.config_parser.gcode_dir
self.tool_temp_range = (self.config_parser.hotend_temp_range.get("min", 0), self.config_parser.hotend_temp_range.get("max", 300))
self.bed_temp_range = (self.config_parser.bed_temp_range.get("min", 0), self.config_parser.bed_temp_range.get("max", 120))
self._loaded_file = None
self.init_ui()
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_status)
self.timer.start(1000)
self.update_status()
def _on_config_changed(self, config_instance):
self.gcode_dir = self.config_parser.gcode_dir
self.tool_temp_range = (self.config_parser.hotend_temp_range.get("min", 0), self.config_parser.hotend_temp_range.get("max", 300))
self.bed_temp_range = (self.config_parser.bed_temp_range.get("min", 0), self.config_parser.bed_temp_range.get("max", 120))
def fresh_status_valve(self):
data = self.api_client.get_status()
if data:
status = data.get("status", {})
job = data.get("job", {})
self.file_name = job.get("job", {}).get("file", {}).get("name", "None")
self.progress = job.get("progress", {}).get("completion", 0) or 0
self.filepos = job.get("progress", {}).get("filepos", 0) or 0
self.display_name = job.get("job", {}).get("file", {}).get("display_name", "None")
self.state = status.get("state", {}).get("text", "Offline")
self.print_time = job.get("progress", {}).get("printTime", 0) or 0
self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0) or 0
temp = status.get("temperature", {})
self.tool_temp_actual = (temp.get("tool0", {}) or {}).get("actual", 0) or 0
self.tool_temp_target = (temp.get("tool0", {}) or {}).get("target", 0) or 0
self.bed_temp_actual = (temp.get("bed", {}) or {}).get("actual", 0) or 0
self.bed_temp_target = (temp.get("bed", {}) or {}).get("target", 0) or 0
@staticmethod
def format_time(seconds):
if seconds is None or seconds == 0:
return "N/A"
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
if h > 0:
return f"{h}h {m:02d}m {s:02d}s"
elif m > 0:
return f"{m}m {s:02d}s"
else:
return f"{s}s"
def _make_card(self, title=""):
card = CardFrame(title)
return card
def _truncate(self, text, max_len=22):
return text if len(text) <= max_len else text[:max_len - 2] + ""
def init_ui(self):
self.fresh_status_valve()
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(8, 8, 8, 8)
main_layout.setSpacing(8)
# ── 左侧信息面板 ──────────────────────────────────
left_frame = QFrame()
left_frame.setStyleSheet("background-color: #333333; border-radius: 10px;")
left_layout = QVBoxLayout(left_frame)
left_layout.setContentsMargins(10, 10, 10, 10)
left_layout.setSpacing(6)
# — 状态徽章 —
self._status_badge = QLabel()
self._status_badge.setFixedHeight(40)
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
font_badge = QFont("sans-serif", 18, QFont.Weight.Bold)
self._status_badge.setFont(font_badge)
left_layout.addWidget(self._status_badge)
# — 文件信息卡片 —
self._file_card = CardFrame("当前文件")
self._file_name_lbl = QLabel("--")
self._file_name_lbl.setStyleSheet("color: #ffffff; font-size: 16px; font-weight: 600; border: none;")
self._file_name_lbl.setWordWrap(True)
self._file_card.content_layout.addWidget(self._file_name_lbl)
left_layout.addWidget(self._file_card)
# — 进度卡片 —
self._progress_card = CardFrame("打印进度")
# 进度条
self._progress_bar = QProgressBar()
self._progress_bar.setTextVisible(True)
self._progress_bar.setFixedHeight(28)
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 5px;
}
""")
self._progress_card.content_layout.addWidget(self._progress_bar)
# 时间行
time_row = QHBoxLayout()
time_row.setSpacing(10)
self._time_elapsed_lbl = QLabel("已用: --")
self._time_elapsed_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
self._time_left_lbl = QLabel("剩余: --")
self._time_left_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
time_row.addWidget(self._time_elapsed_lbl)
time_row.addStretch()
time_row.addWidget(self._time_left_lbl)
self._progress_card.content_layout.addLayout(time_row)
left_layout.addWidget(self._progress_card)
# — 温度卡片 —
self._temp_card = CardFrame("温度")
temp_row = QHBoxLayout()
temp_row.setSpacing(8)
self._tool_gauge = TempGauge("喷头", self.tool_temp_range)
self._bed_gauge = TempGauge("热床", self.bed_temp_range)
temp_row.addWidget(self._tool_gauge)
temp_row.addWidget(self._bed_gauge)
temp_row.addStretch()
self._temp_card.content_layout.addLayout(temp_row)
left_layout.addWidget(self._temp_card)
left_layout.addStretch()
# ── 右侧预留区域 ─────────────────────────────
right_frame = QFrame()
right_frame.setStyleSheet("background-color: #3a3a3a; border-radius: 10px;")
self.right_layout = QVBoxLayout(right_frame)
self.right_layout.setContentsMargins(6, 6, 6, 6)
# self.gcode_viewer = GCodeViewerWidget()
if self.gcode_viewer is not None:
self.right_layout.addWidget(self.gcode_viewer)
# self.gcode_viewer.setUpdatesEnabled(False)
# self.gcode_viewer.hide()
main_layout.addWidget(left_frame, 2)
main_layout.addWidget(right_frame, 3)
# QTimer.singleShot(5000, self.init_gcode_viewer)
# def init_gcode_viewer(self):
# self.gcode_viewer = GCodeViewerWidget()
# self.right_layout.addWidget(self.gcode_viewer)
def update_status(self):
self.fresh_status_valve()
# 状态徽章
status_key = self.state.split()[0] if self.state else "Offline"
color = STATUS_COLORS.get(status_key, "#9E9E9E")
label = STATUS_LABELS.get(status_key, self.state)
self._status_badge.setText(f"{label}")
self._status_badge.setStyleSheet(
f"background-color: #2a2a2a; color: {color}; "
f"border: 2px solid {color}; border-radius: 8px; "
f"font-size: 18px; font-weight: bold; padding: 4px;"
)
# 文件
self._file_name_lbl.setText(self._truncate(self.display_name, 28))
# 进度
prog = min(max(self.progress, 0), 100)
self._progress_bar.setValue(int(prog))
self._progress_bar.setFormat(f"{prog:.1f}%")
self._time_elapsed_lbl.setText(f"已用: {self.format_time(self.print_time)}")
self._time_left_lbl.setText(f"剩余: {self.format_time(self.print_time_left)}")
# 打印中时进度条变蓝
if self.state.startswith("Printing"):
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #2196F3;
border-radius: 5px;
}
""")
else:
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 5px;
}
""")
# 温度
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target, self.tool_temp_range)
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target, self.bed_temp_range)
# G-code 模型加载与进度更新
if self.file_name and self.file_name != "None":
if self.file_name != self._loaded_file:
gcode_path = os.path.join(self.gcode_dir, self.file_name)
if os.path.exists(gcode_path):
try:
self.gcode_viewer.load_gcode(gcode_path)
self._loaded_file = self.file_name
except Exception as e:
print("Failed to load G-code:", e)
# 使用 filepos 替代进度百分比进行精准的偏移量层级更新
if self._loaded_file == self.file_name:
is_printing = self.state.startswith("Printing") or self.state.startswith("Paused")
self.gcode_viewer.update_by_filepos(self.filepos, is_printing)