add(更新opcua客户端、振捣频率按钮、管片任务数据刷新)
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
21
config/opc_config.ini
Normal file
21
config/opc_config.ini
Normal 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
|
||||
@ -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)
|
||||
|
||||
|
||||
def _update_vibration_frequency(self, val):
|
||||
# 更新振捣频率
|
||||
self.main_window.frequency_button_group.set_selected_frequency(val)
|
||||
BIN
images/频率按钮1.png
Normal file
BIN
images/频率按钮1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 B |
BIN
images/频率按钮2.png
Normal file
BIN
images/频率按钮2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@ -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)}")
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
150
view/widgets/frequency_button_group.py
Normal file
150
view/widgets/frequency_button_group.py
Normal 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
|
||||
@ -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__":
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
# 测试代码
|
||||
|
||||
Reference in New Issue
Block a user