from PySide6.QtCore import QTimer, Signal, QObject, Qt from PySide6.QtWidgets import QApplication # 用于获取主线程 import threading from view.main_window import MainWindow from .camera_controller import CameraController from .bottom_control_controller import BottomControlController from .hopper_controller import HopperController from .hopper_controller import UpperHopperPosition from common.constant_config_manager import ConfigManager from service.msg_recorder import MessageRecorder from service.opcua_ui_client import OpcuaUiClient from service.artifact_query_thread import ArtifactInfoQueryThread # 管片任务查询 from busisness.models import ArtifactInfoModel from typing import List class MainController: def __init__(self): # 主界面 self.main_window = MainWindow() self.msg_recorder = MessageRecorder() self.msg_recorder.normal_record("开始自动智能浇筑系统") # 记录系统状态消息 # 初始化子界面和控制器 self._initSubViews() self._initSubControllers() # 加载配置管理器 self.config_manager = ConfigManager() # 启动消息数据库(messages.db)定时清理任务 self.start_msg_database_clean_task() self.MSG_CLEAN_INTERVAL = None # opcua客户端 self.opc_client = OpcuaUiClient() self._start_opc_client() # 连接信号 self.__connectSignals() def showMainWindow(self): # self.main_window.showFullScreen() self.main_window.show() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") self.main_window.segment_task_widget.set_task_time("task2","17:24 PM") def _initSubControllers(self): # 右侧视频显示控制模块 self.camera_controller = CameraController( video_view=self.main_window.vibration_video ) # 底部按钮控制模块 self.bottom_control_controller = BottomControlController( bottom_control_widget=self.main_window.bottom_control_widget, main_window=self.main_window ) # 料斗控制模块(包括 夹爪开合、拱等按钮) self.hopper_controller = HopperController( hopper_view = self.main_window.hopper_widget, conveyor_view = self.main_window.conveyor_system_widget ) def _initSubViews(self): pass def __connectSignals(self): self.main_window.about_to_close.connect(self.handleMainWindowClose) # 处理主界面关闭 self.config_manager.msg_clean_interval_changed.connect(self.onMsgDbCleanIntervalChanged) # 消息清理间隔改变 self.opc_client.opc_signal.value_changed.connect(self._update_opc_value_to_ui, Qt.QueuedConnection) # opcua服务器值改变 def handleMainWindowClose(self): """主界面关闭""" self.msg_recorder.normal_record("关闭自动智能浇筑系统") # 停止系统底部控制器中的线程 if hasattr(self, 'bottom_control_controller'): self.bottom_control_controller.stop_threads() # 停止opc客户端 if hasattr(self, 'opc_client'): self._stop_opc_client() def start_msg_database_clean_task(self): """启动清理消息数据库(messages.db)中过期消息的定时任务""" from PySide6.QtCore import QTimer, QDateTime, QDate, QTime self.MSG_CLEAN_INTERVAL = self.config_manager.get_clean_interval() # 获取消息清理间隔 (单位: 秒) def clean_task(): from common.msg_db_helper import DBHelper DBHelper().clean_expired_messages(days_to_keep=self.config_manager.get_msg_keep_days()) def start_weekly_clean_at_midnight(): # 1. 计算当前时间到下一个凌晨0点的毫秒数 now = QDateTime.currentDateTime() tomorrow = QDate.currentDate().addDays(1) # 明天 next_midnight = QDateTime(tomorrow, QTime(0, 0, 0)) # 明天00:00:00 msecs_to_midnight = now.msecsTo(next_midnight) # 距离下一个凌晨的毫秒数 # 2. 首次触发:到零点后执行一次清理 clean_timer = QTimer() clean_timer.timeout.connect(clean_task) single_shot_timer = QTimer() single_shot_timer.setSingleShot(True) def first_clean(): clean_task() # 执行首次清理 # 之后每隔CLEAN_INTERVAL触发一次消息清理 clean_timer.start(self.MSG_CLEAN_INTERVAL) # 启动一次性定时器, 进行首次清理 single_shot_timer.timeout.connect(first_clean) single_shot_timer.start(msecs_to_midnight) return clean_timer, single_shot_timer # 启动定时器,并保存引用 self.msg_db_clean_timer, self.single_shot_timer = start_weekly_clean_at_midnight() def onMsgDbCleanIntervalChanged(self, new_interval): """当消息数据清理间隔变化时,更新定时器""" if self.MSG_CLEAN_INTERVAL == new_interval: return # 清理间隔未变 if hasattr(self, 'single_shot_timer') and self.single_shot_timer is not None: self.single_shot_timer.stop() self.single_shot_timer.deleteLater() self.single_shot_timer = None if hasattr(self, 'msg_db_clean_timer') and self.msg_db_clean_timer is not None: self.msg_db_clean_timer.stop() # 停止周期性触发消息清理 self.msg_db_clean_timer.deleteLater() self.msg_db_clean_timer = None # 用新间隔重新启动清理任务 self.start_msg_database_clean_task() def _update_opc_value_to_ui(self, node_id, var_name, new_value): """ OPCUA值变化时的UI更新函数 """ try: if var_name == "upper_weight": # 更新上料斗重量 self.hopper_controller.onUpdateUpperHopperWeight(new_value) elif var_name == "lower_weight": # 更新下料斗重量 self.hopper_controller.onUpdateLowerHopperWeight(new_value) elif var_name == "upper_volume": # 更新上料斗方量 self.hopper_controller.onUpdateUpperHopperVolume(new_value) elif var_name == "production_progress": progress = min(new_value, 100) # 限制为100, 进度为去掉百分号之后的整数 self.main_window.arc_progress.setProgress(progress) self.main_window.production_progress.setProgress(progress) elif var_name == "lower_clamp_angle": # 更新下料斗夹爪角度 self.hopper_controller.onUpdateLowerClampAngle(new_value) elif var_name == "upper_clamp_status": # 更新上料斗夹爪状态 0表示关闭 1表示打开 self.hopper_controller.onUpdateUpperClampStatus(new_value) elif var_name == "upper_hopper_position": # 更新上料斗位置 5表示料斗到位,到达振捣室处 66表示在搅拌楼处 self.hopper_controller.onUpdateUpperHopperPosition(new_value) if new_value == UpperHopperPosition.MIXING_TOWER.value: # 到达搅拌楼开启搅拌桨旋转 self.main_window.mixer_widget.startBladeMix() else: self.main_window.mixer_widget.stopBladeMix() elif var_name == "update_segment_tasks": need_update = new_value if need_update: # 需要更新管片任务 self._update_segment_tasks() except Exception as e: print(f"_update_opc_value_to_ui: 界面更新失败: {e}") import traceback traceback.print_exc() def _start_opc_client(self): """启动OPC UA客户端""" import time self.opc_retry_exit = threading.Event() def opc_worker(): # 连接服务器 while not self.opc_retry_exit.is_set(): if self.opc_client.connect(): # 创建订阅 self.opc_client.create_multi_subscription(interval=500) break # 连接成功,退出重连 time.sleep(2) # 启动子线程运行OPCUA逻辑 opc_thread = threading.Thread(target=opc_worker, daemon=True) opc_thread.start() def _stop_opc_client(self): """停止OPC UA客户端""" if hasattr(self, 'opc_retry_exit'): self.opc_retry_exit.set() if hasattr(self, 'opc_client') and self.opc_client.connected: self.opc_client.disconnect() def _update_segment_tasks(self): """更新左侧的管片任务""" # 1. 管片信息查询线程 query_thread = ArtifactInfoQueryThread() # 2. 主线程更新管片任务UI query_thread.query_finished.connect(self.onUpdateUiByArtifactInfo) # 3. 查询管片信息错误 query_thread.query_error.connect(self.onQueryArtifactInfoError) query_thread.start() def onUpdateUiByArtifactInfo(self, artifact_list:List[ArtifactInfoModel]): def convert_to_ampm(time_str: str) -> str: """时间格式转换: 转换为AM/PM形式""" from datetime import datetime time_formats = [ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f" ] for fmt in time_formats: try: dt = datetime.strptime(time_str, fmt) return dt.strftime("%I:%M%p") except ValueError: continue return "--:--" for index, artifact in enumerate(artifact_list, 1): if artifact.MouldCode: self.main_window.segment_task_widget.set_task_id(f"task{index}", artifact.MouldCode) # 模具号 if artifact.BetonVolume: self.main_window.segment_task_widget.set_task_volume(f"task{index}", artifact.BetonVolume) # 浇筑方量 if artifact.BeginTime: time_str = convert_to_ampm(artifact.BeginTime) self.main_window.segment_task_widget.set_task_time(f"task{index}", time_str) # 开始时间 def onQueryArtifactInfoError(self, error_msg:str): # 查询管片信息失败预警 self.msg_recorder.warning_record(error_msg)