Files
AIO_3D_Print_Local_Screen/pages/status_page.py
2026-05-11 00:21:16 +08:00

453 lines
17 KiB
Python
Raw 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
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