Java实现OPCUA通信

视频说明:https://www.bilibili.com/video/BV1TR4y1Q71g/

描述

utgard 的方式过时了,所以建议使用 OPCUA 的方式。

安装 kep :OPCServer:使用KEPServer

这是连接操作说明:OPC UA Client:使用UaExpert

使用的开源库是 milo:https://github.com/eclipse/milo

因为没有实际项目,所以只运行 milo 的示例代码的客户端部分:

https://github.com/eclipse/milo/tree/master/milo-examples/client-examples

使用西门子的 OPC UA Server,相关文档

🟢 S7-1200 OPC UA 通信
🟢 S7-1500 OPC UA服务器S7-1500 OPC UA客户端

代码

Github:https://github.com/ioufev/opcua-milo-demo

蓝奏云:https://ioufev.lanzout.com/i1S7K0kt4dda

过程和问题🔥🔥

证书问题

❓ 问题:运行报证书问题
⚓ 描述:java.io.IOException: parseAlgParameters failed: ObjectIdentifier() -- data isn't an object ID (tag = 48)
👌 JDK 的版本问题,升级 jdk8 到升级到 1.8.0.301及以上,或者使用 jdk11 或 17 运行。参考

❓ 问题:生成证书不带 URI
⚓ 描述:在 windows 上使用 openssl 生成带 URI 信息的自签名证书,没找到操作说明。在 linux 上看教程到还可以。
👉 想到:不自己生成证书,使用 milo 的证书处理类先生成一个证书,以后使用这个生成的证书。

如图,生成证书

证书上的客户端 ID

客户端使用证书连接服务端时,需要服务端信任客户端的证书。

同时,客户端也要通过证书区分不同的客户端,所以在证书上有一个字段是代表客户端 ID 的,类似 MQTT 客户端 ID,要求不重复。

配置代码的时候,需要注意配置的要和证书上一致

生成自签名证书的设置

客户端连接时的配置

UaExpert 第一次使用时生成的证书

在 OPC UA 通信中,应用程序的 ApplicationUri 是重要的,它用于:

● 区分不同的应用程序:如果你有多个应用程序连接到同一个 OPC UA 服务器,每个应用程序需要有一个唯一的标识符,以便服务器能够区分它们。
● 用于安全策略:在 OPC UA 安全策略中,ApplicationUri 可能会用于安全认证和授权。

确保 ApplicationUri 是唯一的,避免与其他应用程序冲突。
通常,使用应用程序的名称或组织的标识符作为 ApplicationUri 的一部分是一种常见做法。

例如,.setApplicationUri("urn:MyCompany:MyApp") 可以用于标识属于 "MyCompany" 组织的 "MyApp" 应用程序。

主机名未知

❓ 问题:不使用匿名连接,只能连接本地,不能连接远程。
⚓ 描述:可以使用 UaExpert 的用户名密码连接,milo 代码测试不能连接,报 UnknownHostException 即 主机名未知 错误。
👌 搜索到相关解答,服务发现时,服务端返回的断点描述的主机名或者本地IP,远程是访问不到的,按照解答参考,修改端点的主机名为远程IP地址即可。

没有选择节点

❓ 问题:no endpoit selected
⚓ 描述:使用默认代码运行,报没有选择节点
👌 服务端可以配置连接方式,安全策略和安全模式

我的代码用的是
安全策略:Basic256Sha256
安全模式:签名并加密

OPC UA 服务端配置的安全策略和安全模式,可能和我的测试代码不一致,需要自己修改。

安全策略和安全模式

代码修改安全策略

服务端信任客户端证书

证书代表身份,服务端信任客户端的证书,那就是允许客户端连接。

按理说,客户端也要有信任服务端证书的步骤,但是的,客户端比较弱势,客户端都去连接服务端了,自然是信任服务端了。

所以双向的互相信任对方的证书就显得多余了,当然在支付领域,肯定是要双向信任的,比如 APP 第一次连接服务端时,服务端在客户端上安装了数字证书,以后客户端连接服务端都要带着证书去操作。

一些理解

OPCUA 官方内容

OPCUA 规范:https://reference.opcfoundation.org/

内容太多,很繁琐,还分好多部分。感觉看看配的图片就行了。

机器翻译后的内容

🍄 OPCUA 规范 第 1 部分:概述和概念 5:概述
🍄 OPCUA 规范 第 1 部分:概述和概念 6:系统概念

可以不使用 OPCUA 这种方式连接吗?

是可以的,很多 PLC 使用的协议是公开的,比如 Modbus,直接连接也没问题。

DA 到 UA

OPC DA 是针对 windows DCOM 的规范,以后肯定不推荐了。

OPC UA 要兼容 DA,但是 要摆脱 windows DCOM,所以推出类似 HTTP 的 opc over tcp 协议。

旧的项目,第三方 OPCserver,比如 kep ,去连接设备获取数据,kep 提供不同的连接方式(DA、UA、ThingWorx)

除非使用的第三方 OPC 只支持 DA,但是感觉这样的 OPCserver 该被淘汰了。

OPCUA 的连接

看别人写的帖子都使用无安全策略的连接方式,是很省事。

我对证书的内容,不太理解明白,所以后续会补充内容。

💧 试过之后,不建议自己生成证书,使用 milo 的证书加载类生成证书非常合适

💧 生成简单的自签名证书(以前测试用的,现在不需要了):Windows 安装 OpenSSL 生成自签名证书

OPCUA 和物模型,和 Java 对象类比

OPCUA 是一种映射方式,非常像 Java 中使用类描述对象。

按照所谓 “物模型” 的说法,设备就是一个对象,

🍄 设备的参数,就是:物模型的属性值,Java 中类的属性(也可以叫变量,字段),OPCUA 中的节点的变量。

🍄 设备的操作方法,就是:物模型的功能,Java 中类的方法(也可以叫函数),OPCUA 中的方法

🍄 设备的出现的各种状况(比如上线,某个组件出故障,某个参数超标),就是:物模型的事件,Java 中的事件,OPCUA中订阅。

对于事件的理解,感觉很像 MQTT 中的发布订阅,如果设备发生了什么故障,把情况通知到订阅的人。

去年做了一个无人船项目,项目不太成功,不过可以来具体举例理解。

🍑 无人船运行过程中,需要知道运行状态:电池的温度、电流电压、剩余电量,船的速度,GPS 坐标,航向角等。
🍑 无人船要能远程控制,通过摄像头获取到远程视频,能在界面上控制船前进、加速、转弯、后退、停止。
🍑 无人船航行过程中发现有人在游泳,或者电池快没电了发出提示,或者航行到了水质参数异常的区域发出提示。

💧 OPCUA 中的引用,和 Java 中一个类引用另一个类的实例作为属性值,很相似。

💧 OPCUA 的节点类,和 Java 中的类也很相似,节点是从根节点到层层子节点,Java 中也是从 Object 类开始加载。

💧 地址空间,一个树形结构,每个节点是一个类,每次看地址空间,感觉就像在 idea 里看 Java 类的结构。

OPCUA 中的数据类型

OPCUA 中的数据类型,连 Java 中的 null 都有对应。

Boolean、
Byte、
ByteString:使用字节定义字符串,感觉和 Java9 中 String 的定义由 char[] 改为 byte[] 很像。
DateTime、
Double、
Float、
Int16、Int32、Int64
UInt16、UInt32、UInt64:无符号类型,没有用一个位表示正负号,只表示零和正数。

💧 milo 中 Unsigned 类封装了无符号类型的表示,比如 Uint16 类型的 12,表示为:Unsigned.ushort(12)

String

节点标识符

OPCUA 中的 Identifier,节点标识符,milo 中 Identifiers 类定义的,

对于想要读取的项,比如 “通道 1.设备 1. 标记 1”,这就是一个 Identifier

OPCUA 的订阅

MQTT 中的订阅,是要有主题的。

💧 OPCUA 的订阅是个事件通知,比如订阅某个变量的值如果超出某个范围,触发事件,发出通知。

请求响应 vs 发布订阅

也可以叫 OPCUA vs MQTT

OPCUA 是个发展的协议,原来就是请求响应模式,

所以大部人使用都是:OPCUA获取到数据后通过MQTT发送出去。

估计OPCUA的有些人觉得不爽,觉得OPCUA也要有发布订阅模式,我看 UAExpert 也有了发布订阅功能,不过还没见人使用,因为 MQTT 的发布订阅很方便。

使用 KEPServerEX:把 OPC 数据通过 MQTT 上传

OPCUA 的通信协议

原来的 OPC 只是个规范,OPCUA 有个基于 TCP 的应用层是二进制格式的协议,即常见的 opc.tcp://

OPCUA 的通信协议,原来似乎是 XML 格式,后来这种模式被 JSON 格式取代了,OPCUA 也与时俱进。

OPCUA 是个应用层协议,使用 TCP 传输,加密传输就是 TCP + TLS。

MQTT 也可使用 WebSocket 作为传输层,传输 MQTT 格式的信息,OPCUA 也可以使用 WebSocket 作为传输层,也就是浏览器作为OPCUA客户端,直接访问OPCUA服务端,暂时还没看到有实现开源库。

OPCUA 的建模

操作就是类似在kep建项:标记1、标记2、标记3。。。
然后保存成文件。

感觉就是:用 XML 格式或者 JSON 格式,来描述服务端有什么节点,节点有什么属性。

在服务端定义节点,如果项少,自然没问题。如果项很多,也就是节点很多。

如果行业中有人定义好了拿出来分享,感觉就是所谓的建模。

补充内容

地址说明

内容来自 kep 关于 OPC UA Client 的帮助文件

Client 驱动程序 地址的语法如下: ns=<namespace index>;<type>=<value>。有关详细信息,请参阅下表。

字段 说明
命名空间索引 地址所在的 OPC UA 服务器命名空间的索引。如果索引为 0,则省略整个 ns =<namespace index="">;</namespace> 子句。
类型 地址类型。OPC UA 支持以下四种地址类型:
i: 用 32 位无符号整数表示的数字地址
s: 由 UTF-8 编码字符Closed有符号 8 位值。组成的字符串地址
g: 采用 {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} 格式的 GUID 地址
b: 不透明的地址 (例如: 字节字符串)
格式化为字符串的地址。此地址可以是数字、字符串、GUID 或不透明。

示例

地址类型 名称空间 示例
数字 2 ns=2;i=13
字符串 3 ns=3;s=Channel1.Device1.Tag1
GUID 0 g=
不透明 2 ns=2;b=M/RbKBsRVkePCePcx24oRA==

milo 代码中节点类 NodeId

public final class NodeId {

    public static final NodeId NULL_NUMERIC = new NodeId(ushort(0), uint(0));
    public static final NodeId NULL_STRING = new NodeId(ushort(0), "");
    public static final NodeId NULL_GUID = new NodeId(ushort(0), new UUID(0, 0));
    public static final NodeId NULL_OPAQUE = new NodeId(ushort(0), ByteString.NULL_VALUE);

    public static final NodeId NULL_VALUE = NULL_NUMERIC;

    private final UShort namespaceIndex;
    private final Object identifier;


    public NodeId(int namespaceIndex, int identifier) {
        this(ushort(namespaceIndex), uint(identifier));
    }


    public NodeId(int namespaceIndex, UInteger identifier) {
        this(ushort(namespaceIndex), identifier);
    }


    public NodeId(int namespaceIndex, String identifier) {
        this(ushort(namespaceIndex), identifier);
    }


    public NodeId(int namespaceIndex, UUID identifier) {
        this(ushort(namespaceIndex), identifier);
    }


    public NodeId(int namespaceIndex, ByteString identifier) {
        this(ushort(namespaceIndex), identifier);
    }


    public NodeId(UShort namespaceIndex, UInteger identifier) {
        checkNotNull(namespaceIndex);
        checkNotNull(identifier);

        this.namespaceIndex = namespaceIndex;
        this.identifier = identifier;
    }


    public NodeId(UShort namespaceIndex, int identifier) {
        checkNotNull(namespaceIndex);

        this.namespaceIndex = namespaceIndex;
        this.identifier = uint(identifier);
    }


    public NodeId(UShort namespaceIndex, String identifier) {
        checkNotNull(namespaceIndex);

        if (identifier == null) identifier = "";

        this.namespaceIndex = namespaceIndex;
        this.identifier = identifier;
    }


    public NodeId(UShort namespaceIndex, UUID identifier) {
        checkNotNull(namespaceIndex);
        checkNotNull(identifier);

        this.namespaceIndex = namespaceIndex;
        this.identifier = identifier;
    }


    public NodeId(UShort namespaceIndex, ByteString identifier) {
        checkNotNull(namespaceIndex);
        checkNotNull(identifier);

        this.namespaceIndex = namespaceIndex;
        this.identifier = identifier;
    }

    NodeId(@NotNull UShort namespaceIndex, @NotNull Object identifier) {
        checkNotNull(namespaceIndex);
        checkNotNull(identifier);

        this.namespaceIndex = namespaceIndex;
        this.identifier = identifier;
    }

    ...
posted @ 2022-10-12 11:31  ioufev  阅读(6928)  评论(21编辑  收藏  举报