diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java index 83cf056..5fd8c6a 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java @@ -53,6 +53,8 @@ public enum PointTypeFlagEnum { * 整数 */ INT((byte) 3, "int", "整数"), + WORD((byte) 8, "word", "整数"), + DWORD((byte) 9, "dword", "整数"), /** * 长整数 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 e3ab6fc..065620d 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 @@ -86,8 +86,8 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { } @Override - public List batchWrite(DeviceBO device, List wValue) { - return List.of(); + public List batchWrite(DeviceBO device, List wValues) { + return batchWriteValue(device, wValues); } /** @@ -157,6 +157,20 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { return res; } + public List batchWriteValue(DeviceBO device, List wValues) { + Server server = getConnector(device); + List list = new ArrayList<>(); + for (WValue wValue : wValues) { + try { + Boolean writeOk = writeValue(server, device, wValue); + list.add(new WResponse(writeOk, wValue, writeOk ? "" : "写入失败!")); + } catch (Exception e) { + list.add(new WResponse(false, wValue, e.getMessage())); + } + } + return list; + } + /** * 获取 OPC DA 服务器中的 Item 对象 *

@@ -228,7 +242,7 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { * @return boolean 返回写入操作是否成功 * @throws WritePointException 如果写入位号值时发生异常, 则抛出此异常 */ - private boolean writeValue(Server server, DeviceBO deviceBO, WValue wValue) { + private Boolean writeValue(Server server, DeviceBO deviceBO, WValue wValue) { try { Item item = getItem(server, deviceBO, wValue.getPoint()); return writeItem(item, wValue); @@ -253,7 +267,7 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { * @throws JIException 如果与 OPC DA 服务器通信时发生错误, 则抛出此异常 * @throws UnSupportException 如果写入值的数据类型不支持, 则抛出此异常 */ - private boolean writeItem(Item item, WValue wValue) throws JIException { + private Boolean writeItem(Item item, WValue wValue) throws JIException { PointTypeFlagEnum valueType = PointTypeFlagEnum.ofCode(wValue.getType()); if (Objects.isNull(valueType)) { throw new CommonException("Unsupported type of " + wValue.getType()); @@ -265,6 +279,8 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { short shortValue = wValue.getValueByClass(Short.class); writeResult = item.write(new JIVariant(shortValue, false)); break; + case DWORD: + case WORD: case INT: int intValue = wValue.getValueByClass(Integer.class); writeResult = item.write(new JIVariant(intValue, false)); @@ -291,6 +307,6 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService { default: break; } - return writeResult > 0; + return writeResult == 0; } } diff --git a/nl-web-app/OPC_DA_测试说明.md b/nl-web-app/OPC_DA_测试说明.md new file mode 100644 index 0000000..8e1b649 --- /dev/null +++ b/nl-web-app/OPC_DA_测试说明.md @@ -0,0 +1,105 @@ +# OPC DA 测试说明 + +## 错误:Class not registered [0x80040154] + +这个错误表示指定的 CLSID 在目标机器上没有注册。需要获取正确的 OPC DA 服务器 CLSID。 + +## 如何获取正确的 CLSID + +### 方法 1:使用 Windows 注册表 + +在 OPC DA 服务器所在的机器(192.168.81.251)上执行: + +```cmd +# 查找所有 OPC 相关的 CLSID +reg query "HKEY_CLASSES_ROOT\CLSID" /s /f "OPC" + +# 或者查找特定的 OPC Server +reg query "HKEY_CLASSES_ROOT\CLSID" /s /f "Kepware" +``` + +### 方法 2:使用 OPC 客户端工具 + +推荐使用以下工具之一: +- **Matrikon OPC Explorer**(免费) +- **Kepware OPC Quick Client** +- **OPC Foundation 的 OPC Test Client** + +步骤: +1. 在工具中连接到 OPC 服务器 +2. 查看服务器属性,可以看到 CLSID +3. 复制 CLSID(不要包含花括号) + +### 方法 3:查看 OPC 服务器文档 + +查看你使用的 OPC DA 服务器的安装文档或配置文件。 + +## 常见 OPC DA 服务器的 CLSID + +| OPC 服务器 | CLSID(不含花括号) | +|-----------|-------------------| +| Kepware OPC Server | 7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729 | +| Matrikon OPC Server | F8582CF2-88FB-11D0-B850-00C0F0104305 | +| RSLinx OPC Server | A06B0CF6-4E1F-11D3-8F4D-00104B33C6E8 | +| Siemens OPC Server | 各版本不同,需查看文档 | + +**注意**:实际的 CLSID 可能因版本不同而不同,请以实际安装的服务器为准。 + +## 修改测试代码 + +在 `ApiTest.java` 中找到以下代码: + +```java +properties.put("clsId", "7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729"); // 修改为实际的 CLSID +``` + +将 CLSID 替换为你从上述方法获取的实际值。 + +## 其他可能的问题 + +### 1. DCOM 配置 + +确保目标机器的 DCOM 配置正确: + +1. 运行 `dcomcnfg` +2. 找到 OPC 服务器 +3. 配置安全设置和启动权限 + +### 2. 防火墙设置 + +确保防火墙允许 DCOM 通信: +- 端口 135(RPC) +- 动态端口范围(通常是 49152-65535) + +### 3. 用户权限 + +确保使用的用户名和密码具有访问 OPC 服务器的权限。 + +### 4. OPC Core Components + +确保目标机器已安装 OPC Core Components: +- 下载地址:https://opcfoundation.org/developer-tools/samples-and-tools-classic/core-components/ + +## 标签地址格式 + +OPC DA 标签地址格式通常为: +``` +Channel1.Device1.Tag1 +``` + +具体格式取决于你的 OPC 服务器配置。可以使用 OPC 客户端工具浏览可用的标签。 + +## 测试步骤 + +1. 确认 OPC DA 服务器正在运行 +2. 获取正确的 CLSID +3. 修改测试代码中的 CLSID +4. 修改标签地址为实际存在的标签 +5. 运行测试 + +## 调试建议 + +如果仍然遇到问题,可以: +1. 先使用 OPC 客户端工具(如 Matrikon OPC Explorer)测试连接 +2. 确认可以成功连接和读取数据后,再运行 Java 测试代码 +3. 检查日志中的详细错误信息 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 da2b262..67b326a 100644 --- a/nl-web-app/src/test/java/org/nl/ApiTest.java +++ b/nl-web-app/src/test/java/org/nl/ApiTest.java @@ -1572,7 +1572,7 @@ public class ApiTest { IotConnect connect = IotConnect.builder() .id(3) - .code("OPC_DA_001") + .code("B_CBJ01") .host("192.168.81.251") .port(0) // OPC DA不使用端口 .properties(JSONObject.toJSONString(properties)) @@ -1585,10 +1585,10 @@ public class ApiTest { IotConfig config = IotConfig.builder() .id(2) .connectId(3) - .alias("tag2") + .alias("to_spec1") .aliasName("标签2") .registerAddress("Channel1.Device1.Tag2") // OPC DA标签地址,格式:通道.设备.标签 - .dataType("INT") + .dataType("STRING") .readonly(false) // 设置为可写 .enabled(true) .description("测试OPC DA写入") @@ -1614,7 +1614,7 @@ public class ApiTest { // 转换为SiteBO SiteBO siteBO = SiteBO.builder() - .deviceCode(connect.getCode()) + .deviceCode("B_CBJ01.B_CBJ01") .alias(config.getAlias()) .aliasName(config.getAliasName()) .registerAddress(config.getRegisterAddress()) @@ -1623,7 +1623,7 @@ public class ApiTest { .build(); // 构建写入值对象 - String writeValue = "100"; // 要写入的值 + String writeValue = "open|123|第三方"; // 要写入的值 WValue wValue = WValue.builder() .point(siteBO) .value(writeValue) @@ -1637,7 +1637,7 @@ public class ApiTest { if (result != null && result) { System.out.println("写入成功!"); - System.out.println("写入结果: " + result); + System.out.println("写入结果: " + true); } else { System.out.println("写入失败!"); } @@ -1815,4 +1815,160 @@ public class ApiTest { System.err.println("6. 建议先使用OPC客户端工具(如Matrikon OPC Explorer)测试连接"); } } + + @Test + public void opcDaBatchWriteTest() { + // 构建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("to_qty1") + .aliasName("输出值1") + .registerAddress("Channel1.Device1.Output1") // OPC DA标签地址格式 + .dataType("WORD") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入1") + .build(); + + IotConfig config2 = IotConfig.builder() + .id(2) + .connectId(4) + .alias("to_spec2") + .aliasName("输出值2") + .registerAddress("Channel1.Device1.Output2") + .dataType("STRING") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入2") + .build(); + + IotConfig config3 = IotConfig.builder() + .id(3) + .connectId(4) + .alias("to_spec1") + .aliasName("输出值3") + .registerAddress("Channel1.Device1.Output3") + .dataType("STRING") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入3") + .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并构建WValue列表 + java.util.List wValueList = java.util.Arrays.asList( + WValue.builder() + .point(SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config1.getAlias()) + .aliasName(config1.getAliasName()) + .registerAddress(config1.getRegisterAddress()) + .dataType(config1.getDataType()) + .readonly(config1.getReadonly()) + .build()) + .value("12") // 写入值1 + .type(config1.getDataType()) + .build(), + WValue.builder() + .point(SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config2.getAlias()) + .aliasName(config2.getAliasName()) + .registerAddress(config2.getRegisterAddress()) + .dataType(config2.getDataType()) + .readonly(config2.getReadonly()) + .build()) + .value("open") // 写入值2 + .type(config2.getDataType()) + .build(), + WValue.builder() + .point(SiteBO.builder() + .deviceCode("B_CBJ01.B_CBJ01") + .alias(config3.getAlias()) + .aliasName(config3.getAliasName()) + .registerAddress(config3.getRegisterAddress()) + .dataType(config3.getDataType()) + .readonly(config3.getReadonly()) + .build()) + .value("dream") // 写入值3 + .type(config3.getDataType()) + .build() + ); + + System.out.println("批量写入标签数量: " + wValueList.size()); + for (WValue wValue : wValueList) { + SiteBO site = wValue.getPoint(); + System.out.println(" - " + site.getAliasName() + " (" + site.getAlias() + "): " + + site.getRegisterAddress() + " [" + site.getDataType() + "] = " + wValue.getValue()); + } + + // 调用驱动批量写入数据 + List result = opcDaProtocolDriver.batchWrite(deviceBO, wValueList); + + System.out.println("批量写入完成!"); + System.out.println("写入结果:"); + + // 输出每个标签的写入结果 + for (WResponse wResponse : result) { + WValue wValue = wResponse.getWValue(); + SiteBO site = wValue.getPoint(); + Boolean success = wResponse.getIsOk(); + System.out.println(" - " + site.getAliasName() + " (" + site.getAlias() + "): " + + "写入值=" + wValue.getValue() + " 结果=" + (success ? "成功" : "失败")); + if (!success && wResponse.getExceptionMessage() != null && !wResponse.getExceptionMessage().isEmpty()) { + System.out.println(" 错误信息: " + wResponse.getExceptionMessage()); + } + } + + 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. 确认标签的数据类型与写入值匹配"); + System.err.println("7. 建议先使用OPC客户端工具(如Matrikon OPC Explorer)测试写入"); + } + } }