update server
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# ui package - Pure PySide6 UI layer
|
||||
@@ -0,0 +1,177 @@
|
||||
# connection_tab.py
|
||||
"""页面1:Modbus 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"),
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
# ============================================================
|
||||
# 以下方法完全兼容旧版 API,main_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))
|
||||
@@ -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
@@ -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)
|
||||
@@ -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;"
|
||||
)
|
||||
Reference in New Issue
Block a user