java 连接OPC服务器之 utgard 连接 KepServer

java 连接OPC服务器之 utgard 连接 KepServerjava 连接 OPC 服务器之 utgard 连接 KepServer 我要做一个 java 开发的项目 这个在网上很少案例 大家基本都是做 web 开发的 我其实之前也是 但是现在有这个需求 就干了 我这里使用的是西门子的 Smart200 系列的 PLC 最初的版本其实是使用 java 代码定时去读取 PLC 的数据

大家好,我是讯享网,很高兴认识大家。

java 连接OPC服务器之 utgard 连接 KepServer

我要做一个java开发的项目, 这个在网上很少案例, 大家基本都是做web开发的, 我其实之前也是。但是现在有这个需求, 就干了。

我这里使用的是西门子的Smart200系列的PLC, 最初的版本其实是使用java代码定时去读取PLC的数据, 找到该类型的最小地址和最大地址, 批量读取, 然后缓存起来, 另一个线程定时把缓存里的数据刷新到各个用到的具体地址。项目完成后, 我拿去给用户演示, 在演示的过程中, 我连续点击一个invertbit的按钮, 点了很多下, 然后机器在我不点击后10几秒还在执行之前的命令。这个有点尴尬了。肯定是自己的代码写的不好, 在写数据这里有出了问题, 排队执行命令, 一个一个执行完为止。然后就是偶尔还会出现连接中断的问题。不过不影响使用, 就是日志里记录了而已。

后来我了解到有个叫OPC服务器的东西, 他可以和PLC等设备进行通信, 把数据缓存到OPC服务器里, 然后OPC Client对 OPC Server 的数据进行读写, OPC Server 再对连接设备进行读写。

使用这东西主要好处是不用考虑不同厂厂的基础设备的连接问题了, OPC Server 基本包含了所有知名厂商的连接驱动。我们只需要对OPC Server 进行读写就好了。 另外, 它确实可以解决 直接连接基层设备响应慢的问题, 毕竟在底层设备连接方面, 人家才是专家。我们的java 程序读写OPC Server 的数据其实是很快的, 不会出现排队执行的问题。

这里以 utgard ,kep Server , 西门子的PCL 为例

  1. 我按照西门子PCL 200 系列 的数据类型, 把kepServer 分为 boolean -> V0.0, char(对应 byte) VB0, short VW0, int VD0, float VD0, 没有长整型, 没有双精度浮点数。
    基层实体

package com.kep.entity.generated;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;

import org.apache.log4j.Logger;
import org.jinterop.dcom.common.JIException;
import org.jinterop.dcom.core.JIVariant;
import org.openscada.opc.lib.common.ConnectionInformation;
import org.openscada.opc.lib.da.Group;
import org.openscada.opc.lib.da.Item;
import org.openscada.opc.lib.da.ItemState;
import org.openscada.opc.lib.da.Server;

import com.kep.entity.KBoolean;
import com.kep.entity.KChar;
import com.kep.entity.KFloalt;
import com.kep.entity.KInt;
import com.kep.entity.KShort;
import com.utils.KCollectionUtils;
import com.utils.KLoggerUtils;

/

  • 多通道或多个CPU需要编程人员自己去给没个连接变量赋值(group),比较麻烦 单个CPU的话只要变量名就好了, 这里会根据 连接名+变量名,
  • 分析出组名, kepServer里的对应变量名 kepServer的命名规范, 通道.连接.组(V/VB/M/BM/MW…).具体位置,
  • 例如:conn.start10.VB.0;conn.start10.Q.0_0
  • @author root
// 所有创建了的连接类实体, 全部缓存起来, 以便定时刷新数据 private static Set<KEntity> entitySet = new HashSet<KEntity>(); private static Set<Group> groupSet = new HashSet<Group>(); private static Map<Group, Set<Item>> groupItmeMap = new HashMap<Group, Set<Item>>(); private static Logger logger = KLoggerUtils.getLogger(KEntity.class); private static Timer initServerTimer; // 延时写入数据, 其实主要目的是开启另外一个线程写数据, 这样就不会导致页面卡死 private static Timer wirteDataTimer; // 读取数据的timer, 也是开启另一个线程去读取数据, 这样就不会导致页面卡住, 如果有数据有误的话 private static Timer readDataTimer; // 每隔200毫秒读取一次数据 private static final long readDataTime = 100; private static ConnectionInformation ci; // 默认连接名 private static String defaultConnName; private static final String ciId = "7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729"; // 例如 :conn.smart.Q.0_0, 那么conn.smart 就是连接名, conn.smart.Q就是组名, 默认自己分组, 不需要手动调整 private static Server server; // 命名规范: 例如:Q0.0, 这里会转为 通道.连接.Q.0_0, 比如:conn.smart10.Q.0_0, 同时会把它分到 // conn.start10.Q组去 private String name; private String connName; private Group group; private Item item; protected KEntityType type; private Object value; public enum KEntityType {// 目前仅支持这几种数据类型 Boolean, Char, Short, Int, Float } private static void createReadSchedule() { readDataTimer.schedule(new TimerTask() { @Override public void run() { try { readData(); } catch (Exception e) { logger.error(e.getMessage(), e); } createReadSchedule(); } }, readDataTime); } private static void readData() {// 刷新数据 refresh(); } / * 初始化连接, 并启动循环读取数据的timer * * @param host OPC 服务器的 IP地址 * @param userName OPC 服务器的电脑系统的用户名 * @param password OPC 服务器的电脑系统的用户名对应的密码 * @param connName OPC Server 的某个连接名 */ public static void initDefaultConn(String host, String userName, String password, String connName) { if (null == readDataTimer) { readDataTimer = new Timer(); createReadSchedule();// 启动循环读取数据 } if (null == ci) { ci = new ConnectionInformation(); ci.setHost(host); ci.setUser(userName);// 计算机的登录名 ci.setPassword(password);// 计算机的密码 ci.setClsid(ciId);// kepServer的ciId, 这个比较特殊 } defaultConnName = connName; if (null == server) { server = new Server(ci, Executors.newSingleThreadScheduledExecutor()); } try { server.connect(); } catch (Exception e) { logger.error(e.getMessage(), e); reInitServer(); } } / * 重新初始化一下Server */ private static void reInitServer() { if (null == initServerTimer) { initServerTimer = new Timer(); } initServerTimer.schedule(new TimerTask() { @Override public void run() { server = new Server(ci, Executors.newSingleThreadScheduledExecutor()); try { server.connect(); } catch (Exception e) { server = null; logger.error(e.getMessage(), e); reInitServer();// 连接失败, 再来过, 直到成功为止 } } }, 2000); } public KEntity(String name) { this.name = name; this.connName = defaultConnName; entitySet.add(this); initEntity(); } public KEntity(String name, String connName) { this.name = name; this.connName = connName; entitySet.add(this); initEntity(); } / * 初始化连接实体 */ private void initEntity() { // 初始化实体类型 if (this instanceof KBoolean) { this.type = KEntityType.Boolean; } else if (this instanceof KChar) { this.type = KEntityType.Char; } else if (this instanceof KShort) { this.type = KEntityType.Short; } else if (this instanceof KInt) { this.type = KEntityType.Int; } else if (this instanceof KFloalt) { this.type = KEntityType.Float; } if (null == server) { return; } // 解析组名 String itgName = null; // boolean 类型的,截取第一个字符, 这样容错性强一些 例如 vb0.2->V0.2的效果 switch (this.type) { case Boolean: itgName = name.substring(0, 1); break; case Char: itgName = name.substring(0, 1) + "B"; break; case Short: itgName = name.substring(0, 1) + "W"; break; case Int: itgName = name.substring(0, 1) + "D"; break; case Float: itgName = name.substring(0, 1) + "D"; break; } String groupName = connName + "." + itgName.toUpperCase(); // 先找一下有没有初始化过这个组 try { group = server.findGroup(groupName); } catch (Exception e) { logger.error("查找组失败 : " + groupName, e); } // 没有初始化过的组, 初始化它 try { if (null == group) { group = server.addGroup(groupName); } } catch (Exception e) { logger.error("添加组失败 : " + groupName, e); } if (null != group) {// 把组缓存起来 groupSet.add(group); } else { return; } // 获取或者初始化组对应的itemSet Set<Item> itemSet = groupItmeMap.get(group); if (null == itemSet) { itemSet = new HashSet<Item>(); groupItmeMap.put(group, itemSet); } // 解析item实际对应kepServer里的变量名 String addr = name.replaceAll("[a-z,A-Z]", "").replaceAll("[.]", "_"); String itemName = groupName + "." + addr; try { item = group.addItem(itemName); // 收集item, 用于后面定时刷新数据时批量刷新, 如果不批量刷新,反应会很慢很慢, 我写过遍历实体刷新的,2秒才刷新完, 不能接受 itemSet.add(item); } catch (Exception e) { logger.error("添加Item失败 : " + groupName + "." + addr, e); } } / * 刷新所有连接实体的数据 */ private static void refresh() { if (KCollectionUtils.notEmpty(groupSet)) { for (Group group : groupSet) { Set<Item> itemSet = groupItmeMap.get(group); if (KCollectionUtils.notEmpty(itemSet)) { try { // 按组进行刷新数据, 批量刷新 Map<Item, ItemState> itemMap = group.read(true, itemSet.toArray(new Item[itemSet.size()])); // 把数据刷新进对应的连接实体 for (KEntity entity : entitySet) { ItemState state = itemMap.get(entity.item); if (null != state) { JIVariant var = state.getValue(); switch (entity.type) { case Boolean: entity.value = var.getObjectAsBoolean(); break; case Char: entity.value = var.getObjectAsChar(); break; case Short: entity.value = var.getObjectAsShort(); break; case Int: entity.value = var.getObjectAsInt(); break; case Float: entity.value = var.getObjectAsFloat(); break; } } } } catch (Exception e) { logger.error(e.getMessage(), e); if (e instanceof JIException) {// 如果是连接问题, 那把移除server, 并重新来过, 不然不会自动连接的 for (KEntity entity : entitySet) { entity.value = null; entity.item = null; entity.group = null; } server = null; groupSet.clear(); groupItmeMap.clear(); reInitServer(); } } } } } } public Object getVal() { return this.value; } public void setVal(Object obj) { if (null == server) { return; } if (null == item) { initEntity();// 刷新group和item } if (null == wirteDataTimer) { wirteDataTimer = new Timer(); } // 延时100毫秒去写入数据, 主要目的是使用另一个线程写数据, 不影响界面的刷新, 防止界面卡顿 wirteDataTimer.schedule(new TimerTask() { @Override public void run() { JIVariant jiv = null; switch (type) { case Boolean: jiv = new JIVariant((Boolean) obj); break; case Char: jiv = new JIVariant((Character) obj); break; case Short: jiv = new JIVariant((Short) obj); break; case Int: jiv = new JIVariant((Integer) obj); break; case Float: jiv = new JIVariant((Float) obj); break; } try { item.write(jiv); } catch (Exception e) { value = null; logger.error(e.getMessage(), e); if (e instanceof JIException) {// 如果是连接问题, 那把移除server, 并重新来过, 不然不会自动连接的 group = null; item = null; server = null; groupSet.clear(); groupItmeMap.clear(); reInitServer(); initEntity();// 刷新group和item } } } }, 100); } 

讯享网

}

图片描述
讯享网连接实体类的目录

import com.kep.entity.generated.KEntity;

public class KBoolean extends KEntity {

讯享网public KBoolean(String name) { super(name); } public KBoolean(String name, String connName) { super(name, connName); } / * boolean类型特殊一点, 没读到数据, 这里就返回false, 不搞null 了 * @return */ public boolean getValue() { return null == getVal() ? false : (boolean) getVal(); } public void setValue(boolean value) { setVal(value); } / * 置位 */ public void setBit() { setValue(true); } / * 复位 */ public void resetBit() { setValue(false); } / * 位反转 */ public void invertBit() { if (null == getVal()) {// 没有读到数值的话, 就不要搞事情了 return; } setValue(!getValue()); } 

}

import com.kep.entity.generated.KEntity;

/

  • 一般我们使用PLC的单个子节基本都是使用的是byte, 但是utgard 并不提供byte类型的数据, 只提供char类型
  • 其实这里使用效果是一样的
  • @author root
public KChar(String name) { super(name); } public KChar(String name, String connName) { super(name, connName); } / * char这里和boolean类型一样, 如果是空的, 就返回 0 * * @return */ public char getValue() { return null == getVal() ? 0 : (char) getVal(); } public void setValue(char value) { setVal(value); } 

}

import com.kep.entity.generated.KEntity;

public class KShort extends KEntity {

讯享网public KShort(String name, String connName) { super(name, connName); } public KShort(String name) { super(name); } public short getValue() { return (short) getVal(); } public void setValue(short value) { setVal(value); } 

}

OPC Server 对应的Int变量

package com.kep.entity;

import com.kep.entity.generated.KEntity;

public class KInt extends KEntity {

public KInt(String name) { super(name); } public KInt(String name, String connName) { super(name, connName); } public Integer getValue() { return (Integer) getVal(); } public void setValue(int value) { setVal(value); } 

}

import com.kep.entity.generated.KEntity;

public class KFloalt extends KEntity{

讯享网public KFloalt(String name) { super(name); } public KFloalt(String name, String connName) { super(name, connName); } public Float getValue() { return (Float) getValue(); } public void setValue(float value) { setVal(value); } 

}

public class Test2 {

public static void main(String[] args) { KEntity.initDefaultConn("192.168.160.141", "plcData", "123", "conn.smart"); KBoolean Q0_7 = new KBoolean("Q0.7"); while (true) { try { System.out.println("数值展示: " + Q0_7.getValue()); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } 

}

Kep Server 的连接目录及变量命名规范(本程序的命名规范)
在这里插入图片描述

小讯
上一篇 2025-01-18 17:54
下一篇 2025-01-19 07:58

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/121554.html