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