diff --git a/nl-iot/pom.xml b/nl-iot/pom.xml index 1d5e76d..5a0ac2e 100644 --- a/nl-iot/pom.xml +++ b/nl-iot/pom.xml @@ -37,6 +37,26 @@ j-interop 2.0.4 + + + org.apache.plc4x + plc4j-api + 0.13.0 + + + + org.apache.plc4x + plc4j-driver-s7 + 0.13.0 + runtime + + + + org.apache.plc4x + plc4j-driver-modbus + 0.13.0 + runtime + \ No newline at end of file diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/bo/DeviceBO.java b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/DeviceBO.java new file mode 100644 index 0000000..7511194 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/DeviceBO.java @@ -0,0 +1,41 @@ +package org.nl.iot.core.driver.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.io.Serializable; + +/** + * 设备BO + * @author: lyd + * @date: 2026/3/11 + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class DeviceBO implements Serializable { + + @Schema(description = "连接ID") + private String id; + + @Schema(description = "连接编码") + private String code; + + @Schema(description = "主机地址/IP") + private String host; + + @Schema(description = "端口") + private Integer port; + + @Schema(description = "协议 - 暂时用不到") + private String protocol; + + @Schema(description = "采集模式 - 暂时用不到") + private String collectMode; + + @Schema(description = "扩展参数(JSON)") + private String properties; +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/bo/SiteBO.java b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/SiteBO.java new file mode 100644 index 0000000..14398ff --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/SiteBO.java @@ -0,0 +1,37 @@ +package org.nl.iot.core.driver.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.io.Serializable; + +/** + * 站点BO - 信号名 + * @author: lyd + * @date: 2026/3/11 + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class SiteBO implements Serializable { + @Schema(description = "设备号") + private String deviceCode; + + @Schema(description = "信号别名") + private String alias; + + @Schema(description = "寄存器地址") + private String registerAddress; + + @Schema(description = "别名含义") + private String aliasName; + + @Schema(description = "数据类型") + private String dataType; + + @Schema(description = "只读(1只读/0可写)") + private Boolean readonly; +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java index d52a25a..17fffb7 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java @@ -1,6 +1,8 @@ package org.nl.iot.core.driver.entity; import lombok.*; +import org.nl.iot.core.driver.bo.DeviceBO; +import org.nl.iot.core.driver.bo.SiteBO; import org.nl.iot.modular.iot.entity.IotConfig; import org.nl.iot.modular.iot.entity.IotConnect; @@ -16,18 +18,24 @@ import org.nl.iot.modular.iot.entity.IotConnect; @NoArgsConstructor @AllArgsConstructor public class RValue { - /** - * 配置 - */ - private IotConfig config; /** * 连接参数 */ - private IotConnect connect; + private DeviceBO deviceBO; /** - * 值, string, 需要根据type确定真实的数据类型 + * 配置 + */ + private SiteBO siteBO; + + /** + * 值, 需要根据type确定真实的数据类型 */ private String value; + + /** + * 异常信息 + */ + private String exceptionMessage; } 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 new file mode 100644 index 0000000..a3f8b75 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WResponse.java @@ -0,0 +1,28 @@ +package org.nl.iot.core.driver.entity; + +import lombok.*; + +/** + * 批量读响应 + * @author: lyd + * @date: 2026/3/12 + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class WResponse { + /** + * 是否写入成功 + */ + private Boolean isOk; + + /** + * 写入的对象 + */ + private WValue wValue; + + private String exceptionMessage; +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java index 413f034..38f767a 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java @@ -2,6 +2,7 @@ package org.nl.iot.core.driver.entity; import lombok.*; import org.nl.common.exception.CommonException; +import org.nl.iot.core.driver.bo.SiteBO; import java.io.Serial; import java.io.Serializable; @@ -22,6 +23,11 @@ public class WValue implements Serializable { @Serial private static final long serialVersionUID = 1L; + /** + * 站点对象,写的是哪个站点 + */ + private SiteBO point; + /** * 值, string, 需要根据type确定真实的数据类型 */ diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java index c64c853..a3b973b 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java @@ -1,29 +1,33 @@ package org.nl.iot.core.driver.protocol.modbustcp; -import com.alibaba.fastjson.JSONObject; import jakarta.annotation.PostConstruct; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.nl.common.exception.CommonException; -import org.nl.iot.core.driver.bo.AttributeBO; +import org.apache.plc4x.java.DefaultPlcDriverManager; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.messages.PlcWriteRequest; +import org.apache.plc4x.java.api.messages.PlcWriteResponse; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.api.value.PlcValue; +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.protocol.modbustcp.com.serotonin.modbus4j.ModbusFactory; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpParameters; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.WriteCoilResponse; +import org.nl.iot.core.driver.protocol.modbustcp.util.JavaToModBusPlcValueConvertUtil; import org.nl.iot.core.driver.protocol.modbustcp.util.ModBusTcpUtils; +import org.nl.iot.core.driver.protocol.modbustcp.util.ModbusPlcValueConvertUtil; 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.util.Map; -import java.util.Objects; +import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * modbus-tcp通信协议的驱动自定义服务实现类 @@ -34,12 +38,7 @@ import java.util.concurrent.ConcurrentHashMap; @Service public class ModBusProtocolDriverImpl implements DriverCustomService { - static ModbusFactory modbusFactory; - - static { - modbusFactory = new ModbusFactory(); - } - private Map connectMap; + private Map connectMap; @Override @PostConstruct @@ -64,138 +63,185 @@ public class ModBusProtocolDriverImpl implements DriverCustomService { } @Override - public RValue read(IotConnect connect, IotConfig config) { - return new RValue(config, connect, readValue(getConnector(connect), connect, config)); + public RValue read(DeviceBO device, SiteBO point) { + return new RValue(device, point, readValue(getConnector(device), point), ""); } @Override - public Boolean write(IotConnect connect, IotConfig config, WValue wValue) { - /* - * 写入设备点位数据 - * - * 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。 - * 通过 Modbus 连接器将指定值写入设备的点位, 并返回写入结果。 - */ - ModbusMaster modbusMaster = getConnector(connect); - return writeValue(modbusMaster,connect, config, wValue); + public List batchRead(DeviceBO device, List points) { + return batchReadValue(getConnector(device), device, points); + } + + @Override + public Boolean write(DeviceBO device, WValue wValue) { + return writeValue(getConnector(device), device, wValue); + } + + @Override + public List batchWrite(DeviceBO device, List wValue) { + return batchWriteValue(getConnector(device), wValue); + } + + @SneakyThrows + public static List batchWriteValue(PlcConnection modbusMaster, List wValues) { + List res = new ArrayList<>(); + PlcWriteRequest.Builder writeRequestBuilder = doBuildWriteRequest(modbusMaster, wValues); + PlcWriteRequest writeRequest = writeRequestBuilder.build(); + // 执行写入(异步+超时控制) + CompletableFuture writeFuture = writeRequest.execute(); + PlcWriteResponse coilResponse = writeFuture.get(10, TimeUnit.SECONDS); + for (WValue wValue : wValues) { + try { + PlcResponseCode responseCode = coilResponse.getResponseCode(wValue.getPoint().getAlias()); + res.add(new WResponse(responseCode == PlcResponseCode.OK, + wValue, responseCode == PlcResponseCode.OK ? "" : String.format( + "写入Modbus失败,设备编码:%s,地址:%s,响应码:%s", wValue.getPoint().getAlias(), + wValue.getPoint().getRegisterAddress(), responseCode + ))); + } catch (Exception e) { + res.add(new WResponse(false, wValue, String.format("写入Modbus失败,设备编码:%s,地址:%s,响应码:%s" + , wValue.getPoint().getAlias(), wValue.getPoint().getRegisterAddress(), e.getMessage() + ))); + } + } + return res; } /** * 获取 Modbus Master 连接器 - *

- * 该方法用于根据设备ID和驱动配置获取或创建 Modbus Master 连接器。 - * 如果连接器已存在, 则直接返回;否则, 根据配置创建新的连接器并初始化。 - * 初始化失败时, 会移除连接器并抛出异常。 - * - * @param connectId 连接ID(一个设备对应多个连接) - * @param driverConfig 驱动配置, 包含连接 Modbus 设备所需的主机地址和端口号 - * @return ModbusMaster 返回与设备关联的 Modbus Master 连接器 - * @throws CommonException 如果连接器初始化失败, 抛出此异常 */ - private ModbusMaster getConnector(IotConnect connect) { - log.debug("Modbus Tcp Connection Info: {}", connect); - ModbusMaster modbusMaster = connectMap.get(connect.getId().toString()); - if (Objects.isNull(modbusMaster)) { - IpParameters params = new IpParameters(); - params.setHost(connect.getHost()); - params.setPort(connect.getPort()); - modbusMaster = modbusFactory.createTcpMaster(params, true); - try { - modbusMaster.init(); - connectMap.put(connect.getId().toString(), modbusMaster); - } catch (ModbusInitException e) { - connectMap.entrySet().removeIf(next -> next.getKey().equals(connect.getId().toString())); - log.error("Connect modbus master error: {}", e.getMessage(), e); - throw new CommonException(e.getMessage()); + private PlcConnection getConnector(DeviceBO deviceBO) { + log.debug("Modbus Tcp Connection Info: {}", deviceBO); + PlcConnection modbusMaster = connectMap.get(deviceBO.getId()); + try { + if (Objects.isNull(modbusMaster)) { + modbusMaster = new DefaultPlcDriverManager().getConnection(ModBusTcpUtils.buildModBusPlcUrl(deviceBO)); } + } catch (Exception e) { + throw new RuntimeException(e); } return modbusMaster; } - /** - * 读取 Modbus 设备点位值 - *

- * 根据点位配置中的功能码(functionCode)和偏移量(offset), 从 Modbus 设备中读取相应类型的值。 - * 支持的功能码包括: - * - 1: 读取线圈状态(Coil Status) - * - 2: 读取输入状态(Input Status) - * - 3: 读取保持寄存器(Holding Register) - * - 4: 读取输入寄存器(Input Register) - * - * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 - * @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息 - * @param type 点位值类型, 用于确定寄存器中数据的解析方式 - * @return String 返回读取到的点位值, 以字符串形式表示。如果功能码不支持, 则返回 "0"。 - */ - private String readValue(ModbusMaster modbusMaster, IotConnect connect, IotConfig config) { - JSONObject pointConfig = JSONObject.parseObject(connect.getProperties()); - String type = config.getDataType(); - int slaveId = pointConfig.getIntValue("slaveId"); - int offset = Integer.parseInt(config.getRegisterAddress()); - int functionCode = ModBusTcpUtils.getFunctionCode(offset); - - // 计算实际的寄存器地址(Modbus协议地址从0开始) - int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); + @SneakyThrows + public String readValue(PlcConnection modbusMaster, SiteBO point) { + // 1. 解析配置 + PlcReadRequest.Builder readBuilder = doBuildReadRequest(modbusMaster, Collections.singletonList(point)); - switch (functionCode) { - case 1: - BaseLocator coilLocator = BaseLocator.coilStatus(slaveId, actualAddress); - Boolean coilValue = ModBusTcpUtils.getMasterValue(modbusMaster, coilLocator); - return String.valueOf(coilValue); - case 2: - BaseLocator inputLocator = BaseLocator.inputStatus(slaveId, actualAddress); - Boolean inputStatusValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputLocator); - return String.valueOf(inputStatusValue); - case 3: - BaseLocator holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type)); - Number holdingValue = ModBusTcpUtils.getMasterValue(modbusMaster, holdingLocator); - return String.valueOf(holdingValue); - case 4: - BaseLocator inputRegister = BaseLocator.inputRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type)); - Number inputRegisterValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputRegister); - return String.valueOf(inputRegisterValue); - default: - return "0"; + // 3. 执行请求 + PlcReadRequest readRequest = readBuilder.build(); + CompletableFuture readFuture = readRequest.execute(); + PlcReadResponse readResponse = readFuture.get(10, TimeUnit.SECONDS); + + // 4. 校验响应码 + PlcResponseCode responseCode = readResponse.getResponseCode(point.getAlias()); + if (responseCode != PlcResponseCode.OK) { + log.warn("读取Modbus失败,设备编码:{},地址:{},响应码:{}", point.getAlias(), point.getRegisterAddress(), responseCode); } + + // 5. 取值并转换 + PlcValue plcValue = readResponse.getPlcValue(point.getAlias()); + if (plcValue == null) { + log.warn("读取到空值,设备编码:{},地址:{}", point.getAlias(), point.getRegisterAddress()); + } + // 根据类型转换 + return ModbusPlcValueConvertUtil.convertPlcValueToString(plcValue, point.getDataType()); + } + + @SneakyThrows + public List batchReadValue(PlcConnection modbusMaster, DeviceBO deviceBO, List points) { + // 1. 解析配置 + PlcReadRequest.Builder readBuilder = doBuildReadRequest(modbusMaster, points); + // 3. 执行请求 + PlcReadRequest readRequest = readBuilder.build(); + CompletableFuture readFuture = readRequest.execute(); + PlcReadResponse readResponse = readFuture.get(10, TimeUnit.SECONDS); + List list = new ArrayList<>(); + // 4.组装数据 + for (SiteBO point : points) { + try { + PlcResponseCode responseCode = readResponse.getResponseCode(point.getAlias()); + // 4. 校验响应码 + if (responseCode != PlcResponseCode.OK) { + list.add(new RValue(deviceBO, point, null, String.format( + "读取Modbus失败,设备编码:%s,地址:%s,响应码:%s", point.getAlias(), point.getRegisterAddress(), responseCode + ))); + continue; + } + // 5. 取值并转换 + PlcValue plcValue = readResponse.getPlcValue(point.getAlias()); + list.add(new RValue(deviceBO, point, ModbusPlcValueConvertUtil.convertPlcValueToString(plcValue, point.getDataType()), "")); + } catch (Exception e) { + list.add(new RValue(deviceBO, point, null, String.format( + "读取Modbus失败,设备编码:%s,地址:%s,响应码:%s", point.getAlias(), point.getRegisterAddress(), e.getMessage() + ))); + } + } + return list; + } + + public static PlcReadRequest.Builder doBuildReadRequest(PlcConnection modbusMaster, List points) { + PlcReadRequest.Builder readBuilder = modbusMaster.readRequestBuilder(); + for (SiteBO point : points) { + int offset = Integer.parseInt(point.getRegisterAddress()); + int functionCode = ModBusTcpUtils.getFunctionCode(offset); + int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); + + // 构建读取请求 + String tagName = point.getAlias(); + // 校验地址格式 + if (!ModbusPlcValueConvertUtil.containerType(point.getDataType())) { + log.warn("Modbus数据类型错误:设备编码:{}", tagName); + continue; + } + String modbusAddress = ModBusTcpUtils.getModBus4JAddress(offset, actualAddress, point.getDataType()); + if (!modbusAddress.contains(":")) { + log.warn("Modbus地址格式错误:{},设备编码:{}" ,modbusAddress, tagName); + continue; + } + readBuilder.addTagAddress(tagName, modbusAddress); + } + return readBuilder; + } + + public static PlcWriteRequest.Builder doBuildWriteRequest(PlcConnection modbusMaster, List wValues) { + PlcWriteRequest.Builder writeRequestBuilder = modbusMaster.writeRequestBuilder(); + // 1. 解析配置 + for (WValue wValue : wValues) { + SiteBO point = wValue.getPoint(); + String type = point.getDataType(); + int offset = Integer.parseInt(point.getRegisterAddress()); + // 功能码 + int functionCode = ModBusTcpUtils.getFunctionCode(offset); + // 寄存器实际地址 + int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); + + // 2. 构建读取请求 + String tagName = point.getAlias(); + // 校验地址格式(可选,用于调试) + String modbusAddress = ModBusTcpUtils.getModBus4JAddress(offset, actualAddress, type); + if (!modbusAddress.contains(":")) { + throw new IllegalArgumentException("Modbus地址格式错误:" + modbusAddress + ",设备编码:" + tagName); + } + // modbusMaster.connect(); + // 2. 设置要写入的值 + writeRequestBuilder.addTagAddress(tagName, modbusAddress, JavaToModBusPlcValueConvertUtil.convert(wValue.getValue(), type)); + } + return writeRequestBuilder; } /** * 向 Modbus 设备写入点位值 - *

- * 根据点位配置中的功能码(functionCode)和偏移量(offset), 将指定值写入 Modbus 设备的相应点位。 - * 支持的功能码包括: - * - 1: 写入线圈状态(Coil Status) - * - 3: 写入保持寄存器(Holding Register) - *

- * 对于功能码 1, 写入布尔值到线圈状态, 并返回写入结果。 - * 对于功能码 3, 写入数值到保持寄存器, 并返回写入成功状态。 - * 其他功能码暂不支持, 返回 false。 - * - * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 - * @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息 - * @param wValue 待写入的值, 包含值类型和具体数值 - * @return boolean 返回写入结果, true 表示写入成功, false 表示写入失败或不支持的功能码 */ - private boolean writeValue(ModbusMaster modbusMaster, IotConnect connect, IotConfig config, WValue wValue) { - JSONObject pointConfig = JSONObject.parseObject(connect.getProperties()); - String type = config.getDataType(); - int slaveId = pointConfig.getIntValue("slaveId"); - int offset = Integer.parseInt(config.getRegisterAddress()); - int functionCode = ModBusTcpUtils.getFunctionCode(offset); - wValue.setType(type); - - // 计算实际的寄存器地址(Modbus协议地址从0开始) - int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); - switch (functionCode) { - case 1: - WriteCoilResponse coilResponse = ModBusTcpUtils.setMasterValue(modbusMaster, slaveId, actualAddress, wValue); - return !coilResponse.isException(); - case 3: - BaseLocator locator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type)); - ModBusTcpUtils.setMasterValue(modbusMaster, locator, wValue); - return true; - default: - return false; - } + @SneakyThrows + private boolean writeValue(PlcConnection modbusMaster, DeviceBO deviceBO, WValue wValue) { + // 1. 解析配置 + PlcWriteRequest.Builder writeRequestBuilder = doBuildWriteRequest(modbusMaster, Collections.singletonList(wValue)); + PlcWriteRequest writeRequest = writeRequestBuilder.build(); + // 3. 执行写入(异步+超时控制) + CompletableFuture writeFuture = writeRequest.execute(); + PlcWriteResponse coilResponse = writeFuture.get(10, TimeUnit.SECONDS); + PlcResponseCode responseCode = coilResponse.getResponseCode(wValue.getPoint().getAlias()); + return responseCode == PlcResponseCode.OK; } } diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/JavaToModBusPlcValueConvertUtil.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/JavaToModBusPlcValueConvertUtil.java new file mode 100644 index 0000000..f7639b1 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/JavaToModBusPlcValueConvertUtil.java @@ -0,0 +1,277 @@ +package org.nl.iot.core.driver.protocol.modbustcp.util; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * 根据modbus类型string 将String转成对应的Java类型 + * eg: (value(String), type(String)) -> ("TRUE", "BOOL") 需要返回 true + * eg: (value(String), type(String)) -> ("12", "INT") 需要返回 12 + * @author: lyd + * @date: 2026/3/12 + */ +public class JavaToModBusPlcValueConvertUtil { + + /** + * 核心转换方法:根据Modbus类型字符串,将String值转为对应的Java类型 + * @param value 字符串值 + * @param type Modbus类型字符串(如"BOOL"、"INT"、"REAL") + * @return 转换后的Java对象 + * @throws IllegalArgumentException 类型不支持或参数异常 + */ + public static Object convert(String value, String type) { + if (value == null) { + throw new IllegalArgumentException("Value cannot be null"); + } + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Type cannot be null or empty"); + } + + String normalizedType = type.trim().toUpperCase(); + + return switch (normalizedType) { + case "BOOL" -> convertToBool(value); + case "BYTE" -> convertToByte(value); + case "WORD" -> convertToWord(value); + case "DWORD" -> convertToDWord(value); + case "LWORD" -> convertToLWord(value); + case "USINT" -> convertToUSInt(value); + case "UINT" -> convertToUInt(value); + case "UDINT" -> convertToUDInt(value); + case "ULINT" -> convertToULInt(value); + case "SINT" -> convertToSInt(value); + case "INT" -> convertToInt(value); + case "DINT" -> convertToDInt(value); + case "LINT" -> convertToLInt(value); + case "REAL" -> convertToReal(value); + case "LREAL" -> convertToLReal(value); + case "CHAR" -> convertToChar(value); + case "WCHAR" -> convertToWChar(value); + case "STRING", "WSTRING" -> value; + case "TIME", "LTIME" -> convertToTime(value); + case "DATE", "LDATE" -> convertToDate(value); + case "TIME_OF_DAY", "LTIME_OF_DAY" -> convertToTimeOfDay(value); + case "DATE_AND_TIME", "LDATE_AND_TIME" -> convertToDateTime(value); + default -> throw new IllegalArgumentException("Unsupported Modbus type: " + type); + }; + } + + // BOOL类型转换 + private static Boolean convertToBool(String value) { + String normalized = value.trim().toUpperCase(); + return switch (normalized) { + case "TRUE", "1", "YES", "ON" -> true; + case "FALSE", "0", "NO", "OFF" -> false; + default -> throw new IllegalArgumentException("Invalid BOOL value: " + value); + }; + } + + // BYTE类型转换(8位有符号) + private static Byte convertToByte(String value) { + try { + return Byte.parseByte(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid BYTE value: " + value, e); + } + } + + // WORD类型转换(16位无符号,用Short存储) + private static Short convertToWord(String value) { + try { + int intValue = Integer.parseInt(value.trim()); + if (intValue < 0 || intValue > 65535) { + throw new IllegalArgumentException("WORD value out of range [0, 65535]: " + value); + } + return (short) intValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid WORD value: " + value, e); + } + } + + // DWORD类型转换(32位无符号,用Integer存储) + private static Integer convertToDWord(String value) { + try { + long longValue = Long.parseLong(value.trim()); + if (longValue < 0 || longValue > 4294967295L) { + throw new IllegalArgumentException("DWORD value out of range [0, 4294967295]: " + value); + } + return (int) longValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid DWORD value: " + value, e); + } + } + + // LWORD类型转换(64位无符号,用Long存储) + private static Long convertToLWord(String value) { + try { + BigInteger bigInt = new BigInteger(value.trim()); + if (bigInt.compareTo(BigInteger.ZERO) < 0 || bigInt.compareTo(new BigInteger("18446744073709551615")) > 0) { + throw new IllegalArgumentException("LWORD value out of range"); + } + return bigInt.longValue(); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid LWORD value: " + value, e); + } + } + + // USINT类型转换(8位无符号) + private static Byte convertToUSInt(String value) { + try { + int intValue = Integer.parseInt(value.trim()); + if (intValue < 0 || intValue > 255) { + throw new IllegalArgumentException("USINT value out of range [0, 255]: " + value); + } + return (byte) intValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid USINT value: " + value, e); + } + } + + // UINT类型转换(16位无符号) + private static Integer convertToUInt(String value) { + try { + int intValue = Integer.parseInt(value.trim()); + if (intValue < 0 || intValue > 65535) { + throw new IllegalArgumentException("UINT value out of range [0, 65535]: " + value); + } + return intValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid UINT value: " + value, e); + } + } + + // UDINT类型转换(32位无符号) + private static Long convertToUDInt(String value) { + try { + long longValue = Long.parseLong(value.trim()); + if (longValue < 0 || longValue > 4294967295L) { + throw new IllegalArgumentException("UDINT value out of range [0, 4294967295]: " + value); + } + return longValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid UDINT value: " + value, e); + } + } + + // ULINT类型转换(64位无符号) + private static BigInteger convertToULInt(String value) { + try { + BigInteger bigInt = new BigInteger(value.trim()); + if (bigInt.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("ULINT value cannot be negative: " + value); + } + return bigInt; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid ULINT value: " + value, e); + } + } + + // SINT类型转换(8位有符号) + private static Byte convertToSInt(String value) { + try { + return Byte.parseByte(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid SINT value: " + value, e); + } + } + + // INT类型转换(16位有符号) + private static Short convertToInt(String value) { + try { + return Short.parseShort(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid INT value: " + value, e); + } + } + + // DINT类型转换(32位有符号) + private static Integer convertToDInt(String value) { + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid DINT value: " + value, e); + } + } + + // LINT类型转换(64位有符号) + private static Long convertToLInt(String value) { + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid LINT value: " + value, e); + } + } + + // REAL类型转换(32位浮点) + private static Float convertToReal(String value) { + try { + return Float.parseFloat(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid REAL value: " + value, e); + } + } + + // LREAL类型转换(64位浮点) + private static Double convertToLReal(String value) { + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid LREAL value: " + value, e); + } + } + + // CHAR类型转换 + private static Character convertToChar(String value) { + if (value.length() != 1) { + throw new IllegalArgumentException("CHAR value must be a single character: " + value); + } + return value.charAt(0); + } + + // WCHAR类型转换(宽字符) + private static Character convertToWChar(String value) { + if (value.length() != 1) { + throw new IllegalArgumentException("WCHAR value must be a single character: " + value); + } + return value.charAt(0); + } + + // TIME类型转换(Duration) + private static Duration convertToTime(String value) { + try { + return Duration.parse(value.trim()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid TIME value (expected ISO-8601 duration format): " + value, e); + } + } + + // DATE类型转换 + private static LocalDate convertToDate(String value) { + try { + return LocalDate.parse(value.trim()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid DATE value (expected ISO-8601 date format): " + value, e); + } + } + + // TIME_OF_DAY类型转换 + private static LocalTime convertToTimeOfDay(String value) { + try { + return LocalTime.parse(value.trim()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid TIME_OF_DAY value (expected ISO-8601 time format): " + value, e); + } + } + + // DATE_AND_TIME类型转换 + private static LocalDateTime convertToDateTime(String value) { + try { + return LocalDateTime.parse(value.trim()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid DATE_AND_TIME value (expected ISO-8601 datetime format): " + value, e); + } + } +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java index 154759b..ee37daa 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java @@ -1,8 +1,13 @@ package org.nl.iot.core.driver.protocol.modbustcp.util; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; +import org.apache.plc4x.java.DefaultPlcDriverManager; +import org.apache.plc4x.java.api.PlcConnection; import org.apache.poi.ss.formula.functions.T; import org.nl.common.exception.CommonException; +import org.nl.iot.core.driver.bo.DeviceBO; import org.nl.iot.core.driver.entity.WValue; import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster; import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType; @@ -20,6 +25,9 @@ import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.Writ @Slf4j public class ModBusTcpUtils { + private static final String MODBUS_CONN_PRX = "modbus-tcp://"; + private static final int DEFAULT_SLAVE_ID = 1; + /** * 获取 Modbus 数据类型 *

@@ -83,29 +91,28 @@ public class ModBusTcpUtils { }; } - /** * 根据Modbus地址偏移量和功能码计算实际的寄存器地址 * Modbus协议中,寄存器地址从0开始,需要减去相应的偏移量 * * @param offset 地址偏移量(1-9999/10001-19999/30001-39999/40001-49999) * @param functionCode 功能码(1-4) - * @return 实际的寄存器地址(从0开始) + * @return 实际的寄存器地址(从1开始) */ public static int getActualAddress(int offset, int functionCode) { switch (functionCode) { case 1: // 线圈状态:1-9999 -> 0-9998 - return offset - 1; + return offset; case 2: // 离散输入:10001-19999 -> 0-9998 - return offset - 10001; + return offset - 10000; case 3: // 保持寄存器:40001-49999 -> 0-9998 - return offset - 40001; + return offset - 40000; case 4: // 输入寄存器:30001-39999 -> 0-9998 - return offset - 30001; + return offset - 30000; default: return 0; } @@ -141,7 +148,35 @@ public class ModBusTcpUtils { return functionCode; } + /** + * 根据Modbus地址偏移量获取对应的功能码 + * 注意:不同厂商对地址范围的定义可能不同,这里采用常见的约定: + * - 00001-09999: 功能码01 (线圈 Coil) + * - 10001-19999: 功能码02 (离散输入 Discrete Input) + * - 30001-39999: 功能码04 (输入寄存器 Input Register) + * - 40001-49999: 功能码03 (保持寄存器 Holding Register) + * + * @param offset 地址偏移量(1-9999/10001-19999/30001-39999/40001-49999) + * @return 对应的功能码(1-4) + * @throws CommonException 当偏移量不在合法范围时抛出异常 + */ + public static String getModBus4JAddress(int offset, int actualAddress, String type) { + String functionCode; + if (offset >= 1 && offset <= 9999) { + functionCode = "coil"; // 线圈 + } else if (offset >= 10001 && offset <= 19999) { + functionCode = "discrete-input"; // 离散输入 + } else if (offset >= 30001 && offset <= 39999) { + functionCode = "input-register"; // 输入寄存器 + } else if (offset >= 40001 && offset <= 49999) { + functionCode = "holding-register"; // 保持寄存器 + } else { + throw new CommonException("无效的偏移量:" + offset); + } + + return functionCode + ":" + actualAddress + ":" + type; + } /** * 从 ModbusMaster 连接器中读取指定点位的数据 @@ -211,4 +246,17 @@ public class ModBusTcpUtils { throw new CommonException(e.getMessage()); } } + + + public static String buildModBusPlcUrl(DeviceBO deviceBO) { + String host = deviceBO.getHost(); + if (host == null || host.trim().isEmpty()) { + throw new IllegalArgumentException("设备IP/域名(host)不能为空"); + } + JSONObject pointConfig = JSONObject.parseObject(deviceBO.getProperties()); + int slaveId = pointConfig.getIntValue("slaveId"); + int finalSlaveId = ObjectUtil.isEmpty(slaveId) ? DEFAULT_SLAVE_ID : slaveId; + return MODBUS_CONN_PRX + host.trim() + ":" + deviceBO.getPort() + + "?default-unit-identifier=" + finalSlaveId; + } } diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModbusPlcValueConvertUtil.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModbusPlcValueConvertUtil.java new file mode 100644 index 0000000..3733e24 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModbusPlcValueConvertUtil.java @@ -0,0 +1,318 @@ +package org.nl.iot.core.driver.protocol.modbustcp.util; +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.api.types.PlcValueType; +import org.apache.plc4x.java.api.exceptions.PlcIncompatibleDatatypeException; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Duration; +import java.util.Objects; + +/** + * 修正版:Modbus PlcValue 转换工具类 + * 适配PLC4X标准API,移除不存在的具体值类型导入,基于PlcValueType枚举和PlcValue接口实现 + */ +public class ModbusPlcValueConvertUtil { + + // 对齐PlcValueType枚举的Modbus类型映射(覆盖PLC4X定义的所有基础类型) + public enum ModbusType { + BOOL(PlcValueType.BOOL), + BYTE(PlcValueType.BYTE), + WORD(PlcValueType.WORD), + DWORD(PlcValueType.DWORD), + LWORD(PlcValueType.LWORD), + USINT(PlcValueType.USINT), + UINT(PlcValueType.UINT), + UDINT(PlcValueType.UDINT), + ULINT(PlcValueType.ULINT), + SINT(PlcValueType.SINT), + INT(PlcValueType.INT), + DINT(PlcValueType.DINT), + LINT(PlcValueType.LINT), + REAL(PlcValueType.REAL), + LREAL(PlcValueType.LREAL), + CHAR(PlcValueType.CHAR), + WCHAR(PlcValueType.WCHAR), + STRING(PlcValueType.STRING), + WSTRING(PlcValueType.WSTRING), + TIME(PlcValueType.TIME), + LTIME(PlcValueType.LTIME), + DATE(PlcValueType.DATE), + LDATE(PlcValueType.LDATE), + TIME_OF_DAY(PlcValueType.TIME_OF_DAY), + LTIME_OF_DAY(PlcValueType.LTIME_OF_DAY), + DATE_AND_TIME(PlcValueType.DATE_AND_TIME), + LDATE_AND_TIME(PlcValueType.LDATE_AND_TIME); + + private final PlcValueType plcValueType; + + ModbusType(PlcValueType plcValueType) { + this.plcValueType = plcValueType; + } + + public PlcValueType getPlcValueType() { + return plcValueType; + } + + // 根据字符串解析ModbusType(兼容大小写) + public static ModbusType fromString(String typeStr) { + if (typeStr == null || typeStr.trim().isEmpty()) { + throw new IllegalArgumentException("Modbus type string cannot be null or empty"); + } + try { + return ModbusType.valueOf(typeStr.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unsupported Modbus type: " + typeStr, e); + } + } + } + + /** + * 判断枚举是否存在modbus类型 + * @param modbusTypeStr + * @return + */ + public static boolean containerType(String modbusTypeStr) { + if (modbusTypeStr == null || modbusTypeStr.trim().isEmpty()) { + return false; + } + try { + ModbusType.valueOf(modbusTypeStr.trim().toUpperCase()); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * 核心转换方法:根据Modbus类型字符串,将PlcValue转为对应Java类型后统一转String + * @param plcValue PLC4X读取的PlcValue对象 + * @param modbusTypeStr Modbus类型字符串(如"BOOL"、"INT"、"REAL") + * @return 转换后的String结果 + * @throws IllegalArgumentException 类型不支持或参数异常 + * @throws PlcIncompatibleDatatypeException 类型不匹配 + */ + public static String convertPlcValueToString(PlcValue plcValue, String modbusTypeStr) { + // 空值校验 + if (plcValue == null) { + throw new IllegalArgumentException("PlcValue cannot be null"); + } + + // 解析Modbus类型 + ModbusType modbusType = ModbusType.fromString(modbusTypeStr); + PlcValueType targetType = modbusType.getPlcValueType(); + + // 根据PlcValueType执行转换 + return switch (targetType) { + case BOOL -> convertBool(plcValue); + case BYTE -> convertByte(plcValue); + case WORD -> convertWord(plcValue); + case DWORD -> convertDWord(plcValue); + case LWORD -> convertLWord(plcValue); + case USINT -> convertUSInt(plcValue); + case UINT -> convertUInt(plcValue); + case UDINT -> convertUDInt(plcValue); + case ULINT -> convertULInt(plcValue); + case SINT -> convertSInt(plcValue); + case INT -> convertInt(plcValue); + case DINT -> convertDInt(plcValue); + case LINT -> convertLInt(plcValue); + case REAL -> convertReal(plcValue); + case LREAL -> convertLReal(plcValue); +// case CHAR -> convertChar(plcValue); + case WCHAR -> convertWChar(plcValue); + case STRING, WSTRING -> convertString(plcValue); + case TIME, LTIME -> convertTime(plcValue); + case DATE, LDATE -> convertDate(plcValue); + case TIME_OF_DAY, LTIME_OF_DAY -> convertTimeOfDay(plcValue); + case DATE_AND_TIME, DATE_AND_LTIME, LDATE_AND_TIME -> convertDateTime(plcValue); + default -> throw new PlcIncompatibleDatatypeException("Unsupported PlcValueType: " + targetType); + }; + } + + // 布尔类型转换(BOOL) + private static String convertBool(PlcValue plcValue) { + if (!plcValue.isBoolean()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a BOOL type"); + } + return Objects.toString(plcValue.getBoolean(), "false"); + } + + // 字节类型(BYTE) + private static String convertByte(PlcValue plcValue) { + if (!plcValue.isByte()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a BYTE type"); + } + return Objects.toString(plcValue.getByte(), "0"); + } + + // 16位无符号字(WORD) + private static String convertWord(PlcValue plcValue) { + if (!plcValue.isShort()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a WORD type"); + } + return Objects.toString(plcValue.getShort(), "0"); + } + + // 32位无符号双字(DWORD) + private static String convertDWord(PlcValue plcValue) { + if (!plcValue.isInteger()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a DWORD type"); + } + return Objects.toString(plcValue.getInteger(), "0"); + } + + // 64位无符号四字(LWORD) + private static String convertLWord(PlcValue plcValue) { + if (!plcValue.isLong()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a LWORD type"); + } + return Objects.toString(plcValue.getLong(), "0"); + } + + // 8位无符号短整型(USINT) + private static String convertUSInt(PlcValue plcValue) { + if (!plcValue.isByte()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a USINT type"); + } + // USINT是无符号字节,转换为正整数 + return Objects.toString(Byte.toUnsignedInt(plcValue.getByte()), "0"); + } + + // 16位无符号整型(UINT) + private static String convertUInt(PlcValue plcValue) { + if (!plcValue.isShort()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a UINT type"); + } + // UINT是无符号16位,转换为正整数 + return Objects.toString(Short.toUnsignedInt(plcValue.getShort()), "0"); + } + + // 32位无符号整型(UDINT) + private static String convertUDInt(PlcValue plcValue) { + if (!plcValue.isInteger()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a UDINT type"); + } + // UDINT是无符号32位,转换为正长整数 + return Objects.toString(Integer.toUnsignedLong(plcValue.getInteger()), "0"); + } + + // 64位无符号整型(ULINT) + private static String convertULInt(PlcValue plcValue) { + if (!plcValue.isBigInteger()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a ULINT type"); + } + return Objects.toString(plcValue.getBigInteger(), "0"); + } + + // 8位有符号短整型(SINT) + private static String convertSInt(PlcValue plcValue) { + if (!plcValue.isByte()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a SINT type"); + } + return Objects.toString(plcValue.getByte(), "0"); + } + + // 16位有符号整型(INT) + private static String convertInt(PlcValue plcValue) { + if (!plcValue.isShort()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a INT type"); + } + return Objects.toString(plcValue.getShort(), "0"); + } + + // 32位有符号整型(DINT) + private static String convertDInt(PlcValue plcValue) { + if (!plcValue.isInteger()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a DINT type"); + } + return Objects.toString(plcValue.getInteger(), "0"); + } + + // 64位有符号整型(LINT) + private static String convertLInt(PlcValue plcValue) { + if (!plcValue.isLong()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a LINT type"); + } + return Objects.toString(plcValue.getLong(), "0"); + } + + // 32位浮点型(REAL/FLOAT) + private static String convertReal(PlcValue plcValue) { + if (!plcValue.isFloat()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a REAL type"); + } + return Objects.toString(plcValue.getFloat(), "0.0"); + } + + // 64位浮点型(LREAL/DOUBLE) + private static String convertLReal(PlcValue plcValue) { + if (!plcValue.isDouble()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a LREAL type"); + } + return Objects.toString(plcValue.getDouble(), "0.0"); + } + + // 字符类型(CHAR) +// private static String convertChar(PlcValue plcValue) { +// if (!plcValue.isCharacter()) { +// throw new PlcIncompatibleDatatypeException("PlcValue is not a CHAR type"); +// } +// return Objects.toString(plcValue.getCharacter(), ""); +// } + + // 宽字符类型(WCHAR) + private static String convertWChar(PlcValue plcValue) { + if (!plcValue.isShort()) { // WCHAR在PLC4X中以Short存储 + throw new PlcIncompatibleDatatypeException("PlcValue is not a WCHAR type"); + } + return Objects.toString((char) plcValue.getShort(), ""); + } + + // 字符串类型(STRING/WSTRING) + private static String convertString(PlcValue plcValue) { + if (!plcValue.isString()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a STRING type"); + } + return Objects.toString(plcValue.getString(), ""); + } + + // 时间类型(TIME/LTIME,对应Duration) + private static String convertTime(PlcValue plcValue) { + if (!plcValue.isDuration()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a TIME type"); + } + Duration duration = plcValue.getDuration(); + return Objects.toString(duration, "PT0S"); + } + + // 日期类型(DATE/LDATE) + private static String convertDate(PlcValue plcValue) { + if (!plcValue.isDate()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a DATE type"); + } + LocalDate date = plcValue.getDate(); + return Objects.toString(date, ""); + } + + // 时间戳类型(TIME_OF_DAY/LTIME_OF_DAY) + private static String convertTimeOfDay(PlcValue plcValue) { + if (!plcValue.isTime()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a TIME_OF_DAY type"); + } + LocalTime time = plcValue.getTime(); + return Objects.toString(time, ""); + } + + // 日期时间类型(DATE_AND_TIME等) + private static String convertDateTime(PlcValue plcValue) { + if (!plcValue.isDateTime()) { + throw new PlcIncompatibleDatatypeException("PlcValue is not a DATE_AND_TIME type"); + } + LocalDateTime dateTime = plcValue.getDateTime(); + return Objects.toString(dateTime, ""); + } +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java b/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java index 1166ff8..94802ff 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java @@ -1,12 +1,16 @@ package org.nl.iot.core.driver.service; 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.modular.iot.entity.IotConfig; import org.nl.iot.modular.iot.entity.IotConnect; +import java.util.List; import java.util.Map; /** @@ -29,39 +33,36 @@ public interface DriverCustomService { */ void schedule(); + /** + * 更新内存 + * @param metadataEvent + */ void event(MetadataEventDTO metadataEvent); - // todo驱动事件,暂时不需要 - /** * 执行读操作 - *

- * 该接口用于从指定设备中读取位号的数据。由于设备类型和通信协议的差异, 读取操作可能无法直接执行, 请根据实际情况灵活处理。 - *

- * 注意: 读取操作可能会抛出异常, 调用方需做好异常处理。 - * - * @param driverConfig 驱动属性配置, 包含驱动相关的配置信息 - * @param pointConfig 位号属性配置, 包含位号相关的配置信息 - * @param device 设备对象, 包含设备的基本信息和属性 - * @param point 位号对象, 包含位号的基本信息和属性 - * @return 返回读取到的数据, 封装在 {@link RValue} 对象中 */ - RValue read(IotConnect device, IotConfig point); + RValue read(DeviceBO device, SiteBO point); + + /** + * 批量读取 + * @param device + * @param point + * @return + */ + List batchRead(DeviceBO device, List point); /** * 执行写操作 - *

- * 该接口用于向指定设备中的位号写入数据。由于设备类型和通信协议的差异, 写入操作可能无法直接执行, 请根据实际情况灵活处理。 - *

- * 注意: 写入操作可能会抛出异常, 调用方需做好异常处理。 - * - * @param driverConfig 驱动属性配置, 包含驱动相关的配置信息 - * @param pointConfig 位号属性配置, 包含位号相关的配置信息 - * @param device 设备对象, 包含设备的基本信息和属性 - * @param point 位号对象, 包含位号的基本信息和属性 - * @param wValue 待写入的数据, 封装在 {@link WValue} 对象中 - * @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常 */ - Boolean write(IotConnect device, IotConfig point, WValue wValue); + Boolean write(DeviceBO device, WValue wValue); + + /** + * 批量写 + * @param device + * @param wValue + * @return + */ + List batchWrite(DeviceBO device, List wValue); } diff --git a/nl-iot/src/main/java/org/nl/iot/core/package-info.java b/nl-iot/src/main/java/org/nl/iot/core/package-info.java index 8e5b81d..77df19d 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/package-info.java +++ b/nl-iot/src/main/java/org/nl/iot/core/package-info.java @@ -1,5 +1,38 @@ /** * 核心包(通信协议工具、配置等) + * BOOL (boolean) + * + * SINT (int 8) + * + * USINT (uint 8) + * + * BYTE (uint 8) + * + * INT (int 16) + * + * UINT (uint 16) + * + * WORD (uint 16) + * + * DINT (int 32) + * + * UDINT (uint 32) + * + * DWORD (uint 32) + * + * LINT (int 64) + * + * ULINT (uint 64) + * + * LWORD (uint 64) + * + * REAL (float) + * + * LREAL (double) + * + * CHAR (char) + * + * WCHAR (2 byte char) * @author: lyd * @date: 2026/2/28 */ 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 8c8a876..dd9a3e0 100644 --- a/nl-web-app/src/test/java/org/nl/ApiTest.java +++ b/nl-web-app/src/test/java/org/nl/ApiTest.java @@ -1,21 +1,21 @@ package org.nl; +import com.alibaba.fastjson.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; -import org.nl.iot.core.driver.bo.AttributeBO; +import org.nl.iot.core.driver.bo.DeviceBO; +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.protocol.modbustcp.ModBusProtocolDriverImpl; -import org.nl.iot.core.driver.protocol.opcda.OpcDaProtocolDriverImpl; -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; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; -import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -32,23 +32,15 @@ public class ApiTest { @Test public void modbusTest() { - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("192.168.81.251").build()); - driverConfig.put("port", AttributeBO.builder().value("502").build()); - - // 构建点位配置 - Map pointConfig = new HashMap<>(); - pointConfig.put("slaveId", AttributeBO.builder().value("1").build()); - pointConfig.put("offset", AttributeBO.builder().value("40001").build()); // 功能码3:保持寄存器 - pointConfig.put("data_type", AttributeBO.builder().value("int16").build()); - // 构建连接对象 + JSONObject o = new JSONObject(); + o.put("slaveId", "1"); IotConnect connect = IotConnect.builder() .id(1) .code("MODBUS_TCP_001") .host("192.168.81.251") .port(502) + .properties(JSONObject.toJSONString(o)) .protocol("modbus-tcp") .enabled(true) .description("测试Modbus TCP连接") @@ -61,614 +53,414 @@ public class ApiTest { .alias("temperature") .aliasName("温度传感器") .registerAddress("40001") - .dataType("int16") + .dataType("INT") .readonly(true) .enabled(true) .description("测试温度读取") .build(); + // 执行读取操作 try { - // 调用read方法进行测试 - RValue result = modBusProtocolDriver.read(driverConfig, pointConfig, connect, config); + System.out.println("========== 开始测试Modbus TCP读取 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("寄存器地址: " + config.getRegisterAddress()); + System.out.println("数据类型: " + config.getDataType()); - // 输出测试结果 - System.out.println("=== Modbus读取测试结果 ==="); - System.out.println("连接信息: " + result.getConnect()); - System.out.println("配置信息: " + result.getConfig()); - System.out.println("读取值: " + result.getValue()); - System.out.println("测试完成!"); + // 转换为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(); + + // 调用驱动读取数据 + RValue result = modBusProtocolDriver.read(deviceBO, siteBO); + + System.out.println("读取成功!"); + System.out.println("读取结果: " + result.getValue()); + System.out.println("========== 测试完成 =========="); } catch (Exception e) { System.err.println("测试失败: " + e.getMessage()); -// e.printStackTrace(); + e.printStackTrace(); } } @Test public void modbusTestWrite() { - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("192.168.81.251").build()); - driverConfig.put("port", AttributeBO.builder().value("502").build()); - - // 构建点位配置 - 写入保持寄存器(功能码3) - Map pointConfig = new HashMap<>(); - pointConfig.put("slaveId", AttributeBO.builder().value("1").build()); - pointConfig.put("offset", AttributeBO.builder().value("40001").build()); // 功能码3:保持寄存器,地址0 - pointConfig.put("data_type", AttributeBO.builder().value("int16").build()); - // 构建连接对象 + JSONObject o = new JSONObject(); + o.put("slaveId", "1"); IotConnect connect = IotConnect.builder() .id(1) .code("MODBUS_TCP_001") .host("192.168.81.251") .port(502) + .properties(JSONObject.toJSONString(o)) .protocol("modbus-tcp") .enabled(true) - .description("测试Modbus TCP写入连接") + .description("测试Modbus TCP连接") .build(); - // 构建配置对象 + // 构建配置对象 - 写入操作,设置为可写 IotConfig config = IotConfig.builder() + .id(2) + .connectId(1) + .alias("output_value") + .aliasName("输出值") + .registerAddress("40001") + .dataType("LREAL") + .readonly(false) // 设置为可写 + .enabled(true) + .description("测试写入操作") + .build(); + + // 执行写入操作 + try { + System.out.println("========== 开始测试Modbus TCP写入 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); + System.out.println("寄存器地址: " + 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 = "25.5"; // 要写入的值 + WValue wValue = WValue.builder() + .point(siteBO) + .value(writeValue) + .type(config.getDataType()) + .build(); + + System.out.println("写入值: " + writeValue); + + // 调用驱动写入数据 + Boolean result = modBusProtocolDriver.write(deviceBO, wValue); + + if (result != null && result) { + System.out.println("写入成功!"); + System.out.println("写入结果: " + result); + } else { + System.out.println("写入失败!"); + } + System.out.println("========== 测试完成 =========="); + + } catch (Exception e) { + System.err.println("测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Test + public void modbusBatchReadTest() { + // 构建连接对象 + JSONObject o = new JSONObject(); + o.put("slaveId", "1"); + IotConnect connect = IotConnect.builder() + .id(1) + .code("MODBUS_TCP_001") + .host("192.168.81.251") + .port(502) + .properties(JSONObject.toJSONString(o)) + .protocol("modbus-tcp") + .enabled(true) + .description("测试Modbus TCP连接") + .build(); + + // 构建多个配置对象进行批量读取 + IotConfig config1 = IotConfig.builder() .id(1) .connectId(1) .alias("temperature") .aliasName("温度传感器") .registerAddress("40001") - .dataType("int16") - .readonly(false) - .enabled(true) - .description("测试温度写入") - .build(); - - try { - // 先读取当前值 - System.out.println("=== Modbus写入测试开始 ==="); - RValue beforeValue = modBusProtocolDriver.read(driverConfig, pointConfig, connect, config); - System.out.println("写入前的值: " + beforeValue.getValue()); - - // 构建写入值对象 - 写入新的温度值(例如:100) - org.nl.iot.core.driver.entity.WValue wValue = new org.nl.iot.core.driver.entity.WValue(); - wValue.setValue("100"); - wValue.setType("int16"); - - // 调用write方法进行写入测试 - Boolean writeResult = modBusProtocolDriver.write(driverConfig, pointConfig, connect, config, wValue); - System.out.println("写入结果: " + (writeResult ? "成功" : "失败")); - - // 等待一小段时间后再次读取,验证写入是否成功 - Thread.sleep(500); - RValue afterValue = modBusProtocolDriver.read(driverConfig, pointConfig, connect, config); - System.out.println("写入后的值: " + afterValue.getValue()); - - // 验证写入是否成功 - if (writeResult && "100".equals(afterValue.getValue())) { - System.out.println("✓ 写入验证成功!值已更新为: " + afterValue.getValue()); - } else { - System.out.println("✗ 写入验证失败!期望值: 100, 实际值: " + afterValue.getValue()); - } - - System.out.println("=== 测试完成 ==="); - - } catch (Exception e) { - System.err.println("测试失败: " + e.getMessage()); -// e.printStackTrace(); - } - } - - @Autowired - private PlcS7ProtocolDriverImpl plcS7ProtocolDriver; - - @Test - public void plcS7TestRead() { - // 初始化驱动 - plcS7ProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("192.168.10.33").build()); - driverConfig.put("port", AttributeBO.builder().value("102").build()); - - // 构建点位配置 - Map pointConfig = new HashMap<>(); - pointConfig.put("dbNum", AttributeBO.builder().value("1").build()); // 数据块编号 - pointConfig.put("byteOffset", AttributeBO.builder().value("0").build()); // 字节偏移量 - pointConfig.put("bitOffset", AttributeBO.builder().value("0").build()); // 位偏移量 - pointConfig.put("blockSize", AttributeBO.builder().value("1").build()); // 数据块大小 - pointConfig.put("data_type", AttributeBO.builder().value("int").build()); // 数据类型 - - // 构建连接对象 - IotConnect connect = IotConnect.builder() - .id(1) - .code("PLC_S7_001") - .host("192.168.10.33") - .port(102) - .protocol("plc-s7") - .enabled(true) - .description("测试PLC S7连接") - .build(); - - // 构建配置对象 - IotConfig config = IotConfig.builder() - .id(1) - .connectId(1) - .alias("pressure") - .aliasName("压力传感器") - .registerAddress("DB1.DBW0") - .dataType("int") + .dataType("INT") .readonly(true) .enabled(true) - .description("测试PLC S7数据读取") + .description("测试温度读取") .build(); - try { - System.out.println("=== PLC S7读取测试开始 ==="); - System.out.println("连接地址: " + driverConfig.get("host").getValue() + ":" + driverConfig.get("port").getValue()); - System.out.println("数据块: DB" + pointConfig.get("dbNum").getValue() + - ", 偏移: " + pointConfig.get("byteOffset").getValue() + - ", 类型: " + pointConfig.get("data_type").getValue()); - - // 调用read方法进行测试 - RValue result = plcS7ProtocolDriver.read(driverConfig, pointConfig, connect, config); - - // 检查结果是否为null - if (result != null) { - System.out.println("\n✓ 读取成功!"); - System.out.println("连接信息: " + result.getConnect()); - System.out.println("配置信息: " + result.getConfig()); - System.out.println("读取值: " + result.getValue()); - } else { - System.err.println("\n✗ 读取失败:返回结果为null"); - System.err.println("可能原因:"); - System.err.println("1. PLC设备未连接或IP地址不正确"); - System.err.println("2. 端口号配置错误(S7-300/400默认102)"); - System.err.println("3. 数据块(DB)不存在或无访问权限"); - System.err.println("4. 数据类型或偏移量配置错误"); - } - - System.out.println("\n=== 测试完成 ==="); - - } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); -// e.printStackTrace(); - } - } - - - @Test - public void plcS7TestWrite() { - // 初始化驱动 - plcS7ProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("192.168.10.33").build()); - driverConfig.put("port", AttributeBO.builder().value("102").build()); - - // 构建点位配置 - Map pointConfig = new HashMap<>(); - pointConfig.put("dbNum", AttributeBO.builder().value("1").build()); // 数据块编号 - pointConfig.put("byteOffset", AttributeBO.builder().value("0").build()); // 字节偏移量 - pointConfig.put("bitOffset", AttributeBO.builder().value("0").build()); // 位偏移量 - pointConfig.put("blockSize", AttributeBO.builder().value("1").build()); // 数据块大小 - pointConfig.put("data_type", AttributeBO.builder().value("int").build()); // 数据类型 - - // 构建连接对象 - IotConnect connect = IotConnect.builder() - .id(1) - .code("PLC_S7_001") - .host("192.168.10.33") - .port(102) - .protocol("plc-s7") - .enabled(true) - .description("测试PLC S7写入连接") - .build(); - - // 构建配置对象 - IotConfig config = IotConfig.builder() - .id(1) + IotConfig config2 = IotConfig.builder() + .id(2) .connectId(1) - .alias("pressure") - .aliasName("压力传感器") - .registerAddress("DB1.DBW0") - .dataType("int") - .readonly(false) + .alias("humidity") + .aliasName("湿度传感器") + .registerAddress("40002") + .dataType("INT") + .readonly(true) .enabled(true) - .description("测试PLC S7数据写入") + .description("测试湿度读取") .build(); +// IotConfig config3 = IotConfig.builder() +// .id(3) +// .connectId(1) +// .alias("press") +// .aliasName("压力传感器") +// .registerAddress("40003") +// .dataType("LREAL") +// .readonly(true) +// .enabled(true) +// .description("测试状态读取") +// .build(); + IotConfig config4 = IotConfig.builder() + .id(4) + .connectId(1) + .alias("move") + .aliasName("有无货") + .registerAddress("40008") + .dataType("SSSS") + .readonly(true) + .enabled(true) + .description("测试状态读取") + .build(); + + // 执行批量读取操作 try { - System.out.println("=== PLC S7写入测试开始 ==="); + System.out.println("========== 开始测试Modbus TCP批量读取 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); - // 先读取当前值 - RValue beforeValue = plcS7ProtocolDriver.read(driverConfig, pointConfig, connect, config); - if (beforeValue != null) { - System.out.println("写入前的值: " + beforeValue.getValue()); - } else { - System.out.println("写入前读取失败,继续执行写入测试..."); - } - - // 构建写入值对象 - 写入新的压力值(例如:200) - WValue wValue = new WValue(); - wValue.setValue("200"); - wValue.setType("int"); - - // 调用write方法进行写入测试 - Boolean writeResult = plcS7ProtocolDriver.write(driverConfig, pointConfig, connect, config, wValue); - System.out.println("写入结果: " + (writeResult ? "成功" : "失败")); - - // 等待一小段时间后再次读取,验证写入是否成功 - Thread.sleep(500); - RValue afterValue = plcS7ProtocolDriver.read(driverConfig, pointConfig, connect, config); + // 转换为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(); - if (afterValue != null) { - System.out.println("写入后的值: " + afterValue.getValue()); - - // 验证写入是否成功 - if (writeResult && "200".equals(afterValue.getValue())) { - System.out.println("✓ 写入验证成功!值已更新为: " + afterValue.getValue()); - } else { - System.out.println("✗ 写入验证失败!期望值: 200, 实际值: " + afterValue.getValue()); - } - } else { - System.err.println("写入后读取失败,无法验证写入结果"); + // 转换为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() + "]"); } - - System.out.println("\n=== 测试完成 ==="); - + + // 调用驱动批量读取数据 + List result = modBusProtocolDriver.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("========== 批量读取测试完成 =========="); + } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); -// e.printStackTrace(); + System.err.println("批量读取测试失败: " + e.getMessage()); + e.printStackTrace(); } } - @Autowired - private OpcUaProtocolDriverImpl opcUaProtocolDriver; @Test - public void opcUaTestRead() { - // 初始化驱动 -// opcUaProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("Lyd-ThinkBook").build()); - driverConfig.put("port", AttributeBO.builder().value("53530").build()); - driverConfig.put("path", AttributeBO.builder().value("/OPCUA/SimulationServer").build()); - - // 构建点位配置 - // 根据模拟器配置:ns=3;s=Temperature - Map pointConfig = new HashMap<>(); - pointConfig.put("namespace", AttributeBO.builder().value("3").build()); // 命名空间3 - pointConfig.put("tag", AttributeBO.builder().value("Temperature").build()); // 节点标识:Temperature - + public void modbusBatchWriteTest() { // 构建连接对象 + JSONObject o = new JSONObject(); + o.put("slaveId", "1"); IotConnect connect = IotConnect.builder() .id(1) - .code("OPC_UA_001") - .host("Lyd-ThinkBook") - .port(53530) - .protocol("opc-ua") + .code("MODBUS_TCP_001") + .host("192.168.81.251") + .port(502) + .properties(JSONObject.toJSONString(o)) + .protocol("modbus-tcp") .enabled(true) - .description("测试OPC UA连接") + .description("测试Modbus TCP连接") .build(); - // 构建配置对象 - IotConfig config = IotConfig.builder() + // 构建多个配置对象进行批量写入 + IotConfig config1 = IotConfig.builder() .id(1) .connectId(1) .alias("temperature") - .aliasName("温度传感器") - .registerAddress("ns=3;s=Temperature") // 与模拟器配置一致 - .dataType("float") // 模拟器中是Float类型 - .readonly(true) + .aliasName("温度") + .registerAddress("40001") + .dataType("INT") + .readonly(false) // 设置为可写 .enabled(true) - .description("测试OPC UA数据读取") + .description("测试批量写入1") .build(); - try { - System.out.println("=== OPC UA读取测试开始 ==="); - System.out.println("连接地址: opc.tcp://" + driverConfig.get("host").getValue() + - ":" + driverConfig.get("port").getValue() + - driverConfig.get("path").getValue()); - System.out.println("节点信息: 命名空间=" + pointConfig.get("namespace").getValue() + - ", 标签=" + pointConfig.get("tag").getValue()); - System.out.println("完整NodeId: " + config.getRegisterAddress()); - - // 调用read方法进行测试 - RValue result = opcUaProtocolDriver.read(driverConfig, pointConfig, connect, config); - - // 检查结果是否为null - if (result != null) { - System.out.println("\n✓ 读取成功!"); - System.out.println("连接信息: " + result.getConnect()); - System.out.println("配置信息: " + result.getConfig()); - System.out.println("读取值: " + result.getValue()); - } else { - System.err.println("\n✗ 读取失败:返回结果为null"); - System.err.println("可能原因:"); - System.err.println("1. OPC UA服务器未启动或IP地址不正确"); - System.err.println("2. 端口号配置错误(默认4840)"); - System.err.println("3. 节点不存在或命名空间索引错误"); - System.err.println("4. 服务器需要身份验证(当前使用匿名访问)"); - } - - System.out.println("\n=== 测试完成 ==="); - - } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); - e.printStackTrace(); - } - } - - - @Test - public void opcUaTestWrite() { - // 初始化驱动 - opcUaProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("Lyd-ThinkBook").build()); - driverConfig.put("port", AttributeBO.builder().value("53530").build()); - driverConfig.put("path", AttributeBO.builder().value("/OPCUA/SimulationServer").build()); - - // 构建点位配置 - // 根据模拟器配置:ns=3;s=Temperature - Map pointConfig = new HashMap<>(); - pointConfig.put("namespace", AttributeBO.builder().value("3").build()); // 命名空间3 - pointConfig.put("tag", AttributeBO.builder().value("Temperature").build()); // 节点标识:Temperature - - // 构建连接对象 - IotConnect connect = IotConnect.builder() - .id(1) - .code("OPC_UA_001") - .host("Lyd-ThinkBook") - .port(53530) - .protocol("opc-ua") - .enabled(true) - .description("测试OPC UA连接") - .build(); - - // 构建配置对象 - IotConfig config = IotConfig.builder() - .id(1) + IotConfig config2 = IotConfig.builder() + .id(2) .connectId(1) - .alias("temperature") - .aliasName("温度传感器") - .registerAddress("ns=3;s=Temperature") // 与模拟器配置一致 - .dataType("float") // 模拟器中是Float类型 - .readonly(true) + .alias("humidity") + .aliasName("湿度") + .registerAddress("40002") + .dataType("INT") + .readonly(false) // 设置为可写 .enabled(true) - .description("测试OPC UA数据读取") + .description("测试批量写入2") .build(); - try { - System.out.println("=== OPC UA写入测试开始 ==="); - - // 先读取当前值 - RValue beforeValue = opcUaProtocolDriver.read(driverConfig, pointConfig, connect, config); - if (beforeValue != null) { - System.out.println("写入前的值: " + beforeValue.getValue()); - } else { - System.out.println("写入前读取失败,继续执行写入测试..."); - } - - // 构建写入值对象 - 写入新的温度值(例如:25.5) - WValue wValue = new WValue(); - wValue.setValue("25.5"); - wValue.setType("float"); - - // 调用write方法进行写入测试 - Boolean writeResult = opcUaProtocolDriver.write(driverConfig, pointConfig, connect, config, wValue); - System.out.println("写入结果: " + (writeResult ? "成功" : "失败")); - - // 等待一小段时间后再次读取,验证写入是否成功 - Thread.sleep(500); - RValue afterValue = opcUaProtocolDriver.read(driverConfig, pointConfig, connect, config); - - if (afterValue != null) { - System.out.println("写入后的值: " + afterValue.getValue()); - - // 验证写入是否成功 - if (writeResult && "25.5".equals(afterValue.getValue())) { - System.out.println("✓ 写入验证成功!值已更新为: " + afterValue.getValue()); - } else { - System.out.println("✗ 写入验证失败!期望值: 25.5, 实际值: " + afterValue.getValue()); - } - } else { - System.err.println("写入后读取失败,无法验证写入结果"); - } - - System.out.println("\n=== 测试完成 ==="); - - } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); - e.printStackTrace(); - } - } - - @Autowired - private OpcDaProtocolDriverImpl opcDaProtocolDriver; - - @Test - public void opcDaTestRead() { - // 初始化驱动 - opcDaProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - // 重要提示: - // 1. 本地连接使用 "127.0.0.1" 或 "localhost" - // 2. username 格式: - // - 本地用户:直接使用用户名,如 "Administrator" 或 "YourUsername" - // - 域用户:使用 "DOMAIN\\username" 格式 - // - 如果 OPC 服务器配置为允许匿名访问,可以尝试空字符串 - // 3. password 必须是 Windows 用户的实际密码 - // 4. 确保 Windows DCOM 配置正确(参考 Matrikon OPC 文档) - - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("127.0.0.1").build()); - driverConfig.put("clsId", AttributeBO.builder().value("F8582CF2-88FB-11D0-B850-00C0F0104305").build()); - - // 方案1:使用当前登录用户(推荐本地测试) - // 获取当前 Windows 用户名:在 CMD 中运行 "echo %USERNAME%" - String currentUser = System.getProperty("user.name"); // 获取当前 Java 进程的用户名 -// driverConfig.put("username", AttributeBO.builder().value(currentUser).build()); -// driverConfig.put("password", AttributeBO.builder().value("6614").build()); // 替换为你的 Windows 密码 - - // 方案2:如果模拟器配置了特定用户,使用该用户 - driverConfig.put("username", AttributeBO.builder().value("Administrator").build()); - driverConfig.put("password", AttributeBO.builder().value("12356").build()); - - // 方案3:尝试匿名访问(某些 OPC 服务器支持) - // driverConfig.put("username", AttributeBO.builder().value("").build()); - // driverConfig.put("password", AttributeBO.builder().value("").build()); - - // 构建点位配置 - // 根据你的模拟器截图,Item ID 是 "Random.Int1" - Map pointConfig = new HashMap<>(); - pointConfig.put("group", AttributeBO.builder().value("Group0").build()); // 组名 - pointConfig.put("tag", AttributeBO.builder().value("Random.Int1").build()); // 标签名,与模拟器中的 Item ID 一致 - - // 构建连接对象 - IotConnect connect = IotConnect.builder() - .id(1) - .code("OPC_DA_001") - .host("localhost") - .protocol("opc-da") - .enabled(true) - .description("测试OPC DA连接") - .build(); - - // 构建配置对象 - IotConfig config = IotConfig.builder() - .id(1) + IotConfig config3 = IotConfig.builder() + .id(3) .connectId(1) - .alias("sensor1") - .aliasName("传感器1") - .registerAddress("Random.Int1") - .dataType("int") - .readonly(true) + .alias("output_value3") + .aliasName("输出值3") + .registerAddress("40003") + .dataType("LREAL") + .readonly(false) // 设置为可写 .enabled(true) - .description("测试OPC DA数据读取") + .description("测试批量写入3") .build(); + // 执行批量写入操作 try { - System.out.println("=== OPC DA读取测试开始 ==="); - System.out.println("连接地址: " + driverConfig.get("host").getValue()); - System.out.println("CLSID: " + driverConfig.get("clsId").getValue()); - System.out.println("组名: " + pointConfig.get("group").getValue()); - System.out.println("标签: " + pointConfig.get("tag").getValue()); + System.out.println("========== 开始测试Modbus TCP批量写入 =========="); + System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort()); - // 调用read方法进行测试 - RValue result = opcDaProtocolDriver.read(driverConfig, pointConfig, connect, config); + // 转换为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(); - // 检查结果是否为null - if (result != null) { - System.out.println("\n✓ 读取成功!"); - System.out.println("连接信息: " + result.getConnect()); - System.out.println("配置信息: " + result.getConfig()); - System.out.println("读取值: " + result.getValue()); - } else { - System.err.println("\n✗ 读取失败:返回结果为null"); - System.err.println("可能原因:"); - System.err.println("1. OPC DA服务器未启动或主机地址不正确"); - System.err.println("2. CLSID配置错误或服务器未注册"); - System.err.println("3. 用户名或密码错误"); - System.err.println("4. 组名或标签名不存在"); - System.err.println("5. DCOM配置不正确"); + // 转换为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") // 写入值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("200") // 写入值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("25.5") // 写入值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()); } - System.out.println("\n=== 测试完成 ==="); + // 调用驱动批量写入数据 + List result = modBusProtocolDriver.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 ? "成功" : "失败")); + } + + System.out.println("========== 批量写入测试完成 =========="); } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); + System.err.println("批量写入测试失败: " + e.getMessage()); e.printStackTrace(); } } - @Test - public void opcDaTestWrite() { - // 初始化驱动 - opcDaProtocolDriver.initial(); - - // 构建驱动配置(连接配置) - Map driverConfig = new HashMap<>(); - driverConfig.put("host", AttributeBO.builder().value("localhost").build()); - driverConfig.put("clsId", AttributeBO.builder().value("F8582CF2-88FB-11D0-B850-00C0F0104305").build()); // OPC DA Server CLSID - driverConfig.put("username", AttributeBO.builder().value("Administrator").build()); - driverConfig.put("password", AttributeBO.builder().value("password").build()); - - // 构建点位配置 - Map pointConfig = new HashMap<>(); - pointConfig.put("group", AttributeBO.builder().value("Group1").build()); // 组名 - pointConfig.put("tag", AttributeBO.builder().value("Channel1.Device1.Tag1").build()); // 标签名 - - // 构建连接对象 - IotConnect connect = IotConnect.builder() - .id(1) - .code("OPC_DA_001") - .host("localhost") - .protocol("opc-da") - .enabled(true) - .description("测试OPC DA写入连接") - .build(); - - // 构建配置对象 - IotConfig config = IotConfig.builder() - .id(1) - .connectId(1) - .alias("sensor1") - .aliasName("传感器1") - .registerAddress("Channel1.Device1.Tag1") - .dataType("int") - .readonly(false) - .enabled(true) - .description("测试OPC DA数据写入") - .build(); - - try { - System.out.println("=== OPC DA写入测试开始 ==="); - - // 先读取当前值 - RValue beforeValue = opcDaProtocolDriver.read(driverConfig, pointConfig, connect, config); - if (beforeValue != null) { - System.out.println("写入前的值: " + beforeValue.getValue()); - } else { - System.out.println("写入前读取失败,继续执行写入测试..."); - } - - // 构建写入值对象 - 写入新的值(例如:100) - WValue wValue = new WValue(); - wValue.setValue("100"); - wValue.setType("int"); - - // 调用write方法进行写入测试 - Boolean writeResult = opcDaProtocolDriver.write(driverConfig, pointConfig, connect, config, wValue); - System.out.println("写入结果: " + (writeResult ? "成功" : "失败")); - - // 等待一小段时间后再次读取,验证写入是否成功 - Thread.sleep(500); - RValue afterValue = opcDaProtocolDriver.read(driverConfig, pointConfig, connect, config); - - if (afterValue != null) { - System.out.println("写入后的值: " + afterValue.getValue()); - - // 验证写入是否成功 - if (writeResult && "100".equals(afterValue.getValue())) { - System.out.println("✓ 写入验证成功!值已更新为: " + afterValue.getValue()); - } else { - System.out.println("✗ 写入验证失败!期望值: 100, 实际值: " + afterValue.getValue()); - } - } else { - System.err.println("写入后读取失败,无法验证写入结果"); - } - - System.out.println("\n=== 测试完成 ==="); - - } catch (Exception e) { - System.err.println("\n✗ 测试异常: " + e.getMessage()); - e.printStackTrace(); - } - } }