Files
Feeding_control_system/opc/opcua_client_subscription.py

237 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
OPC UA 客户端订阅模式示例
使用订阅机制实现实时数据更新
"""
from opcua import Client, Subscription
from opcua.ua import DataChangeNotification
import time
import sys
import threading
class SubHandler:
"""
订阅处理器,处理数据变化通知
"""
def __init__(self):
self.data_changes = {}
self.change_count = 0
# 缓存节点名称,避免在回调中频繁查询
self.node_names = {}
def datachange_notification(self, node, val, data):
"""
数据变化时的回调函数
注意:此函数在订阅线程中调用,必须快速返回,避免耗时操作
"""
self.change_count += 1
# 从缓存获取节点名称,避免发起网络请求
node_id = str(node)
if node_id in self.node_names:
node_name = self.node_names[node_id]
else:
# 如果缓存中没有尝试从节点ID中提取名称备用方案
node_name = node_id
# 存储数据变化
self.data_changes[node_name] = {
'value': val,
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
'node_id': node_id
}
print(f"🔔 数据变化 #{self.change_count}")
print(f" 节点: {node_name}")
print(f" 数值: {val}")
print(f" 时间: {self.data_changes[node_name]['timestamp']}")
print("-" * 50)
class OPCUAClientSubscription:
"""
使用订阅机制的 OPC UA 客户端
"""
def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"):
self.client = Client(server_url)
self.connected = False
self.subscription = None
self.handler = SubHandler()
self.monitored_nodes = {}
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:
self.subscription.delete()
print("🗑️ 已删除订阅")
if self.connected:
self.client.disconnect()
self.connected = False
print("🔌 已断开与 OPC UA 服务器的连接")
def setup_subscription(self, publishing_interval=500):
"""
设置订阅
Args:
publishing_interval: 发布间隔(毫秒)
"""
if not self.connected:
print("请先连接到服务器")
return False
try:
# 创建订阅
self.subscription = self.client.create_subscription(publishing_interval, self.handler)
print(f"📡 已创建订阅,发布间隔: {publishing_interval}ms")
# 获取要监控的节点
objects = self.client.get_objects_node()
upper_device = objects.get_child("2:upper")
lower_device = objects.get_child("2:lower")
# 订阅重量数据
upper_weight_node = upper_device.get_child("2:upper_weight")
lower_weight_node = lower_device.get_child("2:lower_weight")
# 【关键优化】在订阅前预获取并缓存节点名称,避免回调中发起网络请求
try:
upper_name = upper_weight_node.get_display_name().Text
except Exception:
upper_name = "upper_weight"
try:
lower_name = lower_weight_node.get_display_name().Text
except Exception:
lower_name = "lower_weight"
# 缓存节点名称
self.handler.node_names[str(upper_weight_node)] = upper_name
self.handler.node_names[str(lower_weight_node)] = lower_name
print(f"📋 已缓存节点名称: {upper_name}, {lower_name}")
# 开始监控
upper_handle = self.subscription.subscribe_data_change(upper_weight_node)
lower_handle = self.subscription.subscribe_data_change(lower_weight_node)
self.monitored_nodes = {
'upper_weight': {'node': upper_weight_node, 'handle': upper_handle},
'lower_weight': {'node': lower_weight_node, 'handle': lower_handle}
}
print(f"📊 已订阅 {len(self.monitored_nodes)} 个数据节点")
return True
except Exception as e:
print(f"❌ 设置订阅失败: {e}")
import traceback
traceback.print_exc()
return False
def get_current_values(self):
"""获取当前值"""
if not self.monitored_nodes:
return {}
values = {}
for name, info in self.monitored_nodes.items():
try:
values[name] = info['node'].get_value()
except Exception as e:
values[name] = f"读取失败: {e}"
return values
def run_subscription_test(self, duration=30):
"""
运行订阅测试
Args:
duration: 测试持续时间(秒)
"""
if not self.setup_subscription():
return
print(f"\n🚀 开始订阅模式测试,持续 {duration} 秒...")
print("💡 等待数据变化通知...")
print("=" * 60)
start_time = time.time()
last_stats_time = start_time
try:
while time.time() - start_time < duration:
current_time = time.time()
# 每5秒显示一次统计信息
if current_time - last_stats_time >= 5:
elapsed = current_time - start_time
changes_per_minute = (self.handler.change_count / elapsed) * 60 if elapsed > 0 else 0
print(f"\n📈 统计信息 (运行时间: {elapsed:.1f}s)")
print(f" 总变化次数: {self.handler.change_count}")
print(f" 变化频率: {changes_per_minute:.1f} 次/分钟")
if self.handler.data_changes:
print(f" 最新数据:")
for name, data in list(self.handler.data_changes.items())[-2:]: # 显示最后2个
print(f" {name}: {data['value']} ({data['timestamp']})")
last_stats_time = current_time
time.sleep(0.1) # 小延迟避免CPU占用过高
except KeyboardInterrupt:
print("\n⏹️ 测试被用户中断")
finally:
print(f"\n🏁 测试完成")
print(f"📊 总变化次数: {self.handler.change_count}")
print(f"⏱️ 平均变化频率: {(self.handler.change_count / duration) * 60:.1f} 次/分钟")
def main():
"""主函数"""
client = OPCUAClientSubscription("opc.tcp://localhost:4840/zjsh_feed/server/")
try:
# 连接到服务器
if not client.connect():
return
# 运行订阅测试
client.run_subscription_test(duration=60) # 运行60秒
except Exception as e:
print(f"❌ 客户端运行错误: {e}")
import traceback
traceback.print_exc()
finally:
client.disconnect()
if __name__ == "__main__":
if len(sys.argv) > 1:
server_url = sys.argv[1]
client = OPCUAClientSubscription(server_url)
else:
client = OPCUAClientSubscription()
try:
main()
except Exception as e:
print(f"❌ 客户端运行错误: {e}")
sys.exit(1)