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 c1d1f64..83cf056 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 @@ -109,7 +109,7 @@ public enum PointTypeFlagEnum { * @return {@link PointTypeFlagEnum} */ public static PointTypeFlagEnum ofCode(String code) { - Optional any = Arrays.stream(PointTypeFlagEnum.values()).filter(type -> type.getCode().equals(code)).findFirst(); + Optional any = Arrays.stream(PointTypeFlagEnum.values()).filter(type -> type.getCode().equals(code.toLowerCase())).findFirst(); return any.orElse(null); } diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcua/OpcUaProtocolDriverImpl.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcua/OpcUaProtocolDriverImpl.java index 004e517..7b33435 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcua/OpcUaProtocolDriverImpl.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/opcua/OpcUaProtocolDriverImpl.java @@ -5,6 +5,7 @@ import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider; +import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; @@ -12,18 +13,22 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; +import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue; 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; import org.nl.iot.core.driver.entity.RValue; +import org.nl.iot.core.driver.entity.WResponse; import org.nl.iot.core.driver.entity.WValue; import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum; import org.nl.iot.core.driver.enums.PointTypeFlagEnum; 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 jakarta.annotation.PreDestroy; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.*; @@ -46,6 +51,21 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { connectMap = new ConcurrentHashMap<>(16); } +// @PreDestroy +// public void destroy() { +// // 应用关闭时清理所有连接 +// log.info("Cleaning up OPC UA connections..."); +// connectMap.values().forEach(client -> { +// try { +// client.disconnect().get(5, TimeUnit.SECONDS); +// } catch (Exception e) { +// log.warn("Error disconnecting OPC UA client during cleanup: {}", e.getMessage()); +// } +// }); +// connectMap.clear(); +// log.info("OPC UA connections cleanup completed"); +// } + @Override public void schedule() { @@ -58,30 +78,84 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { // When the device is updated or deleted, remove the corresponding connection handle if (MetadataOperateTypeEnum.DELETE.equals(operateType) || MetadataOperateTypeEnum.UPDATE.equals(operateType)) { - connectMap.remove(metadataEvent.getConnectId()); + OpcUaClient client = connectMap.remove(metadataEvent.getConnectId()); + if (client != null) { + try { + client.disconnect().get(5, TimeUnit.SECONDS); + log.info("Disconnected OPC UA client for device: {}", metadataEvent.getConnectId()); + } catch (Exception e) { + log.warn("Error disconnecting OPC UA client: {}", e.getMessage()); + } + } } } @Override - public RValue read(IotConnect connect, IotConfig config) { - return new RValue(config, connect, readValue(getConnector(connect), config)); + public RValue read(DeviceBO device, SiteBO point) { + return readValue(device, point); + } + + @Override + public Boolean write(DeviceBO device, WValue wValue) { + OpcUaClient client = getConnector(device); + return writeValue(client, device, wValue); + } + + @Override + public List batchRead(DeviceBO device, List point) { + return batchReadValue(getConnector(device), device, point); + } + + @Override + public List batchWrite(DeviceBO device, List wValue) { + int maxRetries = 2; // 最大重试次数 + + for (int retry = 0; retry <= maxRetries; retry++) { + try { + OpcUaClient client = getConnector(device); + return batchWriteNodes(client, wValue); + } catch (Exception e) { + log.error("批量写入失败 (重试 {}/{}): {}", retry + 1, maxRetries + 1, e.getMessage()); + + // 如果是会话相关错误,清除缓存的连接并重试 + if (e.getMessage().contains("SessionClosed") || e.getMessage().contains("Bad_SessionClosed")) { + log.warn("检测到会话关闭错误,清除缓存连接并重试"); + connectMap.remove(device.getId()); + if (retry < maxRetries) { + continue; + } + } + + // 最后一次重试失败,为每个写入值创建错误结果 + if (retry == maxRetries) { + List errorResponses = new ArrayList<>(); + for (WValue wVal : wValue) { + errorResponses.add(new WResponse(false, wVal, e.getMessage())); + } + return errorResponses; + } + } + } + + // 理论上不会到达这里 + List errorResponses = new ArrayList<>(); + for (WValue wVal : wValue) { + errorResponses.add(new WResponse(false, wVal, "未知错误")); + } + return errorResponses; } /** * 获取 OPC UA 客户端连接 - * - * @param deviceId 设备ID, 用于标识唯一的设备连接 - * @param driverConfig 驱动配置信息, 包含连接 OPC UA 服务器所需的参数 - * @return OpcUaClient 返回与指定设备关联的 OPC UA 客户端实例 - * @throws CommonException 如果连接 OPC UA 服务器失败, 抛出此异常 */ - private OpcUaClient getConnector(IotConnect connect) { - log.debug("OPC UA server connection info: {}", connect); - OpcUaClient opcUaClient = connectMap.get(connect.getId().toString()); + private OpcUaClient getConnector(DeviceBO device) { + log.debug("OPC UA server connection info: {}", device); + OpcUaClient opcUaClient = connectMap.get(device.getId()); + if (Objects.isNull(opcUaClient)) { - JSONObject driverConfig = JSONObject.parseObject(connect.getProperties()); - String host = connect.getHost(); - int port = connect.getPort(); + JSONObject driverConfig = JSONObject.parseObject(device.getProperties()); + String host = device.getHost(); + int port = device.getPort(); String path = driverConfig.getString("path"); String url = String.format("opc.tcp://%s:%s%s", host, port, path); try { @@ -91,13 +165,15 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { configBuilder -> configBuilder .setIdentityProvider(new AnonymousProvider()) // 使用匿名身份验证 .setRequestTimeout(Unsigned.uint(5000)) // 设置请求超时时间为 5000 毫秒 + .setSessionTimeout(Unsigned.uint(60000)) // 设置会话超时时间为 60 秒 .build() ); - connectMap.put(connect.getId().toString(), opcUaClient); + connectMap.put(device.getId(), opcUaClient); + log.info("Created new OPC UA client for device: {}", device.getId()); } catch (UaException e) { - connectMap.entrySet().removeIf(next -> next.getKey().equals(connect.getId().toString())); - log.error("Failed to connect OPC UA client: {}", e.getMessage(), e); - throw new CommonException(e.getMessage()); + connectMap.entrySet().removeIf(next -> next.getKey().equals(device.getId())); + log.error("Failed to create OPC UA client: {}", e.getMessage(), e); + throw new IllegalArgumentException(e.getMessage()); } } return opcUaClient; @@ -106,139 +182,208 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { /** * 读取 OPC UA 节点的值 * - * @param client OPC UA 客户端实例 - * @param config 点位配置信息 + * @param device 设备信息 + * @param point 点位配置信息 * @return 读取到的节点值 - * @throws CommonException 如果读取操作失败, 抛出此异常 */ - private String readValue(OpcUaClient client, IotConfig config) { + private RValue readValue(DeviceBO device, SiteBO point) { + int maxRetries = 2; // 最大重试次数 + + for (int retry = 0; retry <= maxRetries; retry++) { + try { + OpcUaClient client = getConnector(device); + NodeId nodeId = NodeId.parse(point.getRegisterAddress()); + // 确保客户端已连接 + client.connect().get(10, TimeUnit.SECONDS); + + // 直接使用同步方式读取值,设置更长的超时时间 + DataValue dataValue = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get(10, TimeUnit.SECONDS); + + // 检查读取状态 + if (dataValue.getStatusCode().isGood()) { + Object value = dataValue.getValue().getValue(); + if (value != null) { + return new RValue(device, point, value.toString(), ""); + } else { + return new RValue(device, point, null, "读取数据为空!"); + } + } else { + return new RValue(device, point, null, "读取数据失败: " + dataValue.getStatusCode().getValue()); + } + } catch (Exception e) { + log.error("读取 OPC UA 节点失败 (重试 {}/{}): {}", retry + 1, maxRetries + 1, e.getMessage()); + + // 如果是会话相关错误,清除缓存的连接并重试 + if (e.getMessage().contains("SessionClosed") || e.getMessage().contains("Bad_SessionClosed")) { + log.warn("检测到会话关闭错误,清除缓存连接并重试"); + connectMap.remove(device.getId()); + if (retry < maxRetries) { + continue; + } + } + + // 最后一次重试失败 + if (retry == maxRetries) { + return new RValue(device, point, null, e.getMessage()); + } + } + } + + return new RValue(device, point, null, "读取失败:超过最大重试次数"); + } + + /** + * 批量读取 + * @param connector + * @param device + * @param point + * @return + */ + private List batchReadValue(OpcUaClient connector, DeviceBO device, List points) { + return batchReadNodes(connector, device, points); + } + + /** + * 核心:批量读取节点值 + * @param client OPC UA客户端 + * @param device 设备信息 + * @param points 点位列表 + * @return 读取结果列表(顺序与nodeIds一致) + */ + private List batchReadNodes(OpcUaClient client, DeviceBO device, List points) { + List list = new ArrayList<>(); + int maxRetries = 2; // 最大重试次数 + + for (int retry = 0; retry <= maxRetries; retry++) { + try { + // 确保客户端已连接 + client.connect().get(10, TimeUnit.SECONDS); + + // 异步批量读取(readValues是Milo推荐的批量读值API) + List nodeIds = buildNodeIds(points); + CompletableFuture> future = client.readValues( + 0.0, // MaxAge=0:强制读取最新值 + TimestampsToReturn.Both, // 返回源时间戳+服务器时间戳 + nodeIds // 批量节点列表 + ); + + // 10秒超时获取结果(异步转同步,新手友好) + List dataValues = future.get(10, TimeUnit.SECONDS); + // 转成List + for (int i = 0; i < dataValues.size(); i++) { + DataValue dataValue = dataValues.get(i); + SiteBO site = getSiteByNodeId(points, nodeIds.get(i)); + // 检查读取状态 + if (dataValue.getStatusCode().isGood()) { + Object value = dataValue.getValue().getValue(); + if (value != null) { + list.add(new RValue(device, site, value.toString(), null)); + } else { + list.add(new RValue(device, site, null, "读取数据为空")); + } + } else { + list.add(new RValue(device, site, null, "读取数据失败: " + dataValue.getStatusCode().getValue())); + } + } + // 成功读取,跳出重试循环 + break; + + } catch (Exception e) { + log.error("批量读取 OPC UA 节点失败 (重试 {}/{}): {}", retry + 1, maxRetries + 1, e.getMessage()); + + // 如果是会话相关错误,清除缓存的连接并重新获取 + if (e.getMessage().contains("SessionClosed") || e.getMessage().contains("Bad_SessionClosed")) { + log.warn("检测到会话关闭错误,清除缓存连接并重试"); + connectMap.remove(device.getId()); + if (retry < maxRetries) { + // 重新获取连接 + client = getConnector(device); + continue; + } + } + + // 最后一次重试失败,为每个点位创建错误结果 + if (retry == maxRetries) { + list.clear(); // 清空之前可能的部分结果 + for (SiteBO point : points) { + list.add(new RValue(device, point, null, e.getMessage())); + } + } + } + } + return list; + } + + /** + * 批量写入核心方法 + */ + private List batchWriteNodes(OpcUaClient client, List wValues) { + List responses = new ArrayList<>(); + try { - NodeId nodeId = getNode(config); // 确保客户端已连接 client.connect().get(10, TimeUnit.SECONDS); - // 直接使用同步方式读取值,设置更长的超时时间 - DataValue dataValue = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get(10, TimeUnit.SECONDS); - - // 检查读取状态 - if (dataValue.getStatusCode().isGood()) { - Object value = dataValue.getValue().getValue(); - return value != null ? value.toString() : null; - } else { - log.error("Read opc ua value failed with status: {}", dataValue.getStatusCode()); - throw new CommonException("Read failed with status: " + dataValue.getStatusCode()); + List dateValues = new ArrayList<>(); + List nodeIds = new ArrayList<>(); + for (WValue wValue : wValues) { + nodeIds.add(NodeId.parse(wValue.getPoint().getRegisterAddress())); + dateValues.add(buildDataValueByType(wValue)); } - } catch (InterruptedException e) { - log.error("Read opc ua value error: {}", e.getMessage(), e); - Thread.currentThread().interrupt(); - throw new CommonException(e.getMessage()); - } catch (ExecutionException | TimeoutException e) { - log.error("Read opc ua value error: {}", e.getMessage(), e); - throw new CommonException(e.getMessage()); + + CompletableFuture> future = client.writeValues(nodeIds, dateValues); + // 10秒超时获取结果 + List statusCodeList = future.get(10, TimeUnit.SECONDS); + + // 转成WResponse + for (int i = 0; i < statusCodeList.size(); i++) { + StatusCode statusCode = statusCodeList.get(i); + responses.add(new WResponse(statusCode.isGood(), wValues.get(i), statusCode.isGood() ? "" : statusCode.toString())); + } + + } catch (Exception e) { + log.error("批量写入 OPC UA 节点失败: {}", e.getMessage()); + throw new RuntimeException(e); } - } - - @Override - public Boolean write(IotConnect connect, IotConfig config, WValue wValue) { - OpcUaClient client = getConnector(connect); - return writeValue(client, config, wValue); + return responses; } /** * 写入 OPC UA 节点的值 - * - * @param client OPC UA 客户端实例 - * @param config 点位配置信息 - * @param wValue 写入值 - * @return 写入操作是否成功 - * @throws CommonException 如果写入操作失败, 抛出此异常 */ - private boolean writeValue(OpcUaClient client, IotConfig config, WValue wValue) { + private boolean writeValue(OpcUaClient client, DeviceBO device, WValue wValue) { try { - NodeId nodeId = getNode(config); + NodeId nodeId = NodeId.parse(wValue.getPoint().getRegisterAddress()); // 确保客户端已连接,设置超时时间 client.connect().get(10, TimeUnit.SECONDS); return writeNode(client, nodeId, wValue); - } catch (InterruptedException e) { - log.error("Write opc ua value error: {}", e.getMessage(), e); - Thread.currentThread().interrupt(); - throw new CommonException(e.getMessage()); - } catch (ExecutionException | TimeoutException e) { - log.error("Write opc ua value error: {}", e.getMessage(), e); + } catch (Exception e) { + log.error("写入 opc ua value 出错: {}", e.getMessage(), e); + // 连接异常时,移除缓存的连接 + connectMap.remove(device.getId()); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } throw new CommonException(e.getMessage()); } } + public SiteBO getSiteByNodeId(List points, NodeId nodeId) { + return points.stream().filter(p -> NodeId.parse(p.getRegisterAddress()).toString().equals(nodeId.toString())).findFirst().orElse(null); + } + /** - * 根据点位配置获取 OPC UA 节点 - * - * @param pointConfig 点位配置信息, 包含命名空间和标签 - * @return OPC UA 节点标识 + * 组装nodeids + * @param points + * @return */ - private NodeId getNode(IotConfig config) { + public List buildNodeIds(List points) { // 解析地址:ns=3;s=Temperature - return parseAddress(config.getRegisterAddress()); - } - - /** - * 解析地址字符串,返回NodeId对象 - * @param address 地址字符串,格式要求:ns=3;s=xxx - * @return 包含namespace(固定3)和tag的NodeId对象 - * @throws IllegalArgumentException 输入格式错误或ns值不为3时抛出异常 - */ - public static NodeId parseAddress(String address) { - // 校验输入不为空 - if (address == null || address.trim().isEmpty()) { - throw new IllegalArgumentException("地址字符串不能为空"); + List list = new ArrayList<>(); + for (SiteBO point : points) { + list.add(NodeId.parse(point.getRegisterAddress())); } - - // 按分号拆分字符串 - String[] parts = address.split(";"); - if (parts.length != 2) { - throw new IllegalArgumentException("地址格式错误,正确格式应为:ns=x;s=xxx"); - } - - int namespace = -1; - String tag = null; - - // 遍历拆分后的部分,提取ns和s的值 - for (String part : parts) { - String[] keyValue = part.split("="); - if (keyValue.length != 2) { - throw new IllegalArgumentException("地址格式错误,键值对格式应为:key=value"); - } - - String key = keyValue[0].trim(); - String value = keyValue[1].trim(); - - switch (key) { - case "ns": - // 解析ns的值并校验是否为3 - try { - namespace = Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("ns的值必须是数字", e); - } - if (namespace != 3) { - throw new IllegalArgumentException("ns的值必须等于3"); - } - break; - case "s": - // 提取s对应的标签值 - tag = value; - break; - default: - throw new IllegalArgumentException("不支持的键:" + key + ",仅支持ns和s"); - } - } - - // 校验s的值不为空 - if (tag == null || tag.isEmpty()) { - throw new IllegalArgumentException("s的标签值不能为空"); - } - - // 返回NodeId对象 - return new NodeId(namespace, tag); + return list; } /** @@ -247,10 +392,6 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { * @param client OPC UA 客户端实例 * @param nodeId OPC UA 节点标识 * @param wValue 待写入的值 - * @return 写入操作是否成功 - * @throws ExecutionException 写入操作失败时抛出 - * @throws InterruptedException 写入操作被中断时抛出 - * @throws TimeoutException 写入操作超时时抛出 */ private boolean writeNode(OpcUaClient client, NodeId nodeId, WValue wValue) throws ExecutionException, InterruptedException, TimeoutException { PointTypeFlagEnum valueType = PointTypeFlagEnum.ofCode(wValue.getType()); @@ -295,4 +436,34 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService { } return false; } + + private DataValue buildDataValueByType(WValue wValue) { + PointTypeFlagEnum valueType = PointTypeFlagEnum.ofCode(wValue.getType()); + if (Objects.isNull(valueType)) { + throw new CommonException("Unsupported type of " + wValue.getType()); + } + return switch (valueType) { + case INT -> { + int intValue = wValue.getValueByClass(Integer.class); + yield new DataValue(new Variant(intValue)); + } + case LONG -> { + long longValue = wValue.getValueByClass(Long.class); + yield new DataValue(new Variant(longValue)); + } + case FLOAT -> { + float floatValue = wValue.getValueByClass(Float.class); + yield new DataValue(new Variant(floatValue)); + } + case DOUBLE -> { + double doubleValue = wValue.getValueByClass(Double.class); + yield new DataValue(new Variant(doubleValue)); + } + case BOOLEAN -> { + boolean booleanValue = wValue.getValueByClass(Boolean.class); + yield new DataValue(new Variant(booleanValue)); + } + default -> new DataValue(new Variant(wValue.getValue())); + }; + } } 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 be800b8..9981391 100644 --- a/nl-web-app/src/test/java/org/nl/ApiTest.java +++ b/nl-web-app/src/test/java/org/nl/ApiTest.java @@ -9,6 +9,7 @@ import org.nl.iot.core.driver.entity.RValue; import org.nl.iot.core.driver.entity.WResponse; import org.nl.iot.core.driver.entity.WValue; import org.nl.iot.core.driver.protocol.modbustcp.ModBusProtocolDriverImpl; +import org.nl.iot.core.driver.protocol.opcua.OpcUaProtocolDriverImpl; import org.nl.iot.core.driver.protocol.plcs7.PlcS7ProtocolDriverImpl; import org.nl.iot.modular.iot.entity.IotConfig; import org.nl.iot.modular.iot.entity.IotConnect; @@ -31,6 +32,12 @@ public class ApiTest { @Autowired private ModBusProtocolDriverImpl modBusProtocolDriver; + @Autowired + private PlcS7ProtocolDriverImpl plcS7ProtocolDriver; + + @Autowired + private OpcUaProtocolDriverImpl opcUaProtocolDriver; + @Test public void modbusTest() { // 构建连接对象 @@ -464,9 +471,6 @@ public class ApiTest { } } - @Autowired - private PlcS7ProtocolDriverImpl plcS7ProtocolDriver; - @Test public void plcS7ReadTest() { // 构建PLC S7连接对象 @@ -958,4 +962,516 @@ public class ApiTest { } } + @Test + public void opcUaReadTest() { + // 构建OPC UA连接对象 + JSONObject properties = new JSONObject(); + properties.put("path", "/OPCUA/SimulationServer"); // OPC UA服务器路径 + + IotConnect connect = IotConnect.builder() + .id(3) + .code("OPC_UA_001") + .host("Lyd-ThinkBook") + .port(53530) // OPC UA默认端口 + .properties(JSONObject.toJSONString(properties)) + .protocol("opc-ua") + .enabled(true) + .description("测试OPC UA连接") + .build(); + + // 构建配置对象 - 读取不同类型的节点 + IotConfig config1 = IotConfig.builder() + .id(1) + .connectId(3) + .alias("temperature") + .aliasName("温度传感器") + .registerAddress("ns=3;s=Temperature") // OPC UA节点ID格式 + .dataType("DOUBLE") + .readonly(true) + .enabled(true) + .description("测试OPC UA温度读取") + .build(); + + IotConfig config2 = IotConfig.builder() + .id(2) + .connectId(3) + .alias("pressure") + .aliasName("压力传感器") + .registerAddress("ns=3;i=1009") + .dataType("FLOAT") + .readonly(true) + .enabled(true) + .description("测试OPC UA压力读取") + .build(); + + IotConfig config3 = IotConfig.builder() + .id(3) + .connectId(3) + .alias("status") + .aliasName("设备状态") + .registerAddress("ns=2;s=DeviceStatus") + .dataType("BOOLEAN") + .readonly(true) + .enabled(true) + .description("测试OPC UA状态读取") + .build(); + + // 执行读取操作 + try { + System.out.println("========== 开始测试OPC UA读取 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("服务器路径: " + properties.getString("path")); + + // 转换为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(); +// +// // 测试读取温度值 +// System.out.println("\n--- 测试读取温度值 ---"); +// SiteBO siteBO1 = SiteBO.builder() +// .deviceCode(connect.getCode()) +// .alias(config1.getAlias()) +// .aliasName(config1.getAliasName()) +// .registerAddress(config1.getRegisterAddress()) +// .dataType(config1.getDataType()) +// .readonly(config1.getReadonly()) +// .build(); +// +// RValue result1 = opcUaProtocolDriver.read(deviceBO, siteBO1); +// System.out.println("节点ID: " + config1.getRegisterAddress()); +// System.out.println("数据类型: " + config1.getDataType()); +// System.out.println("读取结果: " + result1.getValue()); +// if (result1.getExceptionMessage() != null) { +// System.out.println("异常信息: " + result1.getExceptionMessage()); +// } + + // 测试读取压力值 + System.out.println("\n--- 测试读取压力值 ---"); + SiteBO siteBO2 = SiteBO.builder() + .deviceCode(connect.getCode()) + .alias(config2.getAlias()) + .aliasName(config2.getAliasName()) + .registerAddress(config2.getRegisterAddress()) + .dataType(config2.getDataType()) + .readonly(config2.getReadonly()) + .build(); + + RValue result2 = opcUaProtocolDriver.read(deviceBO, siteBO2); + System.out.println("节点ID: " + config2.getRegisterAddress()); + System.out.println("数据类型: " + config2.getDataType()); + System.out.println("读取结果: " + result2.getValue()); + if (result2.getExceptionMessage() != null) { + System.out.println("异常信息: " + result2.getExceptionMessage()); + } + + // 测试读取状态值 +// System.out.println("\n--- 测试读取状态值 ---"); +// SiteBO siteBO3 = SiteBO.builder() +// .deviceCode(connect.getCode()) +// .alias(config3.getAlias()) +// .aliasName(config3.getAliasName()) +// .registerAddress(config3.getRegisterAddress()) +// .dataType(config3.getDataType()) +// .readonly(config3.getReadonly()) +// .build(); +// +// RValue result3 = opcUaProtocolDriver.read(deviceBO, siteBO3); +// System.out.println("节点ID: " + config3.getRegisterAddress()); +// System.out.println("数据类型: " + config3.getDataType()); +// System.out.println("读取结果: " + result3.getValue()); +// if (result3.getExceptionMessage() != null) { +// System.out.println("异常信息: " + result3.getExceptionMessage()); +// } + + System.out.println("\n========== OPC UA读取测试完成 =========="); + + } catch (Exception e) { + System.err.println("OPC UA读取测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Test + public void opcUaBatchReadTest() { + // 构建OPC UA连接对象 + JSONObject properties = new JSONObject(); + properties.put("path", "/OPCUA/SimulationServer"); // OPC UA服务器路径 + + IotConnect connect = IotConnect.builder() + .id(3) + .code("OPC_UA_001") + .host("Lyd-ThinkBook") + .port(53530) // OPC UA默认端口 + .properties(JSONObject.toJSONString(properties)) + .protocol("opc-ua") + .enabled(true) + .description("测试OPC UA连接") + .build(); + + + // 构建多个配置对象进行批量读取 + IotConfig config1 = IotConfig.builder() + .id(1) + .connectId(3) + .alias("temperature") + .aliasName("温度传感器") + .registerAddress("ns=3;s=Temperature") + .dataType("DOUBLE") + .readonly(true) + .enabled(true) + .description("测试OPC UA温度读取") + .build(); + + IotConfig config2 = IotConfig.builder() + .id(2) + .connectId(3) + .alias("pressure") + .aliasName("压力传感器") + .registerAddress("ns=3;i=1009") + .dataType("FLOAT") + .readonly(true) + .enabled(true) + .description("测试OPC UA压力读取") + .build(); + + IotConfig config3 = IotConfig.builder() + .id(3) + .connectId(3) + .alias("humidity") + .aliasName("湿度传感器") + .registerAddress("ns=2;s=Humidity") + .dataType("DOUBLE") + .readonly(true) + .enabled(true) + .description("测试OPC UA湿度读取") + .build(); + + IotConfig config4 = IotConfig.builder() + .id(4) + .connectId(3) + .alias("status") + .aliasName("设备状态") + .registerAddress("ns=2;s=DeviceStatus") + .dataType("BOOLEAN") + .readonly(true) + .enabled(true) + .description("测试OPC UA状态读取") + .build(); + + // 执行批量读取操作 + try { + System.out.println("========== 开始测试OPC UA批量读取 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("服务器路径: " + properties.getString("path")); + + // 转换为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(connect.getCode()) + .alias(config1.getAlias()) + .aliasName(config1.getAliasName()) + .registerAddress(config1.getRegisterAddress()) + .dataType(config1.getDataType()) + .readonly(config1.getReadonly()) + .build(), + SiteBO.builder() + .deviceCode(connect.getCode()) + .alias(config2.getAlias()) + .aliasName(config2.getAliasName()) + .registerAddress(config2.getRegisterAddress()) + .dataType(config2.getDataType()) + .readonly(config2.getReadonly()) + .build() +// SiteBO.builder() +// .deviceCode(connect.getCode()) +// .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 = opcUaProtocolDriver.batchRead(deviceBO, siteBOList); + + System.out.println("批量读取完成!"); + System.out.println("读取结果:"); + + // 输出每个节点的读取结果 + for (RValue rValue : result) { + SiteBO site = rValue.getSiteBO(); + String value = rValue.getValue(); + System.out.println(" - " + site.getAliasName() + " (" + site.getAlias() + "): " + + value + (value == null ? " [" + rValue.getExceptionMessage() + "]" : "")); + } + + System.out.println("========== OPC UA批量读取测试完成 =========="); + + } catch (Exception e) { + System.err.println("OPC UA批量读取测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + + @Test + public void opcUaWriteTest() { + // 构建OPC UA连接对象 + JSONObject properties = new JSONObject(); + properties.put("path", "/OPCUA/SimulationServer"); // OPC UA服务器路径 + + IotConnect connect = IotConnect.builder() + .id(3) + .code("OPC_UA_001") + .host("Lyd-ThinkBook") + .port(53530) // OPC UA默认端口 + .properties(JSONObject.toJSONString(properties)) + .protocol("opc-ua") + .enabled(true) + .description("测试OPC UA连接") + .build(); + + // 构建配置对象 - 写入操作,设置为可写 + IotConfig config = IotConfig.builder() + .id(1) + .connectId(3) + .alias("output_value") + .aliasName("输出值") + .registerAddress("ns=3;i=1009") // OPC UA节点ID格式 + .dataType("FLOAT") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试OPC UA写入操作") + .build(); + + // 执行写入操作 + try { + System.out.println("========== 开始测试OPC UA写入 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("服务器路径: " + properties.getString("path")); + System.out.println("节点ID: " + config.getRegisterAddress()); + System.out.println("数据类型: " + config.getDataType()); + + // 转换为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 + SiteBO siteBO = SiteBO.builder() + .deviceCode(connect.getCode()) + .alias(config.getAlias()) + .aliasName(config.getAliasName()) + .registerAddress(config.getRegisterAddress()) + .dataType(config.getDataType()) + .readonly(config.getReadonly()) + .build(); + + // 构建写入值对象 + String writeValue = "1.3"; // 要写入的值 + WValue wValue = WValue.builder() + .point(siteBO) + .value(writeValue) + .type(config.getDataType()) + .build(); + + System.out.println("写入值: " + writeValue); + + // 调用驱动写入数据 + Boolean result = opcUaProtocolDriver.write(deviceBO, wValue); + + if (result != null && result) { + System.out.println("写入成功!"); + System.out.println("写入结果: " + result); + } else { + System.out.println("写入失败!"); + } + System.out.println("========== OPC UA写入测试完成 =========="); + + } catch (Exception e) { + System.err.println("OPC UA写入测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Test + public void opcUaBatchWriteTest() { + // 构建OPC UA连接对象 + JSONObject properties = new JSONObject(); + properties.put("path", "/OPCUA/SimulationServer"); // OPC UA服务器路径 + + IotConnect connect = IotConnect.builder() + .id(3) + .code("OPC_UA_001") + .host("Lyd-ThinkBook") + .port(53530) // OPC UA默认端口 + .properties(JSONObject.toJSONString(properties)) + .protocol("opc-ua") + .enabled(true) + .description("测试OPC UA连接") + .build(); + + // 构建多个配置对象进行批量写入 + IotConfig config1 = IotConfig.builder() + .id(1) + .connectId(3) + .alias("output_value1") + .aliasName("输出值1") + .registerAddress("ns=3;s=Temperature") // OPC UA节点ID格式 + .dataType("DOUBLE") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入1") + .build(); + + IotConfig config2 = IotConfig.builder() + .id(2) + .connectId(3) + .alias("output_value2") + .aliasName("输出值2") + .registerAddress("ns=3;i=1009") + .dataType("FLOAT") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入2") + .build(); + + IotConfig config3 = IotConfig.builder() + .id(3) + .connectId(3) + .alias("output_status") + .aliasName("输出状态") + .registerAddress("ns=2;s=OutputStatus") + .dataType("BOOLEAN") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试批量写入3") + .build(); + + // 执行批量写入操作 + try { + System.out.println("========== 开始测试OPC UA批量写入 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("服务器路径: " + properties.getString("path")); + + // 转换为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(connect.getCode()) + .alias(config1.getAlias()) + .aliasName(config1.getAliasName()) + .registerAddress(config1.getRegisterAddress()) + .dataType(config1.getDataType()) + .readonly(config1.getReadonly()) + .build()) + .value("100.5") // 写入值1 + .type(config1.getDataType()) + .build(), + WValue.builder() + .point(SiteBO.builder() + .deviceCode(connect.getCode()) + .alias(config2.getAlias()) + .aliasName(config2.getAliasName()) + .registerAddress(config2.getRegisterAddress()) + .dataType(config2.getDataType()) + .readonly(config2.getReadonly()) + .build()) + .value("9.55") // 写入值2 + .type(config2.getDataType()) + .build() +// WValue.builder() +// .point(SiteBO.builder() +// .deviceCode(connect.getCode()) +// .alias(config3.getAlias()) +// .aliasName(config3.getAliasName()) +// .registerAddress(config3.getRegisterAddress()) +// .dataType(config3.getDataType()) +// .readonly(config3.getReadonly()) +// .build()) +// .value("true") // 写入值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 = opcUaProtocolDriver.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) { + System.out.println(" 错误信息: " + wResponse.getExceptionMessage()); + } + } + + System.out.println("========== OPC UA批量写入测试完成 =========="); + + } catch (Exception e) { + System.err.println("OPC UA批量写入测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + }