OPC UA
概述
OPC UA(OPC Unified Architecture)是一种工业自动化领域的通信协议和标准,定义了一套统一的信息模型,用于描述设备的各种信息,如设备的属性、功能、数据等。这使得不同设备之间能够以标准化的方式表示和交换信息,避免了因信息表示不一致而导致的互操作性问题。以智能工厂中的机器人为例,无论机器人来自哪个厂家,通过 OPC UA 的信息模型,都可以将其运动状态、任务执行情况等信息以统一的格式提供给其他设备或系统。
特点
- 统一通信标准:OPC UA 提供了一种统一的通信标准,能够将这些异构设备连接在一起,实现设备之间的无缝通信和互操作性。
- 信息模型标准化:OPC UA 定义了一套统一的信息模型,用于描述设备的各种信息。
- 高效数据采集:OPC UA 能够与各种工业设备、传感器等进行通信,高效地采集它们产生的数据。它支持多种数据采集方式,包括周期性采集、事件触发采集等,可以根据实际需求灵活选择。
- 可靠数据传输:OPC UA 采用了可靠的通信协议,能够确保数据在传输过程中的准确性和完整性。它支持数据的加密和压缩,提高了数据传输的安全性和效率。
Eclipse Milo
Milo 是 OPC UA(目前针对 1.03 版本)的开源实现。它包含了一个高性能的栈(通道、序列化、数据结构、安全)以及基于该栈构建的客户端和服务器软件开发工具包(SDK)。
SpringBoot集成Eclipse Milo
版本
java.version 17
SpringBoot.version 3.4.1
org.eclipse.milo.version 0.6.14
添加依赖
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>${org.eclipse.milo.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-client</artifactId>
<version>${org.eclipse.milo.version}</version>
</dependency>
初始化OpcUA并启动
从数据库中查出opc信息,并构建需要订阅的NodeId。
@Service
@Slf4j
public class GlobalBoostrapService implements ApplicationRunner{
@Resource
private SyncOpcInfoService syncOpcInfoService;
@Resource
private SyncOpcNodeService syncOpcNodeService;
@Override
public void run(ApplicationArguments args) {
try{
List<SyncOpcInfo> list = syncOpcInfoService.list();
List<SyncOpcNode> nodes = syncOpcNodeService.list();
if(list.isEmpty()) return;
OpcUaCache.nodeMap = nodes.stream().collect(Collectors.toMap(SyncOpcNode::getId, node -> node));
OpcUaCache.urls = list.stream().collect(Collectors.toMap(SyncOpcInfo::getId, SyncOpcInfo::getUrl));
list.forEach(info -> CompletableFuture.runAsync(()->{
OpcUaClient client = OpcUaClientUtils.getClient(info.getUrl());
if(client == null) return;
List<SyncOpcNode> nodeList = nodes.stream().filter(node -> node.getOpcId().equals(info.getId())).toList();
if(nodeList.isEmpty()) return;
Map<String,SyncOpcNode> map = new HashMap<>();
nodeList.forEach(node -> {
String key = StringUtils.isBlank(node.getIdentifier())?String.valueOf(node.getRealAddress()):node.getIdentifier();
map.put(key,node);
});
OpcUaCache.nodes.put(info.getUrl(),map);
List<NodeId> listNodeId = new ArrayList<>();
nodeList.forEach(node -> {
String identifier = node.getIdentifier();
if(!StringUtils.isBlank(identifier)){
if ("Integer".equalsIgnoreCase(node.getNodeType())) {
listNodeId.add(new NodeId(node.getNameSpace(), Integer.parseInt(identifier)));
}else{
listNodeId.add(new NodeId(node.getNameSpace(), identifier));
}
}else {
listNodeId.add(new NodeId(node.getNameSpace(), node.getRealAddress()));
}
});
OpcUaClientUtils.createSubscription(info.getUrl(), listNodeId, 50);
}));
}catch (Exception e){
log.error("GlobalBoostrapService.run error: {}", e.getMessage(), e);
}
}
}
服务关闭时需断开OPC连接
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(WmsSyncApplication.class, args);
}
@PreDestroy
public void destroy() {
for (Map.Entry<String, OpcUaClient> entry: OpcUaCache.clients.entrySet()) {
String url = entry.getKey();
OpcUaClientUtils.closeClient(url);
}
}
}
OpcUA启动相关类
Client
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import java.util.function.Predicate;
public class Client {
private String url;
public Client() {
}
public Client(String url) {
this.url = url;
}
public String getEndpointUrl() {
return url;
}
public Predicate<EndpointDescription> endpointFilter() {
return e -> true;
}
}
启动类:ClientRunner
此类实现了连接失败自动重连的机制,以及事务完成后关闭连接,释放资源。
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import java.security.Security;
import java.util.concurrent.*;
@Slf4j
public class ClientRunner {
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
static {
Security.addProvider(new BouncyCastleProvider());
}
public Client client;
public ClientRunner(Client client) {
this.client = client;
}
public OpcUaClient run() {
CompletableFuture<OpcUaClient> future = new CompletableFuture<>();//首先new一个任务
OpcUaClient opcUaClient;
try {
String url = client.getEndpointUrl();
opcUaClient = OpcUaClient.create(url);//创建客户端
opcUaClient.connect().get();
OpcUaCache.clients.put(url, opcUaClient);
log.info("{} is connect success", url);
//任务完成后的处理
future.whenCompleteAsync((c, ex) -> {
if (ex != null) {
log.error("Error running example: {}", ex.getMessage(), ex);
}
try {
OpcUaCache.clients.remove(url);
log.info("{} is close success", url);
opcUaClient.disconnect().get();
} catch (InterruptedException | ExecutionException e) {
log.error("Error disconnecting: {}", e.getMessage(), e);
}
});
return opcUaClient;
} catch (Throwable t) {
log.error("Error getting client: {}{}", t.getMessage(), client.getEndpointUrl());
executor.schedule(() -> {
this.run();
},5000, TimeUnit.MILLISECONDS);
}
return null;
}
}
缓存相关:OpcUaCache
import com.sync.entity.SyncOpcNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class OpcUaCache {
/**
* 服务器列表
*/
public static Map<String, String> urls = new ConcurrentHashMap<>();
/**
* 订阅的节点列表
*/
public static Map<String, Map<String,SyncOpcNode>> nodes = new ConcurrentHashMap<>();
public static Map<String, SyncOpcNode> nodeMap = new ConcurrentHashMap<>();
/**
* 连接的客户端列表
*/
public static Map<String, OpcUaClient> clients = new ConcurrentHashMap<>();
/**
* opc缓存数据:key为opc地址,value为订阅的数据
*/
public static Map<String,Object> data = new ConcurrentHashMap<>();
public static OpcUaClient getOpcUaClientByNode(String node){
if(nodeMap.containsKey(node)){
SyncOpcNode syncOpcNode = nodeMap.get(node);
String opcId = syncOpcNode.getOpcId();
if(urls.containsKey(opcId)){
String url = urls.get(opcId);
if(clients.containsKey(url)){
return clients.get(url);
}
}
throw new RuntimeException("PLC connect error,opc url is null");
}
throw new RuntimeException("node is invalid,node:"+node);
}
public static NodeId getNodeIdByNode(String node){
if(nodeMap.containsKey(node)){
SyncOpcNode syncOpcNode = nodeMap.get(node);
String identifier = syncOpcNode.getIdentifier();
if(!StringUtils.isBlank(identifier)){
if ("Integer".equalsIgnoreCase(syncOpcNode.getNodeType())) {
return new NodeId(syncOpcNode.getNameSpace(), Integer.parseInt(identifier));
}else{
return new NodeId(syncOpcNode.getNameSpace(), identifier);
}
}else {
return new NodeId(syncOpcNode.getNameSpace(), syncOpcNode.getRealAddress());
}
}
throw new RuntimeException("未找到节点");
}
}
工具类:OpcUaClientUtils
在此类中提供了获取连接的客户端、连接客户端、关闭客户端、获取单个节点值、写变量、创建订阅等方法。
import com.sync.entity.SyncOpcNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
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.Stack;
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.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import java.util.*;
import java.util.concurrent.ExecutionException;
@Slf4j
public class OpcUaClientUtils {
/**
* 获取连接的客户端
* @param url opc url
* @return OpcUaClient
*/
public static OpcUaClient getClient(String url) {
if(OpcUaCache.clients.containsKey(url)) {
return OpcUaCache.clients.get(url);
}
return connect(url);
}
/**
* 连接客户端
* @param url opc url
* @return 200 连接成功 201 存在连接 500 连接失败
*/
private static OpcUaClient connect(String url) {
Client client = new Client(url);
return new ClientRunner(client).run();
}
/**
* 关闭客户端
* @param url url
*/
public static void closeClient(String url) {
if(OpcUaCache.clients.containsKey(url)) {
OpcUaClient opcUaClient = OpcUaCache.clients.get(url);
try {
opcUaClient.disconnect().get();
OpcUaCache.data.remove(url);
log.info("{} is close success", url);
} catch (Exception e) {
log.error("Error running closeOpcUaClient: {}", e.getMessage(), e);
}
}
}
/**
* 获取单个节点值
* @param client client
* @param nodeId nodeId
*/
public static Object readValue(OpcUaClient client,NodeId nodeId){
// 第一个参数如果设置为0的话会获取最新的值,如果maxAge设置到Int32的最大值,则尝试从缓存中读取值。
// 第二个参数为请求返回的时间戳,第三个参数为要读取的NodeId对象。
DataValue value;
try {
value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
return value.getValue().getValue();
} catch (InterruptedException | ExecutionException e) {
log.error("Error reading value: {}", e.getMessage(), e);
}
return null;
}
public static boolean writeValue(OpcUaClient client, NodeId nodeId, Object value) {
try{
Variant v = new Variant(value);
DataValue dataValue = new DataValue(v,null,null);
StatusCode statusCode = client.writeValue(nodeId, dataValue).get();
if(statusCode.getValue()!=0){
log.error("writeValue:{},={},statusCode={}",nodeId,value,statusCode.getValue());
return false;
}
return true;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 写变量
* @param client client
* @param map map
*/
public static void writeValues(OpcUaClient client, Map<NodeId, Object> map) throws ExecutionException, InterruptedException {
if(map==null || map.isEmpty())
return;
String url = client.getConfig().getEndpoint().getEndpointUrl();
for(Map.Entry<NodeId, Object> entry:map.entrySet()){
Object value = entry.getValue();
Variant v = new Variant(value);
DataValue dataValue = new DataValue(v,null,null);
StatusCode statusCode = client.writeValue(entry.getKey(), dataValue).get();
if(statusCode.getValue()!=0){
log.error("writeValues:{},={},statusCode={}", entry.getKey(), value, statusCode.getValue());
new Thread(()->{
NodeId key_ = entry.getKey();
Object value_ = entry.getValue();
Variant v_ = new Variant(value_);
DataValue dataValue_ = new DataValue(v_,null,null);
for(int i=0;i<5;i++){
try {
Thread.sleep(20);
StatusCode statusCode_ = client.writeValue(key_, dataValue_).get();
log.info("url:{},writeCount:{}", url, i);
log.info("writeValue:{},={},statusCode={},writeCount:{}", key_, value_, statusCode_.getValue(), i);
if(statusCode_.getValue()==0) break;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
}
/**
* 创建订阅,添加受监视的项,然后等待值到达。
* 服务器断线重连,应该调用onSubscriptionTransferFailed()回调,因为客户端重新连接服务器将无法在订阅丢失其所有状态后传输订阅。
* @param nodeIds 创建订阅的变量
* @param sf 订阅间隔,单位ms
*/
public static void createSubscription(String url,List<NodeId> nodeIds, double sf){
OpcUaClient client = getClient(url);
HashSet<NodeId> set = new HashSet<>(nodeIds);
nodeIds.clear();
nodeIds.addAll(set);
while(client==null){
try {
Thread.sleep(1000);
if(OpcUaCache.clients.containsKey(url)) {
client = OpcUaCache.clients.get(url);
}
} catch (InterruptedException e) {
log.error("Error sleeping: {}", e.getMessage(), e);
}
}
try {
log.info("{} createSubscription", url);
OpcUaClient finalClient = client;
client.getSubscriptionManager().addSubscriptionListener(
new org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscriptionManager.SubscriptionListener() {
@Override
public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) {
Stack.sharedExecutor().execute(() -> {
try {
createItemAndWait(url, finalClient,nodeIds,sf);
} catch (InterruptedException | ExecutionException e) {
log.error("Error creating Subscription: {}", e.getMessage(), e);
}
});
}
});
createItemAndWait(url,client,nodeIds,sf);
} catch (InterruptedException | ExecutionException e) {
log.info("{}订阅点位时发生了错误", url, e);
throw new RuntimeException(url+"订阅点位时发生了错误");
}
}
private static void createItemAndWait(
String url,
OpcUaClient client,
List<NodeId> nodeIds,
double sf) throws InterruptedException, ExecutionException {
client.getSubscriptionManager().clearSubscriptions();
//创建发布间隔sf的订阅对象
UaSubscription subscription = client.getSubscriptionManager().createSubscription(sf).get();
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
for (NodeId nodeId : nodeIds) {
ReadValueId readValueId = new ReadValueId(
nodeId, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE
);
UInteger clientHandle = subscription.nextClientHandle();
// 创建监控的参数
MonitoringParameters parameters = new MonitoringParameters(
clientHandle, sf, null, UInteger.valueOf(10), true
);
// 创建监控项请求
// 该请求最后用于创建订阅。
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
readValueId, MonitoringMode.Reporting, parameters
);
requests.add(request);
}
// 创建监控项,并且注册变量值改变时候的回调函数。
subscription.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item,id)-> item.setValueConsumer((item1, value)->{
try {
NodeId nodeId = item1.getReadValueId().getNodeId();
Variant variant = value.getValue();
Map<String, SyncOpcNode> node = new HashMap<>();
if(OpcUaCache.nodes!=null && OpcUaCache.nodes.containsKey(url)) {
node = OpcUaCache.nodes.get(url) ;
}
if(node.containsKey(String.valueOf(nodeId.getIdentifier()))){
OpcUaCache.data.put(node.get(String.valueOf(nodeId.getIdentifier())).getId(), variant.getValue());
}
} catch (Exception e) {
log.error("subscription is error {}", e.getMessage());
}
})).get();
}
}