428 lines
16 KiB
Python
428 lines
16 KiB
Python
# 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"))
|