feat: modbus-tcp写功能及测试
This commit is contained in:
@@ -46,80 +46,6 @@ public class AttributeBO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型, value type, 用于确定value的真实类型
|
|
||||||
*/
|
|
||||||
// private AttributeTypeFlagEnum type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据类型转换数据
|
|
||||||
*
|
|
||||||
* @param clazz T Class
|
|
||||||
* @param <T> T
|
|
||||||
* @return T
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
// public <T> T getValue(Class<T> 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转换为指定的基础数据类型
|
* 将私有变量value转换为指定的基础数据类型
|
||||||
* @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean)
|
* @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean)
|
||||||
|
|||||||
@@ -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的真实类型
|
||||||
|
* <p>
|
||||||
|
* 同位号数据类型一致
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将私有变量value转换为指定的基础数据类型
|
||||||
|
* @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean)
|
||||||
|
* @return 转换后的目标类型实例
|
||||||
|
* @throws CommonException 类型不支持或转换失败时抛出
|
||||||
|
*/
|
||||||
|
public <T> T getValueByClass(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,15 +5,14 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.nl.common.exception.CommonException;
|
import org.nl.common.exception.CommonException;
|
||||||
import org.nl.iot.core.driver.bo.AttributeBO;
|
import org.nl.iot.core.driver.bo.AttributeBO;
|
||||||
import org.nl.iot.core.driver.entity.RValue;
|
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.ModbusFactory;
|
||||||
import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
|
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.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.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.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.core.driver.service.DriverCustomService;
|
||||||
import org.nl.iot.modular.iot.entity.IotConfig;
|
import org.nl.iot.modular.iot.entity.IotConfig;
|
||||||
import org.nl.iot.modular.iot.entity.IotConnect;
|
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));
|
return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config, WValue wValue) {
|
||||||
|
/*
|
||||||
|
* 写入设备点位数据
|
||||||
|
*
|
||||||
|
* 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。
|
||||||
|
* 通过 Modbus 连接器将指定值写入设备的点位, 并返回写入结果。
|
||||||
|
*/
|
||||||
|
ModbusMaster modbusMaster = getConnector(connect.getId().toString(), driverConfig);
|
||||||
|
return writeValue(modbusMaster, pointConfig, wValue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Modbus Master 连接器
|
* 获取 Modbus Master 连接器
|
||||||
* <p>
|
* <p>
|
||||||
@@ -106,27 +117,27 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
|
|||||||
String type = pointConfig.get("data_type").getValueByClass(String.class);
|
String type = pointConfig.get("data_type").getValueByClass(String.class);
|
||||||
int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
|
int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
|
||||||
int offset = pointConfig.get("offset").getValueByClass(Integer.class);
|
int offset = pointConfig.get("offset").getValueByClass(Integer.class);
|
||||||
int functionCode = getFunctionCode(offset);
|
int functionCode = ModBusTcpUtils.getFunctionCode(offset);
|
||||||
|
|
||||||
// 计算实际的寄存器地址(Modbus协议地址从0开始)
|
// 计算实际的寄存器地址(Modbus协议地址从0开始)
|
||||||
int actualAddress = getActualAddress(offset, functionCode);
|
int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode);
|
||||||
|
|
||||||
switch (functionCode) {
|
switch (functionCode) {
|
||||||
case 1:
|
case 1:
|
||||||
BaseLocator<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, actualAddress);
|
BaseLocator<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, actualAddress);
|
||||||
Boolean coilValue = getMasterValue(modbusMaster, coilLocator);
|
Boolean coilValue = ModBusTcpUtils.getMasterValue(modbusMaster, coilLocator);
|
||||||
return String.valueOf(coilValue);
|
return String.valueOf(coilValue);
|
||||||
case 2:
|
case 2:
|
||||||
BaseLocator<Boolean> inputLocator = BaseLocator.inputStatus(slaveId, actualAddress);
|
BaseLocator<Boolean> inputLocator = BaseLocator.inputStatus(slaveId, actualAddress);
|
||||||
Boolean inputStatusValue = getMasterValue(modbusMaster, inputLocator);
|
Boolean inputStatusValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputLocator);
|
||||||
return String.valueOf(inputStatusValue);
|
return String.valueOf(inputStatusValue);
|
||||||
case 3:
|
case 3:
|
||||||
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, getValueType(type));
|
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type));
|
||||||
Number holdingValue = getMasterValue(modbusMaster, holdingLocator);
|
Number holdingValue = ModBusTcpUtils.getMasterValue(modbusMaster, holdingLocator);
|
||||||
return String.valueOf(holdingValue);
|
return String.valueOf(holdingValue);
|
||||||
case 4:
|
case 4:
|
||||||
BaseLocator<Number> inputRegister = BaseLocator.inputRegister(slaveId, actualAddress, getValueType(type));
|
BaseLocator<Number> inputRegister = BaseLocator.inputRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type));
|
||||||
Number inputRegisterValue = getMasterValue(modbusMaster, inputRegister);
|
Number inputRegisterValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputRegister);
|
||||||
return String.valueOf(inputRegisterValue);
|
return String.valueOf(inputRegisterValue);
|
||||||
default:
|
default:
|
||||||
return "0";
|
return "0";
|
||||||
@@ -134,109 +145,41 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 ModbusMaster 连接器中读取指定点位的数据
|
* 向 Modbus 设备写入点位值
|
||||||
* <p>
|
* <p>
|
||||||
* 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。
|
* 根据点位配置中的功能码(functionCode)和偏移量(offset), 将指定值写入 Modbus 设备的相应点位。
|
||||||
* 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常,
|
* 支持的功能码包括:
|
||||||
* 将记录错误日志并抛出 {@link CommonException} 异常。
|
* - 1: 写入线圈状态(Coil Status)
|
||||||
|
* - 3: 写入保持寄存器(Holding Register)
|
||||||
|
* <p>
|
||||||
|
* 对于功能码 1, 写入布尔值到线圈状态, 并返回写入结果。
|
||||||
|
* 对于功能码 3, 写入数值到保持寄存器, 并返回写入成功状态。
|
||||||
|
* 其他功能码暂不支持, 返回 false。
|
||||||
*
|
*
|
||||||
* @param modbusMaster ModbusMaster 连接器, 用于与设备通信
|
* @param modbusMaster ModbusMaster 连接器, 用于与设备通信
|
||||||
* @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息
|
* @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息
|
||||||
* @param <T> 返回值类型, 根据点位的数据类型确定
|
* @param wValue 待写入的值, 包含值类型和具体数值
|
||||||
* @return T 返回读取到的点位数据
|
* @return boolean 返回写入结果, true 表示写入成功, false 表示写入失败或不支持的功能码
|
||||||
* @throws CommonException 如果读取过程中发生异常, 抛出此异常
|
|
||||||
*/
|
*/
|
||||||
private <T> T getMasterValue(ModbusMaster modbusMaster, BaseLocator<T> locator) {
|
private boolean writeValue(ModbusMaster modbusMaster, Map<String, AttributeBO> pointConfig, WValue wValue) {
|
||||||
try {
|
String type = pointConfig.get("data_type").getValueByClass(String.class);
|
||||||
return modbusMaster.getValue(locator);
|
wValue.setType(type);
|
||||||
} catch (ModbusTransportException | ErrorResponseException e) {
|
int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
|
||||||
log.error("Read modbus master value error: {}", e.getMessage(), e);
|
int offset = pointConfig.get("offset").getValueByClass(Integer.class);
|
||||||
throw new CommonException(e.getMessage());
|
int functionCode = ModBusTcpUtils.getFunctionCode(offset);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 计算实际的寄存器地址(Modbus协议地址从0开始)
|
||||||
* 获取 Modbus 数据类型
|
int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode);
|
||||||
* <p>
|
|
||||||
* 根据点位值类型(type)返回对应的 Modbus 数据类型。
|
|
||||||
* - 其他类型: 返回 2 字节有符号整数({@link DataType#TWO_BYTE_INT_SIGNED})
|
|
||||||
* <p>
|
|
||||||
* 提示: 该方法可根据实际项目需求进行扩展, 例如支持字节交换, 大端/小端模式等。
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
switch (functionCode) {
|
switch (functionCode) {
|
||||||
case 1:
|
case 1:
|
||||||
// 线圈状态:1-9999 -> 0-9998
|
WriteCoilResponse coilResponse = ModBusTcpUtils.setMasterValue(modbusMaster, slaveId, actualAddress, wValue);
|
||||||
return offset - 1;
|
return !coilResponse.isException();
|
||||||
case 2:
|
|
||||||
// 离散输入:10001-19999 -> 0-9998
|
|
||||||
return offset - 10001;
|
|
||||||
case 3:
|
case 3:
|
||||||
// 保持寄存器:40001-49999 -> 0-9998
|
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type));
|
||||||
return offset - 40001;
|
ModBusTcpUtils.setMasterValue(modbusMaster, locator, wValue);
|
||||||
case 4:
|
return true;
|
||||||
// 输入寄存器:30001-39999 -> 0-9998
|
|
||||||
return offset - 30001;
|
|
||||||
default:
|
default:
|
||||||
return 0;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 数据类型
|
||||||
|
* <p>
|
||||||
|
* 根据点位值类型(type)返回对应的 Modbus 数据类型。
|
||||||
|
* - 其他类型: 返回 2 字节有符号整数({@link DataType#TWO_BYTE_INT_SIGNED})
|
||||||
|
* <p>
|
||||||
|
* 提示: 该方法可根据实际项目需求进行扩展, 例如支持字节交换, 大端/小端模式等。
|
||||||
|
*
|
||||||
|
* @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 <T> 泛型类型,匹配返回的Class类型
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> Class<T> getJavaValueType(String type) {
|
||||||
|
// 处理空值,避免NullPointerException
|
||||||
|
if (type == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (type.toLowerCase()) {
|
||||||
|
case "int32" ->
|
||||||
|
// 32位有符号整数对应Java的Integer
|
||||||
|
(Class<T>) Integer.class;
|
||||||
|
case "uint32" ->
|
||||||
|
// 32位无符号整数超出Java int范围(int最大2^31-1),需用Long存储
|
||||||
|
(Class<T>) Long.class;
|
||||||
|
case "double" ->
|
||||||
|
// 64位双精度浮点数对应Java的Double
|
||||||
|
(Class<T>) Double.class;
|
||||||
|
case "float32" ->
|
||||||
|
// 32位单精度浮点数对应Java的Float
|
||||||
|
(Class<T>) Float.class;
|
||||||
|
// 可扩展其他常用类型
|
||||||
|
case "int16" -> (Class<T>) Short.class;
|
||||||
|
case "uint16" -> (Class<T>) Integer.class;
|
||||||
|
case "boolean" -> (Class<T>) 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 连接器中读取指定点位的数据
|
||||||
|
* <p>
|
||||||
|
* 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。
|
||||||
|
* 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常,
|
||||||
|
* 将记录错误日志并抛出 {@link CommonException} 异常。
|
||||||
|
*
|
||||||
|
* @param modbusMaster ModbusMaster 连接器, 用于与设备通信
|
||||||
|
* @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息
|
||||||
|
* @param <T> 返回值类型, 根据点位的数据类型确定
|
||||||
|
* @return T 返回读取到的点位数据
|
||||||
|
* @throws CommonException 如果读取过程中发生异常, 抛出此异常
|
||||||
|
*/
|
||||||
|
public static <T> T getMasterValue(ModbusMaster modbusMaster, BaseLocator<T> 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 设备写入线圈状态值
|
||||||
|
* <p>
|
||||||
|
* 该方法通过 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 设备写入指定类型的值
|
||||||
|
* <p>
|
||||||
|
* 该方法通过 ModbusMaster 连接器向指定点位写入值。点位信息由 {@link BaseLocator} 定义,
|
||||||
|
* 写入的值类型由 {@link WValue} 指定。支持写入浮点数类型的数据。
|
||||||
|
* <p>
|
||||||
|
* 如果写入过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常,
|
||||||
|
* 将记录错误日志并抛出 {@link CommonException} 异常。
|
||||||
|
*
|
||||||
|
* @param modbusMaster ModbusMaster 连接器, 用于与设备通信
|
||||||
|
* @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息
|
||||||
|
* @param wValue 待写入的值, 包含值类型和具体数值
|
||||||
|
* @param <T> 返回值类型, 根据点位的数据类型确定
|
||||||
|
* @throws CommonException 如果写入过程中发生异常, 抛出此异常
|
||||||
|
*/
|
||||||
|
public static <T> void setMasterValue(ModbusMaster modbusMaster, BaseLocator<T> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.bo.AttributeBO;
|
||||||
import org.nl.iot.core.driver.entity.RValue;
|
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.IotConfig;
|
||||||
import org.nl.iot.modular.iot.entity.IotConnect;
|
import org.nl.iot.modular.iot.entity.IotConnect;
|
||||||
|
|
||||||
@@ -58,6 +59,6 @@ public interface DriverCustomService {
|
|||||||
* @param wValue 待写入的数据, 封装在 {@link WValue} 对象中
|
* @param wValue 待写入的数据, 封装在 {@link WValue} 对象中
|
||||||
* @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常
|
* @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常
|
||||||
*/
|
*/
|
||||||
// Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue);
|
Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,4 +79,76 @@ public class ApiTest {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modbusTestWrite() {
|
||||||
|
// 构建驱动配置(连接配置)
|
||||||
|
Map<String, AttributeBO> driverConfig = new HashMap<>();
|
||||||
|
driverConfig.put("host", AttributeBO.builder().value("192.168.81.251").build());
|
||||||
|
driverConfig.put("port", AttributeBO.builder().value("502").build());
|
||||||
|
|
||||||
|
// 构建点位配置 - 写入保持寄存器(功能码3)
|
||||||
|
Map<String, AttributeBO> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user