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() |