Files
ReinLoopTest/ReinLoop/ui/debug_tab.py
T
2026-07-30 11:12:31 +08:00

428 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"))