市面上各种收费的UA软件都有一个诱人的功能,那就是直接解析XML获取结点集,而开源的OPC库中,据我所知只有open62541有,这可以极大的简化我们的流程,并且OPC UA Foundation已经建立了相应的NodeSet文件。
这里我们采用milo的nodeparser来解析XML文件。
版本要求是milo的-0.4.0-SNAPSHOT或以上
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.4.0-SNAPSHOT</version>
</dependency>
本文发布时暂没有相应的版本发布,大家可以等一下0.4版本发布,或者和我一样直接在Github上已有的源码基础上更改。
我们将这个地址的代码clone到本地nodeparser-milo
克隆好后,我们需要先做一个步骤。将克隆后的一个文件 UANodeSet.xsd 复制到任意位置,比如我是在桌面。然后我们通过xsd来生成对应的Java文件,详情可参考XSD生成java文件
如果不管用就再找找其他教程,核心就是通过xsd生成java类
接下来将nodeparser中的以下文件,复制到milo的module server-example 中。
这里的generated文件下的类就是我们通过xsd生成的所有类。
复制完后,肯定会飘红,主要是要改一下import的目录,把以前的
- import org.opcfoundation.ua.generated
- 统统改成 import nodeset.generated.;
其实就是把generated文件夹下的import修正一下
还有就是这里的NodeUtils是后面创建的,现在先不管
接着我们新建一个CncNamespace的类
内容如下:
public class CNCNamespace extends ManagedNamespace {
public static final String NAMESPACE_URI = "urn:eclipse:milo:CNC";
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Random random = new Random();
private final DataTypeDictionaryManager dictionaryManager;
private final SubscriptionModel subscriptionModel;
CNCNamespace(OpcUaServer server) {
super(server, NAMESPACE_URI);
subscriptionModel = new SubscriptionModel(server, this);
dictionaryManager = new DataTypeDictionaryManager(getNodeContext(), NAMESPACE_URI);
}
@Override
protected void onStartup() {
super.onStartup();
dictionaryManager.startup();
subscriptionModel.startup();
// Create a "HelloWorld" folder and add it to the node manager
NodeId folderNodeId = newNodeId("CNC");
UaFolderNode folderNode = new UaFolderNode(
getNodeContext(),
folderNodeId,
newQualifiedName("CNC"),
LocalizedText.english("CNC")
);
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
));
addCNCNodes();
// 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
getServer().getScheduledExecutorService().scheduleAtFixedRate(() -> {
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));
getServer().getEventBus().post(eventNode);
eventNode.delete();
} catch (Throwable e) {
logger.error("Error creating EventNode: {}", e.getMessage(), e);
}
}, 0, 2, TimeUnit.SECONDS);
}
}
@Override
protected void onShutdown() {
dictionaryManager.shutdown();
subscriptionModel.shutdown();
super.onShutdown();
}
private void addCNCNodes() {
InputStream nodeSetXml = getClass().getClassLoader().getResourceAsStream("Opc.Ua.CNC.NodeSet.xml");
UaNodeSet nodeSet;
try {
nodeSet = UaNodeSet.parse(nodeSetXml);
parseNodeSet(nodeSet,getNodeContext(),getNodeManager());
} catch (JAXBException e) {
logger.error(e.getMessage());
}
}
@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);
}
}
核心是以下方法:
private void addCNCNodes() {
InputStream nodeSetXml = getClass().getClassLoader().getResourceAsStream("Opc.Ua.CNC.NodeSet.xml");
UaNodeSet nodeSet;
try {
nodeSet = UaNodeSet.parse(nodeSetXml);
parseNodeSet(nodeSet,getNodeContext(),getNodeManager());
} catch (JAXBException e) {
logger.error(e.getMessage());
}
}
这里的parseNodeSet方法需要我们在刚刚提到的NodeSetUtils类中创建,位置通刚才一样,代码如下
@Slf4j
public class NodeUtils {
public static void parseNodeSet(UaNodeSet nodeSet, UaNodeContext context, UaNodeManager nodeManager){
Map<NodeId, NodeAttributes> nodes = nodeSet.getNodes();
ListMultimap<NodeId, Reference> allReferences = nodeSet.getAllReferences();
Map<NodeClass, List<NodeAttributes>> collect = nodes.values().stream()
.collect(groupingBy(NodeAttributes::getNodeClass));
//依据不同的类别在nodeManager中添加结点
for (NodeClass nodeClass : collect.keySet()) {
if (nodeClass.equals(NodeClass.ObjectType)) {
collect.get(NodeClass.ObjectType).stream()
.map(node -> ((ObjectTypeNodeAttributes) node))
.map(node -> node.getUaObjectTypeNode(context))
.forEach(nodeManager::addNode);
log.info("ObjectType success");
} else if (nodeClass.equals(NodeClass.VariableType)) {
collect.get(NodeClass.VariableType).stream()
.map(node -> ((VariableTypeNodeAttributes) node))
.map(node -> node.getUaVariableTypeNode(context))
.forEach(nodeManager::addNode);
log.info("VariableType success");
} else if (nodeClass.equals(NodeClass.Object)) {
collect.get(NodeClass.Object).stream()
.map(node -> ((ObjectNodeAttributes) node))
.map(node -> node.getObjectNode(context))
.forEach(nodeManager::addNode);
log.info("Object success");
} else if (nodeClass.equals(NodeClass.Variable)) {
collect.get(NodeClass.Variable).stream()
.map(node -> ((VariableNodeAttributes) node))
.map(node -> node.getUaVariableNode(context))
.forEach(nodeManager::addNode);
log.info("Variable success");
} else if (nodeClass.equals(NodeClass.Method)) {
collect.get(NodeClass.Method).stream()
.map(node -> ((MethodNodeAttributes) node))
.map(node -> node.getUaMethodNode(context))
.forEach(nodeManager::addNode);
log.info("Method success");
} else if (nodeClass.equals(NodeClass.DataType)) {
collect.get(NodeClass.DataType).stream()
.map(node -> ((DataTypeNodeAttributes) node))
.map(node -> node.getDataTypeNode(context))
.forEach(nodeManager::addNode);
log.info("DataType success");
} else if (nodeClass.equals(NodeClass.View)) {
collect.get(NodeClass.View).stream()
.map(node -> ((ViewNodeAttributes) node))
.map(node -> node.getUaViewNode(context))
.forEach(nodeManager::addNode);
log.info("View success");
} else if (nodeClass.equals(NodeClass.ReferenceType)) {
collect.get(NodeClass.ReferenceType).stream()
.map(node -> ((ReferenceTypeNodeAttributes) node))
.map(node -> node.getReferenceTypeNode(context))
.forEach(nodeManager::addNode);
log.info("ReferenceType success");
}
}
//在nodeManager中添加所有的引用
allReferences.values()
.forEach(nodeManager::addReference);
}
}
我们以其中一个添加的方法为例做分析,其他的大家应该就都懂了
if (nodeClass.equals(NodeClass.Object)) {
collect.get(NodeClass.Object).stream()
.map(node -> ((ObjectNodeAttributes) node))
.map(node -> node.getObjectNode(context))
.forEach(nodeManager::addNode);
log.info("ObjectType success");
我们是根据其结点的类型,然后将其转换为对应的ObjectTypeNodeAttribute类,并调用此类的getUaObjectNode方法,得到对应UaObjectTypeNode类
而对于getUaOBjectTypeNode方法,我们需要在ObjectTypeNodeAttribute类中自己定义,代码如下
public class ObjectNodeAttributes extends NodeAttributes {
/*其他部分省略,只展示添加的方法*/
public UaObjectNode getObjectNode(UaNodeContext context){
return new UaObjectNode(
context,
getNodeId(),
getBrowseName(),
getDisplayName(),
getDescription(),
getWriteMask(),
getUserWriteMask(),
getEventNotifier()
);
}
}
可以看出,其实就是重新定义了一个ObjectNode然后通过new 创建一个新的UaObjectNode对象。其他的类大致如此,注意对比原来的UaNode的构造方法是怎样的。
最后我们在ExampleServer的构造方法中添加新的命名空间
public ExampleServer() throws Exception {
/*其他部分省略*/
ExampleNamespace exampleNamespace = new ExampleNamespace(server);
//添加命名空间,exampleNamespce的ns id是2
exampleNamespace.startup();
CNCNamespace cncNamespace = new CNCNamespace(server);
//添加命名空间,cncNamespce的ns id是3
cncNamespace.startup();
}
最后执行程序,通过uaexpert检验,成功:
这样就刻意解析所有的NodeSet啦,具体的nodeset文件可以在OPC UA Foundation下载。