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
+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"))