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)