项目大概架构
用java通过opc ua协议对plc 1500的设备进行点位的读写操作,达到自动化的目的,以及数据的展示和存储
plc端设置参考下图
然后需要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;
}
}
以上就是我总结的全部内容了