From 88dfc53b9da892a7594531f42a25ff6247b3f89d Mon Sep 17 00:00:00 2001 From: yanganjie Date: Fri, 16 Jan 2026 18:37:21 +0800 Subject: [PATCH] =?UTF-8?q?add(=E6=9B=B4=E6=96=B0opcua=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E3=80=81=E6=8C=AF=E6=8D=A3=E9=A2=91=E7=8E=87=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E3=80=81=E7=AE=A1=E7=89=87=E4=BB=BB=E5=8A=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=88=B7=E6=96=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- busisness/dals.py | 12 +- config/opc_config.ini | 21 ++ controller/main_controller.py | 150 +++++----- images/频率按钮1.png | Bin 0 -> 232 bytes images/频率按钮2.png | Bin 0 -> 1450 bytes service/artifact_query_thread.py | 6 +- service/opcua_ui_client.py | 301 +++++++++++++-------- view/main_window.py | 108 ++++++-- view/widgets/arc_progress_widget.py | 10 +- view/widgets/conveyor_system_widget.py | 13 + view/widgets/frequency_button_group.py | 150 ++++++++++ view/widgets/production_progress_widget.py | 14 +- view/widgets/segment_details_dialog.py | 16 +- 13 files changed, 567 insertions(+), 234 deletions(-) create mode 100644 config/opc_config.ini create mode 100644 images/频率按钮1.png create mode 100644 images/频率按钮2.png create mode 100644 view/widgets/frequency_button_group.py diff --git a/busisness/dals.py b/busisness/dals.py index 1588a68..1cc1a2a 100644 --- a/busisness/dals.py +++ b/busisness/dals.py @@ -71,7 +71,7 @@ class ArtifactDal(BaseDal): artifact.BetonVolume=row["BetonVolume"] artifact.BetonTaskID=row["BetonTaskID"] artifact.HoleRingMarking=row["HoleRingMarking"] - artifact.GapRingMarking=row["GroutingPipeMarking"] + artifact.GroutingPipeMarking=row["GroutingPipeMarking"] artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"] artifact.Status=row["Status"] artifact.BeginTime=row["BeginTime"] @@ -186,12 +186,22 @@ class PDRecordDal(BaseDal): # pdrecord = PDRecordModel() pdrecord.ID=row["ID"] + pdrecord.PDCode=row["PDCode"] pdrecord.TaskID=row["TaskID"] pdrecord.ProjectName=row["ProjectName"] pdrecord.ProduceMixID=row["ProduceMixID"] pdrecord.VinNo=row["VinNo"] pdrecord.BetonVolume=row["BetonVolume"] + pdrecord.MouldCode=row["MouldCode"] + pdrecord.SkeletonID=row["SkeletonID"] + pdrecord.RingTypeCode=row["RingTypeCode"] + pdrecord.SizeSpecification=row["SizeSpecification"] + pdrecord.BuriedDepth=row["BuriedDepth"] + pdrecord.Mode=row["Mode"] pdrecord.Status=row["Status"] + pdrecord.GStatus=row["GStatus"] + pdrecord.Source=row["Source"] + pdrecord.CreateTime=row["CreateTime"] pdrecord.OptTime=row["OptTime"] pdrecords.append(pdrecord) diff --git a/config/opc_config.ini b/config/opc_config.ini new file mode 100644 index 0000000..50613ec --- /dev/null +++ b/config/opc_config.ini @@ -0,0 +1,21 @@ +[OPC_SERVER_CONFIG] +# OPC服务器地址 +server_url = opc.tcp://localhost:4840/zjsh_feed/server/ + +# 心跳检测间隔(秒) +heartbeat_interval = 4 + +# 自动重连间隔(秒) +reconnect_interval = 2 + +# 订阅间隔(毫秒) +sub_interval = 500 + +[OPC_NODE_LIST] +upper_weight = 2:upper,2:upper_weight +lower_weight = 2:lower,2:lower_weight +upper_hopper_position = 2:upper,2:upper_hopper_position +upper_clamp_status = 2:upper,2:upper_clamp_status +vibration_frequency=2:vibration_frequency +production_progress=2:production_progress +segment_tasks=2:segment_tasks \ No newline at end of file diff --git a/controller/main_controller.py b/controller/main_controller.py index 082d9c9..2b9fa38 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -36,7 +36,7 @@ class MainController: # opcua客户端 self.opc_client = OpcuaUiClient() - self._start_opc_client() + self.opc_client.start() # 连接信号 self.__connectSignals() @@ -44,10 +44,6 @@ class MainController: def showMainWindow(self): # self.main_window.showFullScreen() self.main_window.show() - self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") - self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") - self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") - self.main_window.segment_task_widget.set_task_time("task2","17:24 PM") def _initSubControllers(self): # 右侧视频显示控制模块 @@ -78,7 +74,9 @@ class MainController: 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服务器值改变 + self.opc_client.opc_signal.value_changed.connect(self._onOpcValueChanged, Qt.QueuedConnection) # opcua服务器值改变 + + self.opc_client.opc_signal.opc_log.connect(self.msg_recorder.normal_record, Qt.QueuedConnection) # opcua客户端日志 def handleMainWindowClose(self): @@ -87,10 +85,10 @@ class MainController: # 停止系统底部控制器中的线程 if hasattr(self, 'bottom_control_controller'): - self.bottom_control_controller.stop_threads() + self.bottom_control_controller.stop_threads() # 停止opc客户端 if hasattr(self, 'opc_client'): - self._stop_opc_client() + self.opc_client.stop_run() def start_msg_database_clean_task(self): """启动清理消息数据库(messages.db)中过期消息的定时任务""" @@ -147,84 +145,20 @@ class MainController: # 用新间隔重新启动清理任务 self.start_msg_database_clean_task() - def _update_opc_value_to_ui(self, node_id, var_name, new_value): + def _onOpcValueChanged(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() - + update_method = getattr(self, f"_update_{var_name}", None) + if update_method: + update_method(new_value) except Exception as e: - print(f"_update_opc_value_to_ui: 界面更新失败: {e}") + print(f"_onOpcValueChanged: 界面更新失败: {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: + def convert_to_ampm(self, time_str: str) -> str: """时间格式转换: 转换为AM/PM形式""" from datetime import datetime time_formats = [ @@ -238,16 +172,72 @@ class MainController: except ValueError: continue return "--:--" + + def onUpdateUiByArtifactInfo(self, artifact_list:List[ArtifactInfoModel]): 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) + time_str = self.convert_to_ampm(artifact.BeginTime) self.main_window.segment_task_widget.set_task_time(f"task{index}", time_str) # 开始时间 + self.main_window.SetSegmentTaskDetails(f"task{index}", artifact) # 更新管片任务详情 + # 将opc服务中的 segment_tasks的值复原为 0,以便下次触发管片更新 + self.opc_client.write_value_by_name("segment_tasks", 0) + + # ======================== OPCUA值更新界面方法 ====================== + def _update_upper_weight(self, val): + # 更新上料斗重量 + self.hopper_controller.onUpdateUpperHopperWeight(val) + + def _update_lower_weight(self, val): + # 更新下料斗重量 + self.hopper_controller.onUpdateLowerHopperWeight(val) + + def _update_upper_volume(self, val): + # 更新上料斗方量 + self.hopper_controller.onUpdateUpperHopperVolume(val) + + def _update_production_progress(self, val): + # 更新生产进度,限制为100, 进度为去掉百分号之后的整数 + 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) + if val == UpperHopperPosition.MIXING_TOWER.value: + self.main_window.mixer_widget.startBladeMix() + else: + self.main_window.mixer_widget.stopBladeMix() + + def _update_segment_tasks(self, val): + if val: # 需要更新管片任务 + """更新左侧的管片任务""" + if hasattr(self, "query_thread") and self.query_thread.isRunning(): + return + # 1. 管片信息查询线程 + self.query_thread = ArtifactInfoQueryThread() + # 2. 主线程更新管片任务UI + self.query_thread.query_finished.connect(self.onUpdateUiByArtifactInfo) + # 3. 查询管片信息错误 + self.query_thread.query_error.connect(self.onQueryArtifactInfoError) + self.query_thread.start() def onQueryArtifactInfoError(self, error_msg:str): # 查询管片信息失败预警 self.msg_recorder.warning_record(error_msg) - \ No newline at end of file + + def _update_vibration_frequency(self, val): + # 更新振捣频率 + self.main_window.frequency_button_group.set_selected_frequency(val) \ No newline at end of file diff --git a/images/频率按钮1.png b/images/频率按钮1.png new file mode 100644 index 0000000000000000000000000000000000000000..60651af16cddc8bbe52f5e215aeadf450bd433fc GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^5kRcK!3HF!J=s44NU@|l`Z_W&Z0zU$lgJ9>GZqKA zJ29*~C-V}>;VkfoEM{QfI}E~%$MaXD00nD3T^vI)oZsGBD0;|2gf(!QuG-u{R~6ru za~dj>CjYo$@GkD>KIy;5@ASWQOgr=UM&0S(rM3n4|E!z;y!6R_mOf4yVGFeap9eDz l7&ay|9VcEOry@Rg?mKtxcdJEww*no);OXk;vd$@?2>?d_QKSF> literal 0 HcmV?d00001 diff --git a/images/频率按钮2.png b/images/频率按钮2.png new file mode 100644 index 0000000000000000000000000000000000000000..e07a2c74e017426d706af7f8fa32ddcf52912d07 GIT binary patch literal 1450 zcmbVMO^ee&7>=^Sy0V~nQSmTl4=S2ulD64q*r>Zr-7T1PsRipnaGFfpuuUc=Q`_!Q zMDz#fLHq}Tc=96XPtcR#MeyiB{0HKjeuQ0F6%9=0>zU{Mejjgbt}iZ}T971Zv9{sV zVQ#`e^9SL7^5MDnFddFJZl#iRC%kBVX2n{u?TpCGDh0sePPR>>BNLE&S|4`>LzfQHl(qpt2i@h4D`Jm7x$CiSh(+L ztj9tYi4?GUj*Yuq@U+W+L!Dpm5df}r-CV}Lx`bgaA{EtpV8+xT`=ZmvC}vunrM#bb zta=aBT*|D8NhM)a@T9?cZx*Pnnaap8RE*Bu3?iQo((}6>Foz0e%Mi7al7+0$z!rhP z8TbM&6O1QNm-|6`v>&uo-h+dD@u{GHM*1FU^CYBTY7o*6)8eQjquha{!h1Xck)T&$ z_j#>S*-Cgj=)px=UtdGDYQ@wo(^L#qA1ms*q!y)uMjopc0nq!8`M|=Oxla*waHDn`5A)cdY z(cEBnr>kCo#)(3xmlc35ds*a}FHety|MtLycF<)&`iG2|z*6281Ddea4tVRY8K;5r zT2`tPJG5U-9N+CK>lqw+MYhbF6m0S~$0F!$2{f_ew?7x40bZ#&tBvmGZyydElcc%1 zS7#smaKC~A-MRl0j{mv) literal 0 HcmV?d00001 diff --git a/service/artifact_query_thread.py b/service/artifact_query_thread.py index f9a40d5..ab2c72a 100644 --- a/service/artifact_query_thread.py +++ b/service/artifact_query_thread.py @@ -1,12 +1,11 @@ from PySide6.QtWidgets import QWidget -from PySide6.QtCore import QThread, Signal # 只需要导入QThread和Signal即可 -from typing import List +from PySide6.QtCore import QThread, Signal from busisness.blls import ArtifactBll from busisness.models import ArtifactInfoModel class ArtifactInfoQueryThread(QThread): # 定义信号:子线程查询完成 - query_finished = Signal(List[ArtifactInfoModel]) + query_finished = Signal(list) # 定义信号:发送错误信息 query_error = Signal(str) @@ -24,4 +23,5 @@ class ArtifactInfoQueryThread(QThread): else: raise ValueError("未查询到有效数据") except Exception as e: + print(f"更新管片任务数据失败: {str(e)}") self.query_error.emit(f"更新管片任务数据失败: {str(e)}") \ No newline at end of file diff --git a/service/opcua_ui_client.py b/service/opcua_ui_client.py index b12e4d3..8d76e3d 100644 --- a/service/opcua_ui_client.py +++ b/service/opcua_ui_client.py @@ -1,185 +1,266 @@ #!/usr/bin/env python3 -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QObject, Signal, QThread from opcua import Client, ua import time from datetime import datetime +import configparser class OpcuaUiSignal(QObject): - # 定义值变化信号:参数为(node_id, var_name, new_value) value_changed = Signal(str, str, object) + opc_disconnected = Signal(str) # OPC服务断开信号,参数:断开原因 + opc_reconnected = Signal() # OPC重连成功信号 + opc_log = Signal(str) # OPC运行日志信号,参数:日志信息 # Opcua回调处理器 class SubscriptionHandler: def __init__(self, opc_signal:OpcuaUiSignal): - # 初始化nodeid→变量名映射表 self.node_id_to_name = {} self.opc_signal = opc_signal def datachange_notification(self, node, val, data): - """ - python-opcua标准的回调函数 - :param node: 变化的节点对象 - :param val: 节点新值 - :param data: 附加数据(包含时间戳等) - """ try: - # 1. 解析时间戳 - # time_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # try: - # utc_time = data.monitored_item.Value.SourceTimestamp - # beijing_time = utc_time + datetime.timedelta(hours=8) - # time_str = beijing_time.strftime('%Y-%m-%d %H:%M:%S') - # except: - # pass - - # 2. 获取nodeid并解析变量名 node_id = node.nodeid.to_string() - # 从映射表获取可读变量名 var_name = self.node_id_to_name.get(node_id) - - # 3. 打印变化通知 - # print(f"\n 节点值发生变化!") - # print(f" 节点ID: {node_id}") - # print(f" 变量名称: {var_name}") - # print(f" 时间: {time_str}") - # print(f" 新值: {val}") - self.opc_signal.value_changed.emit(node_id, var_name, val) - except Exception as e: - print(f"解析值变化事件失败: {e}") - import traceback - traceback.print_exc() + err_msg = f"opcua解析值变化事件失败: {e}" + self.opc_signal.opc_log.emit(err_msg) -class OpcuaUiClient: - def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"): - """初始化 OPC UA 客户端""" - self.client = Client(server_url) +class OpcuaUiClient(QThread): + def __init__(self, parent=None): + super().__init__(parent) + self.server_url = "" + self.client = None self.connected = False self.subscription = None self.monitored_items = [] + self.is_running = True # 线程运行标志位 + self.node_id_mapping = {} # node_id 和 可读变量名的映射表 + self.is_reconnect_tip_sent = False # 重连失败提示是否已发送 - # 创建Qt信号对象(用于跨线程传递数据) self.opc_signal = OpcuaUiSignal() - self.handler = SubscriptionHandler(self.opc_signal) # 回调处理器 - # 定义需要监控的变量路径(object_name + var_name) - # 格式:(变量可读名称, [object路径, 变量路径]) - self.target_var_paths = [ - ("upper_weight", ["2:upper", "2:upper_weight"]), - ("lower_weight", ["2:lower", "2:lower_weight"]) - ] - self.node_id_mapping = {} # 存储nodeid→变量名的映射表 + self.handler = SubscriptionHandler(self.opc_signal) + + self.target_var_paths = [] + + # 参数 + self.heartbeat_interval = None # 心跳检测间隔 + self.reconnect_interval = None # 首次/掉线重连间隔 + self.sub_interval = None # 订阅间隔 (单位:ms) + + + def stop_run(self): + """停止线程+断开连接""" + self.is_running = False + self.disconnect() + self.wait() + print("opcua客户端线程已退出") def connect(self): - """连接到服务器""" + """连接到OPC服务器""" try: self.client.connect() self.connected = True - print(f"成功连接到 OPC UA 服务器: {self.client.server_url}") + msg = f"成功连接到OPCUA服务器: {self.server_url}" + print(msg) + self.opc_signal.opc_log.emit(msg) + self.is_reconnect_tip_sent = False return True except Exception as e: - print(f"连接服务器失败: {e}") + self.connected = False + err_msg = f"连接OPCUA服务器失败: {e}" + print(err_msg) + if not self.is_reconnect_tip_sent: + self.opc_signal.opc_log.emit(err_msg) + # 标记为已发送,后续不重复在UI上显示 + self.is_reconnect_tip_sent = True return False def disconnect(self): - """断开连接(包含取消订阅)""" - if self.subscription: - try: - self.subscription.delete() - print("已取消节点订阅") - except: - pass - if self.connected: - self.client.disconnect() - self.connected = False - print("已断开与 OPC UA 服务器的连接") + """断开连接""" + self.connected = False + try: + if self.monitored_items: + for item in self.monitored_items: + try: + self.subscription.unsubscribe(item) + except Exception: + pass + self.monitored_items.clear() + if self.subscription: + try: + self.subscription.delete() + except Exception: + pass + self.subscription = None + if self.client: + try: + self.client.disconnect() + except Exception: + pass + self.node_id_mapping.clear() + if hasattr(self, 'handler') and self.handler: + self.handler.node_id_to_name = {} + except Exception as e: + print(f"opcua断开连接异常: {e}") def build_node_id_mapping(self): - """ - 根据object_name+var_name路径获取nodeid,建立映射表 - """ + """根据object_name+var_name路径获取nodeid,建立映射表""" if not self.connected: - print("请先连接到服务器") return False - try: - print("\n 开始构建nodeid映射表...") - objects_node = self.client.get_objects_node() # 获取根Objects节点 - - for var_name, path_list in self.target_var_paths: - # 根据层级路径找到目标节点 - target_node = objects_node.get_child(path_list) - # 提取nodeid字符串 - node_id = target_node.nodeid.to_string() - # 存入映射表 - self.node_id_mapping[node_id] = var_name - print(f"映射成功: {node_id} → {var_name}") - - # 将映射表传给回调处理器 + # self.opc_signal.opc_log.emit("开始构建nodeid映射表...") + objects_node = self.client.get_objects_node() self.handler.node_id_to_name = self.node_id_mapping + for var_name, path_list in self.target_var_paths: + target_node = objects_node.get_child(path_list) + node_id = target_node.nodeid.to_string() + self.node_id_mapping[node_id] = var_name + # self.opc_signal.opc_log.emit("nodeid映射表构建成功") return True except Exception as e: - print(f"构建映射表失败: {e}") - import traceback - traceback.print_exc() + err_msg = f"构建{var_name}映射表失败: {e}" + print(err_msg) + self.opc_signal.opc_log.emit(err_msg) return False - def create_multi_subscription(self, interval=500): + def create_multi_subscription(self, interval=None): """订阅多个变量(基于映射表的nodeid)""" if not self.connected: - print("请先连接到服务器") return - - # 先构建映射表,失败则直接返回 - if not self.node_id_mapping: - if not self.build_node_id_mapping(): - return - + if not self.node_id_mapping and not self.build_node_id_mapping(): + return try: - interval = int(interval) - # 1. 创建订阅 + interval = int(interval) if interval else self.sub_interval self.subscription = self.client.create_subscription(interval, self.handler) - print(f"\n订阅创建成功(间隔:{interval}ms)") - - # 2. 遍历映射表,为每个nodeid创建监控项 + self.opc_signal.opc_log.emit(f"opcua订阅创建成功(间隔:{interval}ms)") for node_id, var_name in self.node_id_mapping.items(): var_node = self.client.get_node(node_id) monitored_item = self.subscription.subscribe_data_change(var_node) self.monitored_items.append(monitored_item) + # self.opc_signal.opc_log.emit(f"已订阅变量: {var_name} (nodeid: {node_id})") print(f"已订阅变量: {var_name} (nodeid: {node_id})") - except Exception as e: - print(f"创建批量订阅失败: {e}") - import traceback - traceback.print_exc() + err_msg = f"创建批量订阅失败: {e}" + print(err_msg) + self.opc_signal.opc_log.emit(err_msg) + + def read_opc_config(self, cfg_path = "config/opc_config.ini"): + """读取OPC配置文件, 初始化所有参数和节点列表""" + try: + cfg = configparser.ConfigParser() + cfg.read(cfg_path, encoding="utf-8") + # 1. 读取服务器基础配置 + self.server_url = cfg.get("OPC_SERVER_CONFIG", "server_url") + self.heartbeat_interval = cfg.getint("OPC_SERVER_CONFIG", "heartbeat_interval") + self.reconnect_interval = cfg.getint("OPC_SERVER_CONFIG", "reconnect_interval") + self.sub_interval = cfg.getint("OPC_SERVER_CONFIG", "sub_interval") + + # 2. 读取OPC节点配置 + node_section = cfg["OPC_NODE_LIST"] + for readable_name, node_path_str in node_section.items(): + node_path_list = node_path_str.split(",") + self.target_var_paths.append( (readable_name, node_path_list) ) + # print("target_var_paths", self.target_var_paths) + except Exception as e: + print(f"读取配置文件失败: {e},使用默认配置启动!") + self.server_url = "opc.tcp://localhost:4840/zjsh_feed/server/" + self.heartbeat_interval = 4 + self.reconnect_interval = 2 + self.sub_interval = 500 + self.target_var_paths = [ + ("upper_weight", ["2:upper", "2:upper_weight"]), + ("lower_weight", ["2:lower", "2:lower_weight"]) + ] + # 参数合法性检验 + self.heartbeat_interval = self.heartbeat_interval if isinstance(self.heartbeat_interval, int) and self.heartbeat_interval >=1 else 4 + self.reconnect_interval = self.reconnect_interval if isinstance(self.reconnect_interval, int) and self.reconnect_interval >=1 else 2 + self.sub_interval = self.sub_interval if isinstance(self.sub_interval, int) and self.sub_interval >=100 else 500 def write_value_by_name(self, var_readable_name, value): - """ - 根据变量可读名称写入值(主要用于修改方量, 类型为 Double类型) - :param var_readable_name: 变量可读名称(如"upper_weight") + """ + 根据变量可读名称写入值(主要用于修改方量, 方量的类型为 Double类型) + :param var_readable_name: 变量可读名称(如"upper_weight") :param value: 要写入的值 """ if not self.connected: - print("请先连接到服务器") + self.opc_signal.opc_log.emit(f"{var_readable_name}写入失败: OPC服务未连接") return - - # 反向查找:通过变量名找nodeid target_node_id = None for node_id, name in self.node_id_mapping.items(): if name == var_readable_name: target_node_id = node_id break - if not target_node_id: - print(f"未找到变量名 {var_readable_name} 对应的nodeid") + # self.opc_signal.opc_log.emit(f"写入失败:未找到变量名 {var_readable_name} 对应的nodeid") return - try: target_node = self.client.get_node(target_node_id) - # 明确指定值类型为Double,避免类型错误 - variant = ua.Variant(float(value), ua.VariantType.Double) - target_node.set_value(variant) + # variant = ua.Variant(float(value), ua.VariantType.Double) + target_node.set_value(value) + # self.opc_signal.opc_log.emit(f"写入成功:{var_readable_name} = {value}") except Exception as e: - print(f"写入值失败: {e}") + err_msg = f"opcua写入值失败: {e}" + print(err_msg) + self.opc_signal.opc_log.emit(err_msg) + # ===== 心跳检测函数 ===== + def _heartbeat_check(self): + """心跳检测: 判断opc服务是否存活""" + try: + self.client.get_node("i=2258").get_value() + return True + except Exception as e: + err_msg = f"心跳检测失败, OPCUA服务已断开 {e}" + print(err_msg) + self.opc_signal.opc_log.emit(err_msg) + return False + + # ===== 掉线重连函数 ===== + def _auto_reconnect(self): + """掉线后自动重连+重建映射+恢复订阅""" + self.opc_signal.opc_disconnected.emit("OPC服务掉线, 开始自动重连...") + try: + self.disconnect() + except Exception as e: + print(f"_auto_reconnect: 断开旧连接时出现异常: {e}") + while self.is_running: + # self.opc_signal.opc_log.emit(f"重试连接OPC服务器: {self.server_url}") + if self.connect(): + self.build_node_id_mapping() + self.create_multi_subscription() + self.opc_signal.opc_reconnected.emit() + self.opc_signal.opc_log.emit("OPCUA服务器重连成功, 所有订阅已恢复正常") + print("OPCUA服务器重连成功, 所有订阅已恢复正常") + break + time.sleep(self.reconnect_interval) + + def _init_connect_with_retry(self): + """连接opc服务器""" + # self.opc_signal.opc_log.emit("OPC客户端初始化, 开始连接服务器...") + print("OPC客户端初始化, 开始连接服务器...") + while self.is_running: + if self.connect(): + self.build_node_id_mapping() + self.create_multi_subscription() + break + # self.opc_signal.opc_log.emit(f"连接OPCUA服务器失败, {self.reconnect_interval}秒后重试...") + time.sleep(self.reconnect_interval) + + def run(self) -> None: + """opcua客户端线程主函数""" + self.read_opc_config() # 读取配置文件 + self.client = Client(self.server_url) # 初始化opc客户端 + + # 连接opc服务器 + self._init_connect_with_retry() + + while self.is_running: + if self.connected: + if not self._heartbeat_check(): + self.connected = False + self._auto_reconnect() + else: + self._auto_reconnect() + time.sleep(self.heartbeat_interval) \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index 0c3fc8f..5a3ae92 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -11,6 +11,7 @@ from .widgets.mixer_widget import MixerWidget from .widgets.conveyor_system_widget import ConveyorSystemWidget from .widgets.task_widget import TaskWidget from .widgets.plan_widget import PlanWidget +from .widgets.frequency_button_group import FrequencyButtonGroup from .widgets.hopper_widget import HopperWidget from .widgets.arc_progress_widget import ArcProgressWidget from .widgets.production_progress_widget import ProductionProgressWidget @@ -24,6 +25,8 @@ from utils.image_paths import ImagePaths from .widgets.segment_details_dialog import SegmentDetailsDialog from .widgets.dispatch_details_dialog import DispatchDetailsDialog +from busisness.models import ArtifactInfoModel + class MainWindow(QWidget): # 定义“即将关闭”的信号 @@ -37,6 +40,11 @@ class MainWindow(QWidget): self.setupLayout() # 设置布局 self.connectSignalToSlot() + # 保存管片任务信息的字典 task1: ArtifactInfoModel1.... (用于显示管片任务详情) + self.artifact_dict = {} + # 当前点击/选中的 管片任务详情对应的任务名(task1\task2\task3) (用于刷新选中的管片任务详情) + self.current_selected_segment_detail_name = None + # 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件 self.installEventFilter(self) @@ -102,10 +110,11 @@ class MainWindow(QWidget): self.conveyor_system_widget = ConveyorSystemWidget(self) # 左侧: 传送带系统 self.segment_task_widget = TaskWidget("管片任务", self) # 左侧:管片任务 self.dispatch_task_widget = TaskWidget("派单任务", self) # 右侧:派单任务 + self.frequency_button_group = FrequencyButtonGroup(self) # 右侧:振捣频率按钮组(220hz/230hz/240hz) self.plan_table_widget = PlanWidget(self) # 右侧: 计划表单 # self.status_monitor = StatusMonitorWidget() # 状态监控部件 self.hopper_widget = HopperWidget() # 中间1:料斗部件 - self.arc_progress = ArcProgressWidget() # 中间2:弧形进度部件 + self.arc_progress = ArcProgressWidget() # 中间2:弧形进度部件 (模具车) self.production_progress = ProductionProgressWidget() # 中间3: 生产进度部件 # self.system_button_widget = SystemButtonWidget() # 系统控制按钮 self.vibration_video = VibrationVideoWidget() # 振捣视频控件 (右侧) @@ -113,17 +122,15 @@ class MainWindow(QWidget): def initSubWidgets(self): # 初始化派单任务的 任务id - self.dispatch_task_widget.set_task_id("task1", "PD0001") - self.dispatch_task_widget.set_task_id("task2", "PD0002") - self.dispatch_task_widget.set_task_id("task3", "PD0003") + # self.dispatch_task_widget.set_task_id("task1", "PD0001") + # self.dispatch_task_widget.set_task_id("task2", "PD0002") + # self.dispatch_task_widget.set_task_id("task3", "PD0003") # 初始化 管片任务 和 派单任务显示的数据 - self.update_segment_tasks() - self.update_dispatch_tasks() + # self._init_segment_tasks() + self._init_dispatch_tasks() - def update_segment_tasks(self): - """从数据库中读取管片任务数据并更新到UI""" - def convert_to_ampm(time_str: str) -> str: + def convert_to_ampm(self, time_str: str) -> str: """ 将时间转换为"hh:mmAM/PM"形式(如03:22PM) Args: @@ -148,24 +155,27 @@ class MainWindow(QWidget): # 所有格式都不匹配时,返回占位符 return "--:--" + def _init_segment_tasks(self): + """从数据库中读取管片任务数据并更新到UI""" try: from busisness.blls import ArtifactBll artifact_dal = ArtifactBll() artifacts = artifact_dal.get_artifact_task() # 获取管片任务数据 # 遍历数据并更新UI - for i, artifact in enumerate(artifacts): - # 更新任务ID和方量到管片任务 - self.segment_task_widget.set_task_id(f"task{i + 1}", artifact.MouldCode) - self.segment_task_widget.set_task_volume(f"task{i + 1}", artifact.BetonVolume) - if artifact.BeginTime: # 更新时间 + for i, artifact in enumerate(artifacts, 1): + if artifact.MouldCode: # 更新模具号 + self.segment_task_widget.set_task_id(f"task{i}", artifact.MouldCode) + if artifact.BetonVolume: # 更新方量 + self.segment_task_widget.set_task_volume(f"task{i}", artifact.BetonVolume) + if artifact.BeginTime: # 更新时间 (管片任务的开始时间) # print("artifact.BeginTime: ", artifact.BeginTime) - self.segment_task_widget.set_task_time(f"task{i + 1}", convert_to_ampm(artifact.BeginTime)) - + self.segment_task_widget.set_task_time(f"task{i}", self.convert_to_ampm(artifact.BeginTime)) + self.SetSegmentTaskDetails(f"task{i}", artifact) # 设置管片任务详情信息 except Exception as e: print(f"更新管片任务数据失败: {e}") - def update_dispatch_tasks(self): + def _init_dispatch_tasks(self): """从数据库中读取派单任务数据并更新到UI""" try: from busisness.blls import PDRecordBll @@ -173,9 +183,13 @@ class MainWindow(QWidget): pdrecords = pdrecord_dal.get_PD_record() # 获取派单任务数据 # 遍历数据并更新UI - for i, record in enumerate(pdrecords): - # 更新方量到派单任务widget - self.dispatch_task_widget.set_task_volume(f"task{i + 1}", record.BetonVolume) + for i, record in enumerate(pdrecords, 1): + if record.MouldCode: # 更新模具号 + self.dispatch_task_widget.set_task_id(f"task{i}", record.MouldCode) + if record.BetonVolume: # 更新方量 + self.dispatch_task_widget.set_task_volume(f"task{i}", record.BetonVolume) + if record.CreateTime: # 更新时间 (派单任务的创建时间) + self.dispatch_task_widget.set_task_time(f"task{i}", self.convert_to_ampm(record.CreateTime)) except Exception as e: print(f"更新派单任务数据失败: {e}") @@ -199,6 +213,7 @@ class MainWindow(QWidget): # self.dispatch_task_widget.move(629, 384) self.update_dispatch_task_position() # 更新派单任务坐标 self.update_plan_table_position() # 更新计划表单坐标 + self.update_frequency_button_group_position() # 更新振捣频率选择按钮坐标 # 中间的垂直子布局 sub_v_layout = QVBoxLayout() @@ -273,13 +288,37 @@ class MainWindow(QWidget): def handleSegmentTaskDetails(self, segment_task_name:str): # 管片任务名 task1、task2、task3 (分别对应第一条管片任务、 第二条管片任务...) - print("main_window: handleSegmentTaskDetails", segment_task_name) - + # print("main_window: handleSegmentTaskDetails", segment_task_name) + if not hasattr(self, "segment_details_dialog"): + self.segment_details_dialog = SegmentDetailsDialog(self) + artifact_info:ArtifactInfoModel = self.artifact_dict.get(segment_task_name) + # 更新管片任务详情按钮弹窗的显示 + self.updateSegmentTaskDetailsDialog(artifact_info) # 显示管片任务详情对话框 - segment_details_dialog = SegmentDetailsDialog(self) - # 这里可以设置对话框显示的内容 如 set_segment_id - # segment_details_dialog.set_segment_id("9999999999") - segment_details_dialog.show() + self.segment_details_dialog.show() + # 更新选中的管片任务详情对应的任务名 + self.current_selected_segment_detail_name = segment_task_name + + def updateSegmentTaskDetailsDialog(self, artifact_info:ArtifactInfoModel): + if artifact_info and hasattr(self, "segment_details_dialog"): + # 这里设置管片详情对话框显示的内容 如 set_segment_id + self.segment_details_dialog.set_segment_id(artifact_info.ArtifactActionID) # 管片ID + # 设置管片详情界面的左边一列 + self.segment_details_dialog.set_left_value(0, artifact_info.ArtifactID) # 管片编号 + self.segment_details_dialog.set_left_value(1, artifact_info.ArtifactIDVice1) # 管片副标识 + self.segment_details_dialog.set_left_value(2, artifact_info.ProduceRingNumber) # 生产环号 + self.segment_details_dialog.set_left_value(3, artifact_info.MouldCode) # 模具编号 + self.segment_details_dialog.set_left_value(4, artifact_info.SkeletonID) # 骨架编号 + self.segment_details_dialog.set_left_value(5, artifact_info.RingTypeCode) # 环类型编号 + self.segment_details_dialog.set_left_value(6, artifact_info.SizeSpecification) # 尺寸规格 + # 设置管片详情界面的右边一列 + self.segment_details_dialog.set_right_value(0, artifact_info.BlockNumber) # 分块号 + self.segment_details_dialog.set_right_value(1, artifact_info.HoleRingMarking) # 出洞环标记 + self.segment_details_dialog.set_right_value(2, artifact_info.GroutingPipeMarking) # 注浆管标记 + self.segment_details_dialog.set_right_value(3, artifact_info.PolypropyleneFiberMarking) # 聚丙烯纤维标记 + self.segment_details_dialog.set_right_value(4, artifact_info.BetonVolume) # 浇筑方量 + self.segment_details_dialog.set_right_value(5, artifact_info.BetonTaskID) # 任务单号 + self.segment_details_dialog.set_right_value(6, artifact_info.BuriedDepth) # 埋深 def handleDispatchTaskDetails(self, dispatch_task_name:str): # 派单任务名 task1、task2、task3 (分别对应第一条派单任务、 第二条派单任务...) @@ -320,11 +359,17 @@ class MainWindow(QWidget): # 更新 计划表单widget的坐标 def update_plan_table_position(self): # 方法1:获取料斗控件左上角坐标(相对于父控件) - arc_pos = self.hopper_widget.pos() + hopper_pos = self.hopper_widget.pos() # print(f"料斗控件左上角坐标(相对父控件):x={arc_pos.x()}, y={arc_pos.y()}") # x+462, y-249 - self.plan_table_widget.move(arc_pos.x()+362, arc_pos.y()+40) + self.plan_table_widget.move(hopper_pos.x()+362, hopper_pos.y()+40) + + def update_frequency_button_group_position(self): + # 方法1:获取模具车控件左上角坐标(相对于父控件) + arc_pos = self.arc_progress.pos() + + self.frequency_button_group.move(arc_pos.x()+572, arc_pos.y() + 125) def update_background(self): """更新主界面背景图片""" @@ -363,6 +408,7 @@ class MainWindow(QWidget): self.update_background() # 重新加载背景图片 self.update_dispatch_task_position() # 更新 派单任务的坐标 self.update_plan_table_position() # 更新计划表单坐标 + self.update_frequency_button_group_position() # 更新振捣频率按钮坐标 def closeEvent(self, e): """窗口关闭时的回调""" @@ -375,6 +421,12 @@ class MainWindow(QWidget): self.close() super().keyPressEvent(event) + # ======= 设置管片任务详情接口 ========== + # self.artifact_dict 管片信息的字典 + def SetSegmentTaskDetails(self, task_name:str, artifact_info:ArtifactInfoModel): + self.artifact_dict[task_name] = artifact_info + if task_name == self.current_selected_segment_detail_name: + self.updateSegmentTaskDetailsDialog(artifact_info) # 刷新管片任务详情按钮弹窗 if __name__ == "__main__": import sys diff --git a/view/widgets/arc_progress_widget.py b/view/widgets/arc_progress_widget.py index aa4b176..0aeca4f 100644 --- a/view/widgets/arc_progress_widget.py +++ b/view/widgets/arc_progress_widget.py @@ -249,7 +249,15 @@ class ArcProgressWidget(QWidget): Args: progress: 传入去掉百分号之后的数值, 如80%, 传入80 """ - self.arc_progress.progress = progress + try: + if isinstance(progress, str): + progress = progress.strip().replace("%", "") + progress_int = int(float(progress)) + progress_int = max(0, min(100, progress_int)) + self.arc_progress.progress = progress_int + except (ValueError, TypeError): + pass # 传入的生产进度类型错误,维持原进度 + # 重量设置 (单位kg) def setWeight(self, weight:float): diff --git a/view/widgets/conveyor_system_widget.py b/view/widgets/conveyor_system_widget.py index 19da6e3..a82baf3 100644 --- a/view/widgets/conveyor_system_widget.py +++ b/view/widgets/conveyor_system_widget.py @@ -60,6 +60,8 @@ class ConveyorSystemWidget(QWidget): if outer_pixmap.isNull(): print(f"警告:图片 {outer_img} 加载失败,请检查路径!") return group + outer_width = outer_pixmap.width() + outer_height = outer_pixmap.height() group.setFixedSize(outer_pixmap.size()) # 设置尺寸, 大小和外框一样 @@ -85,6 +87,14 @@ class ConveyorSystemWidget(QWidget): self.upper_inner_label.move(14, 9) self.upper_inner_label.setAlignment(Qt.AlignBottom) + # 重量文字(上位) + self.upper_weight_label = QLabel("5000kg", upper_bg_widget) + self.upper_weight_label.setAlignment(Qt.AlignCenter) + self.upper_weight_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;") + # self.upper_weight_label.setFixedSize(93, 22) + self.upper_weight_label.setFixedSize(120, 29) + self.upper_weight_label.move(outer_width//2 - 60, outer_height//2 - 46) + return group def _update_upper_inner_height(self, total_weight, current_weight: float): @@ -123,6 +133,9 @@ class ConveyorSystemWidget(QWidget): # 2、将self._last_upper_hopper_weight设置为当前重量 self._last_upper_hopper_weight = weight + # 3、更新重量显示文字 + self.upper_weight_label.setText(f"{weight}kg") + def create_conveyor(self): """创建传送带组件(包含左右齿轮,group容器背景为传送带图片)""" group = QWidget() diff --git a/view/widgets/frequency_button_group.py b/view/widgets/frequency_button_group.py new file mode 100644 index 0000000..5ab7bfd --- /dev/null +++ b/view/widgets/frequency_button_group.py @@ -0,0 +1,150 @@ +from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, + QSizePolicy) +from PySide6.QtCore import Signal, Qt, QObject +from PySide6.QtGui import QFont + +class FrequencyButtonGroup(QWidget): + """振捣频率选择按钮组""" + # 该信号frequency_changed,用于振捣频率的控制逻辑,表示需要按照int类型的频率开始振捣 + frequency_changed = Signal(int) # 选中切换信号(新选中频率) + + # 该信号frequency_cleared,用于停止振捣 + frequency_cleared = Signal() # 取消选中信号(无参数) + + def __init__(self, parent=None): + super().__init__(parent) + self.setFixedSize(115, 159) + self._init_ui() + self._selected_freq = None # 当前选中的频率(None表示未选中) + + + def _init_ui(self): + """初始化UI:垂直排列3个按钮, 设置样式和交互""" + # 1. 布局:垂直排列,间隔16px + self.layout = QVBoxLayout(self) + self.layout.setSpacing(16) # 按钮间间隔16px + self.layout.setContentsMargins(0, 0, 0, 0) # 去除外层边距 + self.layout.setAlignment(Qt.AlignCenter) # 按钮垂直居中 + + # 2. 按钮配置:频率列表、样式 + self.freq_buttons = {} # 存储按钮映射:{频率值: 按钮实例} + freq_list = [220, 230, 240] # 频率设置,设置三个频率 + font = QFont() + font.setPointSize(15) # 字体大小15px + + for freq in freq_list: + btn = QPushButton(f"{freq}Hz", self) + btn.setFont(font) + btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) # 固定尺寸 + btn.setFixedSize(96, 36) # 按钮固定大小(匹配背景图尺寸) + btn.setCursor(Qt.PointingHandCursor) + + # 3. 绑定点击事件 + btn.clicked.connect(lambda checked, target_freq=freq: self._on_btn_clicked(target_freq)) + + # 4. 设置默认样式(未选中状态) + self._set_btn_style(btn, is_selected=False) + + # 5. 添加到布局和映射表 + self.layout.addWidget(btn) + self.freq_buttons[freq] = btn + + # 6. 禁止布局拉伸(按钮垂直居中,不填充多余空间) + self.layout.addStretch() + + def _set_btn_style(self, btn, is_selected: bool): + """设置按钮样式""" + if is_selected: + btn.setStyleSheet(""" + QPushButton { + background-image: url(images/频率按钮2.png); + background-repeat: no-repeat; + background-position: center; + background-origin: content-box; + background-clip: content-box; + color: #05267d; + border: none; + padding: 0px; + } + QPushButton:hover { + opacity: 0.95; + background-image: url(images/频率按钮2.png); + background-repeat: no-repeat; + background-position: center; + background-origin: content-box; + background-clip: content-box; + } + """) + else: + btn.setStyleSheet(""" + QPushButton { + background-image: url(images/频率按钮1.png); + background-repeat: no-repeat; + background-position: center; + background-origin: content-box; + background-clip: content-box; + color: #3bfff8; + border: none; + padding: 0px; + } + QPushButton:hover { + opacity: 0.95; + background-image: url(images/频率按钮1.png); + background-repeat: no-repeat; + background-position: center; + background-origin: content-box; + background-clip: content-box; + } + """) + btn.repaint() + + def _on_btn_clicked(self, target_freq): + """按钮点击事件:支持切换选中/取消选中""" + if target_freq == self._selected_freq: + self.clear_selection() + self.frequency_cleared.emit() + return + + if self._selected_freq is not None: + prev_btn = self.freq_buttons[self._selected_freq] + self._set_btn_style(prev_btn, is_selected=False) + + current_btn = self.freq_buttons[target_freq] + self._set_btn_style(current_btn, is_selected=True) + self._selected_freq = target_freq + self.frequency_changed.emit(target_freq) + + def _set_frequency_show(self, target_freq:int): + """设置振捣频率, 只修改显示, 不控制变频器""" + if self._selected_freq is not None: + prev_btn = self.freq_buttons[self._selected_freq] + self._set_btn_style(prev_btn, is_selected=False) + + current_btn = self.freq_buttons[target_freq] + self._set_btn_style(current_btn, is_selected=True) + self._selected_freq = target_freq + + # ------------------- 外部接口 ------------------- + def set_selected_frequency(self, freq: int): + """ 设置显示选中的频率, 只显示不控制 """ + try: + freq_int = int(freq) + if freq_int not in self.freq_buttons: + # 除了220、230、240,其他的数值(比如:0)都表示取消显示的频率 + self.clear_selection() + return + self._set_frequency_show(freq_int) + except (ValueError, TypeError): + pass # 传入的振捣频率类型错误,维持原频率 + + + def get_selected_frequency(self): + """获取当前选中的频率(外部接口),没有选中返回None""" + return self._selected_freq + + def clear_selection(self): + """清除所有选中状态(内部调用+外部接口)""" + if self._selected_freq is not None: + prev_btn = self.freq_buttons[self._selected_freq] + self._set_btn_style(prev_btn, is_selected=False) + self._selected_freq = None \ No newline at end of file diff --git a/view/widgets/production_progress_widget.py b/view/widgets/production_progress_widget.py index f5e853f..c766307 100644 --- a/view/widgets/production_progress_widget.py +++ b/view/widgets/production_progress_widget.py @@ -176,13 +176,21 @@ class ProductionProgressWidget(QWidget): self.animation.setEndValue(100) self.animation.start() - def setProgress(self, progress: float): + def setProgress(self, progress: int): """ 设置progress之后, 会根据该值调整进度条 Args: - progress: 传入去掉百分号之后的数值, 如80%, 传入80.0 + progress: 传入去掉百分号之后的数值, 如80%, 传入80 """ - self.linear_progress.progress = progress + try: + if isinstance(progress, str): + progress = progress.strip().replace("%", "") + progress_int = int(float(progress)) + progress_int = max(0, min(100, progress_int)) + self.linear_progress.progress = progress_int + except (ValueError, TypeError): + pass # 生产进度更新失败,维持原进度 + if __name__ == "__main__": diff --git a/view/widgets/segment_details_dialog.py b/view/widgets/segment_details_dialog.py index 1a31784..b0bba0f 100644 --- a/view/widgets/segment_details_dialog.py +++ b/view/widgets/segment_details_dialog.py @@ -257,9 +257,9 @@ class SegmentDetailsDialog(QDialog): row: 左列网格行号(0-6,共7行) new_label_text: 新的标签文字(如“管片编号”) """ - if 0 <= row < len(self.left_cells): + if new_label_text and 0 <= row < len(self.left_cells): cell = self.left_cells[row] - cell.label.setText(new_label_text) + cell.label.setText(str(new_label_text)) def set_left_value(self, row, new_value_text:str): """ @@ -268,9 +268,9 @@ class SegmentDetailsDialog(QDialog): row: 左列网格行号(0-6,共7行) new_value_text: 新的值(如“FB789”) """ - if 0 <= row < len(self.left_cells): + if new_value_text and 0 <= row < len(self.left_cells): cell = self.left_cells[row] - cell.value.setText(new_value_text) + cell.value.setText(str(new_value_text)) def set_right_label(self, row, new_label_text:str): """ @@ -279,9 +279,9 @@ class SegmentDetailsDialog(QDialog): row: 右列网格行号(0-6,共7行) new_label_text: 新的标签文字(如“分块号”) """ - if 0 <= row < len(self.right_cells): + if new_label_text and 0 <= row < len(self.right_cells): cell = self.right_cells[row] - cell.label.setText(new_label_text) + cell.label.setText(str(new_label_text)) def set_right_value(self, row, new_value_text:str): """ @@ -290,9 +290,9 @@ class SegmentDetailsDialog(QDialog): row: 右列网格行号(0-6,共7行) new_value_text: 新的值(如“FB789”) """ - if 0 <= row < len(self.left_cells): + if new_value_text and 0 <= row < len(self.left_cells): cell = self.right_cells[row] - cell.value.setText(new_value_text) + cell.value.setText(str(new_value_text)) # 测试代码