Java实现OPC UA Client直接与PLC通讯
文章目錄
- 前言
- 一、Java實現OPC UA Client
- 二、代碼展示
- 1.maven依賴
- 2.Client實現類
- 3.KeyStoreLoader實現類(實際沒用到)
- 4.PLC數據操作類(瀏覽節點并未調通)
- 5.訪問接口(瀏覽節點并未調通)
- 三、關于空間Index和節點Index
前言
在涉及PLC聯網的項目中, 常常會使用OPC UA協議進行數據交互. JAVA如何實現OPC UA Client與OPC UA Server進行交互? 網上常用的交互方式是通過KepServer做中轉的, 怎么不通過中轉直接連接?
這篇文章只是大致介紹一下Java直接與西門子OPC UA Server的通訊方式, 默認讀者已經對這些技術有了最簡單的了解. 基礎部分就不再過多介紹.
關鍵詞: OPC UA, PLC, JAVA, Milo, Client, S7-1500
一、Java實現OPC UA Client
我的需求如下:
在網絡上搜索Java實現OPC UA Client, 已經有很多實現方法. 主流的是引入Milo + KepServer實現, 還有少部分通過s7connector與PLC OPC UA Server連接. 但是這些實現方式都不太滿足我的需求.
Milo + KepServer實現: 依賴KepServer, 需另外采購, 且不一定能通過甲方的軟件審查. 參考JAVA使用OPC UA 方式與設備通信(milo)
s7connector實現: 通過內存地址訪問數據, 不方便也不安全. 參考JAVA與PLC通訊讀取數據(兩種方式)
拜讀一系列的文章之后, 我認為Milo + KepServer實現方式還可以進一步的優化, 可以將KepServer替換為任意一個PLC設備的OPC UA Server, 因為在原理上, Kepserver只是提供了一個標準的OPC UA Server, 和PLC設備的并沒有本質區別.
所以, 在JAVA使用OPC UA 方式與設備通信(milo)的基礎上, 對實現進行了修改, 以滿足我的需求.
二、代碼展示
1.maven依賴
<!--與控制軟件的plc OPC通信--> <dependency><groupId>org.eclipse.milo</groupId><artifactId>sdk-client</artifactId><version>0.6.0</version> </dependency> <dependency><groupId>org.eclipse.milo</groupId><artifactId>dictionary-reader</artifactId><version>0.6.0</version> </dependency> <dependency><groupId>org.eclipse.milo</groupId><artifactId>sdk-server</artifactId><version>0.6.0</version> </dependency> <dependency><groupId>org.eclipse.milo</groupId><artifactId>dictionary-manager</artifactId><version>0.6.0</version> </dependency> <dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk15on</artifactId><version>1.70</version> </dependency>2.Client實現類
package com.xckj.generalcontrol.controlsoft.client;import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig; import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider; import org.eclipse.milo.opcua.stack.client.DiscoveryClient; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;import javax.annotation.PostConstruct; import java.security.Security; import java.util.List; import java.util.concurrent.CompletableFuture;/*** @author lgy* @date */ @Component public class ClientGen {static {Security.addProvider(new BouncyCastleProvider());}private final Logger log = LoggerFactory.getLogger(getClass());private final CompletableFuture<OpcUaClient> future = new CompletableFuture<>();public static OpcUaClient opcUaClient;private String endpointUrl = "opc.tcp://192.168.1.11:4840";@PostConstructpublic void createClient() {try { // Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security"); // Files.createDirectories(securityTempDir); // if (!Files.exists(securityTempDir)) { // throw new Exception("沒有創建安全目錄: " + securityTempDir); // } // log.info("安全目錄: {}", securityTempDir.toAbsolutePath()); // // //加載秘鑰 // KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);//安全策略 None、Basic256、Basic128Rsa15、Basic256Sha256SecurityPolicy securityPolicy = SecurityPolicy.None;List<EndpointDescription> endpoints;try {endpoints = DiscoveryClient.getEndpoints(endpointUrl).get();} catch (Throwable ex) {// 發現服務String discoveryUrl = endpointUrl;if (!discoveryUrl.endsWith("/")) {discoveryUrl += "/";}discoveryUrl += "discovery";log.info("開始連接 URL: {}", discoveryUrl);endpoints = DiscoveryClient.getEndpoints(discoveryUrl).get();}EndpointDescription endpoint = endpoints.stream().filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getUri())).findFirst().orElseThrow(() -> new Exception("沒有連接上端點"));log.info("使用端點: {} [{}/{}]", endpoint.getEndpointUrl(), securityPolicy, endpoint.getSecurityMode());OpcUaClientConfig config = OpcUaClientConfig.builder().setApplicationName(LocalizedText.english("eclipse milo opc-ua client")).setApplicationUri("urn:eclipse:milo:examples:client")//.setCertificate(loader.getClientCertificate())//.setKeyPair(loader.getClientKeyPair()).setEndpoint(endpoint)//根據匿名驗證和第三個用戶名驗證方式設置傳入對象 AnonymousProvider(匿名方式)UsernameProvider(賬戶密碼)//new UsernameProvider("admin","123456").setIdentityProvider(new AnonymousProvider()).setRequestTimeout(UInteger.valueOf(5000)).build();opcUaClient = OpcUaClient.create(config);} catch (Exception e) {log.error("創建客戶端失敗", e);}} }3.KeyStoreLoader實現類(實際沒用到)
package com.xckj.generalcontrol.controlsoft.util;import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil; import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder; import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory;import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.*; import java.security.cert.X509Certificate; import java.util.regex.Pattern;/*** @author lgy*/ public class KeyStoreLoader {private static final Pattern IP_ADDR_PATTERN = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");private static final String CLIENT_ALIAS = "client-ai";private static final char[] PASSWORD = "password".toCharArray();private final Logger logger = LoggerFactory.getLogger(getClass());private X509Certificate clientCertificate;private KeyPair clientKeyPair;public KeyStoreLoader load(Path baseDir) throws Exception {KeyStore keyStore = KeyStore.getInstance("PKCS12");Path serverKeyStore = baseDir.resolve("example-client.pfx");logger.info("Loading KeyStore at {}", serverKeyStore);if (!Files.exists(serverKeyStore)) {keyStore.load(null, PASSWORD);KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair).setCommonName("Eclipse Milo Example Client").setOrganization("digitalpetri").setOrganizationalUnit("dev").setLocalityName("Folsom").setStateName("CA").setCountryCode("US").setApplicationUri("urn:eclipse:milo:examples:client").addDnsName("localhost").addIpAddress("127.0.0.1");// Get as many hostnames and IP addresses as we can listed in the certificate.for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {if (IP_ADDR_PATTERN.matcher(hostname).matches()) {builder.addIpAddress(hostname);} else {builder.addDnsName(hostname);}}X509Certificate certificate = builder.build();keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate});try (OutputStream out = Files.newOutputStream(serverKeyStore)) {keyStore.store(out, PASSWORD);}} else {try (InputStream in = Files.newInputStream(serverKeyStore)) {keyStore.load(in, PASSWORD);}}Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);if (serverPrivateKey instanceof PrivateKey) {clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);PublicKey serverPublicKey = clientCertificate.getPublicKey();clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);}return this;}public X509Certificate getClientCertificate() {return clientCertificate;}public KeyPair getClientKeyPair() {return clientKeyPair;} }4.PLC數據操作類(瀏覽節點并未調通)
package com.xckj.generalcontrol.controlsoft.client;import cn.minsin.core.tools.StringUtil; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.enumerated.*; import org.eclipse.milo.opcua.stack.core.types.structured.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component;import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException;import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;/*** @author lgy* @date */ @Component public class OpcUaOperationSupport {private final Logger logger = LoggerFactory.getLogger(getClass());/*** 瀏覽節點*/public String browseNode() {OpcUaClient client = ClientGen.opcUaClient;try {client.connect().get();return browseNode("", client, Identifiers.RootFolder);} catch (InterruptedException | ExecutionException e) {logger.error("", e);return e.getMessage();}}/*** 單個讀取 PLC*/public String readPlc(int namespaceIndex, int nameIndex) {OpcUaClient client = ClientGen.opcUaClient;try {client.connect().get();NodeId nodeId = new NodeId(namespaceIndex, nameIndex);CompletableFuture<DataValue> readValue = client.readValue(0.0, TimestampsToReturn.Both, nodeId);DataValue value = readValue.get();String plcValue = value.getValue().getValue().toString();String statusCode = String.valueOf(value.getStatusCode());if (value.getStatusCode() != null) {if (value.getStatusCode().isGood()) {logger.info("獲取數據成功。plcValue={}", plcValue);} else {logger.info("獲取數據失敗。");}logger.info("======== > it means successfully read StatusCode = {}", statusCode);} else {logger.error("=====》 讀數據異常,未獲取到數據。。。");}return plcValue;} catch (InterruptedException | ExecutionException e) {logger.error("", e);return e.getMessage();}}/*** 寫入 PLC*/public String writePlc(int namespaceIndex, int itemIndex, String valueType, String editValue) {try {// 創建連接OpcUaClient client = ClientGen.opcUaClient;client.connect().get();// 創建節點NodeId nodeId = new NodeId(namespaceIndex, itemIndex);// 創建Variant對象和DataValue對象Variant v;String valueTypeStr = StringUtil.trim(StringUtil.lowerCase(valueType));switch (valueTypeStr) {case "boolean":v = new Variant(Boolean.valueOf(editValue));break;case "int":v = new Variant(Integer.valueOf(editValue));break;case "short":v = new Variant(Short.valueOf(editValue));break;case "string":default:v = new Variant(editValue);break;}DataValue dv = new DataValue(v, null, null);StatusCode statusCode = client.writeValue(nodeId, dv).get();if (statusCode.isGood()) {logger.info("========== > it means successfully Wrote '{}' to nodeId={}, statusCodes = {}", v, nodeId, statusCode);} else {logger.error("statusCodes: {}", statusCode);}return statusCode.toString();} catch (InterruptedException | ExecutionException e) {logger.error("", e);return e.getMessage();}}/*** 訂閱變量*/public void createSubscription(int namespaceIndex, int itemIndex) {try {OpcUaClient client = ClientGen.opcUaClient;client.connect().get();//創建發布間隔1000ms的訂閱對象UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();//創建監控的參數MonitoringParameters parameters = new MonitoringParameters(uint(1),// 發布間隔1000.0,// filter, 空表示用默認值null,// 隊列大小uint(10),//放棄舊配置true);//創建訂閱的變量NodeId nodeId = new NodeId(namespaceIndex, itemIndex);ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);//創建監控項請求//該請求最后用于創建訂閱。MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);List<MonitoredItemCreateRequest> requests = new ArrayList<>();requests.add(request);//創建監控項,并且注冊變量值改變時候的回調函數。UaSubscription.ItemCreationCallback onItemCreated =(subscriptionItem, id) -> subscriptionItem.setValueConsumer((UaMonitoredItem item1, DataValue value) -> {logger.info("===== > here is callbacks ... 訂閱的回調函數。 ");logger.info("訂閱成功, item1 : {}", item1.getReadValueId().getNodeId().toString());logger.info("subscription value received: item={}, value={}", item1.getReadValueId().getNodeId(), value.getValue());});List<UaMonitoredItem> monitoredItems = subscription.createMonitoredItems(TimestampsToReturn.Both,requests,onItemCreated).get();for (UaMonitoredItem monitoredItem : monitoredItems) {if (monitoredItem.getStatusCode().isGood()) {logger.info("item created for nodeId={}", monitoredItem.getReadValueId().getNodeId());} else {logger.warn("failed to create item for nodeId={} (status={})",monitoredItem.getReadValueId().getNodeId(), monitoredItem.getStatusCode());}}} catch (Exception e) {logger.error("訂閱變量失敗", e);}}/*** 瀏覽節點(抽取方法)*/private String browseNode(String indent, OpcUaClient client, NodeId browseRoot) {BrowseDescription browse = new BrowseDescription(browseRoot,BrowseDirection.Forward,Identifiers.References,true,uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),uint(BrowseResultMask.All.getValue()));try {BrowseResult browseResult = client.browse(browse).get();ReferenceDescription[] references = browseResult.getReferences();StringBuilder nodesInfo = new StringBuilder();for (ReferenceDescription rd : references) {logger.info("{} Node={}", indent, rd.getBrowseName().getName());nodesInfo.append(indent).append(" Node=").append(rd.getBrowseName().getName()).append("\n");// recursively browse to childrenrd.getNodeId().toNodeId(client.getNamespaceTable()).ifPresent(nodeId -> browseNode(indent + " ", client, nodeId));}return nodesInfo.toString();} catch (InterruptedException | ExecutionException e) {logger.error("Browsing nodeId={} failed: {}", browseRoot, e.getMessage(), e);return e.getMessage();}} }5.訪問接口(瀏覽節點并未調通)
package com.xckj.generalcontrol.controlsoft.controller;import com.alibaba.fastjson.JSONObject; import com.xckj.generalcontrol.controlsoft.client.OpcUaOperationSupport; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** @author lgy*/ @RestController @RequestMapping("/opcUa") public class OpcUaController {@Resourceprivate OpcUaOperationSupport opcUaOperationSupport;@PostMapping(value = "/")public String test() {return "hello ";}@PostMapping(value = "/query")public String readPlc(@RequestBody JSONObject paramJson) {int namespaceIndex = paramJson.getIntValue("namespaceIndex");int itemIndex = paramJson.getIntValue("itemIndex");return opcUaOperationSupport.readPlc(namespaceIndex, itemIndex);}@PostMapping(value = "/subscription")public void readSubscriptionPlc(@RequestBody JSONObject paramJson) {int namespaceIndex = paramJson.getIntValue("namespaceIndex");int itemIndex = paramJson.getIntValue("itemIndex");opcUaOperationSupport.createSubscription(namespaceIndex, itemIndex);}@PostMapping(value = "/edit")public String writePlc(@RequestBody JSONObject paramJson) {int namespaceIndex = paramJson.getIntValue("namespaceIndex");int itemIndex = paramJson.getIntValue("itemIndex");String valueType = paramJson.getString("valueType");String editValue = paramJson.getString("editValue");return opcUaOperationSupport.writePlc(namespaceIndex, itemIndex, valueType, editValue);}@PostMapping("/browseNode")public String browseNode() {return opcUaOperationSupport.browseNode();} }三、關于空間Index和節點Index
仔細觀看我實現的代碼與前人寫的與KepServer連接的代碼, 可以發現主要的區別在于需要傳入namespaceIndex和itemIndex兩個.
KepServer: NodeId nodeId = new NodeId(2, item);
S7-1500: NodeId nodeId = new NodeId(namespaceIndex, nameIndex);
用KepServer做Server端, namespaceIndex固定傳2, identifier傳入String類型的NodeId
用S7-1500做Server端, namespaceIndex和identifier需要根據expert顯示的傳入, identifier傳入int類型
那么這兩個值能夠在哪里獲取到呢?
通過PLC工程師常用的UaExpert這個軟件, 我們可以找到plc數據對應的namespaceIndex, itemIndex
kepServer:
S7-1500:
比如想獲取截圖里的plcValue, namespaceIndex應傳4, itemIndex應傳9
總結
以上是生活随笔為你收集整理的Java实现OPC UA Client直接与PLC通讯的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: APUE3e
- 下一篇: 关于在VC + + 2008 VCRed