Compare commits
2 Commits
d0d71b9efe
...
6be1e13f18
| Author | SHA1 | Date | |
|---|---|---|---|
| 6be1e13f18 | |||
| b40ea0112a |
@ -81,6 +81,25 @@ class DBHelper:
|
||||
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__":
|
||||
"""测试用例:向数据库插入测试消息"""
|
||||
|
||||
@ -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
|
||||
channel = 102
|
||||
@ -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):
|
||||
# 底部系统中心按钮 → 触发弹窗显示/隐藏
|
||||
@ -79,6 +83,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() # 每次显示都计算位置
|
||||
@ -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()
|
||||
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)
|
||||
@ -14,10 +14,6 @@ class CameraController:
|
||||
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(
|
||||
cam1_url=cam1_config["rtsp_url"],
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -1,37 +1,49 @@
|
||||
from PySide6.QtCore import QTimer, Signal, QObject # 导入Qt核心类
|
||||
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 core.system import FeedingControlSystem
|
||||
from opc.opcua_server import SimpleOPCUAServer
|
||||
|
||||
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.system = FeedingControlSystem()
|
||||
self.system.initialize()
|
||||
self.system.state.state_updated.connect(self.update_ui_notify)
|
||||
self.opcua_server = SimpleOPCUAServer(self.system.state)
|
||||
self.opcua_server.start()
|
||||
|
||||
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")
|
||||
@ -59,46 +71,183 @@ class MainController:
|
||||
def _initSubViews(self):
|
||||
pass
|
||||
|
||||
def update_ui_notify(self, prop:str,value):
|
||||
"""更新UI状态"""
|
||||
print(f"更新UI状态: {prop} = {value}")
|
||||
|
||||
if prop == "feed_status" and value == 4:
|
||||
# 盖板到位的情况, 更新 管片任务 (目前)
|
||||
self.main_window.update_segment_tasks()
|
||||
return
|
||||
|
||||
if prop == "mould_finish_weight":
|
||||
# 模具车中料的重量
|
||||
current_mould_weight = value
|
||||
if self.system.state._mould_need_weight != 0:
|
||||
# 计算 模具车 和 生产进度
|
||||
progress = int((current_mould_weight / self.system.state._mould_need_weight) * 100) # 去掉百分号之后的整数
|
||||
progress = min(progress, 100) # 限制为100
|
||||
self.main_window.arc_progress.setProgress(progress)
|
||||
self.main_window.production_progress.setProgress(progress)
|
||||
return
|
||||
|
||||
if prop == "upper_volume":
|
||||
# 上料斗方量
|
||||
self.hopper_controller.onUpdateUpperHopperVolume(value)
|
||||
return
|
||||
|
||||
if prop == "lower_weight":
|
||||
# 低位料斗重量
|
||||
self.hopper_controller.onUpdateLowerHopperWeight(value)
|
||||
return
|
||||
|
||||
|
||||
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.system.stop()
|
||||
self.opcua_server.stop()
|
||||
|
||||
# 停止系统底部控制器中的线程
|
||||
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)
|
||||
|
||||
@ -366,6 +366,7 @@ class MainWindow(QWidget):
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""窗口关闭时的回调"""
|
||||
self.hide() # 隐藏界面
|
||||
self.about_to_close.emit()
|
||||
super().closeEvent(e)
|
||||
|
||||
|
||||
@ -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=红)
|
||||
|
||||
@ -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 # 更新搅拌桨状态为停止
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
@ -174,14 +177,18 @@ 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()
|
||||
@ -367,6 +344,13 @@ class SystemDiagnosticsDialog(QDialog):
|
||||
|
||||
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__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@ -4,6 +4,8 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QDoubleValidator
|
||||
import sys
|
||||
|
||||
from common.constant_config_manager import ConfigManager
|
||||
|
||||
"""
|
||||
调整计划方量, 左侧减按钮, 右侧加按钮
|
||||
这里的 最小值、最大值、初始值 需要读取配置文件来决定
|
||||
@ -28,12 +30,24 @@ class CustomLineEdit(QLineEdit):
|
||||
|
||||
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
|
||||
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user