前言
模拟OPC Server服务器的方法除了使用KEPServerEX6软件以外,还可以使用java代码模拟启动一个opc server。下文详细讲解,如何使用java代码,实现模拟一个或者多个opc server服务器。
引入依赖
首先在Maven项目的pom.xml文件中引入所需的依赖
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.6.9</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>dictionary-manager</artifactId>
<version>0.6.9</version>
</dependency>
创建Server
创建opc server代码实现:
/**
* 方法描述: 创建opcUaServer
*
* @param port 端口
* @return {@link OpcUaServer}
* @throws
*/
private OpcUaServer startServer(int port){
Set<EndpointConfiguration> endpointConfigurations=new HashSet<>();
EndpointConfiguration endpointConfiguration=EndpointConfiguration.newBuilder().setBindPort(port).build();
endpointConfigurations.add(endpointConfiguration);
System.out.println(endpointConfiguration.getEndpointUrl());
OpcUaServerConfig serverConfig = OpcUaServerConfig.builder()
.setApplicationName(LocalizedText.english("Server Application"))
.setApplicationUri("urn:eclipse:milo:examples:server")
.setProductUri("urn:eclipse:milo:examples:server")
.setEndpoints(endpointConfigurations)
.build();
OpcUaServer server = new OpcUaServer(serverConfig);
server.startup();
return server;
}
在EndpointConfiguration的newBuilder方法中,我看可以知道,如果我们不设置端口,默认就是 12685.
创建自定义Namespace
创建TestNamespace类,继承org.eclipse.milo sdk-server 中的ManagedNamespaceWithLifecycle类,并声明构造器,代码如下:
public static final String NAMESPACE_URI = "urn:eclipse:milo:opc";
private final Logger logger = LoggerFactory.getLogger(getClass());
private volatile Thread eventThread;
private volatile boolean keepPostingEvents = true;
private final DataTypeDictionaryManager dictionaryManager;
private final SubscriptionModel subscriptionModel;
public TestNamespace(OpcUaServer server) {
super(server, NAMESPACE_URI);
subscriptionModel = new SubscriptionModel(server, this);
dictionaryManager = new DataTypeDictionaryManager(getNodeContext(), NAMESPACE_URI);
getLifecycleManager().addLifecycle(dictionaryManager);
getLifecycleManager().addLifecycle(subscriptionModel);
getLifecycleManager().addLifecycle(new Lifecycle() {
@Override
public void startup() {
startBogusEventNotifier();
}
@Override
public void shutdown() {
try {
keepPostingEvents = false;
eventThread.interrupt();
eventThread.join();
} catch (InterruptedException ignored) {
// ignored
}
}
});
}
重载Lifecycle的方法
重载org.eclipse.milo.opcua.sdk.server.Lifecycle 的 startup方法和shutdown方法,启动时,创建事件通知器。代码如下:
private void startBogusEventNotifier() {
// Set the EventNotifier bit on Server Node for Events.
UaNode serverNode = getServer()
.getAddressSpaceManager()
.getManagedNode(Identifiers.Server)
.orElse(null);
if (serverNode instanceof ServerTypeNode) {
((ServerTypeNode) serverNode).setEventNotifier(ubyte(1));
// Post a bogus Event every couple seconds
eventThread = new Thread(() -> {
while (keepPostingEvents) {
try {
BaseEventTypeNode eventNode = getServer().getEventFactory().createEvent(
newNodeId(UUID.randomUUID()),
Identifiers.BaseEventType
);
eventNode.setBrowseName(new QualifiedName(1, "foo"));
eventNode.setDisplayName(LocalizedText.english("foo"));
eventNode.setEventId(ByteString.of(new byte[]{0, 1, 2, 3}));
eventNode.setEventType(Identifiers.BaseEventType);
eventNode.setSourceNode(serverNode.getNodeId());
eventNode.setSourceName(serverNode.getDisplayName().getText());
eventNode.setTime(DateTime.now());
eventNode.setReceiveTime(DateTime.NULL_VALUE);
eventNode.setMessage(LocalizedText.english("event message!"));
eventNode.setSeverity(ushort(2));
//noinspection UnstableApiUsage
getServer().getEventBus().post(eventNode);
eventNode.delete();
} catch (Throwable e) {
logger.error("Error creating EventNode: {}", e.getMessage(), e);
}
try {
//noinspection BusyWait
Thread.sleep(2_000);
} catch (InterruptedException ignored) {
// ignored
}
}
}, "bogus-event-poster");
eventThread.start();
}
}
创建opc ua 节点方法
/**
* 方法描述: 创建节点方法
*
* @param keys 节点名称集合
* @return
* @throws
*/
public void addNodes(Set<String> keys) {
// Create a "opc" folder and add it to the node manager
NodeId folderNodeId = newNodeId("opc");
UaFolderNode folderNode = new UaFolderNode(
getNodeContext(),
folderNodeId,
newQualifiedName("opc"),
LocalizedText.english("opc")
);
getNodeManager().addNode(folderNode);
// Make sure our new folder shows up under the server's Objects folder.
folderNode.addReference(new Reference(
folderNode.getNodeId(),
Identifiers.Organizes,
Identifiers.ObjectsFolder.expanded(),
false
));
for (String key : keys) {
NodeId typeId = Identifiers.Double;
Variant variant = new Variant(0d);
UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId(key))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName(key))
.setDisplayName(LocalizedText.english(key))
.setDataType(typeId)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
node.setValue(new DataValue(variant));
getNodeManager().addNode(node);
folderNode.addOrganizes(node);
}
}
先创建一个“opc”文件夹并将其添加到节点管理器中,然后根据传入的节点名称,循环遍历,创建到“opc”文件夹下,生成变量类型的节点。
重载ManagedNamespaceWithLifecycle虚拟方法
重载继承类ManagedNamespaceWithLifecycle的虚拟方法
代码如下:
@Override
public void onDataItemsCreated(List<DataItem> dataItems) {
subscriptionModel.onDataItemsCreated(dataItems);
}
@Override
public void onDataItemsModified(List<DataItem> dataItems) {
subscriptionModel.onDataItemsModified(dataItems);
}
@Override
public void onDataItemsDeleted(List<DataItem> dataItems) {
subscriptionModel.onDataItemsDeleted(dataItems);
}
@Override
public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
subscriptionModel.onMonitoringModeChanged(monitoredItems);
}
完整代码实现:
import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.Lifecycle;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
import org.eclipse.milo.opcua.sdk.server.dtd.DataTypeDictionaryManager;
import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.BaseEventTypeNode;
import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.ServerTypeNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;
/**
* @author Lenovo
*/
public class TestNamespace extends ManagedNamespaceWithLifecycle {
public static final String NAMESPACE_URI = "urn:eclipse:milo:opc";
private final Logger logger = LoggerFactory.getLogger(getClass());
private volatile Thread eventThread;
private volatile boolean keepPostingEvents = true;
private final DataTypeDictionaryManager dictionaryManager;
private final SubscriptionModel subscriptionModel;
public TestNamespace(OpcUaServer server) {
super(server, NAMESPACE_URI);
subscriptionModel = new SubscriptionModel(server, this);
dictionaryManager = new DataTypeDictionaryManager(getNodeContext(), NAMESPACE_URI);
getLifecycleManager().addLifecycle(dictionaryManager);
getLifecycleManager().addLifecycle(subscriptionModel);
getLifecycleManager().addLifecycle(new Lifecycle() {
@Override
public void startup() {
startBogusEventNotifier();
}
@Override
public void shutdown() {
try {
keepPostingEvents = false;
eventThread.interrupt();
eventThread.join();
} catch (InterruptedException ignored) {
// ignored
}
}
});
}
/**
* 方法描述: 创建节点方法
*
* @param keys
* @return
* @throws
*/
public void addNodes(Set<String> keys) {
// Create a "HelloWorld" folder and add it to the node manager
NodeId folderNodeId = newNodeId("opc");
UaFolderNode folderNode = new UaFolderNode(
getNodeContext(),
folderNodeId,
newQualifiedName("opc"),
LocalizedText.english("opc")
);
getNodeManager().addNode(folderNode);
// Make sure our new folder shows up under the server's Objects folder.
folderNode.addReference(new Reference(
folderNode.getNodeId(),
Identifiers.Organizes,
Identifiers.ObjectsFolder.expanded(),
false
));
for (String key : keys) {
NodeId typeId = Identifiers.Double;
Variant variant = new Variant(0d);
UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId(key))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName(key))
.setDisplayName(LocalizedText.english(key))
.setDataType(typeId)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
node.setValue(new DataValue(variant));
getNodeManager().addNode(node);
folderNode.addOrganizes(node);
}
}
private void startBogusEventNotifier() {
// Set the EventNotifier bit on Server Node for Events.
UaNode serverNode = getServer()
.getAddressSpaceManager()
.getManagedNode(Identifiers.Server)
.orElse(null);
if (serverNode instanceof ServerTypeNode) {
((ServerTypeNode) serverNode).setEventNotifier(ubyte(1));
// Post a bogus Event every couple seconds
eventThread = new Thread(() -> {
while (keepPostingEvents) {
try {
BaseEventTypeNode eventNode = getServer().getEventFactory().createEvent(
newNodeId(UUID.randomUUID()),
Identifiers.BaseEventType
);
eventNode.setBrowseName(new QualifiedName(1, "foo"));
eventNode.setDisplayName(LocalizedText.english("foo"));
eventNode.setEventId(ByteString.of(new byte[]{0, 1, 2, 3}));
eventNode.setEventType(Identifiers.BaseEventType);
eventNode.setSourceNode(serverNode.getNodeId());
eventNode.setSourceName(serverNode.getDisplayName().getText());
eventNode.setTime(DateTime.now());
eventNode.setReceiveTime(DateTime.NULL_VALUE);
eventNode.setMessage(LocalizedText.english("event message!"));
eventNode.setSeverity(ushort(2));
//noinspection UnstableApiUsage
getServer().getEventBus().post(eventNode);
eventNode.delete();
} catch (Throwable e) {
logger.error("Error creating EventNode: {}", e.getMessage(), e);
}
try {
//noinspection BusyWait
Thread.sleep(2_000);
} catch (InterruptedException ignored) {
// ignored
}
}
}, "bogus-event-poster");
eventThread.start();
}
}
@Override
public void onDataItemsCreated(List<DataItem> dataItems) {
subscriptionModel.onDataItemsCreated(dataItems);
}
@Override
public void onDataItemsModified(List<DataItem> dataItems) {
subscriptionModel.onDataItemsModified(dataItems);
}
@Override
public void onDataItemsDeleted(List<DataItem> dataItems) {
subscriptionModel.onDataItemsDeleted(dataItems);
}
@Override
public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
subscriptionModel.onMonitoringModeChanged(monitoredItems);
}
}
创建OpcServerTest类,进行使用测试:
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.server.EndpointConfiguration;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
/**
* @author tarzaqn
*/
public class OpcServerTest {
public static void main(String[] args) {
OpcUaServer server=startServer(12688);
TestNamespace namespace=new TestNamespace(server);
Set<String> keys=getAllKeys("ehc.txt");
namespace.addNodes(keys);
namespace.startup();
}
/**
* 方法描述: 创建opcUaServer
*
* @param port 端口
* @return {@link OpcUaServer}
* @throws
*/
private static OpcUaServer startServer(int port){
Set<EndpointConfiguration> endpointConfigurations=new HashSet<>();
EndpointConfiguration endpointConfiguration=EndpointConfiguration.newBuilder().setBindPort(port).build();
endpointConfigurations.add(endpointConfiguration);
System.out.println(endpointConfiguration.getEndpointUrl());
OpcUaServerConfig serverConfig = OpcUaServerConfig.builder()
.setApplicationName(LocalizedText.english("Server Application"))
.setApplicationUri("urn:eclipse:milo:examples:server")
.setProductUri("urn:eclipse:milo:examples:server")
.setEndpoints(endpointConfigurations)
.build();
OpcUaServer server = new OpcUaServer(serverConfig);
server.startup();
return server;
}
private static Set<String> getAllKeys(String fileName){
Set<String> keys=new HashSet<>(50);
try {
InputStream is= OpcServerTest.class.getResourceAsStream("/points/"+fileName);
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader in = new BufferedReader(reader);
String line;
while ((line = in.readLine()) != null) {
keys.add(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return keys;
}
}
我把需要创建的点位都放在java maven项目的resources文件下的ehc.txt文件中,通过getAllKeys方法拿到所有的需要创建的点位集合。
启动main方法,控制台输出如下:
我们可以看到控制台输出的 opc.tcp://localhost:12688 就是我们使用java启动opc ua server的连接地址,上面的启动server的代码中,没有设置用户,密码登录,我们在使用opc ua 客户端的时候,可以使用匿名登录访问。
OPC UA 客户端连接测试
使用java 代码连接我们刚才创建的 Opc Ua server,尝试读取我们创建的节点名称,代码如下:
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
/**
* @author tarzan
*/
public class OpcUaClientTest {
public static void main(String[] args) throws Exception {
String endPointUrl="opc.tcp://localhost:12688";
OpcUaClient client=OpcUaUtil.createClient(endPointUrl,null,null);
OpcUaUtil.browse(null,client);
Thread.sleep(Integer.MAX_VALUE);
}
}
经测试,连接读取节点名称成功,控制台输出如下: