diff --git a/common/msg_db_helper.py b/common/msg_db_helper.py index 9da22ea..bf99feb 100644 --- a/common/msg_db_helper.py +++ b/common/msg_db_helper.py @@ -80,6 +80,25 @@ class DBHelper: messages = cursor.fetchall() # 结果:[(content, is_processed, create_time, last_modified), ...] conn.close() return messages[::-1] # 反转后按时间正序排列 + + def clean_expired_messages(self, days_to_keep=30): + """删除超过指定天数(默认30天, 一个月)的过期消息,并压缩数据库""" + from datetime import datetime, timedelta + # 计算过期时间(当前时间 - days_to_keep天) + expire_time = (datetime.now() - timedelta(days=days_to_keep)).strftime("%Y-%m-%d %H:%M:%S") + + conn = sqlite3.connect(self.db_name) + cursor = conn.cursor() + # 删除过期消息 + cursor.execute(''' + DELETE FROM messages + WHERE create_time < ? + ''', (expire_time,)) + conn.commit() + # 压缩数据库,释放删除后的空闲空间 + cursor.execute("VACUUM") + conn.commit() + conn.close() if __name__ == "__main__": diff --git a/config/camera_config.ini b/config/camera_config.ini index b6b58d0..0d7c351 100644 --- a/config/camera_config.ini +++ b/config/camera_config.ini @@ -1,22 +1,22 @@ # camera_config.ini # 相关的摄像头的配置文件 [上位料斗] -ip = 192.168.250.60 +ip = 192.168.250.61 port = 554 username = admin password = XJ123456 -channel = 101 +channel = 102 [下位料斗] ip = 192.168.250.61 port = 554 username = admin password = XJ123456 -channel = 101 +channel = 102 [模具车] -ip = 192.168.250.61 +ip = 192.168.250.60 port = 554 username = admin password = XJ123456 -channel = 101 \ No newline at end of file +channel = 102 \ No newline at end of file diff --git a/controller/bottom_control_controller.py b/controller/bottom_control_controller.py index cc6d632..c2971c6 100644 --- a/controller/bottom_control_controller.py +++ b/controller/bottom_control_controller.py @@ -8,6 +8,8 @@ from view.widgets.message_popup_widget import MessagePopupWidget from service.msg_recorder import MessageRecorder from service.msg_query_thread import MsgQueryThread +from service.device_monitor_thread import DeviceMonitorThread + """ 控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。 以及 版本信息相关弹窗, 如: v1.0 @@ -20,11 +22,11 @@ class BottomControlController: # 系统诊断弹窗 self.system_diagnostics_dialog = SystemDiagnosticsDialog(self.main_window) - self._init_system_diagnostics_dialog_hide_animations() + self.current_diagnostics_row = 0 # 系统诊断弹窗的行号,从0开始 + self.current_diagnostics_col = 0 # 系统诊断弹窗的列号,从0开始 # 系统中心弹窗 self.system_center_dialog = SystemCenterDialog(self.main_window) - self._init_system_center_dialog_hide_animations() # ===================== 消息列表相关 ==================================== # 系统状态消息列表控件 @@ -40,6 +42,11 @@ class BottomControlController: self.msg_query_thread.start() # 启动线程 # ======================================================================= + # ===================== 设备检测(系统诊断相关) ==================================== + self.device_monitor = DeviceMonitorThread() + self.device_monitor.start() + # ======================================================================= + # 绑定主界面底部按钮的信号(如:系统诊断按钮等,点击触发弹窗) self._bind_buttons() # 绑定弹窗中按钮的信号 @@ -51,20 +58,17 @@ class BottomControlController: # 清空当前消息列表(避免重复) target_widget = self.status_msg_widget if message_type == 1 else self.warning_msg_widget target_widget.list_widget.clear() - # target_widget.messages.clear() # 添加新消息 (第三个为 create_time, 第四个为 last_modified) for content, is_processed, create_time, _ in messages: - # 从create_time中提取时分秒 - # time_part = create_time.split()[1] # 空格分隔之后的[1]为时分秒 - # 需要添加到消息列表的消息格式为 时分秒 + 消息原始内容 - # msg_list_content = f"{create_time} {content}" target_widget.add_message(content, is_processed = bool(is_processed), msg_time = create_time) def stop_threads(self): """停止当前控制器中的所有线程""" if hasattr(self, 'msg_query_thread') and self.msg_query_thread.isRunning(): self.msg_query_thread.stop() + if hasattr(self, 'device_monitor') and self.device_monitor.isRunning(): + self.device_monitor.stop_thread() def _bind_buttons(self): # 底部系统中心按钮 → 触发弹窗显示/隐藏 @@ -78,6 +82,9 @@ class BottomControlController: # 底部预警消息列表按钮 → 触发预警消息列表显示/隐藏 self.bottom_control_widget.warning_list_btn.clicked.connect(self.toggle_system_warning_msg_list) + + # 设备检测结果显示(底部的系统诊断按钮处显示) + self.device_monitor.state_result.connect( self.bottom_control_widget.set_system_status, Qt.QueuedConnection) def _bind_dialog_signals(self): """绑定弹窗按钮的信号""" @@ -86,45 +93,17 @@ class BottomControlController: self.system_center_dialog.data_center_clicked.connect(self.handle_data_center) self.system_center_dialog.user_center_clicked.connect(self.handle_user_center) - def _init_system_center_dialog_hide_animations(self): - """初始化系统中心弹窗隐藏动画(淡出+缩小,与显示动画对应)""" - # 1. 淡出动画 - self.hide_opacity_anim = QPropertyAnimation(self.system_center_dialog, b"windowOpacity", self.system_center_dialog) - self.hide_opacity_anim.setDuration(200) - self.hide_opacity_anim.setStartValue(1.0) - self.hide_opacity_anim.setEndValue(0.0) - - # 2. 缩小动画 - self.hide_scale_anim = QPropertyAnimation(self.system_center_dialog, b"geometry", self.system_center_dialog) - self.hide_scale_anim.setDuration(200) - self.hide_scale_anim.setEasingCurve(QEasingCurve.InBack) # 收缩感 - - # 3. 组合动画(父对象设为弹窗) - self.hide_anim_group = QParallelAnimationGroup(self.system_center_dialog) - self.hide_anim_group.addAnimation(self.hide_opacity_anim) - self.hide_anim_group.addAnimation(self.hide_scale_anim) - - # 动画结束后,强制隐藏弹窗 - self.hide_anim_group.finished.connect(self.system_center_dialog.hide) + # 系统诊断弹窗的信号 + self.device_monitor.check_finished.connect(self.reset_diagnostics_row_col) # 重置系统诊断的行号和列号 + self.device_monitor.connect_success.connect(self._handle_diagnostics_connect_success) # 设备正常 + self.device_monitor.connect_failed.connect(self._handle_diagnostics_connect_failed) # 设备异常 + # ------------------- 系统中心弹窗逻辑------------------- def toggle_system_center_dialog(self): """切换系统中心弹窗的显示/隐藏状态""" if self.system_center_dialog.isVisible(): # 已显示 → 隐藏 - # self.system_center_dialog.hide() - # 动态设置缩小动画的起点和终点(基于当前弹窗位置) - current_geo = self.system_center_dialog.geometry() - end_rect = QRect( - current_geo.center().x() - current_geo.width() * 0.4, - current_geo.center().y() - current_geo.height() * 0.4, - int(current_geo.width() * 0.8), - int(current_geo.height() * 0.8) - ) - self.hide_scale_anim.setStartValue(current_geo) - self.hide_scale_anim.setEndValue(end_rect) - - # 启动隐藏动画 (隐藏动画结束,自动隐藏 系统中心弹窗) - self.hide_anim_group.start() + self.system_center_dialog.hide() else: # 未显示 → 计算位置并显示 self._calc_system_center_dialog_position() # 每次显示都计算位置 @@ -143,7 +122,7 @@ class BottomControlController: # 计算弹窗坐标 btn_width = btn.width() dialog_size = self.system_center_dialog.size() - dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) // 2 + dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) // 2 dialog_y = btn_pos_rel_main.y() - dialog_size.height() # 设置弹窗位置 @@ -166,86 +145,121 @@ class BottomControlController: # print("执行用户中心逻辑:如切换用户、修改密码等") # ------------------- 系统诊断弹窗逻辑------------------- - def _init_system_diagnostics_dialog_hide_animations(self): - """初始化系统诊断弹窗隐藏动画(与显示动画反向:滑出+淡出)""" - # 1. 淡出动画(与显示动画时长一致) - self.dia_hide_opacity_anim = QPropertyAnimation( - self.system_diagnostics_dialog, b"windowOpacity", self.system_diagnostics_dialog - ) - self.dia_hide_opacity_anim.setDuration(300) # 显示动画为400ms - self.dia_hide_opacity_anim.setStartValue(1.0) - self.dia_hide_opacity_anim.setEndValue(0.0) - - # 2. 位置动画(从当前位置滑出到下方100px,与显示动画反向) - self.dia_hide_pos_anim = QPropertyAnimation( - self.system_diagnostics_dialog, b"geometry", self.system_diagnostics_dialog - ) - self.dia_hide_pos_anim.setDuration(300) - self.dia_hide_pos_anim.setEasingCurve(QEasingCurve.InQuart) # 滑出曲线与显示反向 - - # 3. 组合动画(同时执行滑出和淡出) - self.dia_hide_anim_group = QParallelAnimationGroup(self.system_diagnostics_dialog) - self.dia_hide_anim_group.addAnimation(self.dia_hide_opacity_anim) - self.dia_hide_anim_group.addAnimation(self.dia_hide_pos_anim) - # 动画结束后强制隐藏弹窗 - self.dia_hide_anim_group.finished.connect(self.system_diagnostics_dialog.hide) - def toggle_system_diagnostics_dialog(self): """切换系统诊断弹窗的显示/隐藏状态""" if self.system_diagnostics_dialog.isVisible(): - # 已显示 → 执行隐藏动画 - self._start_diagnostics_hide_animation() + # 已显示 → 执行隐藏 + self.system_diagnostics_dialog.hide() else: - # 未显示 → 计算位置并显示(触发显示动画) + # 立即执行设备检测 + self.device_monitor.force_immediate_check() + # 未显示 → 计算位置并显示 self._calc_system_diagnostics_dialog_position() self.system_diagnostics_dialog.show() + def _calc_system_diagnostics_dialog_position(self): """计算系统诊断弹窗位置(显示在系统诊断按钮上方)""" btn = self.bottom_control_widget.diagnosis_btn # 诊断按钮 + bottom_widget = self.bottom_control_widget - btn_pos = btn.mapToGlobal(QPoint(0, 0)) - dialog_x = btn_pos.x() - dialog_y = btn_pos.y() - self.system_diagnostics_dialog.height() + # 计算按钮相对于主窗口的位置 + bottom_pos_rel_main = bottom_widget.pos() + btn_pos_rel_bottom = btn.pos() + btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom - # 设置弹窗位置(动画会基于此位置执行滑入效果) + # 计算弹窗坐标 + dialog_size = self.system_diagnostics_dialog.size() + dialog_x = btn_pos_rel_main.x() + dialog_y = btn_pos_rel_main.y() - dialog_size.height() + + # 设置弹窗位置 self.system_diagnostics_dialog.move(dialog_x, dialog_y) - def _start_diagnostics_hide_animation(self): - """启动系统诊断弹窗的隐藏动画(滑出+淡出)""" - current_geo = self.system_diagnostics_dialog.geometry() # 当前位置和尺寸 - # 计算隐藏动画终点(当前位置下方100px,与显示动画起点对应) - end_rect = QRect( - current_geo.x(), - current_geo.y() + 100, # 向下滑出100px - current_geo.width(), - current_geo.height() - ) - # 设置动画参数并启动 - self.dia_hide_pos_anim.setStartValue(current_geo) - self.dia_hide_pos_anim.setEndValue(end_rect) - self.dia_hide_anim_group.start() + def get_diagnostics_row_col(self): + """获取系统诊断弹窗的 行号 和 列号, 都从0开始""" + diagnostics_row = self.current_diagnostics_row + diagnostics_col = self.current_diagnostics_col + self.current_diagnostics_col += 1 + if self.current_diagnostics_col == self.system_diagnostics_dialog.max_col: + self.current_diagnostics_row += 1 + self.current_diagnostics_col = 0 + if self.current_diagnostics_row == self.system_diagnostics_dialog.max_row: + self.current_diagnostics_row = 0 + return diagnostics_row, diagnostics_col + + def reset_diagnostics_row_col(self): + """重置系统诊断弹窗的 行号 和 列号 为0""" + self.current_diagnostics_row = 0 + self.current_diagnostics_col = 0 + + def _handle_diagnostics_connect_success(self, device_name:str, delay:int): + """处理系统诊断弹窗: 设备连接成功 (设备检测正常)""" + row, col = self.get_diagnostics_row_col() + self.system_diagnostics_dialog.set_selected_device(row, col, device_name) + self.system_diagnostics_dialog.set_ms_value(row, col, delay) + if delay <= self.device_monitor.warning_delay: + self.system_diagnostics_dialog.set_circle_status(row, col, "normal") + else: + self.system_diagnostics_dialog.set_circle_status(row, col, "warning") + + def _handle_diagnostics_connect_failed(self, device_name:str): + """处理系统诊断弹窗: 设备检测异常""" + row, col = self.get_diagnostics_row_col() + self.system_diagnostics_dialog.set_selected_device(row, col, device_name) + self.system_diagnostics_dialog.set_ms_value(row, col, -1) + self.system_diagnostics_dialog.set_circle_status(row, col, "error") # ================== 系统状态消息列表: 显示系统消息 =================== def toggle_system_status_msg_list(self): - """系统状态消息按钮点击之后 显示系统消息""" + """切换系统状态消息弹窗的显示/隐藏状态""" if not self.status_msg_widget.isVisible(): - btn_pos = self.bottom_control_widget.status_msg_btn.mapToGlobal(QPoint(0, 0)) - popup_x = btn_pos.x() - popup_y = btn_pos.y() - self.status_msg_widget.height() - self.status_msg_widget.move(popup_x, popup_y) + self._calc_system_status_msg_position() self.status_msg_widget.show() else: - self.status_msg_widget.close() + self.status_msg_widget.hide() + + def _calc_system_status_msg_position(self): + """计算系统状态消息弹窗位置, 并move移动""" + btn = self.bottom_control_widget.status_msg_btn + bottom_widget = self.bottom_control_widget + + # 计算按钮相对于主窗口的位置 + bottom_pos_rel_main = bottom_widget.pos() + btn_pos_rel_bottom = btn.pos() + btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom + + # 计算弹窗坐标 + dialog_size = self.status_msg_widget.size() + dialog_x = btn_pos_rel_main.x() + dialog_y = btn_pos_rel_main.y() - dialog_size.height() + + # 设置弹窗位置 + self.status_msg_widget.move(dialog_x, dialog_y) # ================== 预警消息列表: 显示预警消息 =================== def toggle_system_warning_msg_list(self): - """预警消息列表按钮点击之后 显示预警消息""" + """切换预警消息列表弹窗的显示/隐藏状态""" if not self.warning_msg_widget.isVisible(): - btn_pos = self.bottom_control_widget.warning_list_btn.mapToGlobal(QPoint(0, 0)) - popup_x = btn_pos.x() - popup_y = btn_pos.y() - self.warning_msg_widget.height() - self.warning_msg_widget.move(popup_x, popup_y) + self._calc_system_warning_msg_position() self.warning_msg_widget.show() else: - self.warning_msg_widget.close() \ No newline at end of file + self.warning_msg_widget.hide() + + def _calc_system_warning_msg_position(self): + """计算预警消息列表弹窗位置, 并move移动""" + btn = self.bottom_control_widget.warning_list_btn + bottom_widget = self.bottom_control_widget + + # 计算按钮相对于主窗口的位置 + bottom_pos_rel_main = bottom_widget.pos() + btn_pos_rel_bottom = btn.pos() + btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom + + # 计算弹窗坐标 + dialog_size = self.warning_msg_widget.size() + dialog_x = btn_pos_rel_main.x() + dialog_y = btn_pos_rel_main.y() - dialog_size.height() + + # 设置弹窗位置 + self.warning_msg_widget.move(dialog_x, dialog_y) \ No newline at end of file diff --git a/controller/camera_controller.py b/controller/camera_controller.py index ee37a95..0cadaff 100644 --- a/controller/camera_controller.py +++ b/controller/camera_controller.py @@ -13,10 +13,6 @@ class CameraController: cam1_config = load_camera_config("上位料斗") cam2_config = load_camera_config("下位料斗") cam3_config = load_camera_config("模具车") - - # print("rtsp_url1:", cam1_config["rtsp_url"]) - # print("rtsp_url2:", cam2_config["rtsp_url"]) - # print("rtsp_url3:", cam3_config["rtsp_url"]) # 设置 camera_urls self.video_view.set_camera_urls( diff --git a/controller/hopper_controller.py b/controller/hopper_controller.py index 029ed4d..92ba38f 100644 --- a/controller/hopper_controller.py +++ b/controller/hopper_controller.py @@ -4,6 +4,15 @@ from hardware.transmitter import TransmitterController from hardware.relay import RelayController from view.widgets.hopper_widget import HopperWidget from view.widgets.conveyor_system_widget import ConveyorSystemWidget +from enum import Enum + +class UpperHopperPosition(Enum): + """上料斗位置 + - MIXING_TOWER: 搅拌楼处,对应数值 66 + - VIBRATION_CHAMBER: 振捣室处,对应数值 5 + """ + MIXING_TOWER = 66 # 搅拌楼处 + VIBRATION_CHAMBER = 5 # 振捣室处 # 信号类:后台线程向主线程传递数据 class HopperSignals(QObject): @@ -18,30 +27,30 @@ class HopperController: # 下料斗夹爪测试用例数据 # 注意:目前只控制 下料斗的夹爪角度变化 - self.angle = 10 # 夹爪当前角度 - self.max_angle = 60 # 夹爪最大张开角度 - self.min_angle = 10 # 夹爪最小张开角度 + self.angle = 10.33 # 夹爪当前角度 + self.max_angle = 60.00 # 夹爪最大张开角度 + self.min_angle = 10.00 # 夹爪最小张开角度 self.is_add = True # 角度增加/减小 控制 self.timer_angle = QTimer() # 角度更新定时器 self.timer_angle.setInterval(1000) # 1秒更新一次角度 # 重量读取定时器 - self.timer_weight = QTimer() # 重量读取定时器 - self.timer_weight.setInterval(2000) # 每2秒读取一次重量 + # self.timer_weight = QTimer() # 重量读取定时器 + # self.timer_weight.setInterval(2000) # 每2秒读取一次重量 # 绑定信号 self._connect_signals() # 开启定时器 self.timer_angle.start() - self.timer_weight.start() + # self.timer_weight.start() def _connect_signals(self): # 更新上料斗重量 self.signals.upper_weight_updated.connect(self.onUpdateUpperHopperWeight) # 上料斗重量定时读取 - self.timer_weight.timeout.connect(self.handleReadUpperHopperWeight) + # self.timer_weight.timeout.connect(self.handleReadUpperHopperWeight) # 下料斗夹爪定时更新 self.timer_angle.timeout.connect(self.handleLowerClampAngleUpdate) @@ -62,9 +71,9 @@ class HopperController: """处理下料斗夹爪开合""" # 角度增减逻辑 if self.is_add: - self.angle += 1 + self.angle += 1.22 else: - self.angle -= 1 + self.angle -= 1.33 # 边界控制 if self.angle > self.max_angle: @@ -118,7 +127,35 @@ class HopperController: # 上料斗 夹爪 "开"按钮点击 print("hopper_controller: onUpperClampOpenBottonClicked") # 测试上料斗夹爪,6秒打开60度 - self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=6) + self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=10) + + @Slot(int) + def onUpdateUpperClampStatus(self, status:int): + # 上料斗夹爪状态 1表示打开,0表示关闭 + if status: + # 执行上料斗夹爪打开动画 + self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=10) + else: + # 执行上料斗夹爪关闭动画 + self.hopper_view.upper_clamp_widget.testAnimation(target_angle=0, duration=10) + + def onUpdateUpperHopperPosition(self, position:int): + # 上料斗位置 + if position == UpperHopperPosition.MIXING_TOWER.value: # 上料斗到达搅拌楼,值为66 + # 上料斗在搅拌楼下 + self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗 + self.conveyor_view.moveHopperBelowMixer() # 传送带处的上料斗移动到搅料楼下 + self.conveyor_view.showHopper() # 显示传送带处的上料斗 + elif position == UpperHopperPosition.VIBRATION_CHAMBER.value: # 上料斗就绪,到达振捣室,值为5 + # 上料斗在振捣室处 + self.conveyor_view.hideHopper() # 隐藏传送带处的上料斗 + self.hopper_view.upper_clamp_widget.set_angle(0) # 上料斗夹爪角度设置为0,此时上料斗一定是关闭的 + self.hopper_view.showUpperHopper() # 显示非传送带处的上料斗 + else: + # 上料斗在途中 + self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗 (下料斗处对应的上料斗叫非传送带处上料斗) + self.conveyor_view.moveHopperToTransition() # 传送带处上料斗移动到中间过渡位置 + self.conveyor_view.showHopper() # 显示传送带处的上料斗 @Slot(bool) def onUpperArchBreaking(self, status:bool): diff --git a/controller/main_controller.py b/controller/main_controller.py index c74e8aa..19db865 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QTimer, Signal, QObject # 导入Qt核心类 +from PySide6.QtCore import QTimer, Signal, QObject, Qt from PySide6.QtWidgets import QApplication # 用于获取主线程 import threading from hardware import transmitter @@ -6,8 +6,16 @@ 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 service.msg_recorder import MessageRecorder +from common.constant_config_manager import ConfigManager + +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): @@ -15,18 +23,29 @@ class MainController: self.main_window = MainWindow() self.msg_recorder = MessageRecorder() - self.msg_recorder.normal_record("开始自动智能浇筑系统") + 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.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") @@ -58,9 +77,178 @@ class MainController: 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("关闭自动智能浇筑系统") + self.msg_recorder.normal_record("关闭自动智能浇筑系统") # 记录系统状态消息 + # 停止系统底部控制器中的线程 if hasattr(self, 'bottom_control_controller'): - self.bottom_control_controller.stop_threads() \ No newline at end of file + 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) + \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index 2bf54d9..826470f 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -332,6 +332,7 @@ class MainWindow(QWidget): def closeEvent(self, e): """窗口关闭时的回调""" + self.hide() # 隐藏界面 self.about_to_close.emit() super().closeEvent(e) diff --git a/view/widgets/hopper_widget.py b/view/widgets/hopper_widget.py index bc52c42..d4fc522 100644 --- a/view/widgets/hopper_widget.py +++ b/view/widgets/hopper_widget.py @@ -140,7 +140,7 @@ class HopperWidget(QWidget): self.upper_weight_label.move(outer_width//2 - 60, outer_height//2 - 46) # 额外文字(上位) - self.upper_extra_label = QLabel("2.0方(预估)", self.upper_bg_widget) + self.upper_extra_label = QLabel("2.00方", self.upper_bg_widget) self.upper_extra_label.setAlignment(Qt.AlignCenter) # #262c38 #16ffff #131427 #003669 self.upper_extra_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;") @@ -359,7 +359,8 @@ class HopperWidget(QWidget): """Args: volume : 传入多少方 """ - self.upper_extra_label.setText(f"{volume}方(预估)") + volume_rounded = round(volume, 2) # 四舍五入,保留两位小数 + self.upper_extra_label.setText(f"{volume_rounded:.2f}方") # 上料斗夹爪开合角度设置 def setUpperHopperClampAngle(self, angle: float): @@ -413,8 +414,9 @@ class HopperWidget(QWidget): """Args: angle : 传入多少度 (单位°) """ - self.lower_extra_label.setText(f"开: {angle}°") # 设置下料斗角度标签 - self.lower_clamp_widget.set_angle(angle) # 设置下料斗夹爪开合角度 + angle_rounded = round(angle, 2) # 四舍五入,保留两位小数 + self.lower_extra_label.setText(f"开: {angle_rounded:.2f}°") # 设置下料斗角度标签 + self.lower_clamp_widget.set_angle(angle_rounded) # 设置下料斗夹爪开合角度 # ------------------------------ # 设置上料斗状态(0=绿,1=黄,2=红) diff --git a/view/widgets/mixer_widget.py b/view/widgets/mixer_widget.py index 921a17b..9c5cf82 100644 --- a/view/widgets/mixer_widget.py +++ b/view/widgets/mixer_widget.py @@ -80,6 +80,9 @@ class MixerWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) + # 状态变量:标记搅拌桨是否正在旋转 + self.is_mixing = False # 初始状态为未旋转 + # 两个搅拌桨的转动的动画引用 self.animations = [] # 保存动画引用 @@ -157,12 +160,20 @@ class MixerWidget(QWidget): # 搅拌桨开始搅拌 def startBladeMix(self, duration=700): + if self.is_mixing: # 搅拌桨已经在旋转 + return + self.animations.clear() # 备注:duration控制搅拌桨旋转的速度,值越小旋转得越快 self._start_animation(self.blade1, duration) self._start_animation(self.blade2, duration) + self.is_mixing = True # 更新搅拌桨状态为旋转中 + def stopBladeMix(self): + if not self.is_mixing: # 搅拌桨已经停止 + return + for animation in self.animations: animation.stop() @@ -170,3 +181,5 @@ class MixerWidget(QWidget): self.blade1.reset_to_original() if self.blade2: self.blade2.reset_to_original() + + self.is_mixing = False # 更新搅拌桨状态为停止 diff --git a/view/widgets/system_center_dialog.py b/view/widgets/system_center_dialog.py index 1edee1f..e74e45a 100644 --- a/view/widgets/system_center_dialog.py +++ b/view/widgets/system_center_dialog.py @@ -61,7 +61,6 @@ class SystemCenterDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self._init_ui() - self.init_animations() # 初始化动画 def _init_ui(self): # 弹窗基础设置 @@ -103,47 +102,6 @@ class SystemCenterDialog(QDialog): painter.drawPixmap(self.rect(), self.background) super().paintEvent(event) - def init_animations(self): - """初始化显示动画(可根据喜好选择或组合)""" - # 1. 淡入动画(透明度从0→1) - self.opacity_anim = QPropertyAnimation(self, b"windowOpacity") - self.opacity_anim.setDuration(300) # 动画时长300ms - self.opacity_anim.setStartValue(0.0) - self.opacity_anim.setEndValue(1.0) - self.opacity_anim.setEasingCurve(QEasingCurve.InOutCubic) # 缓动曲线(平滑加速减速) - - # 2. 缩放动画(从80%→100%大小) - self.scale_anim = QPropertyAnimation(self, b"geometry") - self.scale_anim.setDuration(300) - # 起点和终点在显示时动态设置(依赖当前弹窗位置) - self.scale_anim.setEasingCurve(QEasingCurve.OutBack) # 带弹性的缓动曲线(弹出感) - - # 3. 组合动画(同时执行淡入+缩放) - from PySide6.QtCore import QParallelAnimationGroup - self.anim_group = QParallelAnimationGroup(self) - self.anim_group.addAnimation(self.opacity_anim) - self.anim_group.addAnimation(self.scale_anim) - - def showEvent(self, event): - """重写显示事件,每次显示时启动动画""" - # 必须先调用父类showEvent,否则弹窗无法正常显示 - super().showEvent(event) - - # 动态设置缩放动画的起点(基于当前弹窗位置和大小) - current_geometry = self.geometry() # 弹窗当前位置和大小(已通过move设置) - # 起点:缩小到80%,并保持中心位置不变 - start_rect = QRect( - current_geometry.center().x() - current_geometry.width() * 0.4, - current_geometry.center().y() - current_geometry.height() * 0.4, - int(current_geometry.width() * 0.8), - int(current_geometry.height() * 0.8) - ) - self.scale_anim.setStartValue(start_rect) - self.scale_anim.setEndValue(current_geometry) # 终点:原始大小 - - # 启动组合动画 - self.anim_group.start() - if __name__ == "__main__": app = QApplication(sys.argv) dialog = SystemCenterDialog() diff --git a/view/widgets/system_diagnostics_dialog.py b/view/widgets/system_diagnostics_dialog.py index a210f27..f7c5b2c 100644 --- a/view/widgets/system_diagnostics_dialog.py +++ b/view/widgets/system_diagnostics_dialog.py @@ -66,44 +66,47 @@ class CustomDropdown(QWidget): ) # 2. 可点击的箭头标签(QLabel) - self.arrow_label = QLabel() - self.arrow_pixmap = QPixmap(arrow_img_path) - self.arrow_label.setStyleSheet("background-image: url(" ");") - self.arrow_label.setPixmap( - self.arrow_pixmap.scaled(12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation) - ) - self.arrow_label.setCursor(Qt.PointingHandCursor) - self.main_layout.addWidget(self.arrow_label, alignment=Qt.AlignTop) + # self.arrow_label = QLabel() + # self.arrow_pixmap = QPixmap(arrow_img_path) + # self.arrow_label.setStyleSheet("background-image: url(" ");") + # self.arrow_label.setPixmap( + # self.arrow_pixmap.scaled(12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation) + # ) + # self.arrow_label.setCursor(Qt.PointingHandCursor) + # self.main_layout.addWidget(self.arrow_label, alignment=Qt.AlignTop) # 3. 下拉选项列表(默认选中第一个) - self.list_widget = QListWidget() - self.list_widget.setWindowFlags(Qt.Popup) + # 取消了下拉框 2026/1/11 + # self.list_widget = QListWidget() + # self.list_widget.setWindowFlags(Qt.Popup) # 设置选项字体 font = QFont() font.setPixelSize(16) # 添加所有的下拉选项 - for option in options: - item = QListWidgetItem(option) - item.setTextAlignment(Qt.AlignLeft) - item.setFont(font) - self.list_widget.addItem(item) - self.list_widget.setCurrentRow(0) # 默认选中第一项 - self.list_widget.itemClicked.connect(self.select_option) + # 取消了下拉框 2026/1/11 + # for option in options: + # item = QListWidgetItem(option) + # item.setTextAlignment(Qt.AlignLeft) + # item.setFont(font) + # self.list_widget.addItem(item) + # self.list_widget.setCurrentRow(0) # 默认选中第一项 + # self.list_widget.itemClicked.connect(self.select_option) # 双保险监听:全局焦点变化 + 事件过滤 - self.app = QApplication.instance() - self.app.focusChanged.connect(self.on_focus_changed) - self.list_widget.installEventFilter(self) + # 取消了下拉框 2026/1/11 + # self.app = QApplication.instance() + # self.app.focusChanged.connect(self.on_focus_changed) + # self.list_widget.installEventFilter(self) def mousePressEvent(self, event): """重写鼠标点击事件,实现QLabel点击功能""" # 判断点击是否在result_label或arrow_label区域内 # if self.result_label.underMouse() or self.arrow_label.underMouse(): # self.toggle_expand() - if self.arrow_label.underMouse(): - self.toggle_expand() + # if self.arrow_label.underMouse(): # 取消了下拉箭头 2026/1/11 + # self.toggle_expand() super().mousePressEvent(event) # 传递事件,不影响其他组件 def toggle_expand(self): @@ -173,15 +176,19 @@ class CustomDropdown(QWidget): # 获取当前选中的设备名 def get_selected_device(self): return self.result_label.text() - + + # 设置选中的设备名 + def set_selected_device(self, device_name:str): + self.result_label.setText(device_name) class SystemDiagnosticsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowOpacity(0.0) + self.max_row = 8 # 最大行数为8行 + self.max_col = 4 # 最大列数为4列 self._init_ui() - self.init_animations() def _init_ui(self): # 无边框模式 @@ -206,7 +213,7 @@ class SystemDiagnosticsDialog(QDialog): circle_warning_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_YELLOW # 警告状态 circle_error_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_RED # 异常状态 ms_box_path = ImagePaths.SYSTEM_DIAGNOSTICS_MS_BG - dropdown_arrow_path = ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW + dropdown_arrow_path = ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW # 箭头图标 # 字体设置 ms_font = QFont() @@ -214,8 +221,8 @@ class SystemDiagnosticsDialog(QDialog): ms_color = QColor("#14abea") # 生成小框 - for row in range(8): - for col in range(4): + for row in range(self.max_row): + for col in range(self.max_col): box_container = QWidget() box_container.setObjectName(f"box_{row}_{col}") box_container.setStyleSheet( @@ -249,6 +256,7 @@ class SystemDiagnosticsDialog(QDialog): ms_layout.setContentsMargins(6, 0, 0, 0) ms_edit = QLineEdit("5ms") ms_edit.setFont(ms_font) + ms_edit.setReadOnly(True) # 禁用外部点击输入 ms_edit.setStyleSheet( f""" background: none; @@ -284,44 +292,6 @@ class SystemDiagnosticsDialog(QDialog): grid_layout.addWidget(box_container, row, col) - def init_animations(self): - """初始化显示动画:从下方滑入 + 淡入""" - # 1. 透明度动画(从0→1,与系统中心一致但时长不同) - self.opacity_anim = QPropertyAnimation(self, b"windowOpacity") - self.opacity_anim.setDuration(400) - self.opacity_anim.setStartValue(0.0) - self.opacity_anim.setEndValue(1.0) - self.opacity_anim.setEasingCurve(QEasingCurve.OutCubic) # 缓动曲线不同 - - # 2. 位置动画(从下方100px滑入目标位置,核心差异点) - self.pos_anim = QPropertyAnimation(self, b"geometry") - self.pos_anim.setDuration(400) - self.pos_anim.setEasingCurve(QEasingCurve.OutQuart) # 滑入效果更自然 - - # 3. 组合动画(同时执行滑入和淡入) - self.anim_group = QParallelAnimationGroup(self) - self.anim_group.addAnimation(self.opacity_anim) - self.anim_group.addAnimation(self.pos_anim) - - def showEvent(self, event): - super().showEvent(event) # 先调用父类方法 - - # 动态计算动画起点(在当前位置下方100px,保持宽度和高度不变) - current_geometry = self.geometry() # 当前位置和尺寸(需提前用move设置) - # 起点:y坐标增加100px(从下方滑入),x和尺寸不变 - start_rect = QRect( - current_geometry.x(), - current_geometry.y() + 100, # 下方100px - current_geometry.width(), - current_geometry.height() - ) - # 设置动画起点和终点 - self.pos_anim.setStartValue(start_rect) - self.pos_anim.setEndValue(current_geometry) # 终点:目标位置 - - # 启动动画 - self.anim_group.start() - def paintEvent(self, event): """重写绘制事件,手动在透明背景上绘制图片""" if not self.bg_pixmap.isNull(): @@ -345,15 +315,22 @@ class SystemDiagnosticsDialog(QDialog): # ========== 对外接口:获取选中的设备名 ========== def get_selected_device(self, row, col): - """获取指定行列的选中设备名""" + """获取指定行列的选中设备名, 行号和列号都从0开始""" box = self.findChild(QWidget, f"box_{row}_{col}") if box and hasattr(box, "dropdown"): return box.dropdown.get_selected_device() return None + + # ========== 对外接口:设置选中的设备名 ========== + def set_selected_device(self, row, col, device_name:str): + """设置指定行列的选中设备名, 行号和列号都从0开始""" + box = self.findChild(QWidget, f"box_{row}_{col}") + if box and hasattr(box, "dropdown"): + return box.dropdown.set_selected_device(device_name) # ========== 对外接口:获取毫秒值 ========== def get_ms_value(self, row, col): - """获取指定行列的毫秒值(如“5ms”)""" + """获取指定行列的毫秒值(如“5ms”), 行号和列号都从0开始""" box = self.findChild(QWidget, f"box_{row}_{col}") if box and hasattr(box, "ms_edit"): # return box.ms_edit.text() @@ -366,6 +343,13 @@ class SystemDiagnosticsDialog(QDialog): return number_match.group(1) return None + + # ========== 对外接口:设置毫秒值 ========== + def set_ms_value(self, row, col, ms:int): + """设置指定行列的毫秒值, 行号和列号都从0开始""" + box = self.findChild(QWidget, f"box_{row}_{col}") + if box and hasattr(box, "ms_edit"): + box.ms_edit.setText(f"{ms}ms") if __name__ == "__main__": diff --git a/view/widgets/value_adjuster.py b/view/widgets/value_adjuster.py index 153a294..8762b20 100644 --- a/view/widgets/value_adjuster.py +++ b/view/widgets/value_adjuster.py @@ -4,6 +4,8 @@ from PySide6.QtCore import Qt from PySide6.QtGui import QDoubleValidator import sys +from common.constant_config_manager import ConfigManager + """ 调整计划方量, 左侧减按钮, 右侧加按钮 这里的 最小值、最大值、初始值 需要读取配置文件来决定 @@ -27,13 +29,25 @@ class CustomLineEdit(QLineEdit): self.setText(f"{value:.1f}") self.setCursorPosition(0) # 光标移到最前面 (保证数值显示完整) + + # 更新输入框的默认文本 + def updateDefaultText(self, text:str): + self.default_text = text + class ValueAdjuster(QWidget): def __init__(self, parent=None): super().__init__(parent) - self.min_value = 0.0 # 最小值 - self.max_value = 99.0 # 最大值 - self.value = 2.5 # 初始值 (需要显示一位数字) + self.config_manager = ConfigManager() + self.config_manager.adjuster_params_changed.connect(self.on_adjuster_params_changed) # 绑定参数变化槽函数 + + # self.min_value = 0.0 # 最小值 + # self.max_value = 99.0 # 最大值 + # self.value = 2.5 # 初始值 (需要显示一位数字) + + self.min_value = self.config_manager.get_adjuster_min() # 最小值 + self.max_value = self.config_manager.get_adjuster_max() # 最大值 + self.value = self.config_manager.get_adjuster_initial() # 初始值 (需要显示一位小数) self.setFixedSize(102, 32) @@ -120,6 +134,26 @@ class ValueAdjuster(QWidget): layout.addWidget(self.line_edit) layout.addWidget(self.plus_btn) + def on_adjuster_params_changed(self, new_min, new_max, new_initial): + """配置文件中的min、max、initial变化时, 更新控件范围""" + # print("on_adjuster_params_changed:", new_min, new_max, new_initial) + # 1、更新最小值和最大值 + if self.min_value != new_min or self.max_value != new_max: + self.min_value = new_min + self.max_value = new_max + self.line_edit.setValidator(QDoubleValidator(self.min_value, self.max_value, 1, self)) + + # 2、确保当前值在新范围内,不在当前范围内,就更新当前值 + if self.value < new_min: + self.value = new_min + self.line_edit.setText(f"{self.value:.1f}") + elif self.value > new_max: + self.value = new_max + self.line_edit.setText(f"{self.value:.1f}") + + # 3、更新输入编辑框默认值 + self.line_edit.updateDefaultText(f"{new_initial:.1f}") + def on_minus_clicked(self): """减0.1""" new_value = self.value - 0.1 diff --git a/view/widgets/vibration_video_widget.py b/view/widgets/vibration_video_widget.py index 8e72019..8bb0141 100644 --- a/view/widgets/vibration_video_widget.py +++ b/view/widgets/vibration_video_widget.py @@ -172,13 +172,20 @@ class CameraModule(QWidget): video_display_signal = Signal(str, bool) def __init__( - self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, parent=None + self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, show_ai = False, parent=None ): super().__init__(parent) self.setObjectName("cameraModule") self.camera_name = camera_name self.rtsp_url = rtsp_url self.need_rotate_180 = need_rotate_180 # 画面是否需要旋转180度后显示 + self.show_ai = show_ai # 是否需要展示ai(为True会多出AI显示按钮) + + # 初始化AI显示相关变量为None + self.ai_display_group = None + self.ai_display_label = None + self.ai_display_switch = None + self.setup_ui() def setup_ui(self): @@ -195,20 +202,23 @@ class CameraModule(QWidget): self.title_label = QLabel() self.title_label.setAlignment(Qt.AlignLeft) self.title_label.setText(f"{self.camera_name}视频") + # self.title_label.setFixedWidth(212) + self.title_label.setFixedSize(212, 21) self.title_label.setObjectName("cameraTitleLabel") + # background-image: url({ImagePaths.VIDEO_TITLE_BACKGROUND}); + # min-width: 126px; self.title_label.setStyleSheet( f""" #cameraTitleLabel {{ font-size: 18px; color: #16ffff; background-image: url({ImagePaths.VIDEO_TITLE_BACKGROUND}); - min-width: 180px; padding-left: 12px; }} """ ) - # 创建【显示标签+开关】组合容器 + # 1、创建【显示标签+开关】组合容器 self.display_group = QWidget() # 容器:包裹标签和开关 display_group_layout = QHBoxLayout(self.display_group) display_group_layout.setContentsMargins(0, 0, 0, 0) # 容器内边距为0 @@ -239,9 +249,43 @@ class CameraModule(QWidget): display_group_layout.addWidget(self.display_label) display_group_layout.addWidget(self.display_switch, alignment=Qt.AlignLeft) + if self.show_ai: # 当需要显示AI时才生成 + # 2、创建【AI算法标签+开关】组合容器 + self.ai_display_group = QWidget() # 容器:包裹AI显示标签和开关 + ai_display_group_layout = QHBoxLayout(self.ai_display_group) + ai_display_group_layout.setContentsMargins(0, 0, 0, 0) # 容器内边距为0 + ai_display_group_layout.setSpacing(0) + + # AI算法显示标签 + self.ai_display_label = QLabel("AI算法") + self.ai_display_label.setObjectName("aiDisplayLabel") + # font-weight: bold; + self.ai_display_label.setStyleSheet(""" + #aiDisplayLabel { + font-size: 18px; + color: #16ffff; + margin: 0px; + padding-left: 2px; + } + """) + # self.ai_display_label.setFixedWidth(40) + self.ai_display_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + + # AI开关 + self.ai_display_switch = SwitchButton() + self.ai_display_switch.setChecked(False) + # self.ai_display_switch.switched.connect(self.onDisplayButtonSwitched) # 在camera_controller中处理 + + # 将AI显示标签和开关添加到组合容器布局 + ai_display_group_layout.addWidget(self.ai_display_label) + ai_display_group_layout.addWidget(self.ai_display_switch, alignment=Qt.AlignLeft) + # 添加到标题布局 title_layout.addWidget(self.title_label, alignment=Qt.AlignLeft) - title_layout.addWidget(self.display_group, alignment=Qt.AlignLeft) + title_layout.addWidget(self.display_group, alignment=Qt.AlignRight) + if self.show_ai: # 需要添加 AI显示控件 + title_layout.addWidget(self.ai_display_group, alignment=Qt.AlignLeft) + self.title_label.setFixedSize(139, 21) # 调整 xxx视频标签的宽度 # 视频显示容器:使用堆叠布局,让重连按钮和视频标签分层显示 self.video_container = QWidget() @@ -313,7 +357,7 @@ class CameraModule(QWidget): # 显示开关切换槽函数 def onDisplayButtonSwitched(self, state:bool): - # 显示开关打开,state 为 True,显示开关关闭,state 为 False + # 显示开关打开,state 为 True; 显示开关关闭,state 为 False if state: self.stacked_widget.setCurrentWidget(self.raw_label) # 视频显示标签 self.stacked_widget.setHidden(False) @@ -403,8 +447,8 @@ class VibrationVideoWidget(QWidget): # 需要修改为相应的地址!!! # 注:在camera_controller中设置地址url self.cam1 = CameraModule("上位料斗") - self.cam2 = CameraModule("下位料斗") - self.cam3 = CameraModule("模具车") + self.cam2 = CameraModule("下位料斗", show_ai=False) + self.cam3 = CameraModule("模具车", need_rotate_180=False) self.setup_ui() self.connect_signals()