OPC 通讯协议
- OPC 通讯协议基础
- OPC 简介
- OPC 与 OPC UA
- OPC 逻辑对象模型
- OPC 通信方式
- Java 实现 OPC 的方式
- Java 实现 OPC-client
- OPC-DA
- OPC-UA
- 模拟数据进行代码测试
- OPC-DA 代码验证
- OPC-UA 代码验证
OPC 通讯协议基础
OPC 简介
OPC 全称 OLE For Process Control,即用于控制过程的 OLE,是一个工业标准,管理该标准的国际组织是 OPC 基金会。
OPC 出现的目的是为不同的供应商设备与应用程序之间的接口标准化从而使其间的数据交换更加简单,因此,使我们可以开发不依靠于特定开发语言和开发环境的、可以自由组合的过程控制软件。
利用驱动器的系统连接
利用 OPC 控制的系统组成
OPC 的分层结构
OPC 对象中最上层的对象是 OPC 服务器,一个 OPC 服务器中可以设置一个以上的 OPC 组。OPC 服务器常对应于某种特定的控制设备,如 DCS 以及 PLC 等。
OPC 组是可以进行数据访问的多个 OPC 标签的集合,OPC 应用程序可以将需要的数据分组进行批量读取,也可以以组为单位启动或者停止数据访问。此外,OPC 组还提供组内 OPC 标签数据变化时向 OPC 应用程序通知的事件。
OPC 与 OPC UA
OPC DA 与 OPC UA 都是 OPC 协议的标准:
-
OPC 是一种通过微软 COM/DCOM 技术来实现自动化控制的协定,采用 C/S 架构。开发人员只需要按照 OPC 的标准编写 OPC-Client 访问 OPC-Server 进行读写操作即可实现与硬件设备的通信。OPC 的协定中包括:
-
DA (Data Access):访问数据的主要规范;
-
A&E (Alarm and Event):基于事件提供 Client 端订阅,事件触发后 Server 主动提交数据;
-
HDA (History Data Access):历史数据访问;
-
-
OPC UA 是 OPC 协议的新版,其不再依赖于 COM/DCOM 技术,这意味着其具有跨平台性,不再局限于 Windows 系统。OPC UA 提供了可靠的通信机制,接口简单一致。
举例说明两者之间的区别:
对传统的三种不同类型OPC服务器的访问:数据访问 DA、报警和事件 AE、历史数据访问 HDA,要获得一个温度传感器的当前值、一个高温度事件和温度的历史平均值,要依次使用不同的命令执行;
而使用 OPC UA,仅用一个组件就非常容易地完成了。配置和工程的时间也因此可以大大缩短。
OPC 逻辑对象模型
包括3类对象:OPC Server 对象、OPC Group 对象、OPC Item 对象,每类对象都包括一系列接口。
OPC Server 对象
主要功能:
-
创建和管理 OPC Group 对象;
-
管理服务器内部的状态信息;
OPC Group 对象
主要功能:
-
创建和管理 OPC Item 对象;
-
管理 OPC Group 对象的内部状态信息;
-
OPC Server 内部实时数据的读写服务;
属性:
-
name
:组名,由客户端自定义; -
active
:组的激活状态,若为 false 则组失效,无法对服务器进行读写; -
update rate
:更新速率(该值应大于服务器设定的最小值); -
percent data band
:数据死区;
注意:
- Group 分为公共组和私有组:公共组对所有连接到服务器的客户端都有效;而私有组仅对建立该组的客户端有效;
OPC Item 对象
主要功能:
- 用以描述实时数据,代表了与服务器数据源的连接;
属性:
-
name
:项名,在服务器中对应 Item ID; -
active
:项的激活状态; -
value
:项的数据值; -
quality
:项的品质,代表数值的可信度; -
timestamp
:时间戳,代表数据的存取事件;
注意:
-
Item 的存储类型为 VARIANT;Item 的数据类型为 VARTYPE;
-
一个项不能被 OPC 客户端直接访问,因为 OPC 协议中没有对应于项的 COM 接口,对项的访问必须通过 OPC Group 实现;
-
Item 在服务端的定义对应于硬件的实际地址。客户端连接到服务器后创建并添加 OPC Group,并创建一系列的 OPC Item,将逻辑上等价的一组 OPC Item 添加到 OPC Group 中即可通过组对象对数据进行读写操作;
OPC 通信方式
-
同步通信:OPC Client 对 OPC Server 进行读取操作时,OPC Client 必须等到 OPC Server 完成对应操作后才能返回,在此期间 OPC Client 处于一直等待的状态。
-
异步通信:OPC Client 对 OPC Server 进行读取操作时,OPC Client 发送请求后立即返回,不用等待 OPC Server,当 OPC Server 完成操作后再通知 OPC Client 程序。
-
订阅:需要 OPC Server 支持OPC A&E规范,由 OPC Client 设定数据的变化限度,如果数据源的实时数据变化超过了该限度,OPC Server 通过回调返回数据给OPC Client。
Java 实现 OPC 的方式
OPC Client 开发大致流程
- COM 组件初始化;
- 创建服务器 Server 对象;
- 创建组 Group 对象;
- 创建项 Item 对象;
- 添加 Item 到 Group 中;
- 添加 Group 到 Server 对象中;
- 连接服务器,完成相应操作;
- COM 组件关闭
OPC DA
Java 关于 OPC DA 的开源库只有 Utgard 和 Jeasyopc,两者区别如下:
Utgard | Jeasyopc | |
---|---|---|
Linux | 支持 | 不支持 |
Windows | 支持 | 不支持 |
用户名和密码 | 需要 | 不需要 |
组查询 | 支持 | 不支持 |
压力测试(单线程同步) | 略快 7W 点约 4224ms | 略慢 7W 点约 22540ms |
DCOM | 通过 DCOM 实现,需进行配置 | 不需要配置 |
开源库现状 | 作者删库跑路 | 只支持 32 位系统 |
OPC UA
推荐使用 Eclipse 的 milo 开源库
Java 实现 OPC-client
本测试使用的 OPC-Server 软件为 KEPServerEX6,具体下载与使用参考博客OPCServer:使用KEPServer
OPC-DA
因为开源库 Jeasyopc 不支持 windows 和 linux 系统,且只支持 32 位系统,因此此处使用 Utgard 库实现。本测试采用虚拟机实现,使用的系统为 Windows 10 专业版,版本号 1903。
Utgard 开源库通过 DCOM 技术实现,因此首先需要配置 DCOM,参考博客OPC和DCOM配置。
引入相应的依赖
<dependency>
<groupId>org.kohsuke.jinterop</groupId>
<artifactId>j-interop</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.openscada.utgard</groupId>
<artifactId>org.openscada.opc.lib</artifactId>
<version>1.5.0</version>
</dependency>
从 OPC-Server 读取数据
public class OPCRead {
public static void main(String[] args) {
// 配置连接信息
final ConnectionInformation ci = new ConnectionInformation();
ci.setHost("localhost"); // 本机IP
ci.setDomain(""); // 域,为空就行
ci.setUser("OPCServer"); // 用户名
ci.setPassword("OPCServer"); // 密码
// 配置 KEPServer
ci.setClsid("7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729"); // KEPServer 的注册表ID,可以在“组件服务”里看到
final String itemId = "通道 1.设备 1.标记 2"; // KEPServer 上配置的项的名字
// 启动服务
final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor());
try {
// 连接服务器
server.connect();
// 创建 Group,用于对 Item 的访问
final Group group = server.addGroup("test");
// 将要访问的 Item 加入创建的 Group
final Item item = group.addItem(itemId);
// 读取 Item 状态
ItemState itemState = item.read(true);
// 获取 Item 的数据类型,该类型使用常量定义,见 JIVariant 类
int type = 0;
try {
type = itemState.getValue().getType(); // 类型实际是数字,用常量定义的
} catch (JIException e) {
e.printStackTrace();
}
// 打印 Item 相应状态
System.out.println(">>>监控项的数据类型是:" + type);
System.out.println(">>>监控项的时间戳是:" + itemState.getTimestamp().getTime());
System.out.println(">>>监控项的详细信息是:" + itemState);
// 若读到是 short 类型(对应数字 2)
if (type == JIVariant.VT_I2) {
short value = 0;
try {
value = itemState.getValue().getObjectAsShort();
} catch (JIException e) {
e.printStackTrace();
}
System.out.println(">>>short类型值: " + value);
}
// 删除 Group
server.removeGroup(group, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
从 OPC-Server 写入数据
public class OPCWrite {
public static void main(String[] args) {
// 配置连接信息
final ConnectionInformation ci = new ConnectionInformation();
ci.setHost("localhost"); // 本机IP
ci.setDomain(""); // 域,为空就行
ci.setUser("OPCServer"); // 用户名
ci.setPassword("OPCServer"); // 密码
// 配置 KEPServer
ci.setClsid("7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729"); // KEPServer 的注册表ID,可以在“组件服务”里看到
final String itemId = "通道 1.设备 1.标记 2"; // KEPServer 上配置的项的名字
// 启动服务
final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor());
try {
// 连接服务器
server.connect();
// 创建 Group,用于对 Item 的访问
final Group group = server.addGroup("test");
// 将要访问的 Item 加入创建的 Group
final Item item = group.addItem(itemId);
// 写入前:打印 Item 状态及对应数据
printRead(item);
// 写入数据
item.write(new JIVariant("100"));
// 写入后:打印 Item 状态及对应数据
printRead(item);
// 删除 Group
server.removeGroup(group, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 读取 Item 状态、数据
* @param item item
* @throws JIException
*/
public static void printRead(Item item) throws JIException {
// 读取 Item 状态
ItemState itemState = item.read(true);
int type = 0;
try {
type = itemState.getValue().getType(); // 类型实际是数字,用常量定义的
} catch (JIException e) {
e.printStackTrace();
}
// 打印 Item 相应状态
System.out.println(">>>监控项的数据类型是:" + type);
System.out.println(">>>监控项的时间戳是:" + itemState.getTimestamp().getTime());
System.out.println(">>>监控项的详细信息是:" + itemState);
// 若读到是 short 类型(对应数字 2)
if (type == JIVariant.VT_I2) {
short value = 0;
try {
value = itemState.getValue().getObjectAsShort();
} catch (JIException e) {
e.printStackTrace();
}
System.out.println(">>>short类型值: " + value);
}
}
}
OPC-UA
OPC-UA 是目前比较流行的协议,采用开源库 milo 实现,引入相应依赖:
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>0.6.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.6.3</version>
</dependency>
使用 opc-ua 实现数据读写:
public class OpcUaDemo {
public static void main(String[] args) throws Exception {
// 创建OPC UA客户端
OpcUaClient opcUaClient = createClient();
// 开启连接
opcUaClient.connect().get();
// 遍历节点
browseNode(opcUaClient, null);
// 读
readNode(opcUaClient);
// 写
writeNodeValue(opcUaClient);
readNode(opcUaClient);
// 关闭连接
opcUaClient.disconnect().get();
}
/**
* 创建 opc-ua 客户端
* @return OpcUaClient
* @throws Exception
*/
private static OpcUaClient createClient() throws Exception {
String endPointUrl = "opc.tcp://127.0.0.1:49320";
Path securityTmpdir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTmpdir);
if (!Files.exists(securityTmpdir)) {
throw new Exception("unable to create security dir: " + securityTmpdir);
}
return OpcUaClient.create(endPointUrl,
endpoints ->
endpoints.stream()
.filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("KEPServerEX/UA Client Driver"))
.setApplicationUri("urn:Thinkbook-ZQF:Kepware.KEPServerEX.V6:UA%20Client%20Driver")
//访问方式
.setIdentityProvider(new AnonymousProvider())
.setRequestTimeout(UInteger.valueOf(5000))
.build()
);
}
/**
* 遍历树形节点
* @param client 客户端
* @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 = "通道 1.设备 1.标记 1";
//节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
//读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
//标识符
System.out.println(identifier + ": " + String.valueOf(value.getValue().getValue()));
}
/**
* 写入节点数据
*
* @param client
* @throws Exception
*/
private static void writeNodeValue(OpcUaClient client) throws Exception {
//节点
NodeId nodeId = new NodeId(2, "通道 1.设备 1.标记 1");
short i = 3;
//创建数据对象,此处的数据对象一定要定义类型,不然会出现类型错误,导致无法写入
DataValue nowValue = new DataValue(new Variant(i), null, null);
//写入节点数据
StatusCode statusCode = client.writeValue(nodeId, nowValue).join();
System.out.println("结果:" + statusCode.isGood());
}
}
模拟数据进行代码测试
OPC-DA 代码验证
使用 KEPServerEX 6 作为 OPC-Server 进行测试,百度网盘下载链接如下:
链接:https://pan.baidu.com/s/1pigppR62xTsE_4ecXx9m8Q?pwd=3aig
提取码:3aig
在界面上可以右键单击标记名称进行数据格式的设置
单击工具栏最后一个,创建一个 OPC Quick client,可以观察到,此时通道 1.设备 1.标记 2
的值为 0
修改 opc-da 写入程序,写入数值为 300
执行写入程序,查看 client 中的对应值,写入成功
使用读取程序对数据进行读取,同样可以读取到对应的数值
OPC-UA 代码验证
在验证之前首先右键项目,选择属性,修改 OPC-UA 属性中的”允许匿名登录“为是
配置写入数据的方法,将对应的数值从 300 修改为 3
执行代码,查看输出结果