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
+1
View File
@@ -0,0 +1 @@
# ui package - Pure PySide6 UI layer
+177
View File
@@ -0,0 +1,177 @@
# connection_tab.py
"""页面1Modbus TCP 连接参数设置"""
import os
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QPushButton, QFrame,
QSizePolicy, QGraphicsDropShadowEffect
)
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QColor, QIcon
_SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "src"))
# ==========================================
# 工具函数:创建带左侧蓝色竖线的 Section 卡片
# ==========================================
def _make_section_card(parent, title_text: str, colors: dict):
card = QFrame(parent)
card.setProperty("cssClass", "sectionCard")
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
# 添加微弱的模糊阴影效果
shadow = QGraphicsDropShadowEffect(card)
shadow.setColor(QColor(0, 0, 0, 12))
shadow.setBlurRadius(16)
shadow.setOffset(0, 4)
card.setGraphicsEffect(shadow)
outer = QVBoxLayout(card)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(0)
# ---- 标题行(蓝色左竖线 + 标题文字) ----
title_row = QHBoxLayout()
title_row.setContentsMargins(20, 16, 20, 0)
title_row.setSpacing(10)
accent = QWidget()
accent.setProperty("cssClass", "sectionAccent")
accent.setFixedSize(4, 16)
title_row.addWidget(accent)
title_lbl = QLabel(title_text)
title_lbl.setProperty("cssClass", "sectionTitle")
title_row.addWidget(title_lbl)
title_row.addStretch()
outer.addLayout(title_row)
# ---- 内容区 ----
content_widget = QWidget()
content_widget.setStyleSheet("background-color: transparent;")
content_layout = QGridLayout(content_widget)
content_layout.setContentsMargins(20, 14, 20, 18)
content_layout.setHorizontalSpacing(0)
content_layout.setVerticalSpacing(10)
# 列0(标签)固定宽度,列1(输入框)拉伸
content_layout.setColumnMinimumWidth(0, 148)
content_layout.setColumnStretch(1, 1)
outer.addWidget(content_widget)
return card, content_layout
# ==========================================
# 工具函数:创建表单标签(左对齐)
# ==========================================
def _form_label(text: str, parent=None):
lbl = QLabel(text, parent)
lbl.setProperty("cssClass", "formLabel")
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
return lbl
class ConnectionTab(QWidget):
"""连接设置页面"""
def __init__(self, colors: dict, parent=None):
super().__init__(parent)
self.setProperty("cssClass", "tabPage")
self.colors = colors
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 16, 20, 16)
main_layout.setSpacing(14)
# ==========================================
# Section: Modbus TCP
# ==========================================
tcp_card, tcp_layout = _make_section_card(self, "Modbus TCP", colors)
self._build_tcp_section(tcp_layout)
main_layout.addWidget(tcp_card)
# ==========================================
# 按钮组
# ==========================================
btn_row = QHBoxLayout()
btn_row.setContentsMargins(0, 4, 0, 0)
btn_row.setSpacing(12)
# 连接设备按钮
self.connect_btn = QPushButton(" 连接设备")
self.connect_btn.setObjectName("connect_btn")
self.connect_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "connect_device.svg")))
self.connect_btn.setIconSize(QSize(18, 18))
self.connect_btn.setCursor(Qt.PointingHandCursor)
# 保存按钮引用(connect/disconnect 切换文字时使用)
self._connect_text_lbl = self.connect_btn
btn_row.addWidget(self.connect_btn)
btn_row.addStretch()
main_layout.addLayout(btn_row)
main_layout.addStretch()
# ==========================================
# Modbus TCP 表单
# ==========================================
def _build_tcp_section(self, grid: QGridLayout):
row = 0
grid.addWidget(_form_label("模块地址:", self), row, 0)
self.tcp_ip_entry = QLineEdit("192.168.1.12")
self.tcp_ip_entry.setPlaceholderText("输入模块 IP地址")
grid.addWidget(self.tcp_ip_entry, row, 1)
row += 1
grid.addWidget(_form_label("端口:", self), row, 0)
self.tcp_port_entry = QLineEdit("502")
self.tcp_port_entry.setPlaceholderText("默认502")
grid.addWidget(self.tcp_port_entry, row, 1)
row += 1
grid.addWidget(_form_label("读取压力寄存器地址:", self), row, 0)
self.pressure_addr_entry = QLineEdit("0")
self.pressure_addr_entry.setPlaceholderText("寄存器地址")
grid.addWidget(self.pressure_addr_entry, row, 1)
row += 1
grid.addWidget(_form_label("电机地址:", self), row, 0)
self.motor_addr_entry = QLineEdit("0")
self.motor_addr_entry.setPlaceholderText("电机模拟量通道地址")
grid.addWidget(self.motor_addr_entry, row, 1)
row += 1
grid.addWidget(_form_label("流量计地址:", self), row, 0)
self.flowmeter_addr_entry = QLineEdit("1")
self.flowmeter_addr_entry.setPlaceholderText("留空则使用手动输入流量")
grid.addWidget(self.flowmeter_addr_entry, row, 1)
row += 1
grid.addWidget(_form_label("压力表量程:", self), row, 0)
self.pressure_range_entry = QLineEdit("400")
self.pressure_range_entry.setPlaceholderText("压力传感器量程上限")
grid.addWidget(self.pressure_range_entry, row, 1)
row += 1
grid.addWidget(_form_label("流量计量程:", self), row, 0)
self.flow_range_entry = QLineEdit("300")
self.flow_range_entry.setPlaceholderText("流量计量程上限")
grid.addWidget(self.flow_range_entry, row, 1)
# ---- 公开方法 ----
def get_connection_params(self) -> dict:
flow_str = self.flowmeter_addr_entry.text().strip()
return {
"tcp_ip": self.tcp_ip_entry.text().strip(),
"tcp_port": int(self.tcp_port_entry.text() or "502"),
"pressure_addr": int(self.pressure_addr_entry.text() or "504"),
"motor_addr": int(self.motor_addr_entry.text() or "0"),
"flowmeter_addr": int(flow_str) if flow_str else None,
"pressure_range": float(self.pressure_range_entry.text() or "400"),
"flow_range": float(self.flow_range_entry.text() or "300"),
}
+593
View File
@@ -0,0 +1,593 @@
# control_tab.py
"""页面2:系统状态栏(三卡片) + 控制参数(Section 卡片)
重构要点:
- 状态栏:三张横向并排卡片,每张含圆形图标 + 大字数值 + 右上角色点
- 控制参数区:Section 卡片(蓝竖线装饰),QGridLayout 双列布局,输入列拉伸占满约 2/3 页宽
"""
import os
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QComboBox, QPushButton,
QRadioButton, QCheckBox, QFrame, QButtonGroup, QSizePolicy,
QGraphicsDropShadowEffect,
)
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QColor, QIcon
from PySide6.QtSvgWidgets import QSvgWidget
from ui.connection_tab import _make_section_card
_SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "src"))
# ==========================================
# 工具函数:透明容器
# ==========================================
def _transparent_widget() -> QWidget:
"""创建一个透明的空容器(用于包裹多个控件)。"""
w = QWidget()
w.setProperty("cssClass", "transparentBg")
w.style().unpolish(w)
w.style().polish(w)
return w
# ==========================================
# 工具函数:三点状态栏卡片
# ==========================================
def _make_status_card(parent, title: str, value: str, unit: str,
value_color: str, circle_bg: str,
icon_path: str, dot_color: str):
"""创建单张状态卡片(圆形图标 + 大字数值 + 右上角圆点)。
返回 (card, value_label)。
"""
card = QFrame(parent)
card.setProperty("cssClass", "sectionCard")
card.style().unpolish(card)
card.style().polish(card)
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
shadow = QGraphicsDropShadowEffect(card)
shadow.setColor(QColor(0, 0, 0, 10))
shadow.setBlurRadius(14)
shadow.setOffset(0, 2)
card.setGraphicsEffect(shadow)
inner = QVBoxLayout(card)
inner.setContentsMargins(16, 12, 16, 14)
inner.setSpacing(0)
# ---- 右上角圆点 ----
dot_row = QHBoxLayout()
dot_row.setContentsMargins(0, 0, 0, 6)
dot_row.addStretch()
dot = QWidget()
dot.setFixedSize(8, 8)
dot.setStyleSheet(f"background: {dot_color}; border-radius: 4px;")
dot_row.addWidget(dot)
inner.addLayout(dot_row)
# ---- 主体:圆形图标 + 文本 ----
body = QHBoxLayout()
body.setSpacing(30)
# 圆形图标容器
icon_circle = QWidget()
icon_circle.setFixedSize(82, 82)
icon_circle.setStyleSheet(
f"background: {circle_bg}; border-radius: 41px;"
)
icon_inner = QVBoxLayout(icon_circle)
icon_inner.setContentsMargins(0, 0, 0, 0)
icon_inner.setAlignment(Qt.AlignCenter)
svg = QSvgWidget(icon_path)
svg.setFixedSize(48, 48)
icon_inner.addWidget(svg, alignment=Qt.AlignCenter)
body.addWidget(icon_circle)
# 文本列
text_col = QVBoxLayout()
text_col.setSpacing(4)
title_lbl = QLabel(title)
title_lbl.setStyleSheet(
"color: #555555; font-size: 15px; background: transparent; border: none;"
)
text_col.addWidget(title_lbl)
value_row = QHBoxLayout()
value_row.setSpacing(4)
val_lbl = QLabel(value)
val_lbl.setStyleSheet(
f"color: {value_color}; font-size: 56px; font-weight: bold;"
"background: transparent; border: none;"
)
value_row.addWidget(val_lbl)
unit_lbl = QLabel(unit)
unit_lbl.setStyleSheet(
f"color: {value_color}; font-size: 24px; background: transparent;"
"border: none; padding-top: 14px;"
)
value_row.addWidget(unit_lbl)
value_row.addStretch()
text_col.addLayout(value_row)
body.addLayout(text_col, 1)
inner.addLayout(body, 1)
return card, val_lbl
# ==========================================
# 工具函数:行级标签
# ==========================================
def _label(text: str, parent=None) -> QLabel:
"""紧凑表单标签。"""
lbl = QLabel(text, parent)
lbl.setStyleSheet(
"color: #333333; font-size: 14px; font-weight: bold; background: transparent;"
)
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
return lbl
def _unit_label(unit: str, parent=None) -> QLabel:
"""单位标签(灰色小字)。"""
lbl = QLabel(unit, parent)
lbl.setStyleSheet(
"color: #94A3B8; font-size: 12px; background: transparent;"
)
return lbl
# ==========================================
# 主类
# ==========================================
class ControlTab(QWidget):
"""控制设置页面"""
# ---- 信号 ----
target_set_requested = Signal(float)
mode_changed = Signal(str)
pid_update_requested = Signal(float, float, float)
model_load_requested = Signal(str)
models_refresh_requested = Signal()
control_toggle_requested = Signal()
plot_requested = Signal()
manual_valve_set_requested = Signal(float)
log_message_requested = Signal(str)
def __init__(self, colors: dict, parent=None):
super().__init__(parent)
self.setProperty("cssClass", "tabPage")
self.colors = colors
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 16, 20, 16)
main_layout.setSpacing(14)
# ==========================================
# A. 系统状态栏(三卡片)
# ==========================================
status_bar = QHBoxLayout()
status_bar.setSpacing(14)
self._pressure_card, self.current_pressure_lbl = _make_status_card(
self,
title="当前系统压力",
value="0.0",
unit="kPa",
value_color="#0F955D",
circle_bg="#E2F5ED",
icon_path=os.path.join(_SRC_DIR, "pressure.svg"),
dot_color="#0F955D",
)
self._target_card, self.target_pressure_lbl = _make_status_card(
self,
title="设置目标压力",
value="0.0",
unit="kPa",
value_color="#0960D1",
circle_bg="#EBF3FE",
icon_path=os.path.join(_SRC_DIR, "target.svg"),
dot_color="#0960D1",
)
self._valve_card, self.valve_opening_lbl = _make_status_card(
self,
title="控制阀门开度",
value="0.0",
unit="%",
value_color="#E67E22",
circle_bg="#FFF2E8",
icon_path=os.path.join(_SRC_DIR, "valve.svg"),
dot_color="#E67E22",
)
status_bar.addWidget(self._pressure_card)
status_bar.addWidget(self._target_card)
status_bar.addWidget(self._valve_card)
main_layout.addLayout(status_bar)
# ==========================================
# B. 控制参数设置区(Section 卡片)
# ==========================================
ctrl_card, ctrl_grid = _make_section_card(self, "控制设置", colors)
self._build_control_section(ctrl_grid)
main_layout.addWidget(ctrl_card)
main_layout.addStretch()
# 信号连接
self.mode_group.buttonClicked.connect(self._on_mode_changed_internal)
# ==========================================
# 控制设置 — QGridLayout 双列布局,输入列拉伸占满 2/3 页宽
# ==========================================
def _build_control_section(self, grid: QGridLayout):
# 沿用 _make_section_card 的列配置:col 0 标签固定 148px,col 1 输入区拉伸
grid.setVerticalSpacing(16)
# --- B1: 物理工况 (容积 + 流量) ---
row = 0
grid.addWidget(_label("物理工况:"), row, 0)
b1 = _transparent_widget()
b1h = QHBoxLayout(b1)
b1h.setContentsMargins(0, 0, 0, 0)
b1h.setSpacing(6)
b1h.addWidget(_label("容积"))
self.volume_entry = QLineEdit()
self.volume_entry.setFixedWidth(120)
b1h.addWidget(self.volume_entry)
b1h.addWidget(_unit_label("L"))
b1h.addSpacing(32)
b1h.addWidget(_label("流量"))
self.flow_entry = QLineEdit("100")
self.flow_entry.setFixedWidth(120)
b1h.addWidget(self.flow_entry)
b1h.addWidget(_unit_label("L/min"))
b1h.addStretch()
grid.addWidget(b1, row, 1)
# --- B2: 目标压力 + 按钮 ---
row = 1
grid.addWidget(_label("目标压力:"), row, 0)
b2 = _transparent_widget()
b2h = QHBoxLayout(b2)
b2h.setContentsMargins(0, 0, 0, 0)
b2h.setSpacing(6)
self.target_entry = QLineEdit("80.0")
self.target_entry.setFixedWidth(200)
b2h.addWidget(self.target_entry)
b2h.addWidget(_unit_label("kPa"))
b2h.addSpacing(10)
self.set_target_btn = QPushButton("设置目标")
self.set_target_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "target.svg")))
self.set_target_btn.setIconSize(QSize(18, 18))
self.set_target_btn.setStyleSheet(
"QPushButton { background: white; color: #0960D1; border: 1.5px solid #0960D1;"
"border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background: #EBF3FE; }"
)
self.set_target_btn.setCursor(Qt.PointingHandCursor)
self.set_target_btn.clicked.connect(self._on_set_target)
b2h.addWidget(self.set_target_btn)
b2h.addStretch()
grid.addWidget(b2, row, 1)
# --- B3: 控制模式单选 ---
row = 2
grid.addWidget(_label("控制方式:"), row, 0)
b3 = _transparent_widget()
b3h = QHBoxLayout(b3)
b3h.setContentsMargins(0, 0, 0, 0)
b3h.setSpacing(24)
self.mode_group = QButtonGroup(self)
self.radio_rl = QRadioButton("智能自动")
self.radio_pid = QRadioButton("手动PID")
self.radio_manual = QRadioButton("设置开度")
self.mode_group.addButton(self.radio_rl, 0)
self.mode_group.addButton(self.radio_pid, 1)
self.mode_group.addButton(self.radio_manual, 2)
self.radio_rl.setChecked(True)
b3h.addWidget(self.radio_rl)
b3h.addWidget(self.radio_pid)
b3h.addWidget(self.radio_manual)
b3h.addStretch()
grid.addWidget(b3, row, 1)
# --- B4: 模型面板(跨两列,内部标签固定148px与外层col0对齐) ---
row = 3
self.rl_panel = QWidget()
self.rl_panel.setStyleSheet("background: transparent;")
rl_layout = QHBoxLayout(self.rl_panel)
rl_layout.setContentsMargins(0, 0, 0, 0)
rl_layout.setSpacing(8)
rl_lbl = _label("决策模型:")
rl_lbl.setFixedWidth(148)
rl_layout.addWidget(rl_lbl)
self.model_combobox = QComboBox()
self.model_combobox.setFixedWidth(280)
self.model_combobox.setFixedHeight(36)
rl_layout.addWidget(self.model_combobox)
self.load_model_btn = QPushButton("加载模型")
self.load_model_btn.setStyleSheet(
"QPushButton { background: #0960D1; color: white; border: none;"
"border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background: #0856B8; }"
)
self.load_model_btn.setCursor(Qt.PointingHandCursor)
self.load_model_btn.clicked.connect(self._on_load_model)
rl_layout.addWidget(self.load_model_btn)
self.refresh_models_btn = QPushButton("🔄 刷新")
self.refresh_models_btn.setProperty("cssClass", "refresh")
self.refresh_models_btn.setCursor(Qt.PointingHandCursor)
self.refresh_models_btn.clicked.connect(self._on_refresh_models)
rl_layout.addWidget(self.refresh_models_btn)
rl_layout.addStretch()
grid.addWidget(self.rl_panel, row, 0, 1, 2)
# --- B5: PID 面板(跨两列,内部标签固定148px) ---
row = 4
self.pid_panel = QWidget()
self.pid_panel.setStyleSheet("background: transparent;")
self.pid_panel.hide()
pid_layout = QHBoxLayout(self.pid_panel)
pid_layout.setContentsMargins(0, 0, 0, 0)
pid_layout.setSpacing(6)
pid_lbl = _label("PID 调节:")
pid_lbl.setFixedWidth(148)
pid_layout.addWidget(pid_lbl)
pid_layout.addWidget(_label("Kp:"))
self.Kp_entry = QLineEdit("1.0")
self.Kp_entry.setFixedWidth(80)
pid_layout.addWidget(self.Kp_entry)
pid_layout.addWidget(_label("Ki:"))
self.Ki_entry = QLineEdit("0.4")
self.Ki_entry.setFixedWidth(80)
pid_layout.addWidget(self.Ki_entry)
pid_layout.addWidget(_label("Kd:"))
self.Kd_entry = QLineEdit("0")
self.Kd_entry.setFixedWidth(80)
pid_layout.addWidget(self.Kd_entry)
self.update_pid_btn = QPushButton("更新PID参数")
self.update_pid_btn.setStyleSheet(
"QPushButton { background: #0960D1; color: white; border: none;"
"border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background: #0856B8; }"
)
self.update_pid_btn.setCursor(Qt.PointingHandCursor)
self.update_pid_btn.clicked.connect(self._on_update_pid)
pid_layout.addWidget(self.update_pid_btn)
pid_layout.addStretch()
grid.addWidget(self.pid_panel, row, 0, 1, 2)
# --- B6: 手动开度面板(跨两列,内部标签固定148px) ---
row = 5
self.manual_panel = QWidget()
self.manual_panel.setStyleSheet("background: transparent;")
self.manual_panel.hide()
man_layout = QHBoxLayout(self.manual_panel)
man_layout.setContentsMargins(0, 0, 0, 0)
man_layout.setSpacing(6)
man_lbl = _label("设置开度:")
man_lbl.setFixedWidth(148)
man_layout.addWidget(man_lbl)
self.valve_entry = QLineEdit()
self.valve_entry.setFixedWidth(160)
man_layout.addWidget(self.valve_entry)
man_layout.addWidget(_unit_label("%"))
self.set_valve_btn = QPushButton("设置")
self.set_valve_btn.setStyleSheet(
"QPushButton { background: #0960D1; color: white; border: none;"
"border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background: #0856B8; }"
)
self.set_valve_btn.setCursor(Qt.PointingHandCursor)
self.set_valve_btn.clicked.connect(self._on_set_valve)
man_layout.addWidget(self.set_valve_btn)
man_layout.addStretch()
grid.addWidget(self.manual_panel, row, 0, 1, 2)
# --- B7: 控制启停行(跨两列) ---
row = 6
b7 = _transparent_widget()
b7h = QHBoxLayout(b7)
b7h.setContentsMargins(0, 0, 0, 0)
b7h.setSpacing(12)
self.start_btn = QPushButton("开始控制")
self.start_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "start_control.svg")))
self.start_btn.setIconSize(QSize(18, 18))
self.start_btn.setProperty("cssClass", "action")
self.start_btn.setStyleSheet(
"QPushButton { background-color: #0F955D; color: white; border: none;"
"border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background-color: #0D8250; }"
)
self.start_btn.setCursor(Qt.PointingHandCursor)
self.start_btn.clicked.connect(self._on_toggle_control)
self.plot_btn = QPushButton("绘制图线")
self.plot_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "plot.svg")))
self.plot_btn.setIconSize(QSize(18, 18))
self.plot_btn.setStyleSheet(
"QPushButton { background: white; color: #0960D1; border: 1.5px solid #0960D1;"
"border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background: #EBF3FE; }"
)
self.plot_btn.setCursor(Qt.PointingHandCursor)
self.plot_btn.clicked.connect(self._on_plot)
self.collect_data_cb = QCheckBox("同步收集数据集")
b7h.addWidget(self.start_btn)
b7h.addWidget(self.plot_btn)
b7h.addWidget(self.collect_data_cb)
grid.addWidget(b7, row, 0, 1, 2)
# ============================================================
# 以下方法完全兼容旧版 APImain_window.py 无需变动
# ============================================================
# ---- 模式切换 ----
def _on_mode_changed_internal(self, btn):
if btn == self.radio_pid:
mode = "PID"
self.rl_panel.hide()
self.manual_panel.hide()
self.pid_panel.show()
self.collect_data_cb.setEnabled(True)
self.model_combobox.setEnabled(False)
self.load_model_btn.setEnabled(False)
self.refresh_models_btn.setEnabled(False)
elif btn == self.radio_rl:
mode = "RL"
self.pid_panel.hide()
self.manual_panel.hide()
self.rl_panel.show()
self.collect_data_cb.setEnabled(True)
self.model_combobox.setEnabled(True)
self.load_model_btn.setEnabled(True)
self.refresh_models_btn.setEnabled(True)
elif btn == self.radio_manual:
mode = "MANUAL"
self.rl_panel.hide()
self.pid_panel.hide()
self.manual_panel.show()
self.collect_data_cb.setChecked(False)
self.collect_data_cb.setEnabled(False)
else:
mode = "RL"
self.mode_changed.emit(mode)
def init_mode_ui(self):
self.rl_panel.show()
self.pid_panel.hide()
self.manual_panel.hide()
def set_mode_switch_enabled(self, enabled: bool):
self.radio_pid.setEnabled(enabled)
self.radio_rl.setEnabled(enabled)
self.radio_manual.setEnabled(enabled)
# ---- 信号处理 ----
def _on_set_target(self):
try:
target = float(self.target_entry.text())
if 0 <= target <= 3000:
self.target_set_requested.emit(target)
else:
self.target_set_requested.emit(-1)
except ValueError:
self.target_set_requested.emit(-1)
def _on_load_model(self):
selected = self.model_combobox.currentText()
self.model_load_requested.emit(selected)
def _on_refresh_models(self):
self.models_refresh_requested.emit()
def _on_update_pid(self):
try:
kp = float(self.Kp_entry.text())
ki = float(self.Ki_entry.text())
kd = float(self.Kd_entry.text())
self.pid_update_requested.emit(kp, ki, kd)
except ValueError:
self.log_message_requested.emit("错误: PID参数输入无效,请输入有效数字")
def _on_set_valve(self):
try:
valve = float(self.valve_entry.text())
if 0 <= valve <= 120:
self.manual_valve_set_requested.emit(valve)
else:
self.manual_valve_set_requested.emit(-1)
except ValueError:
self.manual_valve_set_requested.emit(-2)
def _on_toggle_control(self):
self.control_toggle_requested.emit()
def _on_plot(self):
self.plot_requested.emit()
# ---- 公开方法 (由 main_window 调用) ----
def set_control_running(self, running: bool):
if running:
self.start_btn.setText("停止控制")
self.start_btn.setIcon(QIcon())
self.start_btn.setProperty("cssClass", "danger")
self.start_btn.setStyleSheet(
"QPushButton { background-color: #EF4444; color: white; border: none;"
"border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background-color: #DC2626; }"
)
else:
self.start_btn.setText("开始控制")
self.start_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "start_control.svg")))
self.start_btn.setIconSize(QSize(18, 18))
self.start_btn.setProperty("cssClass", "action")
self.start_btn.setStyleSheet(
"QPushButton { background-color: #0F955D; color: white; border: none;"
"border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }"
"QPushButton:hover { background-color: #0D8250; }"
)
self.start_btn.style().unpolish(self.start_btn)
self.start_btn.style().polish(self.start_btn)
def update_display(self, pressure: float, target: float, valve: float):
self.current_pressure_lbl.setText(f"{pressure:.1f}")
self.target_pressure_lbl.setText(f"{target:.1f}")
self.valve_opening_lbl.setText(f"{valve:.1f}")
def update_pid_entries(self, kp: float, ki: float, kd: float):
self.Kp_entry.setText(f"{kp:.3f}")
self.Ki_entry.setText(f"{ki:.3f}")
self.Kd_entry.setText(f"{kd:.3f}")
def update_model_list(self, files: list):
self.model_combobox.clear()
if files:
self.model_combobox.addItems(files)
else:
self.model_combobox.addItem("无模型文件")
def get_mode(self) -> str:
if self.radio_pid.isChecked():
return "PID"
elif self.radio_manual.isChecked():
return "MANUAL"
return "RL"
def get_collect_data(self) -> bool:
return self.collect_data_cb.isChecked()
def get_control_params(self) -> dict:
return {
"volume": float(self.volume_entry.text() or "0"),
"flow": float(self.flow_entry.text() or "100"),
}
def get_pid_params(self) -> tuple:
return (
float(self.Kp_entry.text() or "1.0"),
float(self.Ki_entry.text() or "0.4"),
float(self.Kd_entry.text() or "0"),
)
def get_manual_valve(self) -> float:
return float(self.valve_entry.text() or "0")
def enable_plot_button(self, enable: bool):
self.plot_btn.setEnabled(enable)
def set_pid_entries_text(self, kp, ki, kd):
self.Kp_entry.setText(str(kp))
self.Ki_entry.setText(str(ki))
self.Kd_entry.setText(str(kd))
+427
View File
@@ -0,0 +1,427 @@
# debug_tab.py
"""页面3:系统辨识 + 高级设置(Section 卡片 + 蓝竖线装饰)
参考 connection_tab 的页面设计,使用 _make_section_card 创建带蓝色左侧竖线的
纯白卡片,内部以 QGridLayout 双列排列表单项。
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QPushButton,
)
from PySide6.QtCore import Qt, QSettings, Signal
from ui.connection_tab import _make_section_card
# ---- 按钮默认样式(品牌蓝底白字,保证不被父级 inline stylesheet 覆盖) ----
_BTN_STYLE = """
QPushButton {
background-color: #0960D1;
color: white;
border: none;
border-radius: 6px;
padding: 9px 20px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #0856B8;
}
QPushButton:pressed {
background-color: #0960D1;
}
"""
_BTN_STYLE_DANGER = """
QPushButton {
background-color: #EF4444;
color: white;
border: none;
border-radius: 6px;
padding: 9px 20px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #DC2626;
}
QPushButton:pressed {
background-color: #B91C1C;
}
"""
def _compact_label(text: str, parent=None) -> QLabel:
"""紧凑表单标签(无 140px min-width,自然适应文字宽度)。"""
lbl = QLabel(text, parent)
lbl.setStyleSheet(
"color: #333333; font-size: 14px; font-weight: bold;"
"background: transparent;"
)
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
return lbl
def _wrap_widget(child: QWidget) -> QWidget:
"""将子控件放入透明容器(使用 cssClass 而非 inline stylesheet
避免覆盖子控件的 QSS 样式)。"""
w = QWidget()
w.setProperty("cssClass", "transparentBg")
w.style().unpolish(w)
w.style().polish(w)
lay = QHBoxLayout(w)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
lay.addWidget(child, 1)
return w
def _transparent_widget() -> QWidget:
"""创建一个透明的空容器(用于包裹多个控件)。"""
w = QWidget()
w.setProperty("cssClass", "transparentBg")
w.style().unpolish(w)
w.style().polish(w)
return w
class DebugTab(QWidget):
"""模型调试页面"""
# ---- 信号 ----
identify_start_requested = Signal()
identify_stop_requested = Signal()
volume_measure_requested = Signal()
volume_stop_requested = Signal()
def __init__(self, colors: dict, parent=None):
super().__init__(parent)
self.setProperty("cssClass", "tabPage")
self.colors = colors
# 记录按钮当前是否处于"运行中"状态
self._identifying_running = False
self._volume_running = False
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 16, 20, 16)
main_layout.setSpacing(14)
# ==========================================
# Card 1: 系统辨识
# ==========================================
ident_card, ident_grid = _make_section_card(self, "系统辨识", colors)
self._build_ident_section(ident_grid)
main_layout.addWidget(ident_card)
# ==========================================
# Card 2: 高级设置
# ==========================================
adv_card, adv_grid = _make_section_card(self, "高级设置", colors)
self._build_advanced_section(adv_grid)
main_layout.addWidget(adv_card)
adv_card.hide()
main_layout.addStretch()
# 恢复上次保存的设置
self._load_settings()
# ==========================================
# Card 1: 系统辨识 — 4 行双列 + 1 行通栏
# ==========================================
def _build_ident_section(self, grid: QGridLayout):
# 重置 _make_section_card 预设的单列表单列宽配置
for c in range(10):
grid.setColumnMinimumWidth(c, 0)
grid.setColumnStretch(c, 0)
# 紧凑双列布局: 左标签 | 左输入区 | 间距 | 右标签 | 右输入区
grid.setColumnMinimumWidth(0, 60)
grid.setColumnStretch(1, 1)
grid.setColumnMinimumWidth(2, 100)
grid.setColumnMinimumWidth(3, 60)
grid.setColumnStretch(4, 1)
grid.setVerticalSpacing(8)
# ---- 第 1 行:压力上限(kPa) | 过程升温(°C) ----
row = 0
grid.addWidget(_compact_label("压力上限:", self), row, 0)
self.p_max_entry = QLineEdit("200")
grid.addWidget(self._with_unit(self.p_max_entry, "kPa"), row, 1)
grid.addWidget(_compact_label("过程升温:", self), row, 3)
self.T_delta_entry = QLineEdit("30")
grid.addWidget(self._with_unit(self.T_delta_entry, "°C"), row, 4)
# ---- 第 2 行:约束上界 | 下界 ----
row = 1
grid.addWidget(_compact_label("约束上界:", self), row, 0)
self.fit_high_entry = QLineEdit("200")
grid.addWidget(_wrap_widget(self.fit_high_entry), row, 1)
grid.addWidget(_compact_label("下界:", self), row, 3)
self.fit_low_entry = QLineEdit("50")
grid.addWidget(_wrap_widget(self.fit_low_entry), row, 4)
# ---- 第 3 行:容积(L) | 测试按钮 ----
row = 2
grid.addWidget(_compact_label("容积:", self), row, 0)
self.volume_entry = QLineEdit()
grid.addWidget(self._with_unit(self.volume_entry, "L"), row, 1)
self.test_btn = QPushButton("测试")
self.test_btn.setStyleSheet(_BTN_STYLE)
self.test_btn.setCursor(Qt.PointingHandCursor)
self.test_btn.clicked.connect(self._on_measure_volume)
btn_wrap = _transparent_widget()
btn_h = QHBoxLayout(btn_wrap)
btn_h.setContentsMargins(0, 0, 0, 0)
btn_h.addWidget(self.test_btn)
btn_h.addStretch()
grid.addWidget(btn_wrap, row, 4)
# ---- 第 4 行:周期(s) | 阶数 ----
row = 3
grid.addWidget(_compact_label("周期:", self), row, 0)
self.period_entry = QLineEdit("2.5")
grid.addWidget(self._with_unit(self.period_entry, "s"), row, 1)
grid.addWidget(_compact_label("阶数:", self), row, 3)
self.order_entry = QLineEdit("6")
grid.addWidget(_wrap_widget(self.order_entry), row, 4)
# ---- 第 5 行(通栏):序列 + 开始辨识按钮 ----
row = 4
grid.addWidget(_compact_label("序列:", self), row, 0)
seq_wrap = _transparent_widget()
seq_h = QHBoxLayout(seq_wrap)
seq_h.setContentsMargins(0, 0, 0, 0)
seq_h.setSpacing(8)
self.levels_entry = QLineEdit()
seq_h.addWidget(self.levels_entry, 1)
self.ident_result_label = QLabel("等待开始")
self.ident_result_label.setStyleSheet(
"color: #64748B; font-size: 13px; font-weight: 600;"
)
seq_h.addWidget(self.ident_result_label)
self.identify_btn = QPushButton(" ▶ 开始辨识")
self.identify_btn.setStyleSheet(_BTN_STYLE)
self.identify_btn.setCursor(Qt.PointingHandCursor)
self.identify_btn.clicked.connect(self._on_start_identify)
seq_h.addWidget(self.identify_btn)
grid.addWidget(seq_wrap, row, 1, 1, 4) # 跨越列 1-4
# Keep the legacy widgets for internal compatibility, but do not
# expose confidential measurement parameters in the customer UI.
# Volume-test values come only from volume_measurement.json.
for index in range(grid.count()):
widget = grid.itemAt(index).widget()
if widget is not None and widget not in (btn_wrap, seq_wrap):
widget.hide()
self.levels_entry.hide()
# ==========================================
# Card 2: 高级设置 — 2 行双列
# ==========================================
def _build_advanced_section(self, grid: QGridLayout):
# 重置 _make_section_card 预设的单列表单列宽配置
for c in range(10):
grid.setColumnMinimumWidth(c, 0)
grid.setColumnStretch(c, 0)
# 同样采用紧凑双列布局
grid.setColumnMinimumWidth(0, 60)
grid.setColumnStretch(1, 1)
grid.setColumnMinimumWidth(2, 100)
grid.setColumnMinimumWidth(3, 60)
grid.setColumnStretch(4, 1)
grid.setVerticalSpacing(8)
# ---- 第 1 行:死区 ----
row = 0
grid.addWidget(_compact_label("死区:", self), row, 0)
self.dz_entry = QLineEdit()
self.dz_entry.setPlaceholderText("默认2...")
grid.addWidget(_wrap_widget(self.dz_entry), row, 1)
# ---- 第 2 行:单步限幅 | 总限幅 ----
row = 1
grid.addWidget(_compact_label("单步限幅:", self), row, 0)
self.motor_max_entry = QLineEdit()
grid.addWidget(_wrap_widget(self.motor_max_entry), row, 1)
grid.addWidget(_compact_label("总限幅:", self), row, 3)
self.xa_full_entry = QLineEdit()
grid.addWidget(_wrap_widget(self.xa_full_entry), row, 4)
# ---- 第 3 行:模拟量映射最小值 | 最大值 ----
row = 2
grid.addWidget(_compact_label("模拟量映射最小值:", self), row, 0)
self.volthege_min_entry = QLineEdit("819")
grid.addWidget(_wrap_widget(self.volthege_min_entry), row, 1)
grid.addWidget(_compact_label("最大值:", self), row, 3)
self.volthege_max_entry = QLineEdit("4095")
grid.addWidget(_wrap_widget(self.volthege_max_entry), row, 4)
# ---- 第 4 行:确认设置按钮 ----
row = 3
self.confirm_settings_btn = QPushButton("确认设置")
self.confirm_settings_btn.setStyleSheet(_BTN_STYLE)
self.confirm_settings_btn.setCursor(Qt.PointingHandCursor)
self.confirm_settings_btn.clicked.connect(self._save_settings)
btn_wrap = _transparent_widget()
btn_h = QHBoxLayout(btn_wrap)
btn_h.setContentsMargins(0, 0, 0, 0)
btn_h.addWidget(self.confirm_settings_btn)
btn_h.addStretch()
grid.addWidget(btn_wrap, row, 0, 1, 5)
# ==========================================
# 辅助方法
# ==========================================
def _with_unit(self, line_edit: QLineEdit, unit: str) -> QWidget:
"""将输入框与单位标签组合为一个 widget,单位以灰色显示在输入框右侧。"""
w = _transparent_widget()
h = QHBoxLayout(w)
h.setContentsMargins(0, 0, 0, 0)
h.setSpacing(0)
h.addWidget(line_edit, 1)
unit_lbl = QLabel(unit)
unit_lbl.setStyleSheet(
"color: #94A3B8; font-size: 12px; background: transparent;"
"padding: 0 10px 0 6px;"
)
h.addWidget(unit_lbl)
return w
# ---- 信号处理 ----
def _on_start_identify(self):
if self._identifying_running:
self.identify_stop_requested.emit()
else:
self._identifying_running = True
self.identify_btn.setText(" ■ 结束辨识")
self.identify_btn.setStyleSheet(_BTN_STYLE_DANGER)
self.identify_start_requested.emit()
def _on_measure_volume(self):
if self._volume_running:
self.volume_stop_requested.emit()
else:
self._volume_running = True
self.test_btn.setText("停止")
self.test_btn.setStyleSheet(_BTN_STYLE_DANGER)
self.volume_measure_requested.emit()
# ---- 公开方法:任务完成后由 main_window 调用恢复按钮 ----
def set_identify_finished(self):
self._identifying_running = False
self.identify_btn.setText(" ▶ 开始辨识")
self.identify_btn.setStyleSheet(_BTN_STYLE)
def set_identification_feedback(self, text: str, state="neutral"):
"""显示当前辨识审核状态。"""
colors = {
"neutral": "#64748B",
"pending": "#2563EB",
"passed": "#15803D",
"failed": "#B91C1C",
}
color = colors.get(state, colors["neutral"])
self.ident_result_label.setText(text)
self.ident_result_label.setStyleSheet(
f"color: {color}; font-size: 13px; font-weight: 600;"
)
def set_volume_finished(self):
self._volume_running = False
self.test_btn.setText("测试")
self.test_btn.setStyleSheet(_BTN_STYLE)
# ---- 公开数据获取方法(接口与旧版完全兼容) ----
def get_identify_params(self) -> dict:
"""获取辨识参数"""
return {
"p_max": float(self.p_max_entry.text() or "200"),
"T_delta": float(self.T_delta_entry.text() or "30"),
"fit_high": float(self.fit_high_entry.text() or "200"),
"fit_low": float(self.fit_low_entry.text() or "50"),
"volume": float(self.volume_entry.text() or "0"),
"period": float(self.period_entry.text() or "2.5"),
"order": int(self.order_entry.text() or "6"),
"levels": self._parse_levels(),
}
def get_advanced_params(self) -> dict:
"""获取高级设置参数"""
dz = self.dz_entry.text().strip()
mm = self.motor_max_entry.text().strip()
xa = self.xa_full_entry.text().strip()
return {
"dz": float(dz) if dz else None,
"motor_max": float(mm) if mm else None,
"xa_full": float(xa) if xa else None,
"volthege_min": int(self.volthege_min_entry.text() or "0"),
"volthege_max": int(self.volthege_max_entry.text() or "4095"),
}
def _parse_levels(self) -> list:
"""解析序列输入"""
levels_str = self.levels_entry.text().strip()
if not levels_str:
print("未输入序列,使用默认值: 10,20,30,40,50,60,70,80")
return [10, 20, 30, 40, 50, 60, 70, 80]
try:
levels = [int(x.strip()) for x in levels_str.split(',')]
if len(levels) < 2:
print("序列至少需要两个值,使用默认值: 10,20,30,40,50,60,70,80")
return [10, 20, 30, 40, 50, 60, 70, 80]
print(f"使用自定义序列: {levels}")
return levels
except ValueError:
print("序列格式错误,使用默认值: 10,20,30,40,50,60,70,80")
return [10, 20, 30, 40, 50, 60, 70, 80]
def set_volume_text(self, vol: float):
"""设置容积输入框(测量完成后回填)"""
self.volume_entry.setText(f"{vol:.2f}")
# ---- 设置持久化 ----
def _save_settings(self):
"""将高级设置和容积保存到 QSettings,下次启动自动恢复"""
settings = QSettings("ReinLoop", "ReinLoop")
settings.setValue("advanced/dz", self.dz_entry.text())
settings.setValue("advanced/xa_full", self.xa_full_entry.text())
settings.setValue("advanced/volthege_min", self.volthege_min_entry.text())
settings.setValue("advanced/volthege_max", self.volthege_max_entry.text())
settings.setValue("identify/volume", self.volume_entry.text())
print("设置已保存")
def _load_settings(self):
"""从 QSettings 恢复上次保存的设置(contains 确保空值也能覆盖默认值)"""
settings = QSettings("ReinLoop", "ReinLoop")
if settings.contains("advanced/dz"):
self.dz_entry.setText(settings.value("advanced/dz"))
if settings.contains("advanced/xa_full"):
self.xa_full_entry.setText(settings.value("advanced/xa_full"))
if settings.contains("advanced/volthege_min"):
self.volthege_min_entry.setText(settings.value("advanced/volthege_min"))
if settings.contains("advanced/volthege_max"):
self.volthege_max_entry.setText(settings.value("advanced/volthege_max"))
if settings.contains("identify/volume"):
self.volume_entry.setText(settings.value("identify/volume"))
File diff suppressed because it is too large Load Diff
+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)
+47
View File
@@ -0,0 +1,47 @@
# status_bar.py
"""底部状态栏组件:日志 + 连接状态"""
import time
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel
from PySide6.QtCore import Qt
class StatusBar(QWidget):
"""底部状态栏 —— 左侧日志,右侧连接状态"""
def __init__(self, colors: dict, parent=None):
super().__init__(parent)
self.colors = colors
self.setProperty("cssClass", "bottomBar")
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 6, 12, 6)
# ---- 左侧:日志 ----
self.log_label = QLabel("就绪")
self.log_label.setProperty("cssClass", "logLabel")
self.log_label.setMinimumHeight(24)
layout.addWidget(self.log_label, stretch=3)
# ---- 右侧:连接状态(● 未连接 / ● 已连接) ----
self.status_label = QLabel("● 未连接")
self.status_label.setProperty("cssClass", "statusLabel")
self.status_label.setStyleSheet(
f"color: {colors.get('ERROR_RED', '#FF2424')}; font-weight: bold; font-size: 14px;"
)
layout.addWidget(self.status_label, stretch=1, alignment=Qt.AlignRight | Qt.AlignVCenter)
# ---- 公开接口 ----
def set_log(self, message: str):
"""设置日志消息(仅显示最新一条)"""
self.log_label.setText(f"{time.strftime('%H:%M:%S')} - {message}")
def set_connection_status(self, connected: bool, status_text: str = None):
"""设置连接状态显示"""
if status_text is None:
status_text = "● 已连接" if connected else "● 未连接"
color = self.colors.get("SUCCESS_GREEN", "#0F955D") if connected else self.colors.get("ERROR_RED", "#FF2424")
self.status_label.setText(status_text)
self.status_label.setStyleSheet(
f"color: {color}; font-weight: bold; font-size: 14px;"
)