From 79ce8f976bf49ede7564c67694ce2d7bba4f3e6f Mon Sep 17 00:00:00 2001 From: liyongde <1419499670@qq.com> Date: Tue, 3 Mar 2026 16:17:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20modbus-tcp=E5=86=99=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nl/iot/core/driver/bo/AttributeBO.java | 74 ------ .../org/nl/iot/core/driver/entity/WValue.java | 90 ++++++++ .../modbustcp/ModBusProtocolDriverImpl.java | 155 ++++--------- .../modbustcp/util/ModBusTcpUtils.java | 214 ++++++++++++++++++ .../driver/service/DriverCustomService.java | 3 +- nl-web-app/src/test/java/org/nl/ApiTest.java | 72 ++++++ 6 files changed, 427 insertions(+), 181 deletions(-) create mode 100644 nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java create mode 100644 nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java index 3af02da..18df8d8 100644 --- a/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java @@ -46,80 +46,6 @@ public class AttributeBO implements Serializable { */ private String value; - /** - * 类型, value type, 用于确定value的真实类型 - */ -// private AttributeTypeFlagEnum type; - - /** - * 根据类型转换数据 - * - * @param clazz T Class - * @param T - * @return T - */ - @SuppressWarnings("unchecked") -// public T getValue(Class clazz) { -// if (Objects.isNull(type)) { -// throw new CommonException("Unsupported attribute type of " + type); -// } -// if (StringUtils.isEmpty(value)) { -// throw new CommonException("Attribute value is empty"); -// } -// -// final String message = "Attribute type is: {}, can't be cast to class: {}"; -// return switch (type) { -// case STRING -> { -// if (!clazz.equals(String.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) value; -// } -// case BYTE -> { -// if (!clazz.equals(Byte.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Byte.valueOf(value); -// } -// case SHORT -> { -// if (!clazz.equals(Short.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Short.valueOf(value); -// } -// case INT -> { -// if (!clazz.equals(Integer.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Integer.valueOf(value); -// } -// case LONG -> { -// if (!clazz.equals(Long.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Long.valueOf(value); -// } -// case FLOAT -> { -// if (!clazz.equals(Float.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Float.valueOf(value); -// } -// case DOUBLE -> { -// if (!clazz.equals(Double.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Double.valueOf(value); -// } -// case BOOLEAN -> { -// if (!clazz.equals(Boolean.class)) { -// throw new CommonException(message, type.getCode(), clazz.getName()); -// } -// yield (T) Boolean.valueOf(value); -// } -// }; -// } - /** * 将私有变量value转换为指定的基础数据类型 * @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean) 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 new file mode 100644 index 0000000..413f034 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/WValue.java @@ -0,0 +1,90 @@ +package org.nl.iot.core.driver.entity; + +import lombok.*; +import org.nl.common.exception.CommonException; + +import java.io.Serial; +import java.io.Serializable; + +/** + * + * @author: lyd + * @date: 2026/3/3 + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class WValue implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 值, string, 需要根据type确定真实的数据类型 + */ + private String value; + + /** + * 类型, value type, 用于确定value的真实类型 + *

+ * 同位号数据类型一致 + */ + private String type; + + /** + * 将私有变量value转换为指定的基础数据类型 + * @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean) + * @return 转换后的目标类型实例 + * @throws CommonException 类型不支持或转换失败时抛出 + */ + public T getValueByClass(Class clazz) { + // 先校验原始值是否为null(根据业务需求,也可调整为允许null转换) + if (value == null) { + throw new CommonException("原始值value为null,无法转换为" + clazz.getSimpleName()); + } + + // 去除字符串首尾空格(可选,根据业务需求决定是否保留) + String trimmedValue = value.trim(); + if (trimmedValue.isEmpty()) { + throw new CommonException("原始值value为空字符串,无法转换为" + clazz.getSimpleName()); + } + + // 按类型分支处理转换逻辑 + try { + if (clazz == String.class) { + return clazz.cast(value); // String类型直接返回原字符串 + } else if (clazz == Byte.class || clazz == byte.class) { + return clazz.cast(Byte.parseByte(trimmedValue)); + } else if (clazz == Short.class || clazz == short.class) { + return clazz.cast(Short.parseShort(trimmedValue)); + } else if (clazz == Integer.class || clazz == int.class) { + return clazz.cast(Integer.parseInt(trimmedValue)); + } else if (clazz == Long.class || clazz == long.class) { + return clazz.cast(Long.parseLong(trimmedValue)); + } else if (clazz == Double.class || clazz == double.class) { + return clazz.cast(Double.parseDouble(trimmedValue)); + } else if (clazz == Float.class || clazz == float.class) { + return clazz.cast(Float.parseFloat(trimmedValue)); + } else if (clazz == Boolean.class || clazz == boolean.class) { + // 严格匹配布尔值:仅"true"/"false"(忽略大小写),避免"1"/"0"误判 + if ("true".equalsIgnoreCase(trimmedValue)) { + return clazz.cast(Boolean.TRUE); + } else if ("false".equalsIgnoreCase(trimmedValue)) { + return clazz.cast(Boolean.FALSE); + } else { + throw new NumberFormatException("布尔值仅支持\"true\"/\"false\"(忽略大小写),当前值:" + trimmedValue); + } + } else { + // 不支持的类型 + throw new CommonException("不支持的转换类型:" + clazz.getSimpleName() + + ",仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean"); + } + } catch (NumberFormatException e) { + // 转换失败(如字符串非数字、布尔值格式错误) + throw new CommonException("转换失败:无法将值\"" + value + "\"转换为" + clazz.getSimpleName(), e); + } + } +} 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 3f591c6..9ce4677 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 @@ -5,15 +5,14 @@ import lombok.extern.slf4j.Slf4j; import org.nl.common.exception.CommonException; import org.nl.iot.core.driver.bo.AttributeBO; import org.nl.iot.core.driver.entity.RValue; -import org.nl.iot.core.driver.enums.PointTypeFlagEnum; +import org.nl.iot.core.driver.entity.WValue; 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.code.DataType; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ErrorResponseException; import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException; -import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException; 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.ModBusTcpUtils; import org.nl.iot.core.driver.service.DriverCustomService; import org.nl.iot.modular.iot.entity.IotConfig; import org.nl.iot.modular.iot.entity.IotConnect; @@ -55,6 +54,18 @@ public class ModBusProtocolDriverImpl implements DriverCustomService { return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig)); } + @Override + public Boolean write(Map driverConfig, Map pointConfig, IotConnect connect, IotConfig config, WValue wValue) { + /* + * 写入设备点位数据 + * + * 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。 + * 通过 Modbus 连接器将指定值写入设备的点位, 并返回写入结果。 + */ + ModbusMaster modbusMaster = getConnector(connect.getId().toString(), driverConfig); + return writeValue(modbusMaster, pointConfig, wValue); + } + /** * 获取 Modbus Master 连接器 *

@@ -106,27 +117,27 @@ public class ModBusProtocolDriverImpl implements DriverCustomService { String type = pointConfig.get("data_type").getValueByClass(String.class); int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class); int offset = pointConfig.get("offset").getValueByClass(Integer.class); - int functionCode = getFunctionCode(offset); + int functionCode = ModBusTcpUtils.getFunctionCode(offset); // 计算实际的寄存器地址(Modbus协议地址从0开始) - int actualAddress = getActualAddress(offset, functionCode); + int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); switch (functionCode) { case 1: BaseLocator coilLocator = BaseLocator.coilStatus(slaveId, actualAddress); - Boolean coilValue = getMasterValue(modbusMaster, coilLocator); + Boolean coilValue = ModBusTcpUtils.getMasterValue(modbusMaster, coilLocator); return String.valueOf(coilValue); case 2: BaseLocator inputLocator = BaseLocator.inputStatus(slaveId, actualAddress); - Boolean inputStatusValue = getMasterValue(modbusMaster, inputLocator); + Boolean inputStatusValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputLocator); return String.valueOf(inputStatusValue); case 3: - BaseLocator holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, getValueType(type)); - Number holdingValue = getMasterValue(modbusMaster, holdingLocator); + 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, getValueType(type)); - Number inputRegisterValue = getMasterValue(modbusMaster, inputRegister); + BaseLocator inputRegister = BaseLocator.inputRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type)); + Number inputRegisterValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputRegister); return String.valueOf(inputRegisterValue); default: return "0"; @@ -134,109 +145,41 @@ public class ModBusProtocolDriverImpl implements DriverCustomService { } /** - * 从 ModbusMaster 连接器中读取指定点位的数据 + * 向 Modbus 设备写入点位值 *

- * 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。 - * 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常, - * 将记录错误日志并抛出 {@link CommonException} 异常。 + * 根据点位配置中的功能码(functionCode)和偏移量(offset), 将指定值写入 Modbus 设备的相应点位。 + * 支持的功能码包括: + * - 1: 写入线圈状态(Coil Status) + * - 3: 写入保持寄存器(Holding Register) + *

+ * 对于功能码 1, 写入布尔值到线圈状态, 并返回写入结果。 + * 对于功能码 3, 写入数值到保持寄存器, 并返回写入成功状态。 + * 其他功能码暂不支持, 返回 false。 * * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 - * @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息 - * @param 返回值类型, 根据点位的数据类型确定 - * @return T 返回读取到的点位数据 - * @throws CommonException 如果读取过程中发生异常, 抛出此异常 + * @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息 + * @param wValue 待写入的值, 包含值类型和具体数值 + * @return boolean 返回写入结果, true 表示写入成功, false 表示写入失败或不支持的功能码 */ - private T getMasterValue(ModbusMaster modbusMaster, BaseLocator locator) { - try { - return modbusMaster.getValue(locator); - } catch (ModbusTransportException | ErrorResponseException e) { - log.error("Read modbus master value error: {}", e.getMessage(), e); - throw new CommonException(e.getMessage()); - } - } + private boolean writeValue(ModbusMaster modbusMaster, Map pointConfig, WValue wValue) { + String type = pointConfig.get("data_type").getValueByClass(String.class); + wValue.setType(type); + int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class); + int offset = pointConfig.get("offset").getValueByClass(Integer.class); + int functionCode = ModBusTcpUtils.getFunctionCode(offset); - /** - * 获取 Modbus 数据类型 - *

- * 根据点位值类型(type)返回对应的 Modbus 数据类型。 - * - 其他类型: 返回 2 字节有符号整数({@link DataType#TWO_BYTE_INT_SIGNED}) - *

- * 提示: 该方法可根据实际项目需求进行扩展, 例如支持字节交换, 大端/小端模式等。 - * - * @param type 点位值类型, 用于确定 Modbus 数据类型 - * @return int 返回对应的 Modbus 数据类型 - * @throws CommonException 如果点位值类型不支持, 抛出此异常 - */ - private int getValueType(String type) { - - switch (type.toLowerCase()) { - case "int32": - return DataType.FOUR_BYTE_INT_SIGNED; - case "uint32": - return DataType.FOUR_BYTE_INT_UNSIGNED; - case "double": - return DataType.EIGHT_BYTE_FLOAT; - case "float32": - return DataType.FOUR_BYTE_FLOAT; - default: - return DataType.TWO_BYTE_INT_SIGNED; - } - } - - /** - * 根据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 int getFunctionCode(int offset) { - int functionCode; - - if (offset >= 1 && offset <= 9999) { - functionCode = 1; // 线圈 - } else if (offset >= 10001 && offset <= 19999) { - functionCode = 2; // 离散输入 - } else if (offset >= 30001 && offset <= 39999) { - functionCode = 4; // 输入寄存器 - } else if (offset >= 40001 && offset <= 49999) { - functionCode = 3; // 保持寄存器 - } else { - throw new CommonException("无效的偏移量:" + offset); - } - - return functionCode; - } - - /** - * 根据Modbus地址偏移量和功能码计算实际的寄存器地址 - * Modbus协议中,寄存器地址从0开始,需要减去相应的偏移量 - * - * @param offset 地址偏移量(1-9999/10001-19999/30001-39999/40001-49999) - * @param functionCode 功能码(1-4) - * @return 实际的寄存器地址(从0开始) - */ - private int getActualAddress(int offset, int functionCode) { + // 计算实际的寄存器地址(Modbus协议地址从0开始) + int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode); switch (functionCode) { case 1: - // 线圈状态:1-9999 -> 0-9998 - return offset - 1; - case 2: - // 离散输入:10001-19999 -> 0-9998 - return offset - 10001; + WriteCoilResponse coilResponse = ModBusTcpUtils.setMasterValue(modbusMaster, slaveId, actualAddress, wValue); + return !coilResponse.isException(); case 3: - // 保持寄存器:40001-49999 -> 0-9998 - return offset - 40001; - case 4: - // 输入寄存器:30001-39999 -> 0-9998 - return offset - 30001; + BaseLocator locator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type)); + ModBusTcpUtils.setMasterValue(modbusMaster, locator, wValue); + return true; default: - return 0; + return false; } } } 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 new file mode 100644 index 0000000..154759b --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/util/ModBusTcpUtils.java @@ -0,0 +1,214 @@ +package org.nl.iot.core.driver.protocol.modbustcp.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.formula.functions.T; +import org.nl.common.exception.CommonException; +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; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ErrorResponseException; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException; +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.WriteCoilRequest; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.WriteCoilResponse; + +/** + * modbus tcp工具类 + * @author: lyd + * @date: 2026/3/3 + */ +@Slf4j +public class ModBusTcpUtils { + + /** + * 获取 Modbus 数据类型 + *

+ * 根据点位值类型(type)返回对应的 Modbus 数据类型。 + * - 其他类型: 返回 2 字节有符号整数({@link DataType#TWO_BYTE_INT_SIGNED}) + *

+ * 提示: 该方法可根据实际项目需求进行扩展, 例如支持字节交换, 大端/小端模式等。 + * + * @param type 点位值类型, 用于确定 Modbus 数据类型 + * @return int 返回对应的 Modbus 数据类型 + * @throws CommonException 如果点位值类型不支持, 抛出此异常 + */ + public static int getValueType(String type) { + + switch (type.toLowerCase()) { + case "int32": + return DataType.FOUR_BYTE_INT_SIGNED; + case "uint32": + return DataType.FOUR_BYTE_INT_UNSIGNED; + case "double": + return DataType.EIGHT_BYTE_FLOAT; + case "float32": + return DataType.FOUR_BYTE_FLOAT; + default: + return DataType.TWO_BYTE_INT_SIGNED; + } + } + /** + * 根据Modbus等场景常用的数值类型字符串,获取对应的Java类型Class + * @param type 类型字符串(如int32、uint32、double、float32) + * @return 对应的Java类型Class,如果不支持该类型则返回null + * @param 泛型类型,匹配返回的Class类型 + */ + @SuppressWarnings("unchecked") + public static Class getJavaValueType(String type) { + // 处理空值,避免NullPointerException + if (type == null) { + return null; + } + + return switch (type.toLowerCase()) { + case "int32" -> + // 32位有符号整数对应Java的Integer + (Class) Integer.class; + case "uint32" -> + // 32位无符号整数超出Java int范围(int最大2^31-1),需用Long存储 + (Class) Long.class; + case "double" -> + // 64位双精度浮点数对应Java的Double + (Class) Double.class; + case "float32" -> + // 32位单精度浮点数对应Java的Float + (Class) Float.class; + // 可扩展其他常用类型 + case "int16" -> (Class) Short.class; + case "uint16" -> (Class) Integer.class; + case "boolean" -> (Class) Boolean.class; + default -> + // 不支持的类型返回null,也可根据需求抛出异常 + null; + }; + } + + + /** + * 根据Modbus地址偏移量和功能码计算实际的寄存器地址 + * Modbus协议中,寄存器地址从0开始,需要减去相应的偏移量 + * + * @param offset 地址偏移量(1-9999/10001-19999/30001-39999/40001-49999) + * @param functionCode 功能码(1-4) + * @return 实际的寄存器地址(从0开始) + */ + public static int getActualAddress(int offset, int functionCode) { + switch (functionCode) { + case 1: + // 线圈状态:1-9999 -> 0-9998 + return offset - 1; + case 2: + // 离散输入:10001-19999 -> 0-9998 + return offset - 10001; + case 3: + // 保持寄存器:40001-49999 -> 0-9998 + return offset - 40001; + case 4: + // 输入寄存器:30001-39999 -> 0-9998 + return offset - 30001; + default: + return 0; + } + } + + /** + * 根据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 int getFunctionCode(int offset) { + int functionCode; + + if (offset >= 1 && offset <= 9999) { + functionCode = 1; // 线圈 + } else if (offset >= 10001 && offset <= 19999) { + functionCode = 2; // 离散输入 + } else if (offset >= 30001 && offset <= 39999) { + functionCode = 4; // 输入寄存器 + } else if (offset >= 40001 && offset <= 49999) { + functionCode = 3; // 保持寄存器 + } else { + throw new CommonException("无效的偏移量:" + offset); + } + + return functionCode; + } + + + + /** + * 从 ModbusMaster 连接器中读取指定点位的数据 + *

+ * 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。 + * 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常, + * 将记录错误日志并抛出 {@link CommonException} 异常。 + * + * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 + * @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息 + * @param 返回值类型, 根据点位的数据类型确定 + * @return T 返回读取到的点位数据 + * @throws CommonException 如果读取过程中发生异常, 抛出此异常 + */ + public static T getMasterValue(ModbusMaster modbusMaster, BaseLocator locator) { + try { + return modbusMaster.getValue(locator); + } catch (ModbusTransportException | ErrorResponseException e) { + log.error("Read modbus master value error: {}", e.getMessage(), e); + throw new CommonException(e.getMessage()); + } + } + + /** + * 向 Modbus 设备写入线圈状态值 + *

+ * 该方法通过 ModbusMaster 连接器向指定从站(slaveId)的线圈(offset)写入布尔值。 + * 如果写入过程中发生 {@link ModbusTransportException} 异常, 将记录错误日志并抛出 {@link WritePointException} 异常。 + * + * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 + * @param slaveId 从站ID, 标识目标设备 + * @param offset 线圈偏移量, 标识目标线圈 + * @param wValue 待写入的值, 包含布尔值 + * @return WriteCoilResponse 返回写入操作的响应结果 + * @throws CommonException 如果写入过程中发生异常, 抛出此异常 + */ + public static WriteCoilResponse setMasterValue(ModbusMaster modbusMaster, int slaveId, int offset, WValue wValue) { + try { + WriteCoilRequest coilRequest = new WriteCoilRequest(slaveId, offset, wValue.getValueByClass(Boolean.class)); + return (WriteCoilResponse) modbusMaster.send(coilRequest); + } catch (ModbusTransportException e) { + log.error("Write modbus master value error: {}", e.getMessage(), e); + throw new CommonException(e.getMessage()); + } + } + + /** + * 向 Modbus 设备写入指定类型的值 + *

+ * 该方法通过 ModbusMaster 连接器向指定点位写入值。点位信息由 {@link BaseLocator} 定义, + * 写入的值类型由 {@link WValue} 指定。支持写入浮点数类型的数据。 + *

+ * 如果写入过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常, + * 将记录错误日志并抛出 {@link CommonException} 异常。 + * + * @param modbusMaster ModbusMaster 连接器, 用于与设备通信 + * @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息 + * @param wValue 待写入的值, 包含值类型和具体数值 + * @param 返回值类型, 根据点位的数据类型确定 + * @throws CommonException 如果写入过程中发生异常, 抛出此异常 + */ + public static void setMasterValue(ModbusMaster modbusMaster, BaseLocator locator, WValue wValue) { + try { + modbusMaster.setValue(locator, wValue.getValueByClass(ModBusTcpUtils.getJavaValueType(wValue.getType()))); + } catch (ModbusTransportException | ErrorResponseException e) { + log.error("Write modbus master value error: {}", e.getMessage(), e); + throw new CommonException(e.getMessage()); + } + } +} 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 ba4fc5d..bef3456 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 @@ -2,6 +2,7 @@ package org.nl.iot.core.driver.service; import org.nl.iot.core.driver.bo.AttributeBO; import org.nl.iot.core.driver.entity.RValue; +import org.nl.iot.core.driver.entity.WValue; import org.nl.iot.modular.iot.entity.IotConfig; import org.nl.iot.modular.iot.entity.IotConnect; @@ -58,6 +59,6 @@ public interface DriverCustomService { * @param wValue 待写入的数据, 封装在 {@link WValue} 对象中 * @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常 */ -// Boolean write(Map driverConfig, Map pointConfig, IotConnect device, IotConfig point, WValue wValue); + Boolean write(Map driverConfig, Map pointConfig, IotConnect device, IotConfig point, WValue wValue); } 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 ca573f5..9064125 100644 --- a/nl-web-app/src/test/java/org/nl/ApiTest.java +++ b/nl-web-app/src/test/java/org/nl/ApiTest.java @@ -79,4 +79,76 @@ public class ApiTest { 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()); + + // 构建连接对象 + IotConnect connect = IotConnect.builder() + .id(1) + .code("MODBUS_TCP_001") + .host("192.168.81.251") + .port(502) + .protocol("modbus-tcp") + .enabled(true) + .description("测试Modbus TCP写入连接") + .build(); + + // 构建配置对象 + IotConfig config = 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(); + } + } }