From bb533bf8b2351abb200c379b2b411eb69b34a821 Mon Sep 17 00:00:00 2001 From: liyongde <1419499670@qq.com> Date: Tue, 17 Mar 2026 13:34:43 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9Aopcda=E5=8D=8F=E8=AE=AE=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nl/iot/core/driver/entity/WResponse.java | 3 + .../opcda/OpcDaProtocolDriverImpl.java | 85 +++++---- .../opcda/util/JIVariantToStringUtil.java | 152 +++++++++------- nl-web-app/src/test/java/org/nl/ApiTest.java | 167 ++++++++++++++++++ 4 files changed, 311 insertions(+), 96 deletions(-) diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WResponse.java index a3f8b75..70b4cb4 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WResponse.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WResponse.java @@ -24,5 +24,8 @@ public class WResponse { */ private WValue wValue; + /** + * 异常信息 + */ private String exceptionMessage; } diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/OpcDaProtocolDriverImpl.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/OpcDaProtocolDriverImpl.java index cff1637..e3ab6fc 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/OpcDaProtocolDriverImpl.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/OpcDaProtocolDriverImpl.java @@ -3,11 +3,11 @@ package org.nl.iot.core.driver.protocol.opcda; import cn.hutool.core.util.ObjectUtil; import com.alibaba.fastjson.JSONObject; import jakarta.annotation.PostConstruct; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.jinterop.dcom.common.JIException; import org.jinterop.dcom.core.JIVariant; import org.nl.common.exception.CommonException; -import org.nl.iot.core.driver.bo.AttributeBO; import org.nl.iot.core.driver.bo.DeviceBO; import org.nl.iot.core.driver.bo.MetadataEventDTO; import org.nl.iot.core.driver.bo.SiteBO; @@ -22,11 +22,10 @@ import org.nl.iot.core.driver.protocol.opcda.org.openscada.opc.lib.common.NotCon import org.nl.iot.core.driver.protocol.opcda.org.openscada.opc.lib.da.*; import org.nl.iot.core.driver.protocol.opcda.util.JIVariantToStringUtil; import org.nl.iot.core.driver.service.DriverCustomService; -import org.nl.iot.modular.iot.entity.IotConfig; -import org.nl.iot.modular.iot.entity.IotConnect; import org.springframework.stereotype.Service; import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -83,13 +82,11 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { @Override public List batchRead(DeviceBO device, List point) { - // todo - return List.of(); + return batchReadValue(getConnector(device), device, point); } @Override public List batchWrite(DeviceBO device, List wValue) { - // todo return List.of(); } @@ -137,6 +134,29 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { } } + @SneakyThrows + private List batchReadValue(Server server, DeviceBO deviceBO, List points) { + List res = new ArrayList<>(); + Map itemsMap = getItems(server, deviceBO, points); + for (Item item : itemsMap.values()) { + RValue rValue = new RValue(); + rValue.setDeviceBO(deviceBO); + SiteBO site = findSite(points, deviceBO, item.getId()); + rValue.setSiteBO(ObjectUtil.isNotEmpty(site) ? site : null); + try { + // 组装数据 + String value = readItem(item); + rValue.setValue(value); + rValue.setExceptionMessage(""); + } catch (JIException e) { + log.error("读取失败: {} -> {}", item.getId(), e.getMessage()); + rValue.setExceptionMessage("读取失败!"); + } + res.add(rValue); + } + return res; + } + /** * 获取 OPC DA 服务器中的 Item 对象 *

@@ -155,6 +175,31 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { return group.addItem(groupName + "." + siteBO.getDeviceCode() + "." + siteBO.getAlias()); } + @SneakyThrows + public Map getItems(Server server, DeviceBO deviceBO, List points) { + Group group; + String groupName = deviceBO.getCode(); + try { + group = server.findGroup(groupName); + } catch (UnknownGroupException e) { + group = server.addGroup(groupName); + } + String[] arr = points.stream() + .map(site -> groupName + "." + site.getDeviceCode() + "." + site.getAlias()) + .toArray(String[]::new); + return group.addItems(arr); + } + + public SiteBO findSite(List points, DeviceBO deviceBO, String key) { + for (SiteBO point : points) { + String itemKey = deviceBO.getCode() + "." + point.getDeviceCode() + "." + point.getAlias(); + if (itemKey.equals(key)) { + return point; + } + } + return null; + } + /** * 读取 OPC DA 位号值 *

@@ -168,34 +213,6 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { */ public String readItem(Item item) throws JIException { JIVariant jiVariant = item.read(false).getValue(); -// return JIVariantToStringUtil.convertFromOpenSCADA(jiVariant); -// switch (jiVariant.getType()) { -// case JIVariant.VT_I2: -// short shortValue = jiVariant.getObjectAsShort(); -// return String.valueOf(shortValue); -// case JIVariant.VT_UI2: -// short shortValue2 = jiVariant.getObjectAsShort(); -// return String.valueOf(shortValue2 & 0xFFFF); -// case JIVariant.VT_I4: -// int intValue = jiVariant.getObjectAsInt(); -// return String.valueOf(intValue); -// case JIVariant.VT_I8: -// long longValue = jiVariant.getObjectAsLong(); -// return String.valueOf(longValue); -// case JIVariant.VT_R4: -// float floatValue = jiVariant.getObjectAsFloat(); -// return String.valueOf(floatValue); -// case JIVariant.VT_R8: -// double doubleValue = jiVariant.getObjectAsDouble(); -// return String.valueOf(doubleValue); -// case JIVariant.VT_BOOL: -// boolean boolValue = jiVariant.getObjectAsBoolean(); -// return String.valueOf(boolValue); -// case JIVariant.VT_BSTR: -// return jiVariant.getObjectAsString2(); -// default: -// return jiVariant.getObject().toString(); -// } return String.valueOf(JIVariantToStringUtil.getValue(jiVariant)); } diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/util/JIVariantToStringUtil.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/util/JIVariantToStringUtil.java index 5a03bc8..5635771 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/util/JIVariantToStringUtil.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcda/util/JIVariantToStringUtil.java @@ -158,72 +158,100 @@ public class JIVariantToStringUtil { return convertToString(itemValue); } + /** + * 获取JIVariant的实际值,适配不同数据类型 + * @param jiVariant JIVariant对象 + * @return 转换后的实际值,空值类型返回null + * @throws CommonException 当DCOM解析异常时抛出 + */ public static Object getValue(JIVariant jiVariant) { + if (jiVariant == null) { + return null; + } + try { - Object e = jiVariant.getObject(); - if (e instanceof IJIUnsigned) { - return Integer.valueOf(((IJIUnsigned) e).getValue().intValue()); - } else if (e instanceof Boolean) { - return jiVariant.getObjectAsBoolean() ? Integer.valueOf(1) : Integer.valueOf(0); - } else if (e instanceof JIString) { - return ((JIString) e).getString(); - } else if (!(e instanceof JIArray)) { - if (e instanceof Integer) { - return jiVariant.getObject(); - } else if (e instanceof Short) { - return jiVariant.getObject(); - } else if (e instanceof Float) { - return jiVariant.getObject(); - } else if (e instanceof Double) { - return jiVariant.getObject(); - } else { - if (jiVariant.getType() == 0) { - System.err.println("因类型为emtpy 返回 null"); - return null; - } else if (jiVariant.getType() == 1) { - System.err.println("因类型为null 返回 null"); - return null; - } else { - return jiVariant.getObject(); - } - } - } else { - Class clazz = ((JIArray) e).getArrayClass(); - int[] r; - int i; - if (JIUnsignedByte.class.isAssignableFrom(clazz)) { - JIUnsignedByte[] arg7 = (JIUnsignedByte[]) ((JIUnsignedByte[]) ((JIArray) e).getArrayInstance()); - r = new int[arg7.length]; - - for (i = 0; i < arg7.length; ++i) { - r[i] = arg7[i].getValue().byteValue(); - } - - return r; - } else if (!JIUnsignedShort.class.isAssignableFrom(clazz)) { - if (jiVariant.getType() == 0) { - System.err.println("因类型为emtpy 返回 null"); - return null; - } else if (jiVariant.getType() == 1) { - System.err.println("因类型为null 返回 null"); - return null; - } else { - return ((JIArray) e).getArrayInstance(); - } - } else { - JIUnsignedShort[] array = (JIUnsignedShort[]) ((JIUnsignedShort[]) ((JIArray) e) - .getArrayInstance()); - r = new int[array.length]; - - for (i = 0; i < array.length; ++i) { - r[i] = array[i].getValue().intValue(); - } - - return r; - } + int type = jiVariant.getType(); + + // 处理空值类型 + if (type == JIVariant.VT_EMPTY || type == JIVariant.VT_NULL) { + return null; } + + Object value = jiVariant.getObject(); + + // 处理无符号整数类型 + if (value instanceof IJIUnsigned) { + return ((IJIUnsigned) value).getValue().intValue(); + } + + // 处理布尔类型 - 转换为整数表示 + if (value instanceof Boolean) { + return jiVariant.getObjectAsBoolean() ? 1 : 0; + } + + // 处理字符串类型 + if (value instanceof JIString) { + return ((JIString) value).getString(); + } + + // 处理数组类型 + if (value instanceof JIArray) { + return handleArrayValue((JIArray) value, type); + } + + // 处理基本数据类型 + if (isBasicType(value)) { + return value; + } + + // 其他类型直接返回原始对象 + return value; + } catch (JIException e) { - throw new CommonException(e.getMessage()); + throw new CommonException("JIVariant解析异常: " + e.getMessage(), e); } } + + /** + * 处理数组类型的值 + */ + private static Object handleArrayValue(JIArray jiArray, int type) throws JIException { + Class arrayClass = jiArray.getArrayClass(); + Object arrayInstance = jiArray.getArrayInstance(); + + // 处理无符号字节数组 + if (JIUnsignedByte.class.isAssignableFrom(arrayClass)) { + JIUnsignedByte[] byteArray = (JIUnsignedByte[]) arrayInstance; + int[] result = new int[byteArray.length]; + for (int i = 0; i < byteArray.length; i++) { + result[i] = byteArray[i].getValue().byteValue() & 0xFF; // 确保无符号 + } + return result; + } + + // 处理无符号短整型数组 + if (JIUnsignedShort.class.isAssignableFrom(arrayClass)) { + JIUnsignedShort[] shortArray = (JIUnsignedShort[]) arrayInstance; + int[] result = new int[shortArray.length]; + for (int i = 0; i < shortArray.length; i++) { + result[i] = shortArray[i].getValue().intValue() & 0xFFFF; // 确保无符号 + } + return result; + } + + // 其他数组类型直接返回数组实例 + return arrayInstance; + } + + /** + * 判断是否为基本数据类型 + */ + private static boolean isBasicType(Object value) { + return value instanceof Integer || + value instanceof Short || + value instanceof Float || + value instanceof Double || + value instanceof Long || + value instanceof Byte; + } } diff --git a/nl-web-app/src/test/java/org/nl/ApiTest.java b/nl-web-app/src/test/java/org/nl/ApiTest.java index f0982a0..da2b262 100644 --- a/nl-web-app/src/test/java/org/nl/ApiTest.java +++ b/nl-web-app/src/test/java/org/nl/ApiTest.java @@ -1648,4 +1648,171 @@ public class ApiTest { e.printStackTrace(); } } + + @Test + public void opcDaBatchReadTest() { + // 构建OPC DA连接对象 + JSONObject properties = new JSONObject(); + properties.put("clsId", "7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729"); // Kepware OPC Server CLSID + properties.put("username", "administrator"); + properties.put("password", "P@ssw0rd."); + + IotConnect connect = IotConnect.builder() + .id(4) + .code("B_CBJ01") + .host("192.168.81.251") + .port(135) // OPC DA默认端口 + .properties(JSONObject.toJSONString(properties)) + .protocol("opc-da") + .enabled(true) + .description("测试OPC DA连接") + .build(); + + // 构建多个配置对象进行批量读取 + IotConfig config1 = IotConfig.builder() + .id(1) + .connectId(4) + .alias("action1") + .aliasName("温度传感器") + .registerAddress("Channel1.Device1.Temperature") // OPC DA标签地址格式 + .dataType("WORD") + .readonly(true) + .enabled(true) + .description("测试OPC DA温度读取") + .build(); + + IotConfig config2 = IotConfig.builder() + .id(2) + .connectId(4) + .alias("mode") + .aliasName("压力传感器") + .registerAddress("Channel1.Device1.Pressure") + .dataType("WORD") + .readonly(true) + .enabled(true) + .description("测试OPC DA压力读取") + .build(); + + IotConfig config3 = IotConfig.builder() + .id(3) + .connectId(4) + .alias("material1") + .aliasName("湿度传感器") + .registerAddress("Channel1.Device1.Humidity") + .dataType("STRING") + .readonly(true) + .enabled(true) + .description("测试OPC DA湿度读取") + .build(); + +// IotConfig config4 = IotConfig.builder() +// .id(4) +// .connectId(4) +// .alias("status") +// .aliasName("设备状态") +// .registerAddress("Channel1.Device1.Status") +// .dataType("BOOLEAN") +// .readonly(true) +// .enabled(true) +// .description("测试OPC DA状态读取") +// .build(); + + // 执行批量读取操作 + try { + System.out.println("========== 开始测试OPC DA批量读取 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("CLSID: " + properties.getString("clsId")); + System.out.println("用户名: " + properties.getString("username")); + + // 转换为DeviceBO + DeviceBO deviceBO = DeviceBO.builder() + .id(String.valueOf(connect.getId())) + .code(connect.getCode()) + .properties(connect.getProperties()) + .host(connect.getHost()) + .port(connect.getPort()) + .protocol(connect.getProtocol()) + .build(); + + // 转换为SiteBO列表 + java.util.List siteBOList = java.util.Arrays.asList( + SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config1.getAlias()) + .aliasName(config1.getAliasName()) + .registerAddress(config1.getRegisterAddress()) + .dataType(config1.getDataType()) + .readonly(config1.getReadonly()) + .build(), + SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config2.getAlias()) + .aliasName(config2.getAliasName()) + .registerAddress(config2.getRegisterAddress()) + .dataType(config2.getDataType()) + .readonly(config2.getReadonly()) + .build(), + SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config3.getAlias()) + .aliasName(config3.getAliasName()) + .registerAddress(config3.getRegisterAddress()) + .dataType(config3.getDataType()) + .readonly(config3.getReadonly()) + .build() +// SiteBO.builder() +// .deviceCode(connect.getCode()) +// .alias(config4.getAlias()) +// .aliasName(config4.getAliasName()) +// .registerAddress(config4.getRegisterAddress()) +// .dataType(config4.getDataType()) +// .readonly(config4.getReadonly()) +// .build() + ); + + System.out.println("批量读取标签数量: " + siteBOList.size()); + for (SiteBO site : siteBOList) { + System.out.println(" - " + site.getAliasName() + " (" + site.getAlias() + "): " + + site.getRegisterAddress() + " [" + site.getDataType() + "]"); + } + + // 调用驱动批量读取数据 + List result = opcDaProtocolDriver.batchRead(deviceBO, siteBOList); + + System.out.println("批量读取完成!"); + System.out.println("读取结果:"); + + // 输出每个标签的读取结果 + for (RValue rValue : result) { + SiteBO site = rValue.getSiteBO(); + String value = rValue.getValue(); + String exceptionMsg = rValue.getExceptionMessage(); + + if (site != null) { + System.out.println(" - " + site.getAliasName() + " (" + site.getAlias() + "): " + + (value != null ? value : "null") + + (exceptionMsg != null && !exceptionMsg.isEmpty() ? " [" + exceptionMsg + "]" : "")); + } else { + System.out.println(" - 未知标签: " + + (value != null ? value : "null") + + (exceptionMsg != null && !exceptionMsg.isEmpty() ? " [" + exceptionMsg + "]" : "")); + } + } + + System.out.println("========== OPC DA批量读取测试完成 =========="); + + } catch (Exception e) { + System.err.println("OPC DA批量读取测试失败: " + e.getMessage()); + e.printStackTrace(); + + // 提供故障排除建议 + System.err.println("\n========== 故障排除建议 =========="); + System.err.println("1. 检查OPC DA服务器是否正在运行"); + System.err.println("2. 验证CLSID是否正确: " + properties.getString("clsId")); + System.err.println("3. 确认标签地址格式是否正确"); + System.err.println("4. 检查DCOM配置和防火墙设置"); + System.err.println("5. 验证用户权限和密码"); + System.err.println("6. 建议先使用OPC客户端工具(如Matrikon OPC Explorer)测试连接"); + } + } }