OPCUA标准java实现 Milo库

Milo库

今天跟大家来介绍一下一个OPC UA协议的开源库,我们使用的现场设备为西门子的S7-1500 CPU,西门子的S7-1500在V2.1版本后就直接可以作为OPC UA的服务器来供其他客户端访问。所以用OPC协议来进行数据采集就是最好的方式。

计算机语言采用java,所以也花了很大的力气来找OPC UA通信协议的java实现库,尽管OPC Foundation在Github上也有协议的java实现,但是各种学习的资源很有限,学习曲线比较陡峭。然后碰巧在Github上找到了一个OPC UA的开源库,就是今天要介绍的 Milo,据了解该项目的Eclipse旗下的一个物联网的项目,是一个高性能的OPC UA栈,提供了一组客户端和服务端的API,支持对实时数据的访问,监控,报警,订阅数据,支持事件,历史数据访问,和数据建模。

Milo Github链接

Milo 初探

在Milo中大量的采用了java 8的新特性CompletableFuture来进行异步操作,Milo中有大量的操作都是直接返回CompletableFuture对象,还有大量使用函数接口和接口默认方法等新特性,所以JDK的版本要8.0,对CompletableFuture不太熟悉的可以先去了解CompletableFuture的相关概念在来看Milo的官方例子会轻松很多。

添加依赖


好了,下面就添加相关依赖,Milo的依赖有三个Stack,Client SDK,Server SDK。

Client SDK依赖

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>sdk-client</artifactId>
    <version>0.2.4</version>
</dependency>

Server SDK依赖

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>sdk-server</artifactId>
    <version>0.2.4</version>
</dependency>

Stack依赖

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>stack-client</artifactId>
    <version>0.2.4</version>
</dependency>

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>stack-server</artifactId>
    <version>0.2.4</version>
</dependency>

目前最新的版本是0.2.4

开发客户端就添加客户端的依赖,开发服务端就添加服务端的依赖。一般来说Stack依赖并不需要手动添加,在我们添加Client SDK或者Server SDK的时候会包含了Stack依赖。

添加bouncycastle依赖


为什么需要添加bouncycastle依赖?因为创建OPC UA客户端必须要有相关的数字证书,而bouncycastle就作为解析相关的数字证书的库所以要添加相关的bouncycastle依赖。

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.57</version>
</dependency>

所以如果我们开发OPC UA客户端,总的依赖项也很简单,如下:

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>sdk-client</artifactId>
    <version>0.2.4</version>
</dependency>

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.57</version>
</dependency>

搜索服务节点


首先要创建OPC客户端第一件事当然是指定一个URL。以S7-1500的CPU为例,可以在博图软件的组态界面双击CPU然后再属性窗口里面找到OPC选项卡然后里面就会有这个CPU的OPC UA的URL。拿到这个URL后就可以在java中定义这个地址。

//在西门子S7-1500中OPC UA服务器的端口默认为4840
String EndPointUrl = "opc.tcp://localhost:4840";

获取服务节点列表

对应的OPC UA服务地址(也就是上面定义的字符串)的节点并不止一个,因为在一个对应的OPC UA服务地址里面可能也有不一样的服务器安全策略,每种不同安全策略对应一个节点。以S7-1500为例就有下面几种安全策略。

  • 无安全设置
  • Basic128Rsa15 - 签名
  • Basic128Rsa15 - 签名和加密
  • Basic256 - 签名
  • Basic256 - 签名和加密
  • Basic256Sha256 - 签名
  • Basic256Sha256 - 签名和加密

以上策略可以在S7-1500 CPU的组态中选择启用哪一个。然后在java中就会搜索到相应的节点。

EndpointDescription[] endpointDescription = UaTcpStackClient.getEndpoints(EndPointUrl).get();

//过滤掉不需要的安全策略,选择一个自己需要的安全策略
 EndpointDescription endpoint = Arrays.stream(endpoints)
    .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getSecurityPolicyUri()))
    .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned"));

接下来创建配置类,然后再用这个配置类来生成OPC客户端对象。

OpcUaClientConfig config = OpcUaClientConfig.builder()
    .setApplicationName(LocalizedText.english("OPCAPP"))
    .setApplicationUri("urn:LAPTOP-AQ90KJVR:OPCAPP")
    .setCertificate(certificate)
    .setKeyPair(keyPair)
    .setEndpoint(endpoint)
    .setIdentityProvider(new UsernameProvider("username","password"))
    .setRequestTimeout(uint(5000))
    .build();

    OpcUaClient opcClient = new OpcUaClient(config);

下面就来对上面这些代码左一个解释。

在我们调用了builder后就要进行一些基本的客户端设置,setCertificate()有一个X509Certificate对象的形参,表示设置的数字证书(OPCUA应用都需要有数字证书和密匙对来创建,而数字证书和密匙对我们可以自己创建,具体的生成数字证书的方法这里就不讨论了,大家可以到网上找到很多生成整数的例子)。

setKeyPair()接受一个KeyPair对象表示密匙对。

setEndpoint()接受一个EndpointDescription对象,就是设置刚刚我们选择的节点就可以了。

setIdentityProvider()该方法表示指定客户端使用的访问验证方式,接受一个IdentityProvider接口,而Milo库为我们提供了4个IdentityProvider接口的实现。

  • AnonymousProvider
  • CompositeProvider
  • UsernameProvider
  • X509IdentityProvider

我自己比较常用第一个匿名验证和第三个用户名验证方式,因为这两种验证方式也方便简单。

上面的例子中使用的是用户名和密码验证方式,对于该验证方式只需要实例化一个UsernameProvider类,在构造函数中设置用户名和密码。
这样在创建和OPC UA服务器连接的时候会与服务器中设置的授权的用户名和密码比对,符合的话就允许连接。

对于AnonymousProvider匿名验证方式就更简单了,只需要实例化一个AnonymousProvider对象不需要输入任何的实参。匿名连接到OPC UA服务器。

setRequestTimeout()设置请求超时时间,单位为毫秒。

最后通过该config对象最终创建OPCUA的客户端对象OpcUaClient opcClient = new OpcUaClient(config);
在有了这个OpcUaClient对象后我们就能够开始访问OPC UA服务器来进行现场的信息采集了。

浏览节点,读,写

浏览节点


在OPC UA中的读和写是对OPC地址空间中的节点进行访问,地址空间中的节点都实现了Node接口,由于其实现类太多了
这里就不一一罗列出来了。

下面我们就来浏览一个节点:

public void browseNode(OpcUaClient client){
    //开启连接
    client.connect().get();

    List<Node> nodes = client.getAddressSpace.browse(Identifiers.RootFolder).get();

    for(Node node:nodes){
        System.out.println("Node= " + node.getBrowseName().get().getName());
    }
}

正如上面所见我们只需要不到几行的代码就完成了节点的浏览访问,从上面的方法可以看到形参是一个OpcUaClient对象
而该对象我们在上一节已经创建了,我们对传入的OpcUaClient对象调用getAddressSpace()来获取地址空间对象,AddressSpace对象
有很多用于访问节点的方法,这里我们调用browse()方法,该方法接受一个NodeId对象来表示开始浏览的根节点,随后方法会
浏览根节点下的所有节点,并返回一个CompletableFuture<List<Node>>对象(此处用到了java8.0的新特性)。
browse()后调用get()以阻塞的方式等待返回。

在上面例子中的Identifiers.RootFolder是Milo库预定义的根目录,Identifiers中还有很多其他的预定于NodeId,当然我们也可以
自己new一个NodeId出来,这都是可以的。

随后对我们获取到的节点列表进行历遍并且打印每一个节点的名称到标准输出。

以上就是对OPC UA地址空间中的节点进行访问的过程,相当的简单。

获取节点值


获取节点的值也是一样的简单,废话不多说直接上代码。

public void readValue(OpcUaClient client){
    //创建连接
    client.connect().get();

    NodeId nodeId = new NodeId(3,"\"test_value\"");

    DataValue value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();

    System.out.println((Integer)value.getValue().getValue());
}

以上的代码从PLC中读取了名为"test_value"的变量,并且把值打印在了标准输出中。

下面我们来看下上面的代码是怎么回事,首先我们创建了连接,由于Milo库大量采用了CompletableFuture,所以大家会在很多地方看到
调用get()方法来阻塞等待方法返回。然后是创建了一个NodeId对象,该对象的构造函数共有10个重载,我个人比较经常用到的是:

/**
 * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
 * @param identifier     the identifier for a node in the address space of an OPC UA Server.
 */
public NodeId(int namespaceIndex, String identifier) {
    //...
}

以S7-1500 PLC为例,所有的变量的地址空间的索引都是整数3,标识就是PLC中的变量名(注意要带双引号)。
创建好NodeId后就可以读取变量的值了。

调用OpcUaClient对象的readValue()方法读取变量值,该方法接受三个参数

default CompletableFuture<DataValue> readValue(double maxAge,
                                                TimestampsToReturn timestampsToReturn,
                                                NodeId nodeId) {
    //...
}

第一个参数如果设置为0的话会获取最新的值,如果maxAge设置到Int32的最大值,则尝试从缓存中读取值。
第二个参数为请求返回的时间戳,第三个参数为要读取的NodeId对象。

该对象也是返回的CompletableFuture<DataValue>,这里可以发现它返回的是一个DataValue对象,在该对象中有一个Variant
对象来存放真正的值,为了获取PLC变量的值,我们需要从readValue()中返回的DataValue中调用.getValue()来获取
其中的Variant对象,然后再次调用getValue()方法来获得真正的值。Variant中的值的类型是Object,所以获取到值后需要强制转换到我们所需要的值然后再使用。

以上就是读取PLC中的变量值的代码了,也是很简单的对吧。

写变量


下面来展示对变量写入值,代码如下:

public void writeValue(OpcUaClient client, int value){
    //创建连接
    client.connect().get();

    //创建变量节点
    NodeId nodeId = new NodeId(3,"\"test_value\"");

    //创建Variant对象和DataValue对象
    Variant v = new Variant(value);
    DataValue dataValue = new DataValue(v,null,null);

    StatusCode statusCode = client.writeValue(nodeId,dataValue).get();

    System.out.println(statusCode.isGood());
}

向PLC变量写入值的代码跟读取的代码差不多,不同的是需要创建一个Variant然后再用这个Variant对象创建DataValue对象。
OpcUaClient对象的writeValue()的方法接受一个需要写入的变量节点,和一个值对象DataValue,该方法返回的是一个StatusCode对象
,上面的代码把返回来的StutusCode判断是否为Good,并且输出到标准输出中来。

以上就是向PLC变量写入值的代码,Milo库为我们封装了大量的操作,使得在对变量的读写甚至之后介绍到的操作中都更便利了。

订阅变量

对于读取PLC里面的变量,有时候我们更需要的是当变量变化的时候客户端能够收到并且做出相应的反应,而不是对变量作轮询读取。OPC UA提供了创建变量监控和订阅的方式来监控对应变量的变化。

public void createSubscription(OpcUaClient client){

    //创建连接
    client.connect().get();

    //创建发布间隔1000ms的订阅对象
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();

    //创建订阅的变量
    NodeId nodeId = new NodeId(3,"\"test_value\"");
    ReadValueId readValueId = new ReadValueId(nodeId,AttributeId.Value.uid(),null,null);

    //创建监控的参数
    MonitoringParameters parameters = new MonitoringParameters(
        uint(1),
        1000.0,     // sampling interval
        null,       // filter, null means use default
        uint(10),   // queue size
        true        // discard oldest
    );

    //创建监控项请求
    //该请求最后用于创建订阅。
    MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);

    List<MonitoredItemCreateRequest> requests = new ArrayList<>();
    requests.add(request);

    //创建监控项,并且注册变量值改变时候的回调函数。
    List<UaMonitoredItem> items = subscription.createMonitoredItems(
        TimestampsToReturn.Both,
        requests,
        (item,id)->{
            item.setValueConsumer((item, value)->{
                System.out.println("nodeid :"+item.getReadValueId().getNodeId());
                System.out.println("value :"+value.getValue().getValue());
            })
        }
    ).get();
}

上面的代码与之前的例子相比代码量多了很多,下面我们就来解释上面的代码都发生了什么。

首先还是需要创建OPC连接,然后用OpcUaClient对象创建UaSubscription订阅对象,方法.createSubscription()接受一个double类型的参数,表示订阅发布间隔,单位为毫秒。

接下来就是创建需要订阅的变量。

NodeId nodeId = new NodeId(3,"\"test_value\"");
ReadValueId readValueId = new ReadValueId(nodeId,AttributeId.Value.uid(),null,null);

然后创建监控参数对象,监控参数对象用于之后的创建监控请求对象,创建订阅需要用到监控请求对象,MonitoringParameters的构造函数如下:

public MonitoringParameters(UInteger clientHandle, Double samplingInterval, ExtensionObject filter, UInteger queueSize, Boolean discardOldest) {
    this.clientHandle = clientHandle;
    this.samplingInterval = samplingInterval;
    this.filter = filter;
    this.queueSize = queueSize;
    this.discardOldest = discardOldest;
}

这里就接受最重要的两个参数。

第一个参数clientHandle对象很重要,用来标识每个创建的监控项,所以对于不同的监控变量这个值必须不同,并且唯一。可以采用递增的方式来设置这个值,或者在多线程环境下使用具有原子性的数据类型来设置该值。

第二个参数samplingInterval是变量的采样周期,单位为毫秒。以S7-1500为例的话在PLC的组态设置里面也是可以设置采样周期,所以暂不清楚这两种设置方式是否会冲突。

好了,在创建了监控变量ReadValueId对象和监控参数对象MonitoringParameters后就可以用这两个对象来创建监控项请求对象MonitoredItemCreateRequest了,该对象构造函数如下:

public MonitoredItemCreateRequest(ReadValueId itemToMonitor, MonitoringMode monitoringMode, MonitoringParameters requestedParameters) {
    this.itemToMonitor = itemToMonitor;
    this.monitoringMode = monitoringMode;
    this.requestedParameters = requestedParameters;
}

可以看到构造函数以刚刚我们创建的ReadValueIdMonitoringParameters对象作为形参。所以我们创建该对象也很简单

MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);

这样就创建了一个监控项创建请求对象。

因为MonitoredItemCreateRequest对象包含了监控的变量节点和监控的参数,所以接下来我们就可以用MonitoredItemCreateRequest来创建变量订阅了,我们调用一开始获得的UaSubscription对象的createMonitoredItems()方法,该方法的签名如下:

public interface UaSubscription {

    //...

    default CompletableFuture<List<UaMonitoredItem>> createMonitoredItems(
        TimestampsToReturn timestampsToReturn,
        List<MonitoredItemCreateRequest> itemsToCreate,
        BiConsumer<UaMonitoredItem, Integer> itemCreationCallback) {
            //...
    }

    //...
}

可以看到,该方法接受一个MonitoredItemCreateRequest列表,如果有多个需要订阅的变量就可以把所有需要监控的对象都加入到该列表然后调用该方法来创建订阅。如果只有一个订阅的变量,那么把该变量的MonitoredItemCreateRequest对象加入一个List然后把这个List作为实参传递进createMonitoredItems方法即可。

我们再来看第三个参数,第三个参数是一个BiConsumer函数接口,该函数接口会在List<MonitoredItemCreateRequest>里每个监控项创建成功后调用的函数接口。该函数提供了一个UaMonitoredItem参数,我们用该参数可以访问到创建成功的监控项的NodeId信息等等。

所以我们利用函数接口在创建监控成功后,随便为监控项注册变量值改变的回调函数。如下:

List<UaMonitoredItem> items = subscription.createMonitoredItems(
    TimestampsToReturn.Both,
    requests,
    (item,id)->{
        item.setValueConsumer((item, value)->{
            System.out.println("nodeid :"+item.getReadValueId().getNodeId());
            System.out.println("value :"+value.getValue().getValue());
        })
    }
).get();

上面例子中在创建成功的回调函数中对item调用setValueConsumer方法来设置变量值改变的回调函数,这个回调函数就是该变量每次发生改变后所调用的方法,这里的例子是变量改变时打印节点id和变量值到标准输出中。

最后

到这里这篇结束OPC UA的java实现的Milo库的文章就到此结束了,文章中提供了创建OPC客户端对象以及变量浏览,读,写,和订阅的具体例子。虽然这些都是很基本也很简单的操作,但是网上对于Milo库的学习资源真的是少之又少,所以也希望能让大家有一个概念,如果需要了解更高级的功能或更多关于Milo库的架构建议你去到Milo库的Github仓库中的阅读源代码来了解更多更详细的信息。


喜欢我们的文章也可以关注公众号噢!
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章