资讯专栏INFORMATION COLUMN

Java OPC client开发踩坑记

URLOS / 1210人阅读

摘要:目前常见的协议版本号为和,两个协议不完全兼容。第一坑基于开发的非常少,大部分是商业的,售价不菲。端存储的格式如下这个就比较有意思了,是可以自己维护一个存储层级。不但配置繁琐复杂,还会受到各种权限以及防火墙规则的影响。

最近一个项目中需要用到OPC client,从OPC Server中获取数据。主要的编程语言使用Java实现。实际开发中遇到了各种坑,其实也和自己没有这方面的经验有关,现在写一篇文章分享下整个项目中遇到的一些问题。

准备知识

开发OPC Client之前需要一些准备知识,需要一些知识储备,否则根本搞不清楚里面的门道。现在对一些预先准备的知识点做一概述。OPC是什么就不说了。

OPC Server端的协议

OPC Server端目前常见的有以下几种协议:

OPC DA: Data Access协议,是最基本的OPC协议。OPC DA服务器本身不存储数据,只负责显示数据收集点的当前值。客户端可以设置一个refresh interval,定期刷新这个值。目前常见的协议版本号为2.0和3.0,两个协议不完全兼容。也就是用OPC DA 2.0协议的客户端连不上OPC DA 3.0的Server

OPC HDA: Historical Data Access协议。前面说过DA只显示当前状态值,不存储数据。而HDA协议是由数据库提供,提供了历史数据访问的能力。比如价格昂贵的Historian数据库,就是提供HDA协议接口访问OPC的历史数据。HDA的Java客户端目前我没找到免费的。

OPC UA: Unified Architecture统一架构协议。诞生于2008年,摒弃了前面老的OPC协议繁杂,互不兼容等劣势,并且不再需要COM口访问,大大简化了编程的难度。基于OPC UA的开源客户端非常多。不过由于诞生时间较晚,目前在国内工业上未大规模应用,并且这个协议本身就跟旧的DA协议不兼容,客户端没法通用。

我们的目标环境绝大多数是OPC DA 2.0的Server,极个别可能有OPC DA 3.0。当时找到的很多类库实现的都是OPC UA的。

第一坑: 基于JAVA开发的OPC Client非常少,大部分是商业的,售价不菲。现场环境又是OPC DA的Server,开源client只有两个可选,找工具和评估就花了不少时间。

OPC存储格式

OPC存储和传统的关系型数据库存储格式有很大的不同,不同于关系型数据库的表存储,OPC存储格式是树形结构,Server端的存储格式如下:

host
`-- OPC Server Name
    `-- tag1: value, type, timestamp, ...,
    `-- tag2: value, type, timestamp, ...,
    `-- tag3: ...
    ...

每个主机上可能存在多个OPC Server,每个Server下面有若干个tag,就是各个数据收集点当前的值,会定期更新。每个tag包含的内容大致有当前值,值类型,时间戳等等数据。是一种树形结构。所以客户端连接的时候需要指明服务器的ip或主机名,需要连接的OPC服务名,以及监听哪些tag的数据。

Client端存储的格式如下:

Group1
`-- tag1
`-- tag2
`-- tag3
Group2
`-- tag4
`-- tag5
...

这个就比较有意思了,Client是可以自己维护一个存储层级Group。也就是服务端存储的都是一个个tag,客户端可以自己维护一个个Group,分类存放这些tag。所以OPC的Client就和传统的关系型数据库有很大的不同。客户端除了指明上述Server端的信息之外,还需要创建一个个Group,将Server端的tag一个个放到这些Group中,然后对应的tag才能持续的获得数据。

第二坑: 这种存储格式在其他数据库十分罕见,当时这里就迷茫了好一阵子,通过了解协议的人讲解,才明白原来客户端还可以维护一套存储结构。当时没理清楚Group和tag的关系,从服务端看不到Group,客户端却要填一个Group,不知道这个Group从哪来。后来才搞清楚。

COM

Component Object Model对象组件模型,是微软定义的一套软件的二进制接口,可以实现跨编程语言的进程间通信,进而实现复用。

DCOM

Microsoft Distributed Component Object Model,坑最多的一个玩意。字面意思看起来是分布式的COM,简单理解就是可以利用网络传输数据的COM协议,客户端也可以通过互联网分布在各个角落,不再限制在同一台主机上了。

上面描述来看这玩意好像挺美好是吧?实际操作开发中才发现,这玩意简直是坑王之王,对于不熟悉的人来说充满了坑,十分折腾。配置过程可以参考一些文章

DCOM是windows上的服务,使用前需要启用

DCOM是远程连接的协议,需要配置相关的权限,以及防火墙规则放行

特别注意这一点,前两项配置在网上都能找到,这一条是我在经历无数次痛之后才意识到的。DCOM远程连接和http不同,是通过本地用户认证的,需要以本地用户身份登录服务器,拿到相应的权限,才能使用DCOM。有点绕是吧?你可以类比Windows的远程桌面登录,需要拿到服务器的用户名密码才能登录并操作系统,权限受到登录用户的权限所限制。而DCOM就是用的这种方式。关于各种错误网上能找出一大堆解决方案,可能还没一个能解决你的问题的。甚至可能progID无论无何也通不了,始终报错,不得不改用CLSID这种方法,十分坑。

神坑: DCOM。从配置开始就充满了陷阱和坑。不但配置繁琐复杂,还会受到各种权限以及防火墙规则的影响。最恶心的是这玩意随时可能报各种奇葩的错误,由于缺乏足够的错误信息,很难解决,基本凭借经验解决DCOM的故障。

开发过程

收集到足够的准备知识后,就可以开工了。OPC Server是DA 2.0的,因此找到了以下两个开源类库。
JEasyOPC Client

底层依赖JNI,只能跑在windows环境,不能跨平台

整个类库比较古老,使用的dll是32位的,整个项目只能使用32位的JRE运行

同时支持DA 2.0与3.0协议,算是亮点

Utgard

OpenSCADA项目底下的子项目

纯Java编写,具有跨平台特性

全部基于DCOM实现(划重点)

目前只支持DA 2.0协议,3.0协议的支持还在开发中

这两个类库都试过,JEasyOPC底层用了JNI,调用代码量倒不是很大,使用也足够简单,坑也遇到了点,就是64位的JRE运行会报错,说dll是ia32架构的,不能运行于AMD64平台下,换了32位版本的JRE之后运行起来了,但是一直报错Unknown Error,从JNI报出来的,不明所以,实在无力解决,只能放弃。

只剩下Utgard一种选择了,也庆幸目标Server是DA 2.0的,用这个类库完全够用。这个类库全部使用DCOM协议连接OPC Server,所以对于本地连接OPC Server,理论上不需要COM口,但是这个类库全部使用DCOM协议连接,所以依旧需要配置主机名,以及登录的用户名密码。使用之前必须先配置DCOM,其中痛苦不足为外人道也,在上面准备知识部分已经写道了。

经过一番折腾,总算将项目跑起来了,最终参考的工程代码如下(项目实用Gradle构建,代码使用Utgard官方的tutorial范例):
build.gradle:

apply plugin: "java"
apply plugin: "application"

repositories {
    maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
    jcenter()
    maven { url "http://neutronium.openscada.org/maven/" }
}

dependencies {
    compile "org.openscada.utgard:org.openscada.opc.lib:1.3.0-SNAPSHOT"
    compile "org.openscada.utgard:org.openscada.opc.dcom:1.2.0-SNAPSHOT"
    compile "org.jinterop:j-interop:2.0.4"
    compile "ch.qos.logback:logback-core:1.2.3"
    compile "org.slf4j:slf4j-api:1.7.25"
}

mainClassName = "UtgardTutorial1"

src/main/java/UtgardTutorial1.java:

import org.jinterop.dcom.common.JIException;
import org.openscada.opc.lib.common.ConnectionInformation;
import org.openscada.opc.lib.da.AccessBase;
import org.openscada.opc.lib.da.Server;
import org.openscada.opc.lib.da.SyncAccess;

import java.util.concurrent.Executors;

public class UtgardTutorial1 {

    public static void main(String[] args) throws Exception {
        // create connection information
        final ConnectionInformation ci = new ConnectionInformation();
        ci.setHost("localhost");
        ci.setUser("Administrator");
        ci.setPassword("mypassword");
        ci.setProgId("TLSvrRDK.OPCTOOLKIT.DEMO");
//        ci.setClsid("08a3cc25-5953-47c1-9f81-efe3046f2d8c"); // if ProgId is not working, try it using the Clsid instead
        final String itemId = "tag1";
        // create a new server
        final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor());

        try {
            // connect to server
            server.connect();
            // add sync access, poll every 500 ms
            final AccessBase access = new SyncAccess(server, 500);
            access.addItem(itemId, (item, state) ->
                    System.out.println("Resut: " + state.toString()));
            // start reading
            access.bind();
            // wait a little bit
            Thread.sleep(10 * 1000);
            // stop reading
            access.unbind();
        } catch (final JIException e) {
            System.out.println(String.format("%08X: %s", e.getErrorCode(), server.getErrorMessage(e.getErrorCode())));
            e.printStackTrace();
        }
    }
}

最终项目运行输出如下:

 Recieved RESPONSE
Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:29 上午 rpc.DefaultConnection processOutgoing
信息:

总算跑起来了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67311.html

相关文章

  • Notadd 2.0 全新 Node.js 版本~ (开发中) [从 PHP 到 node 的坑记

    摘要:有着最全的协议支持,同时有各种非阻塞拓展,可以说是最符合要求的,但是异步需要对做很大的改动。的计划将基于开发,同时也提供一些无法提供的功能和特性。 一点小遗憾 对于 Notadd 我们本来期望它实现更多... 尽管我们也尝试做了很多努力,但是由于 PHP 本身的局限,以及考虑到开发环境配置的复杂程度,最终使用了折中方案。接下来,我们谈谈整个技术选型历程,也供今后相关开发者做借鉴和参考:...

    Tonny 评论0 收藏0
  • 微信小程序 海报生成坑记

    摘要:最近有个需求是要生成分享海报,让用户可以将图片保存到本地然后分享到朋友圈。本来以为是一个很简单的需求,可是万万没想到,微信会这么坑。 最近有个需求是要生成分享海报,让用户可以将图片保存到本地然后分享到朋友圈。本来以为是一个很简单的需求,可是万万没想到,微信会这么坑。刚开始的思路是这样的: 后台根据小程序传过来的参数获取对应的小程序码,然后与背景图合成之后将base64格式的图片传给小程...

    lidashuang 评论0 收藏0
  • vue-cli3.x 新特性及坑记

    摘要:前言都到了,所以是时候玩转一下的新特性了。安装的包名称由改成了。方法一原因的配置改变了,导致正确的不能用。打开终端,切换到根路径文件里面修改为方法二是默认路径修改了路径会出现错误。按上面的方法修改完,再全局卸载果然就成功了。 showImg(https://segmentfault.com/img/remote/1460000016423946); 前言 vue-cli 都到 3.0....

    xiaoqibTn 评论0 收藏0

发表评论

0条评论

URLOS

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<