update server

This commit is contained in:
2026-07-30 11:12:31 +08:00
commit 4312cb878c
99 changed files with 24034 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
# plot_window.py
"""独立绘图窗口:嵌入 matplotlib (QtAgg 后端) 显示控制数据曲线。
注意:matplotlib backend 由 main.py 在最早期统一设置,此处不再重复调用。
使用 Figure() 直接创建图形,避免 plt.subplots() 污染 pyplot 全局状态导致闪退。
"""
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QWidget
)
from PySide6.QtCore import Qt
class PlotWindow(QDialog):
"""压力控制数据曲线窗口"""
def __init__(self, time_data, pressure_data, target_data, valve_data, parent=None):
super().__init__(parent)
self.setWindowTitle("控制数据曲线图")
self.resize(1100, 800)
self.setAttribute(Qt.WA_DeleteOnClose)
self.time_data = list(time_data)
self.pressure_data = list(pressure_data)
self.target_data = list(target_data)
self.valve_data = list(valve_data)
self._fig = None
self._ax1 = None
self._ax2 = None
self._canvas = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# ---- 控制面板 ----
ctrl_widget = QWidget()
ctrl_layout = QHBoxLayout(ctrl_widget)
ctrl_layout.setContentsMargins(0, 0, 0, 0)
ctrl_layout.setSpacing(8)
ctrl_layout.addWidget(QLabel("时间轴范围 (秒):"))
self.x_min_entry = QLineEdit("0")
self.x_min_entry.setMaximumWidth(80)
ctrl_layout.addWidget(self.x_min_entry)
ctrl_layout.addWidget(QLabel(""))
x_max_default = f"{max(self.time_data):.1f}" if self.time_data else "10"
self.x_max_entry = QLineEdit(x_max_default)
self.x_max_entry.setMaximumWidth(80)
ctrl_layout.addWidget(self.x_max_entry)
apply_btn = QPushButton("应用")
apply_btn.clicked.connect(self._apply_x_limits)
ctrl_layout.addWidget(apply_btn)
reset_btn = QPushButton("重置")
reset_btn.clicked.connect(self._reset_view)
ctrl_layout.addWidget(reset_btn)
all_btn = QPushButton("全部")
all_btn.clicked.connect(self._show_all)
ctrl_layout.addWidget(all_btn)
last30_btn = QPushButton("最后30秒")
last30_btn.clicked.connect(lambda: self._zoom_last_n(30))
ctrl_layout.addWidget(last30_btn)
ctrl_layout.addStretch()
layout.addWidget(ctrl_widget)
# ---- matplotlib 画布 ----
if not self.time_data or len(self.time_data) < 2:
layout.addWidget(QLabel("数据不足,无法绘制图表"))
return
try:
# 使用 Figure() 直接创建,避免 plt.subplots() 将图形注册到 pyplot 全局状态
self._fig = Figure(figsize=(10, 7), dpi=100)
self._ax1 = self._fig.add_subplot(2, 1, 1)
self._ax2 = self._fig.add_subplot(2, 1, 2)
# 压力曲线
self._ax1.plot(self.time_data, self.pressure_data, 'b-o',
linewidth=1, markersize=1, alpha=0.8, label='实际压力')
self._ax1.plot(self.time_data, self.target_data, 'r--',
linewidth=1.5, alpha=0.8, label='目标压力')
self._ax1.set_ylabel('压力 (kPa)', fontsize=12)
self._ax1.set_title('压力控制性能', fontsize=14, fontweight='bold')
self._ax1.legend(loc='upper right', fontsize=10)
self._ax1.grid(True, alpha=0.3)
# 阀门开度曲线
self._ax2.plot(self.time_data, self.valve_data, 'm-o',
linewidth=1, markersize=1, alpha=0.8, label='实际阀门指令')
self._ax2.set_xlabel('时间 (秒)', fontsize=12)
self._ax2.set_ylabel('阀门开度 (%)', fontsize=12)
self._ax2.legend(loc='upper right', fontsize=10)
self._ax2.set_ylim([0, 105])
self._ax2.grid(True, alpha=0.3)
self._fig.tight_layout()
# 创建 canvas
self._canvas = FigureCanvasQTAgg(self._fig)
layout.addWidget(self._canvas, stretch=1)
# 导航工具栏(macOS 上某些版本可能崩溃,加容错)
try:
toolbar = NavigationToolbar2QT(self._canvas, self)
layout.addWidget(toolbar)
except Exception as e:
print(f"[PlotWindow] 工具栏创建失败: {e}")
# 提示标签
hint = QLabel("提示: 使用工具栏缩放/平移 | 拖动矩形区域可局部放大")
hint.setStyleSheet("color: gray; font-size: 12px;")
layout.addWidget(hint)
except Exception as e:
import traceback
traceback.print_exc()
layout.addWidget(QLabel(f"绘图创建失败: {e}"))
def _apply_x_limits(self):
try:
x_min = float(self.x_min_entry.text())
x_max = float(self.x_max_entry.text())
if x_min >= x_max or self._ax1 is None:
return
self._ax1.set_xlim([x_min, x_max])
self._ax2.set_xlim([x_min, x_max])
self._canvas.draw()
except ValueError:
pass
def _reset_view(self):
if not self._ax1 or not self.time_data:
return
x_min = min(self.time_data)
x_max = max(self.time_data)
self._ax1.set_xlim([x_min, x_max])
self._ax2.set_xlim([x_min, x_max])
self.x_min_entry.setText(f"{x_min:.1f}")
self.x_max_entry.setText(f"{x_max:.1f}")
self._canvas.draw()
def _show_all(self):
if not self._ax1 or not self.time_data:
return
x_min = min(self.time_data)
x_max = max(self.time_data)
self._ax1.set_xlim([x_min, x_max])
self._ax2.set_xlim([x_min, x_max])
self.x_min_entry.setText(f"{x_min:.1f}")
self.x_max_entry.setText(f"{x_max:.1f}")
self._canvas.draw()
def _zoom_last_n(self, n_seconds):
if not self._ax1 or not self.time_data:
return
x_max = max(self.time_data)
x_min = max(0, x_max - n_seconds)
self._ax1.set_xlim([x_min, x_max])
self._ax2.set_xlim([x_min, x_max])
self.x_min_entry.setText(f"{x_min:.1f}")
self.x_max_entry.setText(f"{x_max:.1f}")
self._canvas.draw()
def closeEvent(self, event):
"""Qt 会按控件树父子关系自动销毁所有子控件(canvas + toolbar)。
此处只需清空 Python 侧引用,让 Figure 能被 GC 正常回收。
严禁 plt.close(self._fig)plt.close() 内部绕过 Qt 直接销毁 canvas
widget,与 WA_DeleteOnClose 冲突导致 double-free → SIGSEGV 闪退。
"""
self._canvas = None
self._ax1 = None
self._ax2 = None
self._fig = None
super().closeEvent(event)