介绍
在现代制造业中,PLC(可编程逻辑控制器)被广泛应用于控制工厂设备和流程。而OPC UA(OLE for Process Control Unified Architecture)则成为了工业自动化领域中的通信协议标准。本教程将教你如何使用Java编写一个OPC UA Client来实现直接与PLC通讯。
准备工作
在开始编写代码之前,我们需要准备以下工作:
- 安装Java开发环境(JDK)
- 从OPC Foundation官网下载并安装OPC UA库(org.eclipse.milo)
步骤
步骤1:创建一个Java项目
首先,在你的开发环境中创建一个新的Java项目。
步骤2:添加OPC UA库依赖
普通方式
将下载的OPC UA库(org.eclipse.milo)导入到你的Java项目中。具体的导入方式根据你使用的开发环境而定。
maven方式
<dependency><groupId>org.eclipse.milo</groupId><artifactId>sdk-client</artifactId><version>0.6.9</version></dependency>
步骤3:代码教程
在你的Java项目中创建一个新的类,命名为OpcUaUtil。这个类将包含与PLC通讯的所有代码方法。
创建客户端
传入opc地址,和用户名、密码,创建一个opc ua 客户端,代码实现如下:
/*** 方法描述: 创建客户端** @param endPointUrl* @param username* @param password* @return {@link OpcUaClient}* @throws*/public static OpcUaClient createClient(String endPointUrl,String username,String password){log.info(endPointUrl);try {//获取安全策略List<EndpointDescription> endpointDescription = DiscoveryClient.getEndpoints(endPointUrl).get();//过滤出一个自己需要的安全策略EndpointDescription endpoint = endpointDescription.stream().filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())).findFirst().orElse(null);IdentityProvider identityProvider=new AnonymousProvider();if(!StringUtils.isEmpty(username)||!StringUtils.isEmpty(password)){identityProvider=new UsernameProvider(username,password);}// 设置配置信息OpcUaClientConfig config = OpcUaClientConfig.builder()// opc ua 自定义的名称.setApplicationName(LocalizedText.english("plc"))// 地址.setApplicationUri(endPointUrl)// 安全策略等配置.setEndpoint(endpoint).setIdentityProvider(identityProvider)//等待时间.setRequestTimeout(UInteger.valueOf(5000)).build();// 准备连接OpcUaClient opcClient =OpcUaClient.create(config);//开启连接opcClient.connect().get();log.info("连接成功。。。success");return opcClient;} catch (Exception e) {e.printStackTrace();log.error("======== opc connection fail ========");}return null;}
遍历节点
/*** 方法描述: 查询某个节点下的所有节点** @param identifier 节点名* @param client opc ua客户端* @return* @throws*/public static void browse(String identifier, OpcUaClient client) throws Exception {NodeId nodeId = Identifiers.ObjectsFolder;if(!StringUtils.isEmpty(identifier)){nodeId = new NodeId(2, identifier);}BrowseDescription browse = new BrowseDescription(nodeId,BrowseDirection.Forward,Identifiers.References,true,UInteger.valueOf(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),UInteger.valueOf(BrowseResultMask.All.getValue()));BrowseResult browseResult = client.browse(browse).get();ReferenceDescription[] references = browseResult.getReferences();for (ReferenceDescription reference : references) {System.out.println(reference.getNodeId().getIdentifier().toString()+" "+reference.getNodeClass().getValue());if(reference.getNodeClass().getValue()==NodeClass.Object.getValue()){browse(reference.getNodeId().getIdentifier().toString(),client);}}}
注意:这里用BrowseDescription对象过滤了节点类型为Object和Variable的节点,其他类型的不予查询!
读取单个节点的值
public static Object readValue(String identifier, OpcUaClient client){log.info("点位数据:"+ identifier);NodeId nodeId = new NodeId(2, identifier);DataValue value = null;try {value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}if(Objects.nonNull(value)&&Objects.nonNull(value.getValue())&&Objects.nonNull(value.getValue().getValue())) {return value.getValue().getValue();}return null;}
读取多个节点的值
/*** 方法描述: 读取多个点位的值** @param keys 点位集合* @param client 客户端* @return {@link List< DataValue>}* @throws*/public List<DataValue> readValues(Set<String> keys, OpcUaClient client){List<NodeId> nodeIdList=new ArrayList<>(500);keys.forEach(e->{NodeId nodeId = new NodeId(2, e);nodeIdList.add(nodeId);});try {List<DataValue> dataValues=client.readValues(0.0, TimestampsToReturn.Both,nodeIdList).get();return dataValues;} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}return null;};}
注意:返回的数据值集合的顺序和传入的点位集合的顺序是一致的,我们可能根据传入点位的的顺序,匹配对应的值
写入单个节点的值
/*** 写入值** @param identifier* @param value* @throws Exception*/public static void writeValue(String identifier, Object value) throws Exception {//创建变量节点NodeId nodeId = new NodeId(2, identifier);//创建Variant对象和DataValue对象Variant v = new Variant(value);DataValue dataValue = new DataValue(v, null, null);StatusCode statusCode = OPC_UA_CLIENT.writeValue(nodeId, dataValue).get();if(statusCode.isGood()){log.info("数据写入成功");}else {log.error("数据写入失败");}}
写入多个节点的值
/*** 方法描述: 写入多个节点的值** @param keys 节点集合* @param values 值集合* @param client 客户端* @return {@link Object}* @throws*/public Object writeValues(Set<String> keys,List<Object> values,OpcUaClient client){List<NodeId> nodeIs=new ArrayList<>(keys.size());keys.forEach(e->{NodeId nodeId = new NodeId(2, e);nodeIs.add(nodeId);});List<DataValue> dataValues=new ArrayList<>(values.size());values.forEach(e->{Variant value=new Variant(Double.parseDouble(e.toString()));DataValue dataValue=new DataValue(value);dataValues.add(dataValue);});try {client.writeValues(nodeIs,dataValues).get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}return null;}
注意:传入节点集合的顺序要和传入值的集合的顺序一一对应,才能存入和点位匹配的值。
断开连接
/*** 方法描述: 断开连接** @param client* @return* @throws*/public void disconnect(OpcUaClient client){try {client.disconnect().get();} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
完整代码:
import lombok.extern.slf4j.Slf4j;
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.sdk.client.api.identity.IdentityProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
import org.springframework.util.StringUtils;import java.util.*;
import java.util.concurrent.ExecutionException;/*** @author TARZAN*/
@Slf4j
public class OpcUaUtil {/*** 方法描述: 创建客户端** @param endPointUrl* @param username* @param password* @return {@link OpcUaClient}* @throws*/public static OpcUaClient createClient(String endPointUrl,String username,String password){log.info(endPointUrl);try {//获取安全策略List<EndpointDescription> endpointDescription = DiscoveryClient.getEndpoints(endPointUrl).get();//过滤出一个自己需要的安全策略EndpointDescription endpoint = endpointDescription.stream().filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())).findFirst().orElse(null);IdentityProvider identityProvider=new AnonymousProvider();if(!StringUtils.isEmpty(username)||!StringUtils.isEmpty(password)){identityProvider=new UsernameProvider(username,password);}// 设置配置信息OpcUaClientConfig config = OpcUaClientConfig.builder()// opc ua 自定义的名称.setApplicationName(LocalizedText.english("plc"))// 地址.setApplicationUri(endPointUrl)// 安全策略等配置.setEndpoint(endpoint).setIdentityProvider(identityProvider)//等待时间.setRequestTimeout(UInteger.valueOf(5000)).build();// 准备连接OpcUaClient opcClient =OpcUaClient.create(config);//开启连接opcClient.connect().get();log.info("连接成功。。。success");return opcClient;} catch (Exception e) {e.printStackTrace();log.error("======== opc connection fail ========");}return null;}/*** 方法描述: 查询所有节点** @param client* @return {@link Set<String>}* @throws* @author liwenbin* @date 2023年07月03日 13:05:55*/public static Set<String> getAllKeys(OpcUaClient client) throws Exception {return browse(null,client);}/*** 方法描述: 查询某个节点下的所有节点** @param identifier* @param client* @return* @throws*/public static Set<String> browse(String identifier, OpcUaClient client) throws Exception {Set<String> keys=new HashSet<>(500);browse(identifier,keys,client);return keys;}/*** 方法描述: 查询某个节点下的所有节点** @param identifier* @param client* @return* @throws*/private static Set<String> browse(String identifier, Set<String> keys, OpcUaClient client) throws Exception {NodeId nodeId = Identifiers.ObjectsFolder;if(!StringUtils.isEmpty(identifier)){nodeId = new NodeId(2, identifier);}BrowseDescription browse = new BrowseDescription(nodeId,BrowseDirection.Forward,Identifiers.References,true,UInteger.valueOf(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),UInteger.valueOf(BrowseResultMask.All.getValue()));BrowseResult browseResult = client.browse(browse).get();ReferenceDescription[] references = browseResult.getReferences();for (ReferenceDescription reference : references) {System.out.println(reference.getNodeId().getIdentifier().toString()+" "+reference.getNodeClass().getValue());keys.add(identifier);if(reference.getNodeClass().getValue()==NodeClass.Object.getValue()){browse(reference.getNodeId().getIdentifier().toString(),keys,client);}}return keys;}/*** 方法描述: 读取单个节点值** @param identifier* @param client* @return {@link Object}* @throws*/public static Object readValue(String identifier, OpcUaClient client){NodeId nodeId = new NodeId(2, identifier);DataValue value = null;try {client.readValue(0.0, TimestampsToReturn.Both,nodeId);value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}if(Objects.nonNull(value)&&Objects.nonNull(value.getValue())&&Objects.nonNull(value.getValue().getValue())) {return value.getValue().getValue();}return null;}/*** 方法描述: 读取多个点位的值** @param keys 点位集合* @param client 客户端* @return {@link List<DataValue>}* @throws*/public static List<DataValue> readValues(Set<String> keys, OpcUaClient client){List<NodeId> nodeIdList=new ArrayList<>(500);keys.forEach(e->{NodeId nodeId = new NodeId(2, e);nodeIdList.add(nodeId);});try {List<DataValue> dataValues=client.readValues(0.0, TimestampsToReturn.Both,nodeIdList).get();return dataValues;} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}return null;}/*** 写入值** @param identifier* @param value* @throws Exception*/public static void writeValue(String identifier, Object value,OpcUaClient client) throws Exception {//创建变量节点NodeId nodeId = new NodeId(2, identifier);//创建Variant对象和DataValue对象Variant v = new Variant(value);DataValue dataValue = new DataValue(v, null, null);StatusCode statusCode = client.writeValue(nodeId, dataValue).get();if(statusCode.isGood()){log.info("数据写入成功");}else {log.error("数据写入失败");}}/*** 方法描述: 写入多个节点的值** @param keys 节点集合* @param values 值集合* @param client 客户端* @return {@link Object}* @throws*/public static Object writeValues(Set<String> keys,List<Object> values,OpcUaClient client){List<NodeId> nodeIs=new ArrayList<>(keys.size());keys.forEach(e->{NodeId nodeId = new NodeId(2, e);nodeIs.add(nodeId);});List<DataValue> dataValues=new ArrayList<>(values.size());values.forEach(e->{Variant value=new Variant(Double.parseDouble(e.toString()));DataValue dataValue=new DataValue(value);dataValues.add(dataValue);});try {client.writeValues(nodeIs,dataValues).get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}return null;}/*** 方法描述: 断开连接** @param client* @return*/public static void disconnect(OpcUaClient client){try {client.disconnect().get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}}
步骤4:使用OPCUAClient类
现在,在你的Java项目中创建一个OpcUaClientTest 类,并使用OPCUAClient类来实现与PLC的通讯。
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;/*** @author tarzan*/
public class OpcUaClientTest {public static void main(String[] args) throws Exception {String endPointUrl="opc.tcp://localhost:12686";OpcUaClient client=OpcUaUtil.createClient(endPointUrl,null,null);OpcUaUtil.browse(null,client);OpcUaUtil.disconnect(client);Thread.sleep(Integer.MAX_VALUE);}
}
运行测试,控制台输出结果
结论
通过本教程,你已经学会了使用Java编写一个OPC UA Client来实现直接与PLC通讯。你可以根据自己的需求对OPCUAClient类进行扩展和改进。希望本教程对你有所帮助!
注意:以上只是一个简单的示例,实际使用中可能还需要处理异常,添加更多的读写功能等。此外,需要根据具体的PLC类型和配置,设置正确的连接参数和节点ID。