我这边实现了一个可以直接使用的工具类,做个记录
所需jar包
如果是在线使用的话,用到这些就可以
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>0.4.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.4.1</version>
</dependency>
如果离线使用的话,那就需要这些,不想手动一个个下载的话可以给我留言邮箱,我会尽快发给你(不好意思现在这些jar包我也没有现成的了 2021.9.27)
java工具类
/**
* @Author ws
* @Date 2020/06/08
*/
@Slf4j
public class UaClient {
private OpcUaClient client;
private String url;
private String userName;
private String passWord;
private Map<NodeId, String> nodeValues;
private boolean runningState;
public UaClient(String url, @Nullable String userName, @Nullable String passWord) {
this.url = url;
this.userName = userName;
this.passWord = passWord;
}
public boolean createConnection() {
try {
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);
}
KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
client = OpcUaClient.create(url,
endpoints ->
endpoints.stream()
.filter(endpointFilter())
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("eclipse milo opc-ua client"))
.setApplicationUri("urn:eclipse:milo:examples:client")
.setCertificate(loader.getClientCertificate())
.setKeyPair(loader.getClientKeyPair())
.setIdentityProvider(getIdentityProvider())
.setRequestTimeout(uint(5000))
.build()
);
client.connect().get();
client.addFaultListener(new UaFaultListener());
} catch (Exception e) {
log.info("创建ua客户端时发生了错误", e);
return false;
}
nodeValues = new HashMap<>();
runningState = true;
return true;
}
public void disconnect() {
runningState = false;
try {
client.disconnect().get();
Stack.releaseSharedResources();
} catch (InterruptedException | ExecutionException e) {
log.error("设备断开连接的时候发生了错误: {}", e.getMessage(), e);
}
}
public Map<NodeId, String> read() throws ConnectionInterruptException {
if (!runningState) {
throw new ConnectionInterruptException();
}
return this.nodeValues;
}
public void subscription(List<NodeId> nodeIds, double sf) {
try {
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, uint(10), true
);
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
readValueId, MonitoringMode.Reporting, parameters
);
requests.add(request);
}
BiConsumer<UaMonitoredItem, Integer> onItemCreated =
(item, id) -> item.setValueConsumer(this::onSubscriptionValue);
List<UaMonitoredItem> items = subscription.createMonitoredItems(
TimestampsToReturn.Both,
requests,
onItemCreated
).get();
} catch (InterruptedException | ExecutionException e) {
log.info("订阅点位时发生了错误", e);
disconnect();
}
}
private void onSubscriptionValue(UaMonitoredItem item, DataValue value) {
if (item.getStatusCode().isGood()) {
nodeValues.put(
item.getReadValueId().getNodeId(),
value.getValue().getValue().toString()
);
} else {
log.info("点位[{}]读到了脏数据[{}]",
item.getReadValueId().getNodeId(),
value.getValue().getValue().toString()
);
}
}
public boolean write(Map<NodeId, String> writeNodes) {
try {
List<NodeId> nodeIds = new ArrayList<>();
List<DataValue> dataValues = new ArrayList<>();
for (NodeId nodeId : writeNodes.keySet()) {
nodeIds.add(nodeId);
Variant v = new Variant(writeNodes.get(nodeId));
dataValues.add(new DataValue(v, null, null));
}
// write asynchronously....
CompletableFuture<List<StatusCode>> f =
client.writeValues(nodeIds, dataValues);
// ...but block for the results so we write in order
List<StatusCode> statusCodes = f.get();
for (int i = 0; i < statusCodes.size(); i++) {
if (!statusCodes.get(i).isGood()) {
log.info("向点位(NodeId={})写值失败", nodeIds.get(i));
return false;
}
}
} catch (ExecutionException | InterruptedException e) {
log.info("向点位写值时发生了错误", e);
disconnect();
return false;
}
return true;
}
public TypeValue browseTree(@Nullable NodeId startNode) {
// start browsing at root folder
TypeValue typeValue = new TypeValue();
try {
typeValue.setData(browseNode(client,
startNode == null ? Identifiers.RootFolder : startNode));
} catch (ExecutionException | InterruptedException e) {
log.info("遍历树的时候发生了错误", e);
disconnect();
}
return typeValue;
}
private List<TypeValueData> browseNode(OpcUaClient client, NodeId browseRoot) throws ExecutionException, InterruptedException {
List<Node> nodes = client.getAddressSpace().browse(browseRoot).get();
if (nodes.size() == 0) {
return null;
}
List<TypeValueData> dataList = new ArrayList<>();
for (Node node : nodes) {
TypeValueData data = new TypeValueData();
data.setKey(node.getBrowseName().get().getName());
NodeId nodeId = node.getNodeId().get();
data.setValue(nodeId.getNamespaceIndex() + ";" + nodeId.getIdentifier());
// recursively browse to children
List<TypeValueData> list = browseNode(client, node.getNodeId().get());
if (list != null) {
data.setChildren(list);
}
dataList.add(data);
}
return dataList;
}
// 过滤安全验证,使用None
private Predicate<EndpointDescription> endpointFilter() {
return e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri());
}
// 获取登录验证,没有输入用户名密码就匿名登录
private IdentityProvider getIdentityProvider() {
if (userName != null && passWord != null) {
return new UsernameProvider(userName, passWord);
}
return new AnonymousProvider();
}
public class UaFaultListener implements ServiceFaultListener {
@Override
public void onServiceFault(ServiceFault serviceFault) {
log.info("UA的连接发生了错误,即将断开连接");
disconnect();
}
}
}
这其中包括的功能(按照一般使用顺序):
- 连接connect,我这里把milo里面的每个模块整理了一下,把确实的连接的模块放在了一起,也就是说,返回值为true的时候就真的代表已经连接上了,可以直接进行其他操作了;
- 断开连接disconnect
- 读节点,先订阅subscription,再读map,我直接就没有实现轮询读数的功能了,多点位场景下轮询有点鸡肋;
- 写节点write
- 遍历节点browseTree,这里有点特殊,挨个说吧。
- 返回值类型是我这边私有的一个类型,也就是{节点名,节点值,子节点}的结构罢了,调用的时候按需更改吧,哦对了我有一篇关于opc的文章里面实现了一个简易的这样的结构类型(为什么不调用java已有的?因为我觉得我可能会对tree进行一些特殊的更改,我这样自己实现的话改起来会很方便);
- 参数是一个节点NodeId,可以直接 new NodeId(namespaceIndex,Identifier),这两个参数可以在OPCUA Client这些客户端工具上看到,一般来说,namespaceIndex为1,Identifier为服务的程序id+Root(如ControlEase.TNT.OPC.2Root或Matrikon.OPC.Simulation.1Root);如果不选择开始节点的话,遍历会从根节点进行遍历,会多出很多无用的点位;
- 这边是按层序索引的,我是没找到列表索引的,虽然不用,但是好奇为啥没有,难道ua直接把列表索引的功能淘汰了?
另外,这里面还引用了KeyStoreLoader,这个类没什么特殊的,直接用milo库里的就成,我就不沾上来了。