feat: modbus-tcp写功能及测试
This commit is contained in:
@@ -46,80 +46,6 @@ public class AttributeBO implements Serializable {
|
||||
*/
|
||||
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转换为指定的基础数据类型
|
||||
* @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.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<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 连接器
|
||||
* <p>
|
||||
@@ -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<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, actualAddress);
|
||||
Boolean coilValue = getMasterValue(modbusMaster, coilLocator);
|
||||
Boolean coilValue = ModBusTcpUtils.getMasterValue(modbusMaster, coilLocator);
|
||||
return String.valueOf(coilValue);
|
||||
case 2:
|
||||
BaseLocator<Boolean> inputLocator = BaseLocator.inputStatus(slaveId, actualAddress);
|
||||
Boolean inputStatusValue = getMasterValue(modbusMaster, inputLocator);
|
||||
Boolean inputStatusValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputLocator);
|
||||
return String.valueOf(inputStatusValue);
|
||||
case 3:
|
||||
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, getValueType(type));
|
||||
Number holdingValue = getMasterValue(modbusMaster, holdingLocator);
|
||||
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type));
|
||||
Number holdingValue = ModBusTcpUtils.getMasterValue(modbusMaster, holdingLocator);
|
||||
return String.valueOf(holdingValue);
|
||||
case 4:
|
||||
BaseLocator<Number> inputRegister = BaseLocator.inputRegister(slaveId, actualAddress, getValueType(type));
|
||||
Number inputRegisterValue = getMasterValue(modbusMaster, inputRegister);
|
||||
BaseLocator<Number> 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 设备写入点位值
|
||||
* <p>
|
||||
* 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。
|
||||
* 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常,
|
||||
* 将记录错误日志并抛出 {@link CommonException} 异常。
|
||||
* 根据点位配置中的功能码(functionCode)和偏移量(offset), 将指定值写入 Modbus 设备的相应点位。
|
||||
* 支持的功能码包括:
|
||||
* - 1: 写入线圈状态(Coil Status)
|
||||
* - 3: 写入保持寄存器(Holding Register)
|
||||
* <p>
|
||||
* 对于功能码 1, 写入布尔值到线圈状态, 并返回写入结果。
|
||||
* 对于功能码 3, 写入数值到保持寄存器, 并返回写入成功状态。
|
||||
* 其他功能码暂不支持, 返回 false。
|
||||
*
|
||||
* @param modbusMaster ModbusMaster 连接器, 用于与设备通信
|
||||
* @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息
|
||||
* @param <T> 返回值类型, 根据点位的数据类型确定
|
||||
* @return T 返回读取到的点位数据
|
||||
* @throws CommonException 如果读取过程中发生异常, 抛出此异常
|
||||
* @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息
|
||||
* @param wValue 待写入的值, 包含值类型和具体数值
|
||||
* @return boolean 返回写入结果, true 表示写入成功, false 表示写入失败或不支持的功能码
|
||||
*/
|
||||
private <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());
|
||||
}
|
||||
}
|
||||
private boolean writeValue(ModbusMaster modbusMaster, Map<String, AttributeBO> 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 数据类型
|
||||
* <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) {
|
||||
// 计算实际的寄存器地址(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<Number> locator = BaseLocator.holdingRegister(slaveId, actualAddress, ModBusTcpUtils.getValueType(type));
|
||||
ModBusTcpUtils.setMasterValue(modbusMaster, locator, wValue);
|
||||
return true;
|
||||
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.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<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();
|
||||
}
|
||||
}
|
||||
|
||||
@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