add(更新opcua客户端、振捣频率按钮、管片任务数据刷新)

This commit is contained in:
2026-01-16 18:37:21 +08:00
parent 360cb13b73
commit 88dfc53b9d
13 changed files with 567 additions and 234 deletions

View File

@ -71,7 +71,7 @@ class ArtifactDal(BaseDal):
artifact.BetonVolume=row["BetonVolume"] artifact.BetonVolume=row["BetonVolume"]
artifact.BetonTaskID=row["BetonTaskID"] artifact.BetonTaskID=row["BetonTaskID"]
artifact.HoleRingMarking=row["HoleRingMarking"] artifact.HoleRingMarking=row["HoleRingMarking"]
artifact.GapRingMarking=row["GroutingPipeMarking"] artifact.GroutingPipeMarking=row["GroutingPipeMarking"]
artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"] artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"]
artifact.Status=row["Status"] artifact.Status=row["Status"]
artifact.BeginTime=row["BeginTime"] artifact.BeginTime=row["BeginTime"]
@ -186,12 +186,22 @@ class PDRecordDal(BaseDal):
# #
pdrecord = PDRecordModel() pdrecord = PDRecordModel()
pdrecord.ID=row["ID"] pdrecord.ID=row["ID"]
pdrecord.PDCode=row["PDCode"]
pdrecord.TaskID=row["TaskID"] pdrecord.TaskID=row["TaskID"]
pdrecord.ProjectName=row["ProjectName"] pdrecord.ProjectName=row["ProjectName"]
pdrecord.ProduceMixID=row["ProduceMixID"] pdrecord.ProduceMixID=row["ProduceMixID"]
pdrecord.VinNo=row["VinNo"] pdrecord.VinNo=row["VinNo"]
pdrecord.BetonVolume=row["BetonVolume"] 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.Status=row["Status"]
pdrecord.GStatus=row["GStatus"]
pdrecord.Source=row["Source"]
pdrecord.CreateTime=row["CreateTime"]
pdrecord.OptTime=row["OptTime"] pdrecord.OptTime=row["OptTime"]
pdrecords.append(pdrecord) pdrecords.append(pdrecord)

21
config/opc_config.ini Normal file
View File

@ -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

View File

@ -36,7 +36,7 @@ class MainController:
# opcua客户端 # opcua客户端
self.opc_client = OpcuaUiClient() self.opc_client = OpcuaUiClient()
self._start_opc_client() self.opc_client.start()
# 连接信号 # 连接信号
self.__connectSignals() self.__connectSignals()
@ -44,10 +44,6 @@ class MainController:
def showMainWindow(self): def showMainWindow(self):
# self.main_window.showFullScreen() # self.main_window.showFullScreen()
self.main_window.show() 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): def _initSubControllers(self):
# 右侧视频显示控制模块 # 右侧视频显示控制模块
@ -78,7 +74,9 @@ class MainController:
self.config_manager.msg_clean_interval_changed.connect(self.onMsgDbCleanIntervalChanged) # 消息清理间隔改变 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): def handleMainWindowClose(self):
@ -87,10 +85,10 @@ class MainController:
# 停止系统底部控制器中的线程 # 停止系统底部控制器中的线程
if hasattr(self, 'bottom_control_controller'): if hasattr(self, 'bottom_control_controller'):
self.bottom_control_controller.stop_threads() self.bottom_control_controller.stop_threads()
# 停止opc客户端 # 停止opc客户端
if hasattr(self, 'opc_client'): if hasattr(self, 'opc_client'):
self._stop_opc_client() self.opc_client.stop_run()
def start_msg_database_clean_task(self): def start_msg_database_clean_task(self):
"""启动清理消息数据库(messages.db)中过期消息的定时任务""" """启动清理消息数据库(messages.db)中过期消息的定时任务"""
@ -147,84 +145,20 @@ class MainController:
# 用新间隔重新启动清理任务 # 用新间隔重新启动清理任务
self.start_msg_database_clean_task() 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更新函数 OPCUA值变化时的UI更新函数
""" """
try: try:
if var_name == "upper_weight": update_method = getattr(self, f"_update_{var_name}", None)
# 更新上料斗重量 if update_method:
self.hopper_controller.onUpdateUpperHopperWeight(new_value) update_method(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: except Exception as e:
print(f"_update_opc_value_to_ui: 界面更新失败: {e}") print(f"_onOpcValueChanged: 界面更新失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def _start_opc_client(self): def convert_to_ampm(self, time_str: str) -> str:
"""启动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形式""" """时间格式转换: 转换为AM/PM形式"""
from datetime import datetime from datetime import datetime
time_formats = [ time_formats = [
@ -238,16 +172,72 @@ class MainController:
except ValueError: except ValueError:
continue continue
return "--:--" return "--:--"
def onUpdateUiByArtifactInfo(self, artifact_list:List[ArtifactInfoModel]):
for index, artifact in enumerate(artifact_list, 1): for index, artifact in enumerate(artifact_list, 1):
if artifact.MouldCode: if artifact.MouldCode:
self.main_window.segment_task_widget.set_task_id(f"task{index}", artifact.MouldCode) # 模具号 self.main_window.segment_task_widget.set_task_id(f"task{index}", artifact.MouldCode) # 模具号
if artifact.BetonVolume: if artifact.BetonVolume:
self.main_window.segment_task_widget.set_task_volume(f"task{index}", artifact.BetonVolume) # 浇筑方量 self.main_window.segment_task_widget.set_task_volume(f"task{index}", artifact.BetonVolume) # 浇筑方量
if artifact.BeginTime: 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.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): def onQueryArtifactInfoError(self, error_msg:str):
# 查询管片信息失败预警 # 查询管片信息失败预警
self.msg_recorder.warning_record(error_msg) self.msg_recorder.warning_record(error_msg)
def _update_vibration_frequency(self, val):
# 更新振捣频率
self.main_window.frequency_button_group.set_selected_frequency(val)

BIN
images/频率按钮1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
images/频率按钮2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,12 +1,11 @@
from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QWidget
from PySide6.QtCore import QThread, Signal # 只需要导入QThread和Signal即可 from PySide6.QtCore import QThread, Signal
from typing import List
from busisness.blls import ArtifactBll from busisness.blls import ArtifactBll
from busisness.models import ArtifactInfoModel from busisness.models import ArtifactInfoModel
class ArtifactInfoQueryThread(QThread): class ArtifactInfoQueryThread(QThread):
# 定义信号:子线程查询完成 # 定义信号:子线程查询完成
query_finished = Signal(List[ArtifactInfoModel]) query_finished = Signal(list)
# 定义信号:发送错误信息 # 定义信号:发送错误信息
query_error = Signal(str) query_error = Signal(str)
@ -24,4 +23,5 @@ class ArtifactInfoQueryThread(QThread):
else: else:
raise ValueError("未查询到有效数据") raise ValueError("未查询到有效数据")
except Exception as e: except Exception as e:
print(f"更新管片任务数据失败: {str(e)}")
self.query_error.emit(f"更新管片任务数据失败: {str(e)}") self.query_error.emit(f"更新管片任务数据失败: {str(e)}")

View File

@ -1,185 +1,266 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal, QThread
from opcua import Client, ua from opcua import Client, ua
import time import time
from datetime import datetime from datetime import datetime
import configparser
class OpcuaUiSignal(QObject): class OpcuaUiSignal(QObject):
# 定义值变化信号:参数为(node_id, var_name, new_value)
value_changed = Signal(str, str, object) value_changed = Signal(str, str, object)
opc_disconnected = Signal(str) # OPC服务断开信号参数断开原因
opc_reconnected = Signal() # OPC重连成功信号
opc_log = Signal(str) # OPC运行日志信号参数日志信息
# Opcua回调处理器 # Opcua回调处理器
class SubscriptionHandler: class SubscriptionHandler:
def __init__(self, opc_signal:OpcuaUiSignal): def __init__(self, opc_signal:OpcuaUiSignal):
# 初始化nodeid→变量名映射表
self.node_id_to_name = {} self.node_id_to_name = {}
self.opc_signal = opc_signal self.opc_signal = opc_signal
def datachange_notification(self, node, val, data): def datachange_notification(self, node, val, data):
"""
python-opcua标准的回调函数
:param node: 变化的节点对象
:param val: 节点新值
:param data: 附加数据(包含时间戳等)
"""
try: 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() node_id = node.nodeid.to_string()
# 从映射表获取可读变量名
var_name = self.node_id_to_name.get(node_id) 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) self.opc_signal.value_changed.emit(node_id, var_name, val)
except Exception as e: except Exception as e:
print(f"解析值变化事件失败: {e}") err_msg = f"opcua解析值变化事件失败: {e}"
import traceback self.opc_signal.opc_log.emit(err_msg)
traceback.print_exc()
class OpcuaUiClient: class OpcuaUiClient(QThread):
def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"): def __init__(self, parent=None):
"""初始化 OPC UA 客户端""" super().__init__(parent)
self.client = Client(server_url) self.server_url = ""
self.client = None
self.connected = False self.connected = False
self.subscription = None self.subscription = None
self.monitored_items = [] 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.opc_signal = OpcuaUiSignal()
self.handler = SubscriptionHandler(self.opc_signal) # 回调处理器 self.handler = SubscriptionHandler(self.opc_signal)
# 定义需要监控的变量路径object_name + var_name
# 格式:(变量可读名称, [object路径, 变量路径]) self.target_var_paths = []
self.target_var_paths = [
("upper_weight", ["2:upper", "2:upper_weight"]), # 参数
("lower_weight", ["2:lower", "2:lower_weight"]) self.heartbeat_interval = None # 心跳检测间隔
] self.reconnect_interval = None # 首次/掉线重连间隔
self.node_id_mapping = {} # 存储nodeid→变量名的映射表 self.sub_interval = None # 订阅间隔 (单位:ms)
def stop_run(self):
"""停止线程+断开连接"""
self.is_running = False
self.disconnect()
self.wait()
print("opcua客户端线程已退出")
def connect(self): def connect(self):
"""连接到服务器""" """连接到OPC服务器"""
try: try:
self.client.connect() self.client.connect()
self.connected = True 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 return True
except Exception as e: 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 return False
def disconnect(self): def disconnect(self):
"""断开连接(包含取消订阅)""" """断开连接"""
if self.subscription: self.connected = False
try: try:
self.subscription.delete() if self.monitored_items:
print("已取消节点订阅") for item in self.monitored_items:
except: try:
pass self.subscription.unsubscribe(item)
if self.connected: except Exception:
self.client.disconnect() pass
self.connected = False self.monitored_items.clear()
print("已断开与 OPC UA 服务器的连接") 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): def build_node_id_mapping(self):
""" """根据object_name+var_name路径获取nodeid,建立映射表"""
根据object_name+var_name路径获取nodeid,建立映射表
"""
if not self.connected: if not self.connected:
print("请先连接到服务器")
return False return False
try: try:
print("\n 开始构建nodeid映射表...") # self.opc_signal.opc_log.emit("开始构建nodeid映射表...")
objects_node = self.client.get_objects_node() # 获取根Objects节点 objects_node = self.client.get_objects_node()
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.handler.node_id_to_name = self.node_id_mapping 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 return True
except Exception as e: except Exception as e:
print(f"构建映射表失败: {e}") err_msg = f"构建{var_name}映射表失败: {e}"
import traceback print(err_msg)
traceback.print_exc() self.opc_signal.opc_log.emit(err_msg)
return False return False
def create_multi_subscription(self, interval=500): def create_multi_subscription(self, interval=None):
"""订阅多个变量(基于映射表的nodeid)""" """订阅多个变量(基于映射表的nodeid)"""
if not self.connected: if not self.connected:
print("请先连接到服务器")
return return
if not self.node_id_mapping and not self.build_node_id_mapping():
# 先构建映射表,失败则直接返回 return
if not self.node_id_mapping:
if not self.build_node_id_mapping():
return
try: try:
interval = int(interval) interval = int(interval) if interval else self.sub_interval
# 1. 创建订阅
self.subscription = self.client.create_subscription(interval, self.handler) self.subscription = self.client.create_subscription(interval, self.handler)
print(f"\n订阅创建成功(间隔:{interval}ms)") self.opc_signal.opc_log.emit(f"opcua订阅创建成功(间隔:{interval}ms)")
# 2. 遍历映射表为每个nodeid创建监控项
for node_id, var_name in self.node_id_mapping.items(): for node_id, var_name in self.node_id_mapping.items():
var_node = self.client.get_node(node_id) var_node = self.client.get_node(node_id)
monitored_item = self.subscription.subscribe_data_change(var_node) monitored_item = self.subscription.subscribe_data_change(var_node)
self.monitored_items.append(monitored_item) 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})") print(f"已订阅变量: {var_name} (nodeid: {node_id})")
except Exception as e: except Exception as e:
print(f"创建批量订阅失败: {e}") err_msg = f"创建批量订阅失败: {e}"
import traceback print(err_msg)
traceback.print_exc() 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): def write_value_by_name(self, var_readable_name, value):
""" """
根据变量可读名称写入值(主要用于修改方量, 类型为 Double类型) 根据变量可读名称写入值(主要用于修改方量, 方量的类型为 Double类型)
:param var_readable_name: 变量可读名称(如"upper_weight" :param var_readable_name: 变量可读名称(如"upper_weight")
:param value: 要写入的值 :param value: 要写入的值
""" """
if not self.connected: if not self.connected:
print("请先连接到服务器") self.opc_signal.opc_log.emit(f"{var_readable_name}写入失败: OPC服务未连接")
return return
# 反向查找通过变量名找nodeid
target_node_id = None target_node_id = None
for node_id, name in self.node_id_mapping.items(): for node_id, name in self.node_id_mapping.items():
if name == var_readable_name: if name == var_readable_name:
target_node_id = node_id target_node_id = node_id
break break
if not target_node_id: if not target_node_id:
print(f"未找到变量名 {var_readable_name} 对应的nodeid") # self.opc_signal.opc_log.emit(f"写入失败:未找到变量名 {var_readable_name} 对应的nodeid")
return return
try: try:
target_node = self.client.get_node(target_node_id) target_node = self.client.get_node(target_node_id)
# 明确指定值类型为Double避免类型错误 # variant = ua.Variant(float(value), ua.VariantType.Double)
variant = ua.Variant(float(value), ua.VariantType.Double) target_node.set_value(value)
target_node.set_value(variant) # self.opc_signal.opc_log.emit(f"写入成功:{var_readable_name} = {value}")
except Exception as e: 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)

View File

@ -11,6 +11,7 @@ from .widgets.mixer_widget import MixerWidget
from .widgets.conveyor_system_widget import ConveyorSystemWidget from .widgets.conveyor_system_widget import ConveyorSystemWidget
from .widgets.task_widget import TaskWidget from .widgets.task_widget import TaskWidget
from .widgets.plan_widget import PlanWidget from .widgets.plan_widget import PlanWidget
from .widgets.frequency_button_group import FrequencyButtonGroup
from .widgets.hopper_widget import HopperWidget from .widgets.hopper_widget import HopperWidget
from .widgets.arc_progress_widget import ArcProgressWidget from .widgets.arc_progress_widget import ArcProgressWidget
from .widgets.production_progress_widget import ProductionProgressWidget 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.segment_details_dialog import SegmentDetailsDialog
from .widgets.dispatch_details_dialog import DispatchDetailsDialog from .widgets.dispatch_details_dialog import DispatchDetailsDialog
from busisness.models import ArtifactInfoModel
class MainWindow(QWidget): class MainWindow(QWidget):
# 定义“即将关闭”的信号 # 定义“即将关闭”的信号
@ -37,6 +40,11 @@ class MainWindow(QWidget):
self.setupLayout() # 设置布局 self.setupLayout() # 设置布局
self.connectSignalToSlot() self.connectSignalToSlot()
# 保存管片任务信息的字典 task1: ArtifactInfoModel1.... (用于显示管片任务详情)
self.artifact_dict = {}
# 当前点击/选中的 管片任务详情对应的任务名(task1\task2\task3) (用于刷新选中的管片任务详情)
self.current_selected_segment_detail_name = None
# 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件 # 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件
self.installEventFilter(self) self.installEventFilter(self)
@ -102,10 +110,11 @@ class MainWindow(QWidget):
self.conveyor_system_widget = ConveyorSystemWidget(self) # 左侧: 传送带系统 self.conveyor_system_widget = ConveyorSystemWidget(self) # 左侧: 传送带系统
self.segment_task_widget = TaskWidget("管片任务", self) # 左侧:管片任务 self.segment_task_widget = TaskWidget("管片任务", self) # 左侧:管片任务
self.dispatch_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.plan_table_widget = PlanWidget(self) # 右侧: 计划表单
# self.status_monitor = StatusMonitorWidget() # 状态监控部件 # self.status_monitor = StatusMonitorWidget() # 状态监控部件
self.hopper_widget = HopperWidget() # 中间1料斗部件 self.hopper_widget = HopperWidget() # 中间1料斗部件
self.arc_progress = ArcProgressWidget() # 中间2弧形进度部件 self.arc_progress = ArcProgressWidget() # 中间2弧形进度部件 (模具车)
self.production_progress = ProductionProgressWidget() # 中间3: 生产进度部件 self.production_progress = ProductionProgressWidget() # 中间3: 生产进度部件
# self.system_button_widget = SystemButtonWidget() # 系统控制按钮 # self.system_button_widget = SystemButtonWidget() # 系统控制按钮
self.vibration_video = VibrationVideoWidget() # 振捣视频控件 (右侧) self.vibration_video = VibrationVideoWidget() # 振捣视频控件 (右侧)
@ -113,17 +122,15 @@ class MainWindow(QWidget):
def initSubWidgets(self): def initSubWidgets(self):
# 初始化派单任务的 任务id # 初始化派单任务的 任务id
self.dispatch_task_widget.set_task_id("task1", "PD0001") # 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("task2", "PD0002")
self.dispatch_task_widget.set_task_id("task3", "PD0003") # self.dispatch_task_widget.set_task_id("task3", "PD0003")
# 初始化 管片任务 和 派单任务显示的数据 # 初始化 管片任务 和 派单任务显示的数据
self.update_segment_tasks() # self._init_segment_tasks()
self.update_dispatch_tasks() self._init_dispatch_tasks()
def update_segment_tasks(self): def convert_to_ampm(self, time_str: str) -> str:
"""从数据库中读取管片任务数据并更新到UI"""
def convert_to_ampm(time_str: str) -> str:
""" """
将时间转换为"hh:mmAM/PM"形式(如03:22PM) 将时间转换为"hh:mmAM/PM"形式(如03:22PM)
Args: Args:
@ -148,24 +155,27 @@ class MainWindow(QWidget):
# 所有格式都不匹配时,返回占位符 # 所有格式都不匹配时,返回占位符
return "--:--" return "--:--"
def _init_segment_tasks(self):
"""从数据库中读取管片任务数据并更新到UI"""
try: try:
from busisness.blls import ArtifactBll from busisness.blls import ArtifactBll
artifact_dal = ArtifactBll() artifact_dal = ArtifactBll()
artifacts = artifact_dal.get_artifact_task() # 获取管片任务数据 artifacts = artifact_dal.get_artifact_task() # 获取管片任务数据
# 遍历数据并更新UI # 遍历数据并更新UI
for i, artifact in enumerate(artifacts): for i, artifact in enumerate(artifacts, 1):
# 更新任务ID和方量到管片任务 if artifact.MouldCode: # 更新模具号
self.segment_task_widget.set_task_id(f"task{i + 1}", artifact.MouldCode) self.segment_task_widget.set_task_id(f"task{i}", artifact.MouldCode)
self.segment_task_widget.set_task_volume(f"task{i + 1}", artifact.BetonVolume) if artifact.BetonVolume: # 更新方量
if artifact.BeginTime: # 更新时间 self.segment_task_widget.set_task_volume(f"task{i}", artifact.BetonVolume)
if artifact.BeginTime: # 更新时间 (管片任务的开始时间)
# print("artifact.BeginTime: ", 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: except Exception as e:
print(f"更新管片任务数据失败: {e}") print(f"更新管片任务数据失败: {e}")
def update_dispatch_tasks(self): def _init_dispatch_tasks(self):
"""从数据库中读取派单任务数据并更新到UI""" """从数据库中读取派单任务数据并更新到UI"""
try: try:
from busisness.blls import PDRecordBll from busisness.blls import PDRecordBll
@ -173,9 +183,13 @@ class MainWindow(QWidget):
pdrecords = pdrecord_dal.get_PD_record() # 获取派单任务数据 pdrecords = pdrecord_dal.get_PD_record() # 获取派单任务数据
# 遍历数据并更新UI # 遍历数据并更新UI
for i, record in enumerate(pdrecords): for i, record in enumerate(pdrecords, 1):
# 更新方量到派单任务widget if record.MouldCode: # 更新模具号
self.dispatch_task_widget.set_task_volume(f"task{i + 1}", record.BetonVolume) 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: except Exception as e:
print(f"更新派单任务数据失败: {e}") print(f"更新派单任务数据失败: {e}")
@ -199,6 +213,7 @@ class MainWindow(QWidget):
# self.dispatch_task_widget.move(629, 384) # self.dispatch_task_widget.move(629, 384)
self.update_dispatch_task_position() # 更新派单任务坐标 self.update_dispatch_task_position() # 更新派单任务坐标
self.update_plan_table_position() # 更新计划表单坐标 self.update_plan_table_position() # 更新计划表单坐标
self.update_frequency_button_group_position() # 更新振捣频率选择按钮坐标
# 中间的垂直子布局 # 中间的垂直子布局
sub_v_layout = QVBoxLayout() sub_v_layout = QVBoxLayout()
@ -273,13 +288,37 @@ class MainWindow(QWidget):
def handleSegmentTaskDetails(self, segment_task_name:str): def handleSegmentTaskDetails(self, segment_task_name:str):
# 管片任务名 task1、task2、task3 (分别对应第一条管片任务、 第二条管片任务...) # 管片任务名 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) self.segment_details_dialog.show()
# 这里可以设置对话框显示的内容 如 set_segment_id # 更新选中的管片任务详情对应的任务名
# segment_details_dialog.set_segment_id("9999999999") self.current_selected_segment_detail_name = segment_task_name
segment_details_dialog.show()
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): def handleDispatchTaskDetails(self, dispatch_task_name:str):
# 派单任务名 task1、task2、task3 (分别对应第一条派单任务、 第二条派单任务...) # 派单任务名 task1、task2、task3 (分别对应第一条派单任务、 第二条派单任务...)
@ -320,11 +359,17 @@ class MainWindow(QWidget):
# 更新 计划表单widget的坐标 # 更新 计划表单widget的坐标
def update_plan_table_position(self): def update_plan_table_position(self):
# 方法1获取料斗控件左上角坐标相对于父控件 # 方法1获取料斗控件左上角坐标相对于父控件
arc_pos = self.hopper_widget.pos() hopper_pos = self.hopper_widget.pos()
# print(f"料斗控件左上角坐标相对父控件x={arc_pos.x()}, y={arc_pos.y()}") # print(f"料斗控件左上角坐标相对父控件x={arc_pos.x()}, y={arc_pos.y()}")
# x+462, y-249 # 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): def update_background(self):
"""更新主界面背景图片""" """更新主界面背景图片"""
@ -363,6 +408,7 @@ class MainWindow(QWidget):
self.update_background() # 重新加载背景图片 self.update_background() # 重新加载背景图片
self.update_dispatch_task_position() # 更新 派单任务的坐标 self.update_dispatch_task_position() # 更新 派单任务的坐标
self.update_plan_table_position() # 更新计划表单坐标 self.update_plan_table_position() # 更新计划表单坐标
self.update_frequency_button_group_position() # 更新振捣频率按钮坐标
def closeEvent(self, e): def closeEvent(self, e):
"""窗口关闭时的回调""" """窗口关闭时的回调"""
@ -375,6 +421,12 @@ class MainWindow(QWidget):
self.close() self.close()
super().keyPressEvent(event) 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__": if __name__ == "__main__":
import sys import sys

View File

@ -249,7 +249,15 @@ class ArcProgressWidget(QWidget):
Args: Args:
progress: 传入去掉百分号之后的数值, 如80%, 传入80 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) # 重量设置 (单位kg)
def setWeight(self, weight:float): def setWeight(self, weight:float):

View File

@ -60,6 +60,8 @@ class ConveyorSystemWidget(QWidget):
if outer_pixmap.isNull(): if outer_pixmap.isNull():
print(f"警告:图片 {outer_img} 加载失败,请检查路径!") print(f"警告:图片 {outer_img} 加载失败,请检查路径!")
return group return group
outer_width = outer_pixmap.width()
outer_height = outer_pixmap.height()
group.setFixedSize(outer_pixmap.size()) # 设置尺寸, 大小和外框一样 group.setFixedSize(outer_pixmap.size()) # 设置尺寸, 大小和外框一样
@ -85,6 +87,14 @@ class ConveyorSystemWidget(QWidget):
self.upper_inner_label.move(14, 9) self.upper_inner_label.move(14, 9)
self.upper_inner_label.setAlignment(Qt.AlignBottom) 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 return group
def _update_upper_inner_height(self, total_weight, current_weight: float): def _update_upper_inner_height(self, total_weight, current_weight: float):
@ -123,6 +133,9 @@ class ConveyorSystemWidget(QWidget):
# 2、将self._last_upper_hopper_weight设置为当前重量 # 2、将self._last_upper_hopper_weight设置为当前重量
self._last_upper_hopper_weight = weight self._last_upper_hopper_weight = weight
# 3、更新重量显示文字
self.upper_weight_label.setText(f"{weight}kg")
def create_conveyor(self): def create_conveyor(self):
"""创建传送带组件包含左右齿轮group容器背景为传送带图片""" """创建传送带组件包含左右齿轮group容器背景为传送带图片"""
group = QWidget() group = QWidget()

View File

@ -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

View File

@ -176,13 +176,21 @@ class ProductionProgressWidget(QWidget):
self.animation.setEndValue(100) self.animation.setEndValue(100)
self.animation.start() self.animation.start()
def setProgress(self, progress: float): def setProgress(self, progress: int):
""" """
设置progress之后, 会根据该值调整进度条 设置progress之后, 会根据该值调整进度条
Args: 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__": if __name__ == "__main__":

View File

@ -257,9 +257,9 @@ class SegmentDetailsDialog(QDialog):
row: 左列网格行号(0-6,共7行) row: 左列网格行号(0-6,共7行)
new_label_text: 新的标签文字(如“管片编号”) 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 = 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): def set_left_value(self, row, new_value_text:str):
""" """
@ -268,9 +268,9 @@ class SegmentDetailsDialog(QDialog):
row: 左列网格行号(0-6,共7行) row: 左列网格行号(0-6,共7行)
new_value_text: 新的值(如“FB789”) 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 = 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): def set_right_label(self, row, new_label_text:str):
""" """
@ -279,9 +279,9 @@ class SegmentDetailsDialog(QDialog):
row: 右列网格行号(0-6,共7行) row: 右列网格行号(0-6,共7行)
new_label_text: 新的标签文字(如“分块号”) 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 = 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): def set_right_value(self, row, new_value_text:str):
""" """
@ -290,9 +290,9 @@ class SegmentDetailsDialog(QDialog):
row: 右列网格行号(0-6,共7行) row: 右列网格行号(0-6,共7行)
new_value_text: 新的值(如“FB789”) 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 = self.right_cells[row]
cell.value.setText(new_value_text) cell.value.setText(str(new_value_text))
# 测试代码 # 测试代码