修改了界面显示(生产进度条、上料斗位置状态)、增加了将角度信息上传opc

This commit is contained in:
2026-04-07 16:07:36 +08:00
parent ecba4d726a
commit 5e8f291738
8 changed files with 312 additions and 92 deletions

View File

@ -22,6 +22,7 @@ lower_is_arch = 2:lower,2:lower_is_arch
lower_angle = 2:lower,2:lower_angle lower_angle = 2:lower,2:lower_angle
mould_finish_weight = 2:mould,2:mould_finish_weight mould_finish_weight = 2:mould,2:mould_finish_weight
mould_need_weight = 2:mould,2:mould_need_weight mould_need_weight = 2:mould,2:mould_need_weight
mould_finish_ratio=2:mould,2:mould_finish_ratio
mould_frequency = 2:mould,2:mould_frequency mould_frequency = 2:mould,2:mould_frequency
mould_vibrate_status = 2:mould,2:mould_vibrate_status mould_vibrate_status = 2:mould,2:mould_vibrate_status
feed_status = 2:mould,2:feed_status feed_status = 2:mould,2:feed_status

View File

@ -1,18 +1,20 @@
from PySide6.QtCore import QTimer, Signal, QObject, Slot from PySide6.QtCore import QTimer, Signal, QObject, Slot
import threading import threading
from hardware.transmitter import TransmitterController # from hardware.transmitter import TransmitterController
from hardware.relay import RelayController # from hardware.relay import RelayController
from view.widgets.hopper_widget import HopperWidget from view.widgets.hopper_widget import HopperWidget
from view.widgets.conveyor_system_widget import ConveyorSystemWidget from view.widgets.conveyor_system_widget import ConveyorSystemWidget
from enum import Enum from enum import Enum
class UpperHopperPosition(Enum): class UpperHopperPosition(Enum):
"""上料斗位置 """上料斗位置
- MIXING_TOWER: 搅拌楼处,对应数值 66 - MIXING_TOWER: 搅拌楼处,对应数值 1
- VIBRATION_CHAMBER: 振捣室处,对应数值 5 - VIBRATION_CHAMBER: 振捣室处,对应数值 2
- IN_TRANSIT: 途中,对应数值 3
""" """
MIXING_TOWER = 66 # 搅拌楼处 MIXING_TOWER = 1 # 搅拌楼处
VIBRATION_CHAMBER = 5 # 振捣室处 VIBRATION_CHAMBER = 2 # 振捣室处
IN_TRANSIT = 3 # 运输途中
# 信号类:后台线程向主线程传递数据 # 信号类:后台线程向主线程传递数据
class HopperSignals(QObject): class HopperSignals(QObject):
@ -42,7 +44,7 @@ class HopperController:
self._connect_signals() self._connect_signals()
# 开启定时器 # 开启定时器
self.timer_angle.start() # self.timer_angle.start()
# self.timer_weight.start() # self.timer_weight.start()
def _connect_signals(self): def _connect_signals(self):
@ -58,14 +60,14 @@ class HopperController:
# 上料斗 "开"按钮点击 # 上料斗 "开"按钮点击
self.hopper_view.upper_open_btn.clicked.connect(self.onUpperClampOpenBottonClicked) self.hopper_view.upper_open_btn.clicked.connect(self.onUpperClampOpenBottonClicked)
# 上料斗 "破拱"按钮 # 上料斗 "破拱"按钮点击
self.hopper_view.upper_arch_breaking_signal.connect(self.onUpperArchBreaking) self.hopper_view.upper_arch_breaking_signal.connect(self.onUpperArchBreakingClicked)
# 下料斗 "开"按钮点击 # 下料斗 "开"按钮点击
self.hopper_view.lower_open_btn.clicked.connect(self.onLowerClampOpenBottonClicked) self.hopper_view.lower_open_btn.clicked.connect(self.onLowerClampOpenBottonClicked)
# 下料斗 "破拱"按钮 # 下料斗 "破拱"按钮点击
self.hopper_view.lower_arch_breaking_signal.connect(self.onLowerArchBreaking) self.hopper_view.lower_arch_breaking_signal.connect(self.onLowerArchBreakingClicked)
def handleLowerClampAngleUpdate(self): def handleLowerClampAngleUpdate(self):
"""处理下料斗夹爪开合""" """处理下料斗夹爪开合"""
@ -102,15 +104,16 @@ class HopperController:
@Slot() @Slot()
def handleReadUpperHopperWeight(self): def handleReadUpperHopperWeight(self):
# 后台读取上料斗重量 # 后台读取上料斗重量
def upper_weight_task(): # def upper_weight_task():
loc_tra = TransmitterController(RelayController()) # loc_tra = TransmitterController(RelayController())
# 上料斗重量 (目前只有上料斗安装变送器, 可以读取到重量) # # 上料斗重量 (目前只有上料斗安装变送器, 可以读取到重量)
upper_weight = loc_tra.read_data(1) # upper_weight = loc_tra.read_data(1)
# 发送信号到主线程更新UI # # 发送信号到主线程更新UI
if upper_weight is not None: # if upper_weight is not None:
self.signals.upper_weight_updated.emit(upper_weight) # self.signals.upper_weight_updated.emit(upper_weight)
threading.Thread(target=upper_weight_task, daemon=True).start() # threading.Thread(target=upper_weight_task, daemon=True).start()
pass
@Slot(float) @Slot(float)
def onUpdateLowerClampAngle(self, angle:float): def onUpdateLowerClampAngle(self, angle:float):
@ -131,36 +134,51 @@ class HopperController:
@Slot(int) @Slot(int)
def onUpdateUpperClampStatus(self, status:int): def onUpdateUpperClampStatus(self, status:int):
# 上料斗夹爪状态 1表示打0表示关闭 # 上料斗夹爪状态 1:半开2全0:关闭)
if status: if status == 1:
# 执行上料斗夹爪打开动画 # 上料斗门半开
self.hopper_view.upper_clamp_widget.testAnimation(target_angle=30, duration=5)
elif status == 2:
# 上料斗门全开
self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=10) self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=10)
else: elif status == 0:
# 执行上料斗夹爪关闭动画 # 上料斗门关闭
self.hopper_view.upper_clamp_widget.testAnimation(target_angle=0, duration=10) self.hopper_view.upper_clamp_widget.testAnimation(target_angle=0, duration=6)
def onUpdateUpperHopperPosition(self, position:int): def onUpdateUpperHopperPosition(self, position:int):
# 上料斗位置 # 上料斗位置
if position == UpperHopperPosition.MIXING_TOWER.value: # 上料斗到达搅拌楼值为66 if position == UpperHopperPosition.MIXING_TOWER.value: # 上料斗到达搅拌楼
# 上料斗在搅拌楼下 # 上料斗在搅拌楼下
self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗 self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗
self.conveyor_view.moveHopperBelowMixer() # 传送带处的上料斗移动到搅料楼下 self.conveyor_view.moveHopperBelowMixer() # 传送带处的上料斗移动到搅料楼下
self.conveyor_view.showHopper() # 显示传送带处的上料斗 self.conveyor_view.showHopper() # 显示传送带处的上料斗
elif position == UpperHopperPosition.VIBRATION_CHAMBER.value: # 上料斗就绪,到达振捣室值为5 elif position == UpperHopperPosition.VIBRATION_CHAMBER.value: # 上料斗就绪,到达振捣室
# 上料斗在振捣室处 # 上料斗在振捣室处
self.conveyor_view.hideHopper() # 隐藏传送带处的上料斗 self.conveyor_view.hideHopper() # 隐藏传送带处的上料斗
self.hopper_view.upper_clamp_widget.set_angle(0) # 上料斗夹爪角度设置为0此时上料斗一定是关闭的 self.hopper_view.upper_clamp_widget.set_angle(0) # 上料斗夹爪角度设置为0此时上料斗一定是关闭的
self.hopper_view.showUpperHopper() # 显示非传送带处的上料斗 self.hopper_view.showUpperHopper() # 显示非传送带处的上料斗
else: elif position == UpperHopperPosition.IN_TRANSIT.value:
# 上料斗在途中 # 上料斗在途中
self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗 (下料斗处对应的上料斗叫非传送带处上料斗) self.hopper_view.hideUpperHopper() # 隐藏非传送带处的上料斗 (下料斗处对应的上料斗叫非传送带处上料斗)
self.conveyor_view.moveHopperToTransition() # 传送带处上料斗移动到中间过渡位置 self.conveyor_view.moveHopperToTransition() # 传送带处上料斗移动到中间过渡位置
self.conveyor_view.showHopper() # 显示传送带处的上料斗 self.conveyor_view.showHopper() # 显示传送带处的上料斗
@Slot(bool) @Slot(bool)
def onUpperArchBreaking(self, status:bool): def onUpperArchBreakingStatusChanged(self, status:bool):
"""上料斗破拱: status 为True表示 开启破拱, 为False表示 关闭破拱""" """上料斗破拱状态改变: status 为True表示 破拱状态, 为False表示 不破拱状态"""
print("hopper_controller: onUpperArchBreaking ", status) if status == True: # 破拱状态
self.hopper_view.upper_arch_breaking_status = True
self.hopper_view.upper_arch_label.setHidden(False)
else: # 不破拱状态
self.hopper_view.upper_arch_breaking_status = False
self.hopper_view.upper_arch_label.setHidden(True)
@Slot(bool)
def onUpperArchBreakingClicked(self, status:bool):
"""上料斗破拱按钮点击: status 为True表示 开启破拱, 为False表示 关闭破拱"""
print("hopper_controller: onUpperArchBreakingClicked ", status)
# 这里需要控制网络继电器开启上料斗破拱可能还需要同步opc的上料斗破拱状态
@Slot(int) @Slot(int)
def onUpperHopperStatusChanged(self, status:int): def onUpperHopperStatusChanged(self, status:int):
@ -179,9 +197,20 @@ class HopperController:
print("hopper_controller: onLowerClampOpenBottonClicked") print("hopper_controller: onLowerClampOpenBottonClicked")
@Slot(bool) @Slot(bool)
def onLowerArchBreaking(self, status:bool): def onLowerArchBreakingStatusChanged(self, status:bool):
"""下料斗破拱: status 为True表示 开启破拱, 为False表示 关闭破拱""" """下料斗破拱状态改变: status 为True表示 破拱状态, 为False表示 不破拱状态"""
print("hopper_controller: onLowerArchBreaking ", status) if status == True: # 破拱状态
self.hopper_view.lower_arch_breaking_status = True
self.hopper_view.lower_arch_label.setHidden(False)
else: # 不破拱状态
self.hopper_view.lower_arch_breaking_status = False
self.hopper_view.lower_arch_label.setHidden(True)
@Slot(bool)
def onLowerArchBreakingClicked(self, status:bool):
"""下料斗破拱按钮点击: status 为True表示 开启破拱, 为False表示 关闭破拱"""
print("hopper_controller: onLowerArchBreakingClicked ", status)
# 这里需要控制网络继电器开启下料斗破拱可能还需要同步opc的下料斗破拱状态
@Slot(int) @Slot(int)
def onLowerHopperStatusChanged(self, status:int): def onLowerHopperStatusChanged(self, status:int):

View File

@ -15,7 +15,13 @@ from service.pdrecord_query_thread import PDRecordQueryThread # 派单任务查
from busisness.models import ArtifactInfoModel from busisness.models import ArtifactInfoModel
from busisness.models import PDRecordModel from busisness.models import PDRecordModel
from typing import List from typing import List
from enum import Enum
class FeedStatus(Enum):
"""下料状态
- FEED_FINISH: 下料完成,对应数值 11
"""
FEED_FINISH = 11 # 下料完成
class MainController: class MainController:
def __init__(self): def __init__(self):
@ -40,6 +46,15 @@ class MainController:
self.opc_client = OpcuaUiClient() self.opc_client = OpcuaUiClient()
self.opc_client.start() self.opc_client.start()
# 刷新派单任务和管片任务定时器
self.refresh_task_timer = QTimer()
self.refresh_task_timer.setInterval(2000)
self.refresh_task_timer.timeout.connect(self._refresh_segment_and_pd_task)
self.refresh_task_timer.start()
# 下料状态
self.feed_status = None
# 连接信号 # 连接信号
self.__connectSignals() self.__connectSignals()
@ -86,10 +101,30 @@ class MainController:
# 主界面的计划表单中的计划方量修改控制 # 主界面的计划表单中的计划方量修改控制
self.main_window.plan_volume_modified_signal.connect(self.handleVolumeModified) # 计划方量修改 self.main_window.plan_volume_modified_signal.connect(self.handleVolumeModified) # 计划方量修改
def _refresh_segment_and_pd_task(self):
"""
定时器槽函数: 每2秒执行一次
写入OPC节点触发管片任务/派单任务刷新
"""
try:
# 1. 触发管片任务刷新
self._update_sys_segment_refresh(1)
# 2. 触发派单任务刷新
self._update_sys_pd_refresh(1)
except Exception as e:
# 捕获异常并记录,避免定时器崩溃
error_msg = f"定时刷新管片/派单任务失败:{str(e)}"
self.msg_recorder.warning_record(error_msg)
def handleMainWindowClose(self): def handleMainWindowClose(self):
"""主界面关闭""" """主界面关闭"""
self.msg_recorder.normal_record("关闭自动智能浇筑系统") self.msg_recorder.normal_record("关闭自动智能浇筑系统")
# 停止管片任务和派单任务定时器
if hasattr(self, 'refresh_task_timer') and self.refresh_task_timer.isActive():
self.refresh_task_timer.stop()
self.refresh_task_timer.deleteLater()
# 停止系统底部控制器中的线程 # 停止系统底部控制器中的线程
if hasattr(self, 'bottom_control_controller'): if hasattr(self, 'bottom_control_controller'):
self.bottom_control_controller.stop_threads() self.bottom_control_controller.stop_threads()
@ -102,13 +137,13 @@ class MainController:
pd_mode: 1,自动派单 2,手动派单 pd_mode: 1,自动派单 2,手动派单
""" """
if auto_status: # 自动派单 if auto_status: # 自动派单
self.opc_client.write_value_by_name("pd_mode", 1) self.opc_client.write_value_by_name("pd_set_mode", 1)
else: # 手动派单 else: # 手动派单
self.opc_client.write_value_by_name("pd_mode", 2) self.opc_client.write_value_by_name("pd_set_mode", 2)
def handleVolumeModified(self, volume_json_str:str): def handleVolumeModified(self, volume_json_str:str):
"""处理 修改方量 (计划表单中 和 派单详情中)""" """处理 修改方量 (计划表单中 和 派单详情中)"""
self.opc_client.write_value_by_name("pd_plan_volume", volume_json_str) self.opc_client.write_value_by_name("pd_set_volume", volume_json_str)
def start_msg_database_clean_task(self): def start_msg_database_clean_task(self):
"""启动清理消息数据库(messages.db)中过期消息的定时任务""" """启动清理消息数据库(messages.db)中过期消息的定时任务"""
@ -181,51 +216,96 @@ class MainController:
def onUpdateUiByArtifactInfo(self, artifact_list:List[ArtifactInfoModel]): def onUpdateUiByArtifactInfo(self, artifact_list:List[ArtifactInfoModel]):
# 更新管片任务 # 更新管片任务
self.main_window.update_segment_tasks(artifact_list) self.main_window.update_segment_tasks(artifact_list)
# 将opc服务中的 segment_tasks的值复原为 0以便下次触发管片更新 # 将opc服务中的 sys_segment_refresh的值复原为 0以便下次触发管片更新
self.opc_client.write_value_by_name("segment_tasks", 0) self.opc_client.write_value_by_name("sys_segment_refresh", 0)
def onUpdateUiByPDRecord(self, pdrecord_list:List[PDRecordModel]): def onUpdateUiByPDRecord(self, pdrecord_list:List[PDRecordModel]):
# 更新派单任务 # 更新派单任务
self.main_window.update_dispatch_tasks(pdrecord_list) self.main_window.update_dispatch_tasks(pdrecord_list)
# 将opc服务中的 dispatch_tasks的值复原为 0以便下次触发派单更新 # 将opc服务中的 sys_pd_refresh的值复原为 0以便下次触发派单更新
self.opc_client.write_value_by_name("dispatch_tasks", 0) self.opc_client.write_value_by_name("sys_pd_refresh", 0)
# ======================== OPCUA值更新界面方法 ====================== # ======================== OPCUA值更新界面方法 ======================
# ============================= 上料斗相关 ==============
def _update_upper_weight(self, val): def _update_upper_weight(self, val):
# 更新上料斗重量 # 更新上料斗重量
self.hopper_controller.onUpdateUpperHopperWeight(val) self.hopper_controller.onUpdateUpperHopperWeight(val)
def _update_lower_weight(self, val): def _update_upper_is_arch(self, val):
# 更新料斗重量 # 更新料斗是否破拱
self.hopper_controller.onUpdateLowerHopperWeight(val) self.hopper_controller.onUpperArchBreakingStatusChanged(val)
def _update_upper_door_closed(self, val:int):
# 更新上料斗门的闭合状态
# 1:半开2全开0:关闭)
self.hopper_controller.onUpdateUpperClampStatus(val)
def _update_upper_volume(self, val): def _update_upper_volume(self, val):
# 更新上料斗方量 # 更新上料斗方量
self.hopper_controller.onUpdateUpperHopperVolume(val) self.hopper_controller.onUpdateUpperHopperVolume(val)
def _update_production_progress(self, val): def _update_upper_door_position(self, val):
# 更新生产进度限制为100, 进度为去掉百分号之后的整数 # 更新上料斗位置 2表示料斗到位到达振捣室处 1表示在搅拌楼处
progress = val
self.main_window.arc_progress.setProgress(progress)
self.main_window.production_progress.setProgress(progress)
def _update_lower_clamp_angle(self, val):
# 更新下料斗夹爪角度
self.hopper_controller.onUpdateLowerClampAngle(val)
def _update_upper_clamp_status(self, val):
# 更新上料斗夹爪状态 0表示关闭 1表示打开
self.hopper_controller.onUpdateUpperClampStatus(val)
def _update_upper_hopper_position(self, val):
# 更新上料斗位置 5表示料斗到位到达振捣室处 66表示在搅拌楼处
self.hopper_controller.onUpdateUpperHopperPosition(val) self.hopper_controller.onUpdateUpperHopperPosition(val)
if val == UpperHopperPosition.MIXING_TOWER.value: if val == UpperHopperPosition.MIXING_TOWER.value:
self.main_window.mixer_widget.startBladeMix() self.main_window.mixer_widget.startBladeMix()
else: else:
self.main_window.mixer_widget.stopBladeMix() self.main_window.mixer_widget.stopBladeMix()
def _update_segment_tasks(self, val): # ============================= 下料斗相关 ==============
def _update_lower_weight(self, val):
# 更新下料斗重量
self.hopper_controller.onUpdateLowerHopperWeight(val)
def _update_lower_is_arch(self, val:bool):
# 更新下料斗是否破拱
self.hopper_controller.onLowerArchBreakingStatusChanged(val)
def _update_lower_angle(self, val):
# 更新下料斗夹爪角度
self.hopper_controller.onUpdateLowerClampAngle(val)
# ========================== 模具车相关 ==============
def _update_mould_finish_weight(self, finish_weight:int):
# 更新模具车中的下料重量
# finish_weight已下料重量
self.main_window.arc_progress.setWeight(finish_weight)
def _update_mould_finish_ratio(self, val:float):
# 更新生产进度限制为100, progress进度为去掉百分号之后的整数
# 注意: 此时的val是浮点数的比例需要保留两位小数后乘以100后得到progress
if self.feed_status == FeedStatus.FEED_FINISH.value:
return # 下料完成不需要更新进度条就是100%
progress = round(val, 2) * 100 # 保留两位小数后乘100
self.main_window.arc_progress.setProgress(progress)
self.main_window.production_progress.setProgress(progress)
def _update_mould_frequency(self, val):
# 更新振捣频率
self.main_window.frequency_button_group.set_selected_frequency(val) # 频率选择按钮上显示的选中的频率
self.main_window.arc_progress.setFrequency(val) # 模具车上显示的振捣频率
def _update_mould_vibrate_status(self, vibrate_status:bool):
# 更新模具车上显示的振捣状态
# vibrate_status: False:未振捣、True:振捣中
if vibrate_status:
self.main_window.arc_progress.setState("振捣中")
else:
self.main_window.arc_progress.setState("未振捣")
self.opc_client.write_value_by_name("mould_frequency", 0) # 将振捣频率设置为0
def _update_feed_status(self, status:int):
# 下料(浇筑)状态
self.feed_status = status # 更新下料状态
if status == FeedStatus.FEED_FINISH.value:
# 下料完成
# 1、将生产进度条调整为100%的状态
self.main_window.arc_progress.setProgress(100)
self.main_window.production_progress.setProgress(100)
# ===================================== 系统状态相关 ====================
def _update_sys_segment_refresh(self, val):
if val: # 需要更新管片任务 if val: # 需要更新管片任务
"""更新左侧的管片任务""" """更新左侧的管片任务"""
if hasattr(self, "segment_query_thread") and self.segment_query_thread.isRunning(): if hasattr(self, "segment_query_thread") and self.segment_query_thread.isRunning():
@ -238,7 +318,7 @@ class MainController:
self.segment_query_thread.query_error.connect(self.onQueryInfoError) self.segment_query_thread.query_error.connect(self.onQueryInfoError)
self.segment_query_thread.start() self.segment_query_thread.start()
def _update_dispatch_tasks(self, val): def _update_sys_pd_refresh(self, val):
if val: # 需要更新派单任务 if val: # 需要更新派单任务
"""更新右侧的派单任务""" """更新右侧的派单任务"""
if hasattr(self, "dispatch_query_thread") and self.dispatch_query_thread.isRunning(): if hasattr(self, "dispatch_query_thread") and self.dispatch_query_thread.isRunning():
@ -255,26 +335,8 @@ class MainController:
# 查询信息失败预警 # 查询信息失败预警
self.msg_recorder.warning_record(error_msg) self.msg_recorder.warning_record(error_msg)
def _update_vibration_frequency(self, val): # ========================================= 派单相关 =====================
# 更新振捣频率 def _update_pd_set_mode(self, mode:int):
self.main_window.frequency_button_group.set_selected_frequency(val) # 频率选择按钮上显示的选中的频率
self.main_window.arc_progress.setFrequency(val) # 模具车上显示的振捣频率
def _update_mould_vibrate_status(self, vibrate_status:bool):
# 更新模具车上显示的振捣状态
# vibrate_status: False:未振捣、True:振捣中
if vibrate_status:
self.main_window.arc_progress.setState("振捣中")
else:
self.main_window.arc_progress.setState("未振捣")
self.opc_client.write_value_by_name("vibration_frequency", 0) # 将振捣频率设置为0
def _update_mould_finish_weight(self, finish_weight:int):
# 更新模具车中的下料重量
# finish_weight已下料重量
self.main_window.arc_progress.setWeight(finish_weight)
def _update_pd_mode(self, mode:int):
# 更新计划表单中的 派单模式(主界面下发状态的下面的切换开关),自动派单/手动派单 # 更新计划表单中的 派单模式(主界面下发状态的下面的切换开关),自动派单/手动派单
mode_mapping = { mode_mapping = {
1: "自动派单", 1: "自动派单",
@ -288,4 +350,4 @@ class MainController:
# 修改系统配置文件中的 派单状态为自动派单... # 修改系统配置文件中的 派单状态为自动派单...
else: else:
self.main_window.plan_table_widget.set_auto_dispatch(False) # 关闭,手动派单 self.main_window.plan_table_widget.set_auto_dispatch(False) # 关闭,手动派单
# 修改系统配置文件中的 派单状态为手动派单... # 修改系统配置文件中的 派单状态为手动派单

View File

@ -0,0 +1,128 @@
from opcua import Client, ua
import traceback
import threading
import time
class OPCAngleUploader:
"""下料斗夹爪角度上传OPC"""
def __init__(self, opc_server_url="opc.tcp://localhost:4840/zjsh_feed/server/",
angle_node_id = "ns=2;i=13", upload_interval=0.5,
angle_threshold=5.0, angle_min = 3.0):
# OPC服务器配置
self.opc_server_url = opc_server_url
self.upload_interval = upload_interval # 固定0.5秒上传
self.angle_threshold = angle_threshold # 角度变化阈值(超过阈值才更新角度)
self.angle_min = angle_min # 角度最小值(低于该值为误判的角度,不更新)
# 配置 lower_angle 节点IDns=2;i=13
self.angle_node_id = angle_node_id # lower_angle节点ID
self.angle_node = None # 缓存节点对象,避免重复获取
# 线程安全相关
self.latest_angle = None # 最新角度缓存
self.last_uploaded_angle = None # 上一次成功上传opc的角度
self.angle_lock = threading.Lock() # 角度读写锁
self.exit_event = threading.Event() # 线程退出信号
self.upload_thread = None # 上传线程
# OPC客户端状态
self.opc_client = None
self.is_connected = False
def connect_opc(self):
"""连接OPC服务器, 获取lower_angle节点对象"""
try:
# 关闭旧连接
if self.opc_client:
try:
self.opc_client.disconnect()
except:
pass
# 初始化客户端并连接
self.opc_client = Client(self.opc_server_url)
self.opc_client.connect()
# 获取目标节点(仅获取一次,提升效率)
self.angle_node = self.opc_client.get_node(self.angle_node_id)
self.is_connected = True
# print(f"[OPC] 连接成功,已获取节点:{self.angle_node_id}lower_angle")
return True
except Exception as e:
print(f"[OPC] 连接/获取节点失败:{e}")
self.is_connected = False
self.angle_node = None
return False
def update_angle(self, angle):
"""线程安全更新最新角度值"""
with self.angle_lock:
self.latest_angle = angle
def _upload_worker(self):
"""上传线程核心: 每隔0.5秒写入角度节点"""
# 初始连接失败则5秒重试
while not self.exit_event.is_set() and not self.connect_opc():
# print(f"[OPC] 初始连接失败5秒后重试...")
time.sleep(5)
# 循环上传
while not self.exit_event.is_set():
try:
# 连接失效则重连
if not self.is_connected or not self.angle_node:
self.connect_opc()
time.sleep(1)
continue
# 线程安全读取角度
with self.angle_lock:
current_angle = self.latest_angle
last_angle = self.last_uploaded_angle
if current_angle is None:
time.sleep(self.upload_interval)
continue
need_upload = False
if last_angle is None:
need_upload = True
else:
angle_diff = abs(float(current_angle) - float(last_angle))
if angle_diff >= self.angle_threshold:
need_upload = True
# 写入角度到OPC节点
if need_upload and current_angle > self.angle_min:
angle_variant = ua.Variant(float(current_angle), ua.VariantType.Float)
self.angle_node.set_value(angle_variant)
# 更新上一次上传的角度
with self.angle_lock:
self.last_uploaded_angle = current_angle
# 0.5秒间隔
time.sleep(self.upload_interval)
except Exception as e:
print(f"[OPC] 上传异常:{e}")
traceback.print_exc()
self.is_connected = False # 标记连接失效,触发重连
time.sleep(1)
def start_upload(self):
"""启动上传线程(守护线程,随进程退出)"""
if not self.upload_thread or not self.upload_thread.is_alive():
self.exit_event.clear()
self.upload_thread = threading.Thread(target=self._upload_worker, daemon=True)
self.upload_thread.start()
def stop_upload(self):
"""停止线程并关闭OPC连接"""
self.exit_event.set()
if self.upload_thread and self.upload_thread.is_alive():
self.upload_thread.join(timeout=2)
if self.opc_client:
try:
self.opc_client.disconnect()
except:
pass
self.is_connected = False

View File

@ -208,7 +208,7 @@ class MainWindow(QWidget):
if artifact.MouldCode: # 更新模具号 if artifact.MouldCode: # 更新模具号
self.segment_task_widget.set_task_id(f"task{index}", artifact.MouldCode) self.segment_task_widget.set_task_id(f"task{index}", artifact.MouldCode)
if artifact.BetonVolume: # 更新浇筑方量 if artifact.BetonVolume is not None: # 更新浇筑方量
self.segment_task_widget.set_task_volume(f"task{index}", artifact.BetonVolume) self.segment_task_widget.set_task_volume(f"task{index}", artifact.BetonVolume)
if artifact.BeginTime: # 更新时间 (管片任务的开始时间) if artifact.BeginTime: # 更新时间 (管片任务的开始时间)
# print("artifact.BeginTime: ", artifact.BeginTime) # print("artifact.BeginTime: ", artifact.BeginTime)
@ -285,7 +285,7 @@ class MainWindow(QWidget):
# 设置派单任务的tasks信息 # 设置派单任务的tasks信息
if record.MouldCode: if record.MouldCode:
self.dispatch_task_widget.set_task_id(task_name, record.MouldCode) self.dispatch_task_widget.set_task_id(task_name, record.MouldCode)
if record.FBetonVolume: if record.FBetonVolume is not None:
self.dispatch_task_widget.set_task_volume(task_name, record.FBetonVolume) self.dispatch_task_widget.set_task_volume(task_name, record.FBetonVolume)
if record.CreateTime: if record.CreateTime:
self.dispatch_task_widget.set_task_time(task_name, self.convert_to_ampm(record.CreateTime)) self.dispatch_task_widget.set_task_time(task_name, self.convert_to_ampm(record.CreateTime))

View File

@ -193,7 +193,7 @@ class HopperWidget(QWidget):
# 此时,点击按钮为关闭破拱 # 此时,点击按钮为关闭破拱
self.lower_arch_breaking_status = False self.lower_arch_breaking_status = False
self.lower_arch_label.setHidden(True) self.lower_arch_label.setHidden(True)
self.upper_arch_breaking_signal.emit(self.lower_arch_breaking_status) self.lower_arch_breaking_signal.emit(self.lower_arch_breaking_status)
def create_lower_hopper(self): def create_lower_hopper(self):
"""创建下位料斗Widget""" """创建下位料斗Widget"""

View File

@ -89,11 +89,11 @@ class LinearProductionProgress(QWidget):
self.fg_label.move(0, 0) self.fg_label.move(0, 0)
self.fg_label.raise_() self.fg_label.raise_()
# 百分比标签宽33px高19px右偏9px # 百分比标签宽39px高19px右偏9px
self.percent_label = QLabel(self) self.percent_label = QLabel(self)
self.percent_label.setText("0%") self.percent_label.setText("0%")
self.percent_label.setAlignment(Qt.AlignCenter) self.percent_label.setAlignment(Qt.AlignCenter)
self.percent_label.setFixedSize(33, 19) self.percent_label.setFixedSize(39, 19)
# self.percent_label.setStyleSheet( # self.percent_label.setStyleSheet(
# """ # """
# color: white; # color: white;
@ -135,9 +135,9 @@ class LinearProductionProgress(QWidget):
fg_width = int(450 * (self._progress + 4) / 100) fg_width = int(450 * (self._progress + 4) / 100)
self.fg_label.setFixedWidth(fg_width) self.fg_label.setFixedWidth(fg_width)
# 计算百分比标签位置:进度条右边缘 - 9px偏移 - 标签宽度33px # 计算百分比标签位置:进度条右边缘 - 9px偏移 - 标签宽度39px
if fg_width > 60: # 当上层进度条宽度大于60px开始移动 if fg_width > 60: # 当上层进度条宽度大于60px开始移动
label_x = fg_width - 9 - 33 label_x = fg_width - 9 - 39
# 移动百分比标签 # 移动百分比标签
self.percent_label.move(label_x, 0) self.percent_label.move(label_x, 0)
else: else:

View File

@ -131,8 +131,8 @@ class TaskWidget(QWidget):
# 水平布局2方量 + / + 时间 + 状态图标 # 水平布局2方量 + / + 时间 + 状态图标
row2_layout = QHBoxLayout() row2_layout = QHBoxLayout()
# 方量标签 # 方量标签
volume_label = QLabel("方量 200") volume_label = QLabel("方量 99")
volume_label.setStyleSheet("color: #a1c1d7; font-size: 14px;padding-left: 6px;") volume_label.setStyleSheet("color: #a1c1d7; font-size: 14px;padding-left: 1px;")
controls["volume_label"] = volume_label controls["volume_label"] = volume_label
row2_layout.addWidget(volume_label) row2_layout.addWidget(volume_label)