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 def get_gcode_dir(): config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json") try: with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) return config.get("GCODE_DIR", "/home/lhye200/.octoprint/uploads") except: return "/home/lhye200/.octoprint/uploads" GCODE_DIR = get_gcode_dir() # ── 状态主题色 ────────────────────────────────────────── 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) # ── GCode 2D 预览(暂注释,待开发)───────────────────── # class GCode2DPreviewWidget(QGraphicsView): # def __init__(self, parent=None): # super().__init__(parent) # self.scene = QGraphicsScene(self) # self.setScene(self.scene) # self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;") # self.setRenderHint(QPainter.RenderHint.Antialiasing) # self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # self.scale(1, -1) # # def draw_paths(self, lines_data): # self.scene.clear() # bounding_rect = QRectF() # for color_str, data in lines_data.items(): # points = data.get("points", []) # line_width = data.get("line_width", 2) # if not points: # continue # path = QPainterPath() # if isinstance(points[0][0], (int, float)): # path.moveTo(float(points[0][0]), float(points[0][1])) # for pt in points[1:]: # path.lineTo(float(pt[0]), float(pt[1])) # else: # for line_pts in points: # if not line_pts: # continue # path.moveTo(float(line_pts[0][0]), float(line_pts[0][1])) # for pt in line_pts[1:]: # path.lineTo(float(pt[0]), float(pt[1])) # path_item = QGraphicsPathItem(path) # pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white") # pen = QPen(pen_color) # pen.setWidth(int(line_width)) # pen.setCosmetic(True) # path_item.setPen(pen) # self.scene.addItem(path_item) # bounding_rect = bounding_rect.united(path.boundingRect()) # if bounding_rect.width() < 1 or bounding_rect.height() < 1: # bounding_rect = QRectF(0, 0, 220, 220) # bounding_rect.adjust(-10, -10, 10, 10) # self.scene.setSceneRect(bounding_rect) # self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) # ── 状态页面 ──────────────────────────────────────────── 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.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.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.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) placeholder = QLabel("GCode 预览\n⚙ 待开发") placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter) placeholder.setStyleSheet("color: #666666; font-size: 24px; font-weight: 600; border: none;") right_layout.addWidget(placeholder) 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) #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