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 初探
在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;
}
可以看到構造函數以剛剛我們創建的ReadValueId
和MonitoringParameters
對象作爲形參。所以我們創建該對象也很簡單
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倉庫中的閱讀源代碼來了解更多更詳細的信息。
喜歡我們的文章也可以關注公衆號噢!