186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
from PySide6.QtCore import QObject, Signal
|
|||
|
|
from opcua import Client, ua
|
|||
|
|
import time
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
class OpcuaUiSignal(QObject):
|
|||
|
|
# 定义值变化信号:参数为(node_id, var_name, new_value)
|
|||
|
|
value_changed = Signal(str, str, object)
|
|||
|
|
|
|||
|
|
# 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()
|
|||
|
|
|
|||
|
|
class OpcuaUiClient:
|
|||
|
|
def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"):
|
|||
|
|
"""初始化 OPC UA 客户端"""
|
|||
|
|
self.client = Client(server_url)
|
|||
|
|
|
|||
|
|
self.connected = False
|
|||
|
|
self.subscription = None
|
|||
|
|
self.monitored_items = []
|
|||
|
|
|
|||
|
|
# 创建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→变量名的映射表
|
|||
|
|
|
|||
|
|
def connect(self):
|
|||
|
|
"""连接到服务器"""
|
|||
|
|
try:
|
|||
|
|
self.client.connect()
|
|||
|
|
self.connected = True
|
|||
|
|
print(f"成功连接到 OPC UA 服务器: {self.client.server_url}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"连接服务器失败: {e}")
|
|||
|
|
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 服务器的连接")
|
|||
|
|
|
|||
|
|
def build_node_id_mapping(self):
|
|||
|
|
"""
|
|||
|
|
根据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.handler.node_id_to_name = self.node_id_mapping
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"构建映射表失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def create_multi_subscription(self, interval=500):
|
|||
|
|
"""订阅多个变量(基于映射表的nodeid)"""
|
|||
|
|
if not self.connected:
|
|||
|
|
print("请先连接到服务器")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 先构建映射表,失败则直接返回
|
|||
|
|
if not self.node_id_mapping:
|
|||
|
|
if not self.build_node_id_mapping():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
interval = int(interval)
|
|||
|
|
# 1. 创建订阅
|
|||
|
|
self.subscription = self.client.create_subscription(interval, self.handler)
|
|||
|
|
print(f"\n订阅创建成功(间隔:{interval}ms)")
|
|||
|
|
|
|||
|
|
# 2. 遍历映射表,为每个nodeid创建监控项
|
|||
|
|
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)
|
|||
|
|
print(f"已订阅变量: {var_name} (nodeid: {node_id})")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"创建批量订阅失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
def write_value_by_name(self, var_readable_name, value):
|
|||
|
|
"""
|
|||
|
|
根据变量可读名称写入值(主要用于修改方量, 类型为 Double类型)
|
|||
|
|
:param var_readable_name: 变量可读名称(如"upper_weight")
|
|||
|
|
:param value: 要写入的值
|
|||
|
|
"""
|
|||
|
|
if not self.connected:
|
|||
|
|
print("请先连接到服务器")
|
|||
|
|
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")
|
|||
|
|
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)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"写入值失败: {e}")
|
|||
|
|
|