java 通过OPC UA协议连接西门子1500设备实现数据读写

376 阅读5分钟

项目大概架构

用java通过opc ua协议对plc 1500的设备进行点位的读写操作,达到自动化的目的,以及数据的展示和存储

plc端设置参考下图

5dc0c9d35b923da6de014331b16d34e.png

然后需要plc开发人员把所有的点位信息映射DB块中去,不然外部是访问不了的

开启java端的奇妙旅程

导入依赖

<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>sdk-client</artifactId>
    <version>0.3.6</version>
</dependency>
<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>sdk-server</artifactId>
    <version>0.3.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.milo/stack-core -->
<dependency>
    <groupId>org.eclipse.milo</groupId>
    <artifactId>stack-core</artifactId>
    <version>0.3.6</version>
</dependency>

开始编写连接工具类

客户端启动类

@Component
public class ClientRunner {
    @Value(value = "${opcua.endpointUrl}")
    private String endpointUrl;

    // 用于异步处理 OpcUaClient 的创建或连接操作。
    private final CompletableFuture<OpcUaClient> future = new CompletableFuture<>();

    /**
     * @MethodName: run
     * @Description: 启动
     * @return
     * @throws Exception
     */
    public OpcUaClient run() throws Exception {

       OpcUaClient client = createClient();

       future.whenCompleteAsync((c, ex) -> {
          if (ex != null) {
             log.error("Error running example: {}", ex.getMessage(), ex);
          }

          try {
             c.disconnect().get();
             Stack.releaseSharedResources();
          } catch (InterruptedException | ExecutionException e) {
             log.error("Error disconnecting:", e.getMessage(), e);
          }
       });

       return client;

    }

    /**
     * @MethodName: createClient
     * @Description: 创建客户端
     * @return
     * @throws Exception
     */
    private OpcUaClient createClient() throws Exception {
//     // 创建存储证书的安全路径,匿名连接的话,不需要证书
//     Path securityTempDir = Paths.get(properties.getCertPath(), "security");
//     Files.createDirectories(securityTempDir);
//     if (!Files.exists(securityTempDir)) {
//        log.error("unable to create security dir: " + securityTempDir);
//        return null;
//     }

       // 搜索OPC节点
       List<EndpointDescription> endpoints = null;
       try {
          endpoints = DiscoveryClient.getEndpoints(endpointUrl).get();
       } catch (Throwable e) {
          String discoveryUrl = endpointUrl;

          if (!discoveryUrl.endsWith("/")) {
             discoveryUrl += "/";
          }
          discoveryUrl += "discovery";

          log.info("Trying explicit discovery URL: {}", discoveryUrl);
          endpoints = DiscoveryClient.getEndpoints(discoveryUrl).get();
       }

       //过滤掉不需要的安全策略,选择一个自己需要的安全策略
       EndpointDescription endpoint = endpoints.stream()
             .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())).filter(endpointFilter())
             .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned"));

       OpcUaClientConfig config = OpcUaClientConfig.builder()
             .setApplicationName(LocalizedText.english("eclipse milo opc-ua client"))
             .setApplicationUri("urn:eclipse:milo:examples:client")
             .setEndpoint(endpoint)
             //.setIdentityProvider(new UsernameProvider("Administrator", "123456"))//用户名密码登录
             .setIdentityProvider(new AnonymousProvider())//匿名登录
             .setRequestTimeout(UInteger.valueOf(5000))//设置超时时间
             //.setKeepAliveInterval(UInteger.valueOf(10)) // 设置 OPC UA 客户端的保活间隔为 10 秒,表示客户端每隔 10 秒向服务器发送一次心跳消息,以保持连接活跃
             .build();

       return OpcUaClient.create(config);
    }

    /**
     * @MethodName: endpointFilter
     * @Description: endpointFilter
     * @return
     */
    private Predicate<EndpointDescription> endpointFilter() {
       return e -> true;
    }

    /**
     * @return the future
     */
    public CompletableFuture<OpcUaClient> getFuture() {
       return future;
    }

}

客户端操作类

public class ClientHandler {

    // 客户端实例
    private volatile OpcUaClient client = null;

    @Autowired
    private ClientRunner clientRunner;
    @Autowired
    private OldPortPointLocationMapper pointLocationMapper;

    // 心跳点位
    private static final String heartbeatNodeIdentifier = ""test"."test"";
    private static final Integer heartbeatNodeIndex = 3;

    /**
     * 链接
     * @return
     * @throws Exception
     * 项目启动时调用,启动链接
     */
    @PostConstruct
    public synchronized String connect() throws Exception {
       if (client != null) {
          log.info("客户端已创建");
       }
       try {
          client = clientRunner.run();
          client.connect().get(10,  TimeUnit.SECONDS);
          // 开始订阅
          this.subscribe(subscribeList());
       }catch (TimeoutException e){
          safeDisconnect(client);
          log.error("opc ua超时,检查网络状态或plc状态。。。",e);
       }
       catch (Exception e) {
          log.error("链接失败",e);
          safeDisconnect(client);
       }
       return "连接成功";
    }

    /**
     * 订阅列表
     * @return
     */
    private List<NodeEntity> subscribeList(){
       List<Integer> addressList = Arrays.stream(LightEnum.values())
             .map(LightEnum::getAddress)
             .collect(Collectors.toList());
       //根据id拆查询点位地址
       List<OldPortPointLocationDo> pointLocationDos = pointLocationMapper.selectBatchIds(addressList);
       List<NodeEntity> list = new ArrayList<>();
       for(OldPortPointLocationDo d: pointLocationDos){
          list.add(new NodeEntity(heartbeatNodeIndex, d.getPracticalAddress(), null, null,d.getId()));
       }
       return list;
    }


    /**
     * @MethodName: disconnect
     * @Description: 监听上下文关闭时间,断开链接
     * @return
     * @throws Exception
     */
    @PreDestroy
    public String disconnect() throws Exception {
       if (client == null) {
          return "连接已断开";
       }
       // 断开连接
       clientRunner.getFuture().complete(client);
       client = null;
       return "断开连接成功";
    }

    /**
     * 心跳检测
     */
    @Scheduled(fixedRateString = "60000")
    public void checkHeartbeat() throws Exception {
       if (client == null) {
          log.warn("客户端未连接,尝试重新连接...");
          try {
             connect();
          } catch (Exception e) {
             log.error("心跳检测重连失败", e);
          }
       }
       try {
          NodeEntity node = new NodeEntity();
          node.setIndex(heartbeatNodeIndex);
          node.setIdentifier(heartbeatNodeIdentifier);

          Map<String, Object> read = read(node);
          read.forEach((k,v)->{
             log.info("[心跳检测] 节点 {}({}) 当前值: {}", k, heartbeatNodeIndex, v);
          });
       } catch (Exception e) {
          log.error("[心跳异常] 检测失败: {}", e.getMessage());
          if (client != null) {
             try {
                client.disconnect().get(3,  TimeUnit.SECONDS); // 同步等待断开完成
                log.info("已主动断开异常连接");
             } catch (Exception ex) {
                log.error("断开连接失败", ex);
             } finally {
                client = null; // 确保置空
             }
          }
       }
    }

    /**
     * @MethodName: subscribe
     * @Description: 订阅节点变量
     * @throws Exception
     */
    public String subscribe(List<NodeEntity> nodes) throws Exception {
        log.info("开始订阅");
        if (client == null) {
           this.connect();
        }
        // 查询订阅对象,没有则创建
        UaSubscription subscription = null;
        try  {
           ImmutableList<UaSubscription> subscriptionList = client.getSubscriptionManager().getSubscriptions();
           if (CollectionUtils.isEmpty(subscriptionList)) {
              subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
           } else {
              subscription = subscriptionList.get(0);
           }
           // 监控项请求列表
           List<MonitoredItemCreateRequest> requests = new ArrayList<>();
           if (!CollectionUtils.isEmpty(nodes)) {
              for (NodeEntity node : nodes) {
                 // 创建监控的参数
                 MonitoringParameters parameters = new MonitoringParameters(subscription.nextClientHandle(), 1000.0, // 时间间隔
                       null, // 过滤器中若值为null,则使用默认值
                       Unsigned.uint(100), // 队列大小
                       true // 丢弃最旧的
                 );
                 // 创建订阅的变量, 创建监控项请求
                 MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
                       new ReadValueId(new NodeId(node.getIndex(), node.getIdentifier()), AttributeId.Value.uid(),
                             null, null),
                       MonitoringMode.Reporting, parameters);
                 requests.add(request);
              }
           }
           // 创建监控项,并且注册变量值改变时候的回调函数
           subscription.createMonitoredItems(TimestampsToReturn.Both, requests, (item, id) -> {
              item.setValueConsumer((i, v) -> {
                 //todo 通过websocket发送到前端
                 log.info("订阅到的值是:item={}, value={}", i.getReadValueId().getNodeId(), v.getValue());
                 //订阅信息里面会返回点位的地址,这个地址在数据库里面也存储的一份,所以这里直接调用查询数据库的方法
                 OldPortPointLocationDo byPracticalAddress = this.getByPracticalAddress(i.getReadValueId().getNodeId().getIdentifier().toString());
                 LightEnum lightEnum = LightEnum.of(byPracticalAddress.getId());
                 PointVo vo = new PointVo(lightEnum.getMessage(),lightEnum.getValue(),v.getValue().getValue());
                 WebSocketServer.sendMessage(vo);
              });
           }).get(10, TimeUnit.SECONDS);
        }catch (TimeoutException e){
           log.error("订阅超时,检查网络状态或plc状态。。。",e);
        } catch (Exception e) {
           log.error("订阅失败",e);
        }
        return "订阅成功";
    }

    /**
     * @MethodName: write
     * @Description: 变节点量写入
     * @param node
     * @throws Exception
     */
    public String write(NodeEntity node) throws Exception {
       if (client == null) {
          this.connect();
       }
       if(node.getValue() == null){
          throw new ParamerException("写入参数不能为空");
       }
       Variant value = null;
       StatusCode statusCode = null;
       NodeId nodeId = new NodeId(node.getIndex(), node.getIdentifier());
       try {
          switch (node.getType()) {
             case "int":
                value = new Variant(Integer.parseInt(node.getValue().toString()));
                break;
             case "boolean":
                value = new Variant(Boolean.parseBoolean(node.getValue().toString()));
                break;
             case "float":
                value = new Variant(Float.valueOf(node.getValue().toString()));
                break;
             case "string":
             default:
                value = new Variant(node.getValue().toString());
                break;
          }
          DataValue dataValue = new DataValue(value, null, null);
          statusCode = client.writeValue(nodeId, dataValue).get(10, TimeUnit.SECONDS);
       }catch (TimeoutException e){
          this.safeDisconnect(client);
          throw new OpcUaException("opc ua链接超时");
       } catch (Exception e) {
          throw new OpcUaException("节点【" + node.getIdentifier() + "】写入失败");
       }
       return "节点【" + node.getIdentifier() + "】写入状态:" + statusCode.isGood();
    }

    /**
     * @MethodName: read
     * @Description: 读取
     * @param node
     * @return
     * @throws Exception
     */
    public Map<String,Object> read(NodeEntity node) throws Exception {
       if (client == null) {
          this.connect();
       }
       Variant variant = null;
       Map<String,Object> result = new HashMap<>();
       try{
          // 如果需要读取多个节点的值的话就需要循环的去读
          NodeId nodeId = new NodeId(node.getIndex(), node.getIdentifier());
          VariableNode vnode = client.getAddressSpace().createVariableNode(nodeId);
          DataValue value = vnode.readValue().get(10, TimeUnit.SECONDS);
          variant = value.getValue();
          if (variant == null || variant.getValue() == null) {
             throw new OpcUaException("节点【" + node.getIdentifier() + "】没有值");
          }
          result.put(node.getIdentifier(),  variant.getValue());
       } catch (TimeoutException e){
          this.safeDisconnect(client);//关闭连接
          throw new OpcUaException("opc ua链接超时");
       } catch (Exception e) {
          throw new OpcUaException("节点【" + node.getIdentifier() + "】读取失败");
       }
       return result;
    }

    /**
     * 关闭opc ua连接
     * @param client
     */
    private void safeDisconnect(OpcUaClient client) {
       if (client != null) {
          try {
             client.disconnect().get(3, TimeUnit.SECONDS);
          } catch (Exception e) {
             log.warn("关闭连接失败", e);
          }finally {
             this.client = null;
          }
       }
    }
}

至此设备点位的读写和订阅操作已经全部完成,此时让我们去看看如何进行方法的调用把!

控制层

@Autowired
private OperateService operateService;

@Operation(summary="点位写入")
@PostMapping("/write")
public ResponseResult write(@Valid @RequestBody NodeEntity node) throws Exception {
    String write = operateService.write(node);
    return new ResponseResult<>(EnumResultType.SUCCESS.toString(), ApplicationConstant.WRITE_SUCCESS,write);
}

@Operation(summary="点位读取")
@PostMapping("/read")
public ResponseResult read(@Valid @RequestBody NodeEntity node) throws Exception {
    Map<String, Object> read = operateService.read(node);
    return new ResponseResult<>(EnumResultType.SUCCESS.toString(), ApplicationConstant.READ_SUCCESS,read);
}

业务层

public String write(NodeEntity node) throws Exception {
    return clientHandler.write(node);
}

public Map<String,Object> read(NodeEntity node) throws Exception {
    return clientHandler.read(node);
}

补充一下部分实体类代码

public class NodeEntity {
    @NonNull
    private Integer index;    //索引空间

    @NonNull
    private String identifier; //设备点位地址

    private Object value;     //写入值

    private String type;      //写入类型

    private Integer dbid;     //数据库id

}
plc中的点位信息存储在数据库中,这里我用的是枚举类进行映射的
public enum LightEnum {

    OPEN("打开按钮","open",1),
    CLOSE("关闭按钮","close",2)
  

    

    private final String message;
    private final String value;
    private final  Integer address;//存储是点位实际读写的地址的id(数据库点位id)

    LightEnum(String message, String value,Integer address) {
        this.message = message;
        this.value = value;
        this.address = address;
    }
    public static LightEnum of(Integer value){
        if (value == null){
            return null;
        }
        for (LightEnum stateType : values()) {
            if (stateType.getAddress().equals(value)){
                return stateType;
            }
        }
        return null;
    }
}

以上就是我总结的全部内容了