本文使用Milo库实现OPC UA客户端,以达到通过java读、写、订阅变量的目的。
官网:Milo Github链接
官网地址有时候访问很慢,也可以使用国内的地址:https://gitcode.net/mirrors/eclipse/milo
OPC UA服务端:KEPServerEX 6
UI客户端:
以下代码都是在一个类中执行的,并使用main方法进行测试,因此所有方法都是private和static修饰的。
Milo的依赖有2个,Client SDK,Server SDK。
客户端:
<dependency>
<groupId>org.eclipse.milogroupId>
<artifactId>sdk-clientartifactId>
<version>0.6.3version>
dependency>
服务端:
<dependency>
<groupId>org.eclipse.milogroupId>
<artifactId>sdk-serverartifactId>
<version>0.6.3version>
dependency>
/**
* 创建OPC UA客户端
* @return
* @throws Exception
*/
private static OpcUaClient createClient() throws Exception {
//opc ua服务端地址
private final static String endPointUrl = "opc.tcp://192.168.0.169:49320";
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
return OpcUaClient.create(endPointUrl,
endpoints ->
endpoints.stream()
.filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("eclipse milo opc-ua client"))
.setApplicationUri("urn:eclipse:milo:examples:client")
//访问方式
.setIdentityProvider(new AnonymousProvider())
.setRequestTimeout(UInteger.valueOf(5000))
.build()
);
}
注意:
new AnonymousProvider()
表示使用匿名方式访问,也可以通过new UsernameProvider(userName, password)
方式访问。/**
* 遍历树形节点
*
* @param client OPC UA客户端
* @param uaNode 节点
* @throws Exception
*/
private static void browseNode(OpcUaClient client, UaNode uaNode) throws Exception {
List<? extends UaNode> nodes;
if (uaNode == null) {
nodes = client.getAddressSpace().browseNodes(Identifiers.ObjectsFolder);
} else {
nodes = client.getAddressSpace().browseNodes(uaNode);
}
for (UaNode nd : nodes) {
//排除系统行性节点,这些系统性节点名称一般都是以"_"开头
if (Objects.requireNonNull(nd.getBrowseName().getName()).contains("_")) {
continue;
}
System.out.println("Node= " + nd.getBrowseName().getName());
browseNode(client, nd);
}
}
/**
* 读取节点数据
*
* @param client OPC UA客户端
* @throws Exception
*/
private static void readNode(OpcUaClient client) throws Exception {
int namespaceIndex = 2;
String identifier = "TD-01.SB-01.AG-01";
//节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
//读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
//标识符
String identifier = String.valueOf(nodeId.getIdentifier());
System.out.println(identifier + ": " + String.valueOf(value.getValue().getValue()));
}
namespaceIndex
可以通过UaExpert客户端去查询,一般来说这个值是2。identifier
也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称/**
* 写入节点数据
*
* @param client
* @throws Exception
*/
private static void writeNodeValue(OpcUaClient client) throws Exception {
//节点
NodeId nodeId = new NodeId(2, "TD-01.SB-01.AG-01");
short i = 3;
//创建数据对象,此处的数据对象一定要定义类型,不然会出现类型错误,导致无法写入
DataValue nowValue = new DataValue(new Variant(i), null, null);
//写入节点数据
StatusCode statusCode = client.writeValue(nodeId, nowValue).join();
System.out.println("结果:" + statusCode.isGood());
}
/**
* 订阅(单个)
*
* @param client
* @throws Exception
*/
private static void subscribe(OpcUaClient client) throws Exception {
//创建发布间隔1000ms的订阅对象
client
.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(t -> {
//节点
NodeId nodeId = new NodeId(2, "TD-01.SB-01.AG-01");
ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
//创建监控的参数
MonitoringParameters parameters = new MonitoringParameters(UInteger.valueOf(atomic.getAndIncrement()), 1000.0, null, UInteger.valueOf(10), true);
//创建监控项请求
//该请求最后用于创建订阅。
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
//创建监控项,并且注册变量值改变时候的回调函数。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) -> item.setValueConsumer((it, val) -> {
System.out.println("nodeid :" + it.getReadValueId().getNodeId());
System.out.println("value :" + val.getValue().getValue());
})
);
}).get();
//持续订阅
Thread.sleep(Long.MAX_VALUE);
}
订阅成功后,main线程会被阻塞,修改OPC UA中的变量值后,就可以即时查看到订阅的消息。订阅时间可以通过修改Thread.sleep
的沉睡时间实现。
/**
* 批量订阅
*
* @param client
* @throws Exception
*/
private static void managedSubscriptionEvent(OpcUaClient client) throws Exception {
final CountDownLatch eventLatch = new CountDownLatch(1);
//处理订阅业务
handlerNode(client);
//持续监听
eventLatch.await();
}
/**
* 处理订阅业务
*
* @param client OPC UA客户端
*/
private static void handlerNode(OpcUaClient client) {
try {
//创建订阅
ManagedSubscription subscription = ManagedSubscription.create(client);
//你所需要订阅的key
List<String> key = new ArrayList<>();
key.add("TD-01.SB-01.AG-01");
key.add("TD-01.SB-01.AG-02");
List<NodeId> nodeIdList = new ArrayList<>();
for (String s : key) {
nodeIdList.add(new NodeId(2, s));
}
//监听
List<ManagedDataItem> dataItemList = subscription.createDataItems(nodeIdList);
for (ManagedDataItem managedDataItem : dataItemList) {
managedDataItem.addDataValueListener((t) -> {
System.out.println(managedDataItem.getNodeId().getIdentifier().toString() + ":" + t.getValue().getValue().toString());
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
注意:正常情况下通过OpcUaClient
去订阅,没问题,但是当服务端断开之后,milo会抛出异常,当服务端重新启动成功后,OpcUaClient
可以自动断线恢复,但是恢复之后会发现之前订阅的数据没法访问了。要解决这个问题,只需要断线重连后重新订阅即可。
/**
* 自定义订阅监听
*/
private static class CustomSubscriptionListener implements UaSubscriptionManager.SubscriptionListener {
private OpcUaClient client;
CustomSubscriptionListener(OpcUaClient client) {
this.client = client;
}
public void onKeepAlive(UaSubscription subscription, DateTime publishTime) {
logger.debug("onKeepAlive");
}
public void onStatusChanged(UaSubscription subscription, StatusCode status) {
logger.debug("onStatusChanged");
}
public void onPublishFailure(UaException exception) {
logger.debug("onPublishFailure");
}
public void onNotificationDataLost(UaSubscription subscription) {
logger.debug("onNotificationDataLost");
}
/**
* 重连时 尝试恢复之前的订阅失败时 会调用此方法
* @param uaSubscription 订阅
* @param statusCode 状态
*/
public void onSubscriptionTransferFailed(UaSubscription uaSubscription, StatusCode statusCode) {
logger.debug("恢复订阅失败 需要重新订阅");
//在回调方法中重新订阅
handlerNode(client);
}
}
/**
* 批量订阅
*
* @param client
* @throws Exception
*/
private static void managedSubscriptionEvent(OpcUaClient client) throws Exception {
final CountDownLatch eventLatch = new CountDownLatch(1);
//添加订阅监听器,用于处理断线重连后的订阅问题
client.getSubscriptionManager().addSubscriptionListener(new CustomSubscriptionListener(client));
//处理订阅业务
handlerNode(client);
//持续监听
eventLatch.await();
}
public class OpcUaClientTest {
private static Logger logger = LoggerFactory.getLogger(OpcUaClientTest.class);
private final static String endPointUrl = "opc.tcp://192.168.0.169:49320";
private static AtomicInteger atomic = new AtomicInteger(1);
public static void main(String[] args) throws Exception {
//创建OPC UA客户端
OpcUaClient opcUaClient = createClient();
//开启连接
opcUaClient.connect().get();
//遍历节点
browseNode(opcUaClient, null);
//读
readNode(opcUaClient);
//写
writeNodeValue(opcUaClient);
//订阅
subscribe(opcUaClient);
//批量订阅
//managedSubscriptionEvent(opcUaClient);
//关闭连接
opcUaClient.disconnect().get();
}
}