#!/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)