331 lines
12 KiB
Python
331 lines
12 KiB
Python
|
#!/usr/bin/env python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
"""
|
|||
|
OPC UA 模拟服务器
|
|||
|
用于测试在线脚本中的OPC UA通信功能
|
|||
|
"""
|
|||
|
|
|||
|
import time
|
|||
|
import threading
|
|||
|
import logging
|
|||
|
from typing import Dict, Any
|
|||
|
from opcua import Server, ua
|
|||
|
|
|||
|
# 配置日志
|
|||
|
logging.basicConfig(level=logging.INFO)
|
|||
|
logger = logging.getLogger(__name__)
|
|||
|
|
|||
|
|
|||
|
class OpcUaTestServer:
|
|||
|
"""OPC UA 测试服务器"""
|
|||
|
|
|||
|
def __init__(self, host='localhost', port=4840):
|
|||
|
self.host = host
|
|||
|
self.port = port
|
|||
|
self.endpoint = f"opc.tcp://{host}:{port}/freeopcua/server/"
|
|||
|
self.server = None
|
|||
|
self.server_thread = None
|
|||
|
self.update_thread = None
|
|||
|
self.running = False
|
|||
|
self.nodes: Dict[str, Any] = {}
|
|||
|
|
|||
|
# 初始化服务器
|
|||
|
self._setup_server()
|
|||
|
|
|||
|
def _setup_server(self):
|
|||
|
"""设置OPC UA服务器"""
|
|||
|
self.server = Server()
|
|||
|
self.server.set_endpoint(self.endpoint)
|
|||
|
|
|||
|
# 设置服务器名称和应用URI
|
|||
|
self.server.set_server_name("VWED OPC UA Test Server")
|
|||
|
|
|||
|
# 设置安全策略
|
|||
|
self.server.set_security_policy([
|
|||
|
ua.SecurityPolicyType.NoSecurity,
|
|||
|
ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
|
|||
|
ua.SecurityPolicyType.Basic256Sha256_Sign
|
|||
|
])
|
|||
|
|
|||
|
# 创建命名空间
|
|||
|
uri = "http://vwed.test.opcua.server"
|
|||
|
self.namespace_idx = self.server.register_namespace(uri)
|
|||
|
|
|||
|
# 获取根节点
|
|||
|
root = self.server.get_root_node()
|
|||
|
|
|||
|
# 创建对象节点
|
|||
|
myobj = root.add_object(self.namespace_idx, "VWEDTestObject")
|
|||
|
|
|||
|
# 创建各种类型的变量节点用于测试
|
|||
|
self._create_test_variables(myobj)
|
|||
|
|
|||
|
def _create_test_variables(self, parent_node):
|
|||
|
"""创建测试变量节点"""
|
|||
|
|
|||
|
# 1. 基本数据类型变量
|
|||
|
self.nodes['boolean_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "BooleanVariable", True
|
|||
|
)
|
|||
|
self.nodes['boolean_var'].set_writable()
|
|||
|
|
|||
|
self.nodes['int_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "IntegerVariable", 42
|
|||
|
)
|
|||
|
self.nodes['int_var'].set_writable()
|
|||
|
|
|||
|
self.nodes['float_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "FloatVariable", 3.14159
|
|||
|
)
|
|||
|
self.nodes['float_var'].set_writable()
|
|||
|
|
|||
|
self.nodes['string_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "StringVariable", "Hello VWED"
|
|||
|
)
|
|||
|
self.nodes['string_var'].set_writable()
|
|||
|
|
|||
|
# 2. 动态变化的变量(模拟传感器数据)
|
|||
|
self.nodes['temperature'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "Temperature", 25.0
|
|||
|
)
|
|||
|
self.nodes['temperature'].set_writable()
|
|||
|
|
|||
|
self.nodes['pressure'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "Pressure", 101.3
|
|||
|
)
|
|||
|
self.nodes['pressure'].set_writable()
|
|||
|
|
|||
|
self.nodes['counter'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "Counter", 0
|
|||
|
)
|
|||
|
self.nodes['counter'].set_writable()
|
|||
|
|
|||
|
# 3. 只读变量
|
|||
|
self.nodes['readonly_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "ReadOnlyVariable", "Read Only Value"
|
|||
|
)
|
|||
|
# 不设置writable,默认为只读
|
|||
|
|
|||
|
# 4. 数组变量
|
|||
|
self.nodes['array_var'] = parent_node.add_variable(
|
|||
|
self.namespace_idx, "ArrayVariable", [1, 2, 3, 4, 5]
|
|||
|
)
|
|||
|
self.nodes['array_var'].set_writable()
|
|||
|
|
|||
|
# 5. 创建不同NodeId类型的节点进行测试
|
|||
|
|
|||
|
# 字符串NodeId: s=StringNodeId
|
|||
|
self.nodes['string_node'] = parent_node.add_variable(
|
|||
|
ua.NodeId("StringNodeId", self.namespace_idx), "StringNodeVariable", "String Node Value"
|
|||
|
)
|
|||
|
self.nodes['string_node'].set_writable()
|
|||
|
|
|||
|
# 整数NodeId: i=1001
|
|||
|
self.nodes['int_node'] = parent_node.add_variable(
|
|||
|
ua.NodeId(1001, self.namespace_idx), "IntNodeVariable", 1001
|
|||
|
)
|
|||
|
self.nodes['int_node'].set_writable()
|
|||
|
|
|||
|
# GUID NodeId
|
|||
|
import uuid
|
|||
|
guid = uuid.UUID('550e8400-e29b-41d4-a716-446655440000')
|
|||
|
self.nodes['guid_node'] = parent_node.add_variable(
|
|||
|
ua.NodeId(guid, self.namespace_idx), "GuidNodeVariable", "GUID Node Value"
|
|||
|
)
|
|||
|
self.nodes['guid_node'].set_writable()
|
|||
|
|
|||
|
# 字节NodeId
|
|||
|
byte_id = b'ByteNodeId'
|
|||
|
self.nodes['byte_node'] = parent_node.add_variable(
|
|||
|
ua.NodeId(byte_id, self.namespace_idx), "ByteNodeVariable", "Byte Node Value"
|
|||
|
)
|
|||
|
self.nodes['byte_node'].set_writable()
|
|||
|
|
|||
|
logger.info("创建测试变量节点完成")
|
|||
|
|
|||
|
def start_server(self):
|
|||
|
"""启动OPC UA服务器"""
|
|||
|
if self.running:
|
|||
|
logger.warning("服务器已经在运行")
|
|||
|
return
|
|||
|
|
|||
|
logger.info(f"启动OPC UA服务器: {self.endpoint}")
|
|||
|
|
|||
|
try:
|
|||
|
self.server.start()
|
|||
|
self.running = True
|
|||
|
|
|||
|
# 启动数据更新线程
|
|||
|
self.update_thread = threading.Thread(target=self._update_data_loop, daemon=True)
|
|||
|
self.update_thread.start()
|
|||
|
|
|||
|
logger.info("OPC UA服务器启动成功")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"启动OPC UA服务器失败: {e}")
|
|||
|
raise
|
|||
|
|
|||
|
def _update_data_loop(self):
|
|||
|
"""更新测试数据的循环"""
|
|||
|
counter = 0
|
|||
|
while self.running:
|
|||
|
try:
|
|||
|
# 更新计数器
|
|||
|
self.nodes['counter'].set_value(counter)
|
|||
|
|
|||
|
# 更新温度(模拟传感器数据,20-30度之间波动)
|
|||
|
import math
|
|||
|
temperature = 25.0 + 5.0 * math.sin(counter * 0.1)
|
|||
|
self.nodes['temperature'].set_value(temperature)
|
|||
|
|
|||
|
# 更新压力(模拟传感器数据,100-102之间波动)
|
|||
|
pressure = 101.0 + math.cos(counter * 0.05)
|
|||
|
self.nodes['pressure'].set_value(pressure)
|
|||
|
|
|||
|
# 更新布尔值(每5秒切换一次)
|
|||
|
boolean_value = (counter // 5) % 2 == 0
|
|||
|
self.nodes['boolean_var'].set_value(boolean_value)
|
|||
|
|
|||
|
# 更新数组(循环移位)
|
|||
|
current_array = self.nodes['array_var'].get_value()
|
|||
|
if isinstance(current_array, list) and len(current_array) > 0:
|
|||
|
# 循环移位
|
|||
|
new_array = current_array[1:] + [current_array[0] + counter]
|
|||
|
self.nodes['array_var'].set_value(new_array)
|
|||
|
|
|||
|
counter += 1
|
|||
|
time.sleep(1) # 每秒更新一次
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"数据更新错误: {e}")
|
|||
|
time.sleep(1)
|
|||
|
|
|||
|
def stop_server(self):
|
|||
|
"""停止服务器"""
|
|||
|
logger.info("正在停止OPC UA服务器...")
|
|||
|
self.running = False
|
|||
|
|
|||
|
if self.server:
|
|||
|
try:
|
|||
|
self.server.stop()
|
|||
|
logger.info("OPC UA服务器已停止")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"停止服务器时出错: {e}")
|
|||
|
|
|||
|
def get_server_info(self):
|
|||
|
"""获取服务器信息"""
|
|||
|
return {
|
|||
|
'host': self.host,
|
|||
|
'port': self.port,
|
|||
|
'endpoint': self.endpoint,
|
|||
|
'running': self.running,
|
|||
|
'namespace_index': self.namespace_idx,
|
|||
|
'test_nodes': list(self.nodes.keys()),
|
|||
|
}
|
|||
|
|
|||
|
def get_node_info(self):
|
|||
|
"""获取节点信息(用于测试参考)"""
|
|||
|
if not self.running:
|
|||
|
return None
|
|||
|
|
|||
|
node_info = {}
|
|||
|
try:
|
|||
|
for name, node in self.nodes.items():
|
|||
|
node_id = node.nodeid
|
|||
|
print("node_id::::", node_id)
|
|||
|
# node
|
|||
|
value = node.get_value()
|
|||
|
|
|||
|
# 解析NodeId类型
|
|||
|
node_id_str = ""
|
|||
|
identifier = node_id.identifier
|
|||
|
if isinstance(identifier, int):
|
|||
|
node_id_str = f"i={identifier}"
|
|||
|
elif isinstance(identifier, str):
|
|||
|
node_id_str = f"s={identifier}"
|
|||
|
elif hasattr(identifier, 'hex'): # UUID
|
|||
|
node_id_str = f"g={identifier}"
|
|||
|
elif isinstance(identifier, bytes):
|
|||
|
import base64
|
|||
|
node_id_str = f"b={base64.b64encode(identifier).decode()}"
|
|||
|
else:
|
|||
|
node_id_str = f"i={identifier}"
|
|||
|
|
|||
|
node_info[name] = {
|
|||
|
'node_id': node_id_str,
|
|||
|
'namespace_index': node_id.namespace_index,
|
|||
|
'value': value,
|
|||
|
'data_type': type(value).__name__
|
|||
|
}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"获取节点信息失败: {e}")
|
|||
|
|
|||
|
return node_info
|
|||
|
|
|||
|
def set_node_value(self, node_name: str, value: Any):
|
|||
|
"""设置节点值(用于测试)"""
|
|||
|
try:
|
|||
|
if node_name in self.nodes:
|
|||
|
self.nodes[node_name].set_value(value)
|
|||
|
logger.info(f"设置节点值: {node_name} = {value}")
|
|||
|
else:
|
|||
|
logger.warning(f"节点不存在: {node_name}")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"设置节点值失败: {e}")
|
|||
|
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数 - 启动测试服务器"""
|
|||
|
print("=" * 60)
|
|||
|
print("VWED OPC UA 测试服务器")
|
|||
|
print("=" * 60)
|
|||
|
|
|||
|
# 创建并启动服务器
|
|||
|
server = OpcUaTestServer(host='localhost', port=4840)
|
|||
|
server.start_server()
|
|||
|
|
|||
|
server_info = server.get_server_info()
|
|||
|
print(f"\n服务器信息:")
|
|||
|
print(f" 端点: {server_info['endpoint']}")
|
|||
|
print(f" 命名空间索引: {server_info['namespace_index']}")
|
|||
|
print(f" 运行状态: {server_info['running']}")
|
|||
|
|
|||
|
# 显示节点信息
|
|||
|
print(f"\n测试节点列表:")
|
|||
|
node_info = server.get_node_info()
|
|||
|
if node_info:
|
|||
|
for name, info in node_info.items():
|
|||
|
print(f" {name}:")
|
|||
|
print(f" NodeId: ns={info['namespace_index']};{info['node_id']}")
|
|||
|
print(f" Value: {info['value']} ({info['data_type']})")
|
|||
|
|
|||
|
print(f"\n测试示例:")
|
|||
|
print("python -c \"")
|
|||
|
print("from services.online_script.built_in_modules.opc_ua_module import VWEDOpcUaModule")
|
|||
|
print("m = VWEDOpcUaModule('test')")
|
|||
|
print(f"print(m.read_opc_value_with_namespace({server_info['namespace_index']}, 'i=1001'))")
|
|||
|
print("\"")
|
|||
|
|
|||
|
try:
|
|||
|
while True:
|
|||
|
time.sleep(10)
|
|||
|
# 显示当前动态数据状态
|
|||
|
node_info = server.get_node_info()
|
|||
|
if node_info:
|
|||
|
print(f"\n[{time.strftime('%H:%M:%S')}] 动态数据:")
|
|||
|
dynamic_nodes = ['counter', 'temperature', 'pressure', 'boolean_var']
|
|||
|
for node_name in dynamic_nodes:
|
|||
|
if node_name in node_info:
|
|||
|
value = node_info[node_name]['value']
|
|||
|
print(f" {node_name}: {value}")
|
|||
|
|
|||
|
except KeyboardInterrupt:
|
|||
|
print("\n\n正在关闭服务器...")
|
|||
|
server.stop_server()
|
|||
|
print("服务器已关闭")
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|