417 lines
15 KiB
Python
417 lines
15 KiB
Python
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
|