# main_window.py """主窗口:组装所有 UI 组件,连接 Core 层业务逻辑""" import time import os import threading from collections import deque from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget, QLabel, QPushButton, QTabBar, QFrame ) from PySide6.QtCore import Qt, QTimer, Signal, QObject, QSize from PySide6.QtGui import QFont, QIcon from PySide6.QtSvgWidgets import QSvgWidget from ui.status_bar import StatusBar from ui.connection_tab import ConnectionTab from ui.control_tab import ControlTab from ui.debug_tab import DebugTab from ui.plot_window import PlotWindow from core.connection_manager import ConnectionManager from core.control_engine import ControlEngine from core.model_manager import ModelManager from core.data_collector import DataCollector from core.identification import IdentificationManager from core.identification_config import download_identification_config from core.identification_feedback import ( acknowledge_identification_feedback, get_identification_feedback, register_identification_result, ) from core.volume_config import ( acknowledge_volume_config_request, create_volume_config_request, poll_volume_config_request, ) from core.device_heartbeat import heartbeat_device from controllers import IncrementalPID import PcControl IDENTIFICATION_FEEDBACK_POLL_INTERVAL_MS = 2000 IDENTIFICATION_CONFIG_RETRY_INTERVAL_MS = 2000 VOLUME_REQUEST_STATUS_POLL_INTERVAL_MS = 2000 DEVICE_HEARTBEAT_INTERVAL_MS = 10_000 class _Bridge(QObject): """线程安全的信号桥:后台线程通过 signal.emit() → 主线程 slot 更新 UI""" log_signal = Signal(str) status_signal = Signal(bool, str) display_signal = Signal(float, float, float) pid_ui_signal = Signal(float, float, float) models_loaded_signal = Signal(object) model_load_complete = Signal(bool, str) sample_signal = Signal(object, object) # (valve_cmd, pressure) volume_result_signal = Signal(float) identification_config_loaded = Signal(int, object, object) identification_upload_complete = Signal(bool, object, object) identification_feedback_loaded = Signal(int, object, object) volume_request_created = Signal(int, object, object) volume_config_loaded = Signal(int, object, object) heartbeat_completed = Signal(object) class MainWindow(QMainWindow): """主窗口:ReinLoop-V1.0""" def __init__(self, colors: dict): super().__init__() self.colors = colors self.setWindowTitle("ReinLoop-V1.0 - 收敛有界") self.resize(900, 700) # ---- 业务层初始化 ---- self.pid = IncrementalPID(kp=1.0, ki=0.4, kd=0, dt=0.1, out_min=0, out_max=100, xa_full=1062.5) self.conn_mgr = ConnectionManager() self.model_mgr = ModelManager() self.data_collector = DataCollector() self.ident_mgr = IdentificationManager() self.engine = ControlEngine(self.pid) # 注入依赖到控制引擎 self.engine.set_connection_manager(self.conn_mgr) self.engine.set_model_manager(self.model_mgr) self.engine.set_data_collector(self.data_collector) # ---- 信号桥 (线程安全) ---- self._bridge = _Bridge() self._ident_config_request_id = 0 self._ident_config_request_inflight = False self._ident_workflow_active = False self._ident_waiting_for_updated_config = False self._ident_current_config = None self._ident_feedback_run_id = None self._ident_feedback_request_id = 0 self._ident_feedback_request_inflight = False self._ident_feedback_registered = False self._ident_feedback_last_error = None self._volume_workflow_active = False self._volume_measurement_running = False self._volume_config_request_id = 0 self._volume_config_request_inflight = False self._volume_cloud_request_id = None self._volume_request_expires_at_ms = None self._heartbeat_inflight = False self._heartbeat_last_error = None # 确认的目标压力 self.confirmed_target_pressure = 80.0 # 数据历史 (用于绘图,最多保留 10 小时 / 720000 点) _MAX = 720_000 self.pressure_data = deque(maxlen=_MAX) self.target_data = deque(maxlen=_MAX) self.valve_data = deque(maxlen=_MAX) self.time_data = deque(maxlen=_MAX) self.start_time = None # 绘图窗口引用 self._plot_window = None # ---- 搭建 UI ---- self._setup_ui() self._connect_signals() # ---- 初始化回调 ---- self._setup_callbacks() # 初始扫描模型列表 self.model_mgr.scan_models() def _setup_ui(self): """构建主窗口布局:顶部两层结构 (Layer1: Logo标题行, Layer2: Tab栏)""" central = QWidget() self.setCentralWidget(central) main_layout = QVBoxLayout(central) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) _src = os.path.join(os.path.dirname(__file__), "..", "src") # ========================================== # 1. 顶部区域 — Layer 1: 标题行(纯白背景) # ========================================== title_row = QWidget() title_row.setProperty("cssClass", "titleRow") title_layout = QHBoxLayout(title_row) title_layout.setContentsMargins(24, 12, 24, 12) title_layout.setSpacing(0) # Logo SVG logo_path = os.path.normpath(os.path.join(_src, "logo.svg")) logo_widget = QSvgWidget(logo_path) logo_widget.setFixedSize(100, 33) title_layout.addWidget(logo_widget) # 竖线分隔符 vdivider = QFrame() vdivider.setFrameShape(QFrame.VLine) vdivider.setFrameShadow(QFrame.Plain) vdivider.setFixedWidth(1) vdivider.setFixedHeight(28) vdivider.setStyleSheet("background-color: #E2E8F0; border: none;") title_layout.addSpacing(16) title_layout.addWidget(vdivider) title_layout.addSpacing(16) # 标题 "ReinLoop V1.0" title_lbl = QLabel("ReinLoop V1.0") title_lbl.setProperty("cssClass", "navTitle") title_layout.addWidget(title_lbl) title_layout.addStretch() main_layout.addWidget(title_row) # -- 分割线 1:标题行与 Tab 行之间的灰色细线 -- div1 = QFrame() div1.setFrameShape(QFrame.HLine) div1.setFixedHeight(1) div1.setStyleSheet("background-color: #E2E8F0; border: none;") main_layout.addWidget(div1) # ========================================== # 1. 顶部区域 — Layer 2: Tab 栏行(浅灰背景 #F8FAFC) # ========================================== tab_container = QWidget() tab_container.setProperty("cssClass", "tabRow") tab_container_layout = QHBoxLayout(tab_container) tab_container_layout.setContentsMargins(24, 0, 24, 0) tab_container_layout.setSpacing(0) # 使用 QTabBar(仅导航栏)+ 独立 QStackedWidget(内容区) self.tab_bar = QTabBar() self.tab_bar.setProperty("cssClass", "mainTab") self.tab_bar.setIconSize(QSize(22, 22)) self.tab_bar.setCursor(Qt.PointingHandCursor) self.tab_bar.setExpanding(False) self.tab_bar.setDrawBase(False) # Tab 图标路径(灰色/蓝色两套),保存为类属性方便动态切换 self._icons_gray = [ os.path.normpath(os.path.join(_src, "link_icon_gray.svg")), os.path.normpath(os.path.join(_src, "control_icon_gray.svg")), os.path.normpath(os.path.join(_src, "debug_icon_gray.svg")) ] self._icons_blue = [ os.path.normpath(os.path.join(_src, "link_icon.svg")), os.path.normpath(os.path.join(_src, "control_icon.svg")), os.path.normpath(os.path.join(_src, "debug_icon.svg")) ] # 初始默认全灰图标,文字前加半角空格确保图标与文字间距 self.tab_bar.addTab(QIcon(self._icons_gray[0]), " 连接设置") self.tab_bar.addTab(QIcon(self._icons_gray[1]), " 控制设置") self.tab_bar.addTab(QIcon(self._icons_gray[2]), " 模型调试") tab_container_layout.addWidget(self.tab_bar) tab_container_layout.addStretch() main_layout.addWidget(tab_container) # -- 分割线 2:Tab 行与主体内容之间的灰色细线 -- div2 = QFrame() div2.setFrameShape(QFrame.HLine) div2.setFixedHeight(1) div2.setStyleSheet("background-color: #E2E8F0; border: none;") main_layout.addWidget(div2) # ========================================== # 2. 页面堆栈 # ========================================== self.stack = QStackedWidget() self.tab_connection = ConnectionTab(self.colors) self.tab_control = ControlTab(self.colors) self.tab_debug = DebugTab(self.colors) self.stack.addWidget(self.tab_connection) self.stack.addWidget(self.tab_control) self.stack.addWidget(self.tab_debug) main_layout.addWidget(self.stack, stretch=1) # ========================================== # 3. 底部状态栏 # ========================================== self.status_bar = StatusBar(self.colors) main_layout.addWidget(self.status_bar) # 默认选中第一个标签 self.tab_bar.setCurrentIndex(0) self.stack.setCurrentIndex(0) def _switch_tab(self, index: int): """切换标签页,并动态刷新图标与 QSS 状态""" self.stack.setCurrentIndex(index) # 阻断信号,防止改变时死循环 self.tab_bar.blockSignals(True) self.tab_bar.setCurrentIndex(index) self.tab_bar.blockSignals(False) # 动态轮询,更新图标:选中项换蓝色高亮图标,其余换回灰色 for i in range(self.tab_bar.count()): if i == index: self.tab_bar.setTabIcon(i, QIcon(self._icons_blue[i])) else: self.tab_bar.setTabIcon(i, QIcon(self._icons_gray[i])) # 强制让控件重新 polish 样式,确保 QSS 中的 :selected 样式立即生效 self.tab_bar.style().unpolish(self.tab_bar) self.tab_bar.style().polish(self.tab_bar) # ========================================== # 回调设置(Core → UI 通过信号桥) # ========================================== def _setup_callbacks(self): """将 Core 层的回调全部桥接到主线程信号""" bridge = self._bridge # 日志 → status_bar def on_log(msg): bridge.log_signal.emit(msg) self.conn_mgr.set_log_callback(on_log) self.model_mgr.set_log_callback(on_log) self.data_collector.set_log_callback(on_log) self.ident_mgr.set_log_callback(on_log) self.engine.set_log_callback(on_log) # 连接状态 → status_bar self.conn_mgr.set_status_callback( lambda connected, text: bridge.status_signal.emit(connected, text) ) # 控制显示更新 → control_tab self.engine.set_display_update_callback( lambda p, t, v: bridge.display_signal.emit(p, t, v) ) # PID UI 更新 → control_tab self.engine.set_pid_ui_update_callback( lambda kp, ki, kd: bridge.pid_ui_signal.emit(kp, ki, kd) ) # 控制启停 → control_tab 按钮状态 + 模式锁定 self.engine.set_started_callback( self._on_engine_started ) self.engine.set_stopped_callback( self._on_engine_stopped ) # 模型列表加载完成 self.model_mgr.set_models_loaded_callback( lambda files: bridge.models_loaded_signal.emit(files) ) # 模型加载完成 self.model_mgr.set_load_complete_callback( lambda success, msg: bridge.model_load_complete.emit(success, msg) ) # 辨识/容积采样 self.ident_mgr.set_sample_callback( lambda u, p: bridge.sample_signal.emit(u, p) ) # 容积结果 self.ident_mgr.set_volume_result_callback( lambda vol: bridge.volume_result_signal.emit(vol) ) self.ident_mgr.set_identification_upload_callback( lambda success, filename, error: bridge.identification_upload_complete.emit( success, filename, error ) ) def _connect_signals(self): """连接信号桥到各 UI 组件槽函数""" bridge = self._bridge bridge.log_signal.connect(self.status_bar.set_log) bridge.status_signal.connect(self.status_bar.set_connection_status) bridge.display_signal.connect(self._on_display_update) bridge.pid_ui_signal.connect(self.tab_control.update_pid_entries) bridge.models_loaded_signal.connect(self.tab_control.update_model_list) bridge.model_load_complete.connect(self._on_model_load_complete) bridge.sample_signal.connect(self._on_sample_update) bridge.volume_result_signal.connect(self.tab_debug.set_volume_text) bridge.identification_config_loaded.connect( self._on_identification_config_loaded ) bridge.identification_upload_complete.connect( self._on_identification_upload_complete ) bridge.identification_feedback_loaded.connect( self._on_identification_feedback_loaded ) bridge.volume_request_created.connect(self._on_volume_request_created) bridge.volume_config_loaded.connect(self._on_volume_config_loaded) bridge.heartbeat_completed.connect(self._on_heartbeat_completed) # ---- UI 按钮 → Core 方法 ---- # 连接设置页 self.tab_connection.connect_btn.clicked.connect(self._on_connect_toggle) # 控制设置页 self.tab_control.target_set_requested.connect(self._on_set_target) self.tab_control.mode_changed.connect(self._on_mode_changed) self.tab_control.pid_update_requested.connect(self._on_update_pid) self.tab_control.model_load_requested.connect(self.model_mgr.load_model) self.tab_control.models_refresh_requested.connect(self.model_mgr.scan_models) self.tab_control.control_toggle_requested.connect(self._on_control_toggle) self.tab_control.plot_requested.connect(self._on_plot) self.tab_control.manual_valve_set_requested.connect(self._on_set_manual_valve) self.tab_control.log_message_requested.connect(self.status_bar.set_log) # 模型调试页 self.tab_debug.identify_start_requested.connect(self._on_identify_start) self.tab_debug.identify_stop_requested.connect(self._on_identify_stop) self.tab_debug.volume_measure_requested.connect(self._on_volume_measure) self.tab_debug.volume_stop_requested.connect(self._on_volume_stop) # 轮询辨识任务完成状态(100ms),任务结束后恢复按钮 self._ident_poll_timer = QTimer(self) self._ident_poll_timer.setInterval(100) self._ident_poll_timer.timeout.connect(self._poll_ident_done) self._ident_feedback_timer = QTimer(self) self._ident_feedback_timer.setInterval( IDENTIFICATION_FEEDBACK_POLL_INTERVAL_MS ) self._ident_feedback_timer.timeout.connect( self._request_identification_feedback ) self._ident_config_retry_timer = QTimer(self) self._ident_config_retry_timer.setInterval( IDENTIFICATION_CONFIG_RETRY_INTERVAL_MS ) self._ident_config_retry_timer.timeout.connect( self._request_identification_config ) self._volume_request_status_timer = QTimer(self) self._volume_request_status_timer.setInterval( VOLUME_REQUEST_STATUS_POLL_INTERVAL_MS ) self._volume_request_status_timer.timeout.connect( self._request_volume_config ) self._heartbeat_timer = QTimer(self) self._heartbeat_timer.setInterval(DEVICE_HEARTBEAT_INTERVAL_MS) self._heartbeat_timer.timeout.connect(self._request_device_heartbeat) self._heartbeat_timer.start() self._request_device_heartbeat() # ---- 导航栏 Tab 切换 → 页面切换 ---- self.tab_bar.currentChanged.connect(self._switch_tab) def _request_device_heartbeat(self): if self._heartbeat_inflight: return self._heartbeat_inflight = True def heartbeat_thread(): try: heartbeat_device() except Exception as exc: self._bridge.heartbeat_completed.emit(str(exc)) else: self._bridge.heartbeat_completed.emit(None) threading.Thread( target=heartbeat_thread, name="device-heartbeat", daemon=True, ).start() def _on_heartbeat_completed(self, error): self._heartbeat_inflight = False if error and error != self._heartbeat_last_error: self.status_bar.set_log(f"设备在线状态未上报: {error}") self._heartbeat_last_error = error # ========================================== # 槽函数 # ========================================== def _on_connect_toggle(self): if self.conn_mgr.is_connected(): self.status_bar.set_log("正在断开连接...") self.conn_mgr.disconnect() self.tab_connection._connect_text_lbl.setText(" 连接设备") self.status_bar.set_log("已断开设备连接") else: self.status_bar.set_log("正在连接设备,请稍候...") QApplication.processEvents() # 强制刷新 UI,让日志立即可见 params = self.tab_connection.get_connection_params() success = self.conn_mgr.connect(**params) if success: self.tab_connection._connect_text_lbl.setText(" 断开连接") self.status_bar.set_log("设备连接成功") else: self.status_bar.set_log("设备连接失败,请检查参数和硬件连接") def _on_set_target(self, target: float): if target < 0 or target > 3000: self.status_bar.set_log("错误: 目标压力必须在0-3000 kPa范围内") return self.confirmed_target_pressure = target self.engine.target_pressure = target # 运行中实时同步到控制引擎 # 智能自动模式(RL):每次设置目标时自动从 PLC 读取流量 if self.engine.mode == "RL": if self.conn_mgr.is_connected(): q = self.conn_mgr.read_flow() if q is not None: self.engine.flow = q self.tab_control.flow_entry.setText(f"{q:.1f}") self.status_bar.set_log( f"目标压力已设置为: {target} kPa | 已从模块读取流量: {q:.1f} L/min") return self.status_bar.set_log(f"目标压力已设置为: {target} kPa") def _on_mode_changed(self, mode: str): self.engine.mode = mode def _on_update_pid(self, kp: float, ki: float, kd: float): self.pid.kp = kp self.pid.ki = ki self.pid.kd = kd self.pid._calculate_coefficients() self.status_bar.set_log(f"PID参数已更新: Kp={kp:.3f}, Ki={ki:.3f}, Kd={kd:.3f}") def _on_engine_started(self): self.tab_control.set_control_running(True) self.tab_control.set_mode_switch_enabled(False) def _on_engine_stopped(self): self.tab_control.set_control_running(False) self.tab_control.set_mode_switch_enabled(True) def _on_control_toggle(self): if self.engine.is_running: self.engine.stop() self.status_bar.set_log("控制已停止") else: try: self._do_start_control() except Exception as e: import traceback err = traceback.format_exc() self.status_bar.set_log(f"启动控制失败: {e}") # 同时写入日志文件(打包后无控制台) try: import os, sys from datetime import datetime log_dir = os.path.join(os.path.dirname(sys.executable), "logs") os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, "reinloop_error.log") with open(log_file, "a", encoding="utf-8") as f: f.write(f"[{datetime.now().strftime('%Y%m%d_%H%M%S')}] _on_control_toggle 异常:\n{err}\n") except Exception: pass def _do_start_control(self): """实际启动控制的逻辑(从 _on_control_toggle 拆出,便于异常隔离)""" # 自动从 PLC 读取流量值并填入输入框 if self.conn_mgr.is_connected(): q = self.conn_mgr.read_flow() if q is not None: self.tab_control.flow_entry.setText(f"{q:.1f}") self.status_bar.set_log(f"已从模块读取流量: {q:.1f} L/min") # 收集参数 params = self.tab_control.get_control_params() pid_params = self.tab_control.get_pid_params() adv = self.tab_debug.get_advanced_params() # 同步电机限幅参数到 PcControl(总限幅→x_max, 模拟量映射→volthege_min/max) PcControl.set_motor_limits( volthege_min_val=adv.get("volthege_min"), volthege_max_val=adv.get("volthege_max"), x_max_val=adv.get("xa_full") ) self.engine.mode = self.tab_control.get_mode() self.engine.flow = params["flow"] self.engine.volume = params["volume"] self.engine.target_pressure = self.confirmed_target_pressure self.engine.dz = adv["dz"] self.engine.motor_max = adv["motor_max"] xa_full_val = adv.get("xa_full") self.engine.xa_full = float(xa_full_val) if xa_full_val is not None else 1062.5 self.pid.xa_full = float(xa_full_val) if xa_full_val is not None else 1062.5 self.engine.collect_data = self.tab_control.get_collect_data() if self.engine.mode == "MANUAL": self.engine.manual_valve = self.tab_control.get_manual_valve() # 重置数据 _MAX = 720_000 self.pressure_data = deque(maxlen=_MAX) self.target_data = deque(maxlen=_MAX) self.valve_data = deque(maxlen=_MAX) self.time_data = deque(maxlen=_MAX) self.start_time = time.time() # 启动定时器(周期 = PID dt,与 control_tick 内的 sleep 配合保证精确周期) # 先停止旧 timer(防止重复启动累积多个 timer) if hasattr(self, '_ui_timer') and self._ui_timer is not None: self._ui_timer.stop() self._ui_timer = QTimer(self) self._ui_timer.timeout.connect(self._poll_display) timer_ms = max(1, int(self.pid.dt * 1000)) self._ui_timer.start(timer_ms) self.engine.start() def _poll_display(self): """定时轮询:在主线程执行一个控制周期(读压力→计算→写电机→更新UI) 全部在主线程运行,避免 Cython 编译后在 PyInstaller 子线程中 segfault。 """ self.engine.control_tick() def _on_display_update(self, pressure: float, target: float, valve: float): """更新 UI 显示 + 记录数据(deque 自动滚动,最多保留 10 小时)""" self.tab_control.update_display(pressure, target, valve) if self.start_time is not None: elapsed = time.time() - self.start_time self.time_data.append(elapsed) self.pressure_data.append(pressure) self.target_data.append(target) self.valve_data.append(valve) def _on_model_load_complete(self, success: bool, message: str): self.status_bar.set_log(message) def _on_sample_update(self, valve_cmd, pressure): if valve_cmd is not None: self.tab_control.valve_opening_lbl.setText(f"{valve_cmd:.1f}") if pressure is not None: self.tab_control.current_pressure_lbl.setText(f"{pressure:.1f}") def _on_set_manual_valve(self, valve: float): if valve == -1: self.status_bar.set_log("错误: 目标阀开度超出范围") elif valve == -2: self.status_bar.set_log("错误: 请输入有效的数字") else: self.engine.manual_valve = valve self.status_bar.set_log(f"阀门开度已设置为: {valve}%") def _on_plot(self): if not self.pressure_data: self.status_bar.set_log("没有可绘制的数据") return # 关闭已有窗口(先断开 finished 信号,防止回调中 enable_plot_button(True) # 与下面紧跟的 enable_plot_button(False) 产生竞争,导致按钮状态闪烁) if self._plot_window is not None: try: self._plot_window.finished.disconnect(self._on_plot_closed) except RuntimeError: pass # 信号可能已被断开 self._plot_window.close() self._plot_window = None self.tab_control.enable_plot_button(False) try: self._plot_window = PlotWindow( self.time_data, self.pressure_data, self.target_data, self.valve_data, parent=self ) except Exception as e: import traceback self.status_bar.set_log(f"绘图失败: {e}") traceback.print_exc() self.tab_control.enable_plot_button(True) return self._plot_window.finished.connect(self._on_plot_closed) self._plot_window.show() self.status_bar.set_log("绘图窗口已打开") def _on_plot_closed(self): self._plot_window = None self.tab_control.enable_plot_button(True) def _on_identify_start(self): if self._volume_workflow_active: self.status_bar.set_log("请先停止容积测试") self.tab_debug.set_identify_finished() return self._ident_workflow_active = True self._ident_waiting_for_updated_config = False self._ident_current_config = None self._ident_config_request_id += 1 self._ident_config_request_inflight = False self._ident_feedback_request_id += 1 self._ident_feedback_request_inflight = False self._ident_feedback_registered = False self._ident_feedback_run_id = None self._ident_feedback_last_error = None self._ident_feedback_timer.stop() self._ident_config_retry_timer.stop() self.tab_debug.set_identification_feedback("正在获取参数", "pending") self.status_bar.set_log("正在从云端获取辨识参数...") self._request_identification_config() def _request_identification_config(self): """异步获取 9 参数 CSV;失败重试时禁止并发请求。""" if (not self._ident_workflow_active or self._ident_config_request_inflight): return request_id = self._ident_config_request_id self._ident_config_request_inflight = True def download_thread(): try: config = download_identification_config() except Exception as exc: self._bridge.identification_config_loaded.emit( request_id, None, str(exc) ) else: self._bridge.identification_config_loaded.emit( request_id, config, None ) threading.Thread( target=download_thread, name="identification-config-download", daemon=True, ).start() def _on_identification_config_loaded(self, request_id, config, error): """在主线程使用云端参数启动辨识,忽略已被停止的旧请求。""" if (request_id != self._ident_config_request_id or not self._ident_workflow_active): return self._ident_config_request_inflight = False if error: if self._ident_waiting_for_updated_config: self.status_bar.set_log( f"等待新辨识参数,将继续重试: {error}" ) else: self.status_bar.set_log(f"无法启动辨识: {error}") self.tab_debug.set_identification_feedback( "参数获取失败", "failed" ) self._ident_workflow_active = False self.tab_debug.set_identify_finished() return if (self._ident_waiting_for_updated_config and config == self._ident_current_config): self.status_bar.set_log( "辨识未通过,正在等待云端更新 9 参数 CSV..." ) return self._ident_waiting_for_updated_config = False self._ident_config_retry_timer.stop() self._ident_current_config = dict(config) # xa_full 同时参与 PRBS 行程换算和 PcControl 电机限幅。 PcControl.set_motor_limits(x_max_val=config["xa_full"]) self.status_bar.set_log("云端参数已加载,正在启动辨识数据采集...") self.tab_debug.set_identification_feedback("正在辨识", "pending") started = self.ident_mgr.start_identification( conn_mgr=self.conn_mgr, running_flag_check=lambda: self.engine.is_running, **config, ) if not started: self._ident_workflow_active = False self.tab_debug.set_identification_feedback("启动失败", "failed") self.tab_debug.set_identify_finished() return self._ident_poll_timer.start() def _on_identification_upload_complete(self, success, filename, error): """辨识 CSV 上传后登记本轮审核任务。""" if not self._ident_workflow_active: return if not success or not filename: self._ident_workflow_active = False self.tab_debug.set_identification_feedback("上传失败", "failed") self.status_bar.set_log(f"辨识结果上传失败: {error or '未知错误'}") self.tab_debug.set_identify_finished() return self._ident_feedback_run_id = filename self._ident_feedback_request_id += 1 self._ident_feedback_request_inflight = False self._ident_feedback_registered = False self._ident_feedback_last_error = None self.tab_debug.set_identification_feedback("等待反馈", "pending") self.status_bar.set_log("辨识 CSV 已上传,正在等待云端返回 0/1...") self._request_identification_feedback() self._ident_feedback_timer.start() def _request_identification_feedback(self): """登记本轮 CSV,并轮询服务器返回的数字 0/1。""" if (not self._ident_workflow_active or not self._ident_feedback_run_id or self._ident_feedback_request_inflight): return request_id = self._ident_feedback_request_id run_id = self._ident_feedback_run_id registered = self._ident_feedback_registered self._ident_feedback_request_inflight = True def feedback_thread(): try: if not registered: register_identification_result(run_id) payload = {"registered": True} else: payload = { "registered": True, "feedback": get_identification_feedback(run_id), } except Exception as exc: self._bridge.identification_feedback_loaded.emit( request_id, None, str(exc) ) else: self._bridge.identification_feedback_loaded.emit( request_id, payload, None ) threading.Thread( target=feedback_thread, name="identification-feedback-poll", daemon=True, ).start() def _on_identification_feedback_loaded(self, request_id, payload, error): if (request_id != self._ident_feedback_request_id or not self._ident_workflow_active): return self._ident_feedback_request_inflight = False if error: if error != self._ident_feedback_last_error: self.status_bar.set_log( f"辨识反馈查询失败,将继续重试: {error}" ) self._ident_feedback_last_error = error return self._ident_feedback_last_error = None if payload.get("registered") and not self._ident_feedback_registered: self._ident_feedback_registered = True self._request_identification_feedback() return feedback = payload.get("feedback") if feedback is None: return self._ident_feedback_timer.stop() run_id = self._ident_feedback_run_id self._ident_feedback_request_id += 1 self._ident_feedback_request_inflight = False self._ident_feedback_registered = False self._ident_feedback_run_id = None self._ack_identification_feedback(run_id) if feedback == 1: self._ident_workflow_active = False self.tab_debug.set_identification_feedback("已通过", "passed") self.status_bar.set_log("辨识结果:已通过") self.tab_debug.set_identify_finished() return self.tab_debug.set_identification_feedback("未通过", "failed") self.status_bar.set_log( "辨识结果:未通过,等待重新制定并上传 9 参数 CSV" ) self._ident_waiting_for_updated_config = True self._ident_config_request_inflight = False self._request_identification_config() self._ident_config_retry_timer.start() def _ack_identification_feedback(self, run_id): def ack_thread(): try: acknowledge_identification_feedback(run_id) except Exception as exc: self._bridge.log_signal.emit(f"确认辨识反馈失败: {exc}") threading.Thread( target=ack_thread, name="identification-feedback-ack", daemon=True, ).start() def _on_identify_stop(self): self._ident_workflow_active = False self._ident_waiting_for_updated_config = False self._ident_config_request_id += 1 self._ident_config_request_inflight = False self._ident_feedback_request_id += 1 self._ident_feedback_request_inflight = False self._ident_feedback_run_id = None self._ident_feedback_timer.stop() self._ident_config_retry_timer.stop() self.ident_mgr.stop() self.tab_debug.set_identification_feedback("已停止", "neutral") self.tab_debug.set_identify_finished() self.status_bar.set_log("已停止辨识及反馈监听") def _on_volume_measure(self): if self._ident_workflow_active: self.status_bar.set_log("请先结束辨识及反馈流程") self.tab_debug.set_volume_finished() return if self._volume_workflow_active: return self._volume_workflow_active = True self._volume_config_request_id += 1 self._volume_config_request_inflight = False self._volume_cloud_request_id = None self._volume_request_expires_at_ms = None self.status_bar.set_log("正在向云端发送容积参数请求指令...") self._create_volume_config_request() def _create_volume_config_request(self): """点击测试后只创建一次请求指令。""" generation = self._volume_config_request_id self._volume_config_request_inflight = True def create_thread(): try: request_info = create_volume_config_request() except Exception as exc: self._bridge.volume_request_created.emit( generation, None, str(exc) ) else: self._bridge.volume_request_created.emit( generation, request_info, None ) threading.Thread( target=create_thread, name="volume-request-create", daemon=True, ).start() def _on_volume_request_created(self, generation, request_info, error): if (generation != self._volume_config_request_id or not self._volume_workflow_active): if request_info and request_info.get("request_id"): self._ack_volume_config_request(request_info["request_id"]) return self._volume_config_request_inflight = False if error: self._volume_workflow_active = False self.status_bar.set_log(f"发送容积参数请求失败: {error}") self.tab_debug.set_volume_finished() return self._volume_cloud_request_id = request_info["request_id"] self._volume_request_expires_at_ms = request_info["expires_at_ms"] self.status_bar.set_log( "容积参数请求已发送,等待公司端上传本次 JSON..." ) self._volume_request_status_timer.start() self._request_volume_config() def _request_volume_config(self): """只查询已创建 requestId 的状态,不重复创建请求指令。""" if (not self._volume_workflow_active or self._volume_config_request_inflight or not self._volume_cloud_request_id): return generation = self._volume_config_request_id cloud_request_id = self._volume_cloud_request_id self._volume_config_request_inflight = True def download_thread(): try: result = poll_volume_config_request(cloud_request_id) except Exception as exc: self._bridge.volume_config_loaded.emit( generation, None, str(exc) ) else: self._bridge.volume_config_loaded.emit( generation, result, None ) threading.Thread( target=download_thread, name="volume-request-status", daemon=True, ).start() def _on_volume_config_loaded(self, generation, result, error): """检测本次请求关联的几分钟内 JSON,成功后加载一次。""" if (generation != self._volume_config_request_id or not self._volume_workflow_active): return self._volume_config_request_inflight = False if error: if (self._volume_request_expires_at_ms is not None and time.time() * 1000 >= self._volume_request_expires_at_ms): self._finish_volume_request("等待公司端参数超时") else: self.status_bar.set_log( f"查询本次容积参数请求失败,将继续等待: {error}" ) return if result.get("expired"): self._finish_volume_request("等待公司端参数超时") return if not result.get("ready"): return self._volume_request_status_timer.stop() cloud_request_id = self._volume_cloud_request_id self._volume_cloud_request_id = None self._volume_request_expires_at_ms = None self._ack_volume_config_request(cloud_request_id) config = result["config"] PcControl.set_motor_limits(x_max_val=config["xa_full"]) started = self.ident_mgr.start_volume_measurement( conn_mgr=self.conn_mgr, running_flag_check=lambda: self.engine.is_running, **config, ) if not started: self._volume_workflow_active = False self.tab_debug.set_volume_finished() return self._volume_measurement_running = True self.status_bar.set_log("已获取云端 8 参数,开始容积测量") self._ident_poll_timer.start() def _finish_volume_request(self, message): cloud_request_id = self._volume_cloud_request_id self._volume_workflow_active = False self._volume_config_request_inflight = False self._volume_cloud_request_id = None self._volume_request_expires_at_ms = None self._volume_request_status_timer.stop() if cloud_request_id: self._ack_volume_config_request(cloud_request_id) self.status_bar.set_log(message) self.tab_debug.set_volume_finished() def _ack_volume_config_request(self, request_id): def ack_thread(): try: acknowledge_volume_config_request(request_id) except Exception as exc: self._bridge.log_signal.emit(f"清理容积参数请求失败: {exc}") threading.Thread( target=ack_thread, name="volume-request-ack", daemon=True, ).start() def _on_volume_stop(self): cloud_request_id = self._volume_cloud_request_id self._volume_workflow_active = False self._volume_config_request_id += 1 self._volume_config_request_inflight = False self._volume_cloud_request_id = None self._volume_request_expires_at_ms = None self._volume_request_status_timer.stop() if cloud_request_id: self._ack_volume_config_request(cloud_request_id) if self._volume_measurement_running: self.ident_mgr.stop() self._volume_measurement_running = False self.tab_debug.set_volume_finished() self.status_bar.set_log("已停止容积测试") def _poll_ident_done(self): """100ms 轮询:检测辨识/测量任务是否自然结束,恢复按钮状态""" if self.ident_mgr.is_running: return self._ident_poll_timer.stop() if not self._ident_workflow_active: self.tab_debug.set_identify_finished() if self._volume_measurement_running: self._volume_measurement_running = False self._volume_workflow_active = False self.status_bar.set_log("容积测量完成") self.tab_debug.set_volume_finished() elif not self._volume_workflow_active: self.tab_debug.set_volume_finished() def closeEvent(self, event): """窗口关闭时清理资源""" self._ident_config_request_id += 1 self._ident_config_request_inflight = False self._ident_workflow_active = False self._ident_feedback_request_id += 1 self._ident_feedback_request_inflight = False self._ident_feedback_timer.stop() self._ident_config_retry_timer.stop() self._volume_workflow_active = False self._volume_config_request_id += 1 self._volume_request_status_timer.stop() self._heartbeat_timer.stop() if self._volume_cloud_request_id: self._ack_volume_config_request(self._volume_cloud_request_id) self._volume_cloud_request_id = None self.ident_mgr.stop() if self.engine.is_running: self.engine.stop() if self.conn_mgr.is_connected(): self.conn_mgr.disconnect() if self._plot_window is not None: self._plot_window.close() event.accept()