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", parent=None): super().__init__(parent) self.setFixedSize(100, 80) self._label = label self._actual = 0.0 self._target = 0.0 def set_value(self, actual, target): 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 = 16, 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 / 300, 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 / 300, 1)) p.setPen(QPen(QColor("#ffffff"), 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) p.setPen(QPen(QColor("#e0e0e0"))) p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, f"{self._actual:.0f}°") if self._target > 0: font2 = QFont("sans-serif", 10) p.setFont(font2) p.setPen(QPen(QColor("#888888"))) p.drawText(44, 34, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, f"→ {self._target:.0f}°") font3 = QFont("sans-serif", 10, QFont.Weight.Bold) p.setFont(font3) p.setPen(QPen(QColor("#aaaaaa"))) p.drawText(0, h - 20, w, 20, Qt.AlignmentFlag.AlignCenter, self._label) # ── 状态页面 ──────────────────────────────────────────── class StatusPage(QWidget): def __init__(self, api_client, parent=None): super().__init__(parent) self.api_client = api_client 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._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 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._bed_gauge = TempGauge("热床") 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;") right_layout = QVBoxLayout(right_frame) right_layout.setContentsMargins(6, 6, 6, 6) self.gcode_viewer = GCodeViewerWidget() right_layout.addWidget(self.gcode_viewer) main_layout.addWidget(left_frame, 2) main_layout.addWidget(right_frame, 3) 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._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target) # 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) #TODO: Better Gcode Parser, this one is too slow for large files, need to optimize or use a separate thread to load # def load_gcode_vertices(self, path): # vertices = [] # x = 0 # y = 0 # z = 0 # with open(path, "r", encoding="utf-8", errors="ignore") as f: # for line in f: # line = line.strip() # if not line: # continue # if line.startswith("G0") or line.startswith("G1"): # old_x = x # old_y = y # old_z = z # mx = re.search(r"X([-0-9.]+)", line) # my = re.search(r"Y([-0-9.]+)", line) # mz = re.search(r"Z([-0-9.]+)", line) # if mx: # x = float(mx.group(1)) # if my: # y = float(my.group(1)) # if mz: # z = float(mz.group(1)) # vertices.append({ # "x1": old_x, # "y1": old_y, # "z1": old_z, # "x2": x, # "y2": y, # "z2": z, # }) # return vertices