背景
前面我們搭建了一個本地的 PLC
仿真環(huán)境,并通過 KEPServerEX6
讀取 PLC
上的數(shù)據(jù),最后還使用 UAExpert
作為OPC客戶端完成從 KEPServerEX6
這個OPC服務器的數(shù)據(jù)讀取與訂閱功能。在這篇文章中,我們將通過 SpringBoot
集成 Milo
庫實現(xiàn)一個 OPC UA
客戶端,包括連接、遍歷節(jié)點、讀取、寫入、訂閱與批量訂閱等功能。
Milo庫
Milo
庫的 GitHub
地址:https://github.com/eclipse/milo
Milo
庫提供了 OPC UA
的服務端和客戶端 SDK
,顯然,我們這里僅用到了OPC UA Client SDK。
引入依賴
SpringBoot
后端項目中引入 Milo
庫依賴(客戶端 SDK
)。
實現(xiàn)OPCUA客戶端
連接
/**
* 創(chuàng)建OPC UA客戶端
*
* @param ip
* @param port
* @param suffix
* @return
* @throws Exception
*/
public OpcUaClient connectOpcUaServer(String ip, String port, String suffix) throws Exception {
String endPointUrl = "opc.tcp://" + ip + ":" + port + suffix;
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
OpcUaClient opcUaClient = OpcUaClient.create(endPointUrl,
endpoints ->
endpoints.stream()
.filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("eclipse milo opc-ua client"))
.setApplicationUri("urn:eclipse:milo:examples:client")
//訪問方式
.setIdentityProvider(new AnonymousProvider())
.setRequestTimeout(UInteger.valueOf(5000))
.build()
);
opcUaClient.connect().get();
Thread.sleep(2000); // 線程休眠一下再返回對象,給創(chuàng)建過程一個時間。
return opcUaClient;
}
遍歷節(jié)點
/**
* 遍歷樹形節(jié)點
*
* @param client OPC UA客戶端
* @param uaNode 節(jié)點
* @throws Exception
*/
public void listNode(OpcUaClient client, UaNode uaNode) throws Exception {
List<? extends UaNode> nodes;
if (uaNode == null) {
nodes = client.getAddressSpace().browseNodes(Identifiers.ObjectsFolder);
} else {
nodes = client.getAddressSpace().browseNodes(uaNode);
}
for (UaNode nd : nodes) {
//排除系統(tǒng)性節(jié)點,這些系統(tǒng)性節(jié)點名稱一般都是以"_"開頭
if (Objects.requireNonNull(nd.getBrowseName().getName()).contains("_")) {
continue;
}
System.out.println("Node= " + nd.getBrowseName().getName());
listNode(client, nd);
}
}
讀取指定節(jié)點
/**
* 讀取節(jié)點數(shù)據(jù)
*
* namespaceIndex可以通過UaExpert客戶端去查詢,一般來說這個值是2。
* identifier也可以通過UaExpert客戶端去查詢,這個值=通道名稱.設備名稱.標記名稱
*
* @param client
* @param namespaceIndex
* @param identifier
* @throws Exception
*/
public void readNodeValue(OpcUaClient client, int namespaceIndex, String identifier) throws Exception {
//節(jié)點
NodeId nodeId = new NodeId(namespaceIndex, identifier);
//讀取節(jié)點數(shù)據(jù)
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 狀態(tài)
System.out.println("Status: " + value.getStatusCode());
//標識符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(id + ": " + value.getValue().getValue());
}
寫入指定節(jié)點
/**
* 寫入節(jié)點數(shù)據(jù)
*
* @param client
* @param namespaceIndex
* @param identifier
* @param value
* @throws Exception
*/
public void writeNodeValue(OpcUaClient client, int namespaceIndex, String identifier, Float value) throws Exception {
//節(jié)點
NodeId nodeId = new NodeId(namespaceIndex, identifier);
//創(chuàng)建數(shù)據(jù)對象,此處的數(shù)據(jù)對象一定要定義類型,不然會出現(xiàn)類型錯誤,導致無法寫入
DataValue newValue = new DataValue(new Variant(value), null, null);
//寫入節(jié)點數(shù)據(jù)
StatusCode statusCode = client.writeValue(nodeId, newValue).join();
System.out.println("結果:" + statusCode.isGood());
}
訂閱指定節(jié)點
/**
* 訂閱(單個)
*
* @param client
* @param namespaceIndex
* @param identifier
* @throws Exception
*/
private static final AtomicInteger atomic = new AtomicInteger();
public void subscribe(OpcUaClient client, int namespaceIndex, String identifier) throws Exception {
//創(chuàng)建發(fā)布間隔1000ms的訂閱對象
client
.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(t -> {
//節(jié)點
NodeId nodeId = new NodeId(namespaceIndex, identifier);
ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
//創(chuàng)建監(jiān)控的參數(shù)
MonitoringParameters parameters = new MonitoringParameters(UInteger.valueOf(atomic.getAndIncrement()), 1000.0, null, UInteger.valueOf(10), true);
//創(chuàng)建監(jiān)控項請求
//該請求最后用于創(chuàng)建訂閱。
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
//創(chuàng)建監(jiān)控項,并且注冊變量值改變時候的回調函數(shù)。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) -> item.setValueConsumer((it, val) -> {
System.out.println("nodeid :" + it.getReadValueId().getNodeId());
System.out.println("value :" + val.getValue().getValue());
})
);
}).get();
//持續(xù)訂閱
Thread.sleep(Long.MAX_VALUE);
}
批量訂閱指定節(jié)點
/**
* 批量訂閱
*
* @param client
* @throws Exception
*/
public void subscribeBatch(OpcUaClient client) throws Exception {
final CountDownLatch eventLatch = new CountDownLatch(1);
//處理訂閱業(yè)務
handlerMultipleNode(client);
//持續(xù)監(jiān)聽
eventLatch.await();
}
/**
* 處理訂閱業(yè)務
*
* @param client OPC UA客戶端
*/
private void handlerMultipleNode(OpcUaClient client) {
try {
//創(chuàng)建訂閱
ManagedSubscription subscription = ManagedSubscription.create(client);
List<NodeId> nodeIdList = new ArrayList<>();
for (String id : batchIdentifiers) {
nodeIdList.add(new NodeId(batchNamespaceIndex, id));
}
//監(jiān)聽
List<ManagedDataItem> dataItemList = subscription.createDataItems(nodeIdList);
for (ManagedDataItem managedDataItem : dataItemList) {
managedDataItem.addDataValueListener((t) -> {
System.out.println(managedDataItem.getNodeId().getIdentifier().toString() + ":" + t.getValue().getValue().toString());
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
關于斷線重連的批量訂閱,可以參考文末源碼,我沒有進行實際測試。
測試
連接KEPServerEX6的OPC UA服務器
將上一篇文章中的 KEPServerEX6
作為 OPC UA
服務器來測試我們實現(xiàn)的客戶端功能。這里 namespaceIndex
和 identifier
參考 KEPServerEX6
的配置或者 UAExpert
的右上角 Attribute
顯示。
public class OpcUaStart {
public void start() throws Exception {
OpcUaClientService opcUaClientService = new OpcUaClientService();
// 與OPC UA服務端建立連接,并返回客戶端實例
OpcUaClient client = opcUaClientService.connectOpcUaServer("127.0.0.1", "49320", "");
// 遍歷所有節(jié)點
opcUaClientService.listNode(client, null);
// 讀取指定節(jié)點的值
// opcUaClientService.readNodeValue(client, 2, "Demo.1500PLC.D1");
// opcUaClientService.readNodeValue(client, 2, "Demo.1500PLC.D2");
// 向指定節(jié)點寫入數(shù)據(jù)
opcUaClientService.writeNodeValue(client, 2, "Demo.1500PLC.D1", 6f);
// 訂閱指定節(jié)點
// opcUaClientService.subscribe(client, 2, "Demo.1500PLC.D1");
// 批量訂閱多個節(jié)點
List<String> identifiers = new ArrayList<>();
identifiers.add("Demo.1500PLC.D1");
identifiers.add("Demo.1500PLC.D2");
opcUaClientService.setBatchNamespaceIndex(2);
opcUaClientService.setBatchIdentifiers(identifiers);
// opcUaClientService.subscribeBatch(client);
opcUaClientService.subscribeBatchWithReconnect(client);
}
}
記得在啟動類中開啟 OPC UA
的客戶端。
@SpringBootApplication
public class SpringbootOpcuaApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(SpringbootOpcuaApplication.class, args);
OpcUaStart opcUa = new OpcUaStart();
opcUa.start();
}
}
連接Milo提供的測試性OPC UA服務器
Milo
官方提供了一個開放的 OPC UA
服務器: opc.tcp://milo.digitalpetri.com:62541/milo
,可以先使用 UAExpert
測試連接(我用的是匿名連接),查看其中的節(jié)點及地址信息。
public class OpcUaStart {
public void start() throws Exception {
OpcUaClientService opcUaClientService = new OpcUaClientService();
// 與OPC UA服務端建立連接,并返回客戶端實例
OpcUaClient client = opcUaClientService.connectOpcUaServer("milo.digitalpetri.com", "62541", "/milo");
// 遍歷所有節(jié)點
// opcUaClientService.listNode(client, null);
// 讀取指定節(jié)點的值
opcUaClientService.readNodeValue(client, 2, "Dynamic/RandomInt32");
opcUaClientService.readNodeValue(client, 2, "Dynamic/RandomInt64");
// 向指定節(jié)點寫入數(shù)據(jù)
// opcUaClientService.writeNodeValue(client, 2, "Demo.1500PLC.D1", 6f);
// 訂閱指定節(jié)點
// opcUaClientService.subscribe(client, 2, "Dynamic/RandomDouble");
// 批量訂閱多個節(jié)點
List<String> identifiers = new ArrayList<>();
identifiers.add("Dynamic/RandomDouble");
identifiers.add("Dynamic/RandomFloat");
opcUaClientService.setBatchNamespaceIndex(2);
opcUaClientService.setBatchIdentifiers(identifiers);
// opcUaClientService.subscribeBatch(client);
opcUaClientService.subscribeBatchWithReconnect(client);
}
}
測試結果如下:
可能遇到的問題
UaException: status=Bad_SessionClosed, message=The session was closed by the client.
原因分析: opcUaClient.connect().get();
是一個異步的過程,可能在讀寫的時候,連接還沒有創(chuàng)建好。
解決方法: Thread.sleep(2000);
// 線程休眠一下再返回對象,給創(chuàng)建過程一個時間。
Reference
https://blog.csdn.net/u013457145/article/details/121283612
Source Code
https://github.com/heartsuit/demo-spring-boot/tree/master/springboot-opcua
If you have any questions or any bugs are found, please feel free to contact me.文章來源:http://www.zghlxwxcb.cn/news/detail-483626.html
Your comments and suggestions are welcome!文章來源地址http://www.zghlxwxcb.cn/news/detail-483626.html
到了這里,關于SpringBoot集成Milo庫實現(xiàn)OPC UA客戶端:連接、遍歷節(jié)點、讀取、寫入、訂閱與批量訂閱的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!