237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
#!/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)
|