JAVA使用OPC UA 方式与设备通信(milo)

本文档详细介绍了如何配置KEPware服务端以支持OPCUA匿名访问,并提供了基于Java的Milo库实现的客户端代码示例,包括连接、读写操作及订阅功能。同时,对比了OPCUA和Jeasyopc在并发性能测试中的表现,结果显示OPCUA在高并发下稳定性更好,适合生产环境使用。
摘要由CSDN通过智能技术生成


背景

基于生产过程控制系统(MES)与生产设备(PLC)通信的需求,PLC型号西门子S7-1500,设备数据采集使用KEPwareEX6.4 , 将kepware 作为服务端来开发一个Java服务,用于生产过程控制系统与设备数据交互,达到控制生产过程的目的。


一、配置kepware服务端

补充:KEPware6.4免费体验版

1.添加监控设备

注意:查看每个节点状态为良好,kepware服务端可正常读写PLC数据。
在这里插入图片描述

2. 配置远端访问路径:

注意:安全策略运行无证书模式。
在这里插入图片描述

3. 开启OPC UA 匿名访问模式

方便测试,简化配置流程。在这里插入图片描述

服务端配置完成!得到数据访问路径:
opc.tcp://10.106.11.161:49300 支持匿名访问。

二、编写JAVA客户端

1. 引入jar包

基于Milo实现

		<dependency>
			<groupId>org.eclipse.milo</groupId>
			<artifactId>sdk-client</artifactId>
			<version>0.6.0-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>org.eclipse.milo</groupId>
			<artifactId>dictionary-reader</artifactId>
			<version>0.6.0-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>org.eclipse.milo</groupId>
			<artifactId>sdk-server</artifactId>
			<version>0.6.0-SNAPSHOT</version>
		</dependency>

		<dependency>
			<groupId>org.eclipse.milo</groupId>
			<artifactId>dictionary-manager</artifactId>
			<version>0.6.0-SNAPSHOT</version>
		</dependency>

2. 创建证数工具类

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;

    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;
    }
    X509Certificate getClientCertificate() {
        return clientCertificate;
    }
    KeyPair getClientKeyPair() {
        return clientKeyPair;
    }
}

3. 创建客户端工具类

@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;
    @Autowired
    private OpcUaConfig opcUaConfig;
    @PostConstruct
    public 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、Basic256Sha256
            SecurityPolicy securityPolicy = SecurityPolicy.None;

            List<EndpointDescription> endpoints;
            try {
                endpoints = DiscoveryClient.getEndpoints(opcUaConfig.getEndpointUrl()).get();
            } catch (Throwable ex) {
                // 发现服务
                String discoveryUrl = opcUaConfig.getEndpointUrl();
                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()))
                    .filter(opcUaConfig.endpointFilter())
                    .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(uint(5000))
                    .build();
            opcUaClient = OpcUaClient.create(config);
        } catch (Exception e) {
            log.error("创建客户端失败" + e.getMessage());
        }
    }
}

4. 创建操作读写接口

注意:包含单个节点地址数据读写,订阅模式(推荐)。

@Component
public class OpcUaOperationSupport {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 浏览节点
     */
    public void browseNode() {
        OpcUaClient client = ClientGen.opcUaClient;
        try {
            client.connect().get();
            browseNode("", client, Identifiers.RootFolder);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    /**
     * 单个读取 PLC
     *
     * @param server
     * @return
     */
    public ResultBean readPLC(OpcModel server) {
        ResultBean resultBean = new ResultBean(false, "");
        String accessPath = server.getAccessPath();
        String itemName = server.getItemName();
        String item = accessPath + "." + itemName;
        OpcUaClient client = ClientGen.opcUaClient;
        try {
            client.connect().get();
            NodeId nodeId = new NodeId(2, item);
            CompletableFuture<DataValue> readValue = client.readValue(0.0, TimestampsToReturn.Both, nodeId);
            DataValue value = readValue.get();
            String plcValue = value.getValue().getValue().toString();
            String statusCode = value.getStatusCode().toString();
            if (value.getStatusCode() != null) {
                if (value.getStatusCode().isGood()) {
                    resultBean.setMsg("获取数据成功。");
                    resultBean.setResult(plcValue);
                    resultBean.setSuccess(true);
                } else {
                    resultBean.setMsg("获取数据失败。");
                    resultBean.setResult(statusCode);
                    resultBean.setSuccess(false);
                }
                logger.info("======== >  it means successfully read StatusCode = {}", statusCode);
            } else {
                resultBean.setMsg("获取数据出现异常。");
                resultBean.setResult("出错了。。。。");
                resultBean.setSuccess(false);
                logger.error("=====》  读数据异常,未获取到数据。。。");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return resultBean;
    }

    /**
     * 写入 PLC 
     * @param server
     * @param editValue
     * @return
     */
    public ResultBean writePLC(OpcModel server, String editValue) {
        ResultBean resultBean = new ResultBean(false, "");
        String accessPath = server.getAccessPath();
        String itemName = server.getItemName();
        String item = accessPath + "." + itemName;
        try {
//          创建连接
            OpcUaClient client = ClientGen.opcUaClient;
            client.connect().get();
//          创建节点
            NodeId nodeId = new NodeId(2, item);
//          创建Variant对象和DataValue对象
            Variant v;
            boolean b = true;
            switch (editValue) {
                case "1":
                    v = new Variant(b);
                    break;
                case "0":
                    b = false;
                    v = new Variant(b);
                    break;
                default:
                    v = new Variant(editValue);
                    break;
            }
            DataValue dv = new DataValue(v, null, null);
            StatusCode statusCode = client.writeValue(nodeId, dv).get();
            if (statusCode.isGood()) {
                resultBean.setMsg("写入数据成功。");
                resultBean.setSuccess(true);
                resultBean.setResult(editValue);
                logger.info("========== >  it means successfully Wrote '{}' to nodeId={}, statusCodes = {}", v, nodeId, statusCode.toString());
            } else {
                resultBean.setMsg("写入数据失败。item = " + item);
                resultBean.setSuccess(false);
                resultBean.setResult(statusCode.toString());
                logger.error("statusCodes:" + statusCode.toString());
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return resultBean;
    }

    /**
     * 订阅变量
     *
     * @param server
     * @return
     */
    public ResultBean createSubscription(OpcModel server) {
        ResultBean resultBean = new ResultBean(false, "");
        String accessPath = server.getAccessPath();
        String itemName = server.getItemName();
        String item = accessPath + "." + itemName;
        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(2, item);
            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 ... 订阅的回调函数。 ");
                                resultBean.setMsg("订阅成功, item1 :" + item1.getReadValueId().getNodeId().toString());
                                resultBean.setResult(value.getValue().toString());
                                resultBean.setSuccess(true);
                                logger.info("subscription value received: item={}, value={}", item1.getReadValueId().getNodeId(), value.getValue());
                            }
                    );
            List<UaMonitoredItem> monitoredItems = subscription.createMonitoredItems(
                    TimestampsToReturn.Both,
                    newArrayList(request),
                    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());
                }
            }
            return resultBean;
        } catch (Exception e) {
            logger.error("订阅变量失败");
            resultBean.setMsg(e.getMessage());
            resultBean.setResult("订阅变量进入异常。");
            resultBean.setSuccess(false);
        }
        return resultBean;
    }

    /**
     * 查看历史变量记录
     *
     * @param server
     * @param editValue
     * @return
     */
    public ResultBean historyRead(OpcModel server, String editValue) {
        return null;
    }
    
    /**
     * 浏览节点(抽取方法)
     *
     * @param indent
     * @param client
     * @param browseRoot
     */
    private void 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();

            List<ReferenceDescription> references = toList(browseResult.getReferences());

            for (ReferenceDescription rd : references) {
                logger.info("{} Node={}", indent, rd.getBrowseName().getName());

                // recursively browse to children
                rd.getNodeId().toNodeId(client.getNamespaceTable())
                        .ifPresent(nodeId -> browseNode(indent + "  ", client, nodeId));
            }
        } catch (InterruptedException | ExecutionException e) {
            logger.error("Browsing nodeId={} failed: {}", browseRoot, e.getMessage(), e);
        }
    }
}

5. 调用层

@RestController
@AllArgsConstructor
public class OpcUAController {

    @Autowired
    private OpcUaOperationSupport opcUaOperationSupport;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String test() {

        return "hello  ";
    }
    @RequestMapping(value = "/query")
    public ResultBean readPLC(OpcModel server, HttpServletResponse response) throws Exception {
        response = null;
        ResultBean resultBean = opcUaOperationSupport.readPLC(server);
        return resultBean;
    }

    @RequestMapping(value = "/subscription")
    public ResultBean readSubscriptionPLC(OpcModel server, HttpServletResponse response) throws Exception {
        response = null;
        ResultBean resultBean = opcUaOperationSupport.createSubscription(server);
        return resultBean;
    }

    @RequestMapping(value = "/edit")
    public ResultBean writePLC(OpcModel server, String editValue, HttpServletResponse response) throws Exception {
        response = null;
        ResultBean resultBean = opcUaOperationSupport.writePLC(server, editValue);
        return resultBean;
    }
}

订阅模式测试结果如下:
在这里插入图片描述

三、OPCUA 与Jeasy opc性能测试对比

1. OPC UA 方式

条件:20线程,5秒,重复30次,一边读(订阅模式)一边写
在这里插入图片描述订阅模式读:
在这里插入图片描述普通模式写:
在这里插入图片描述测试结果统计:1200个用例全部执行通过。
在这里插入图片描述随机查看写的接口返回情况:
在这里插入图片描述查看后台,运行正常,无报错:
在这里插入图片描述

注意:StatusCode{name=Bad_TypeMismatch, value=0x80740000, quality=bad} 控制台报错,是由于类型错误导致。以上用例仅兼容了Boolean 类型和String 类型,但是测试用例里面收入了Short 类型,所以会有部分类型错误,对类型进行转换即可。

2. Jeasy opc 方式

条件:20线程,5秒,重复30次,一边读一边写
在这里插入图片描述
并且把读和写分担在两个服务上面,端口8866:
在这里插入图片描述端口 8877 :
在这里插入图片描述测试结果统计:读和写均出现错误
在这里插入图片描述报错信息如下:在正常执行过一段时间后,两个服务都出现宕机重启的情况
在这里插入图片描述在这里插入图片描述


五、总结

综述所述,在追求效率的生产过程中,OPC UA 模式稳定性更强,承受并发效率更高,而Jeasy opc 在并发量高的情况下,直接挂机了,虽然采取补偿措施(读写分离+挂机自动重启),但是还是会影响正常使用,所以推荐使用OPC UA 方式。

补充:其实Jeasy opc 在重启后可以支撑一会儿并发,但是多执行几次就会出现宕机情况。而且并发越是频繁,宕机也越频繁,所以还是不推荐用于中大型工厂的过程控制系统。它更适用于对数据时效性不高的场景。

OPC UA源码参考:Milo

使用Java语言与OPC UA协议进行通信,你可以使用Eclipse Milo,它是一个开源的OPC UA实现,并提供了Java客户端和服务器库。 下面是一个使用Eclipse MiloJava代码示例,演示如何订阅设备的事件和告警: ```java // 创建OPC UA客户端 OpcUaClient client = OpcUaClient.create(endpoint); // 创建订阅管理器 UaSubscriptionManager subscriptionManager = client.getSubscriptionManager(); // 订阅事件 UaSubscription eventSubscription = subscriptionManager.createSubscription(1000.0).get(); NodeId eventNodeId = new NodeId(namespaceIndex, "EventNode"); UaMonitoredItem eventItem = subscriptionManager.createMonitoredItem( eventSubscription, new MonitoringParameters( uint(1), 1000.0, null, uint(10), true ), ReadValueId.from(eventNodeId, AttributeId.Value.uid(), null), (item, value) -> { // 处理事件 System.out.println("Received event: " + value.getValue()); } ).get(); // 订阅告警 UaSubscription alarmSubscription = subscriptionManager.createSubscription(1000.0).get(); NodeId alarmNodeId = new NodeId(namespaceIndex, "AlarmNode"); UaMonitoredItem alarmItem = subscriptionManager.createMonitoredItem( alarmSubscription, new MonitoringParameters( uint(2), 1000.0, null, uint(10), true ), ReadValueId.from(alarmNodeId, AttributeId.Value.uid(), null), (item, value) -> { // 处理告警 System.out.println("Received alarm: " + value.getValue()); } ).get(); // 启动订阅 eventSubscription.publishingEnabled(true); alarmSubscription.publishingEnabled(true); subscriptionManager.registerSubscription(eventSubscription); subscriptionManager.registerSubscription(alarmSubscription); ``` 在这个示例中,我们使用Eclipse Milo创建了一个OPC UA客户端,然后创建了一个订阅管理器。我们使用`createSubscription()`方法创建了两个订阅,分别用于订阅事件和告警。然后,我们使用`createMonitoredItem()`方法创建了两个监视项,用于监视事件和告警节点的值,并在它们的值发生变化时触发回调函数来处理它们。 最后,我们启用了订阅,并将它们注册到订阅管理器中,以便在后台处理OPC UA服务器发送的事件和告警。 请注意,这只是一个简单的示例,具体实现可能因OPC UA服务器的不同而有所不同。你需要根据实际情况调整代码并处理异常情况。
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值