fix: modbus协议读写改造
This commit is contained in:
@@ -37,6 +37,26 @@
|
||||
<artifactId>j-interop</artifactId>
|
||||
<version>2.0.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.plc4x</groupId>
|
||||
<artifactId>plc4j-api</artifactId>
|
||||
<version>0.13.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.plc4x</groupId>
|
||||
<artifactId>plc4j-driver-s7</artifactId>
|
||||
<version>0.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.plc4x</groupId>
|
||||
<artifactId>plc4j-driver-modbus</artifactId>
|
||||
<version>0.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
41
nl-iot/src/main/java/org/nl/iot/core/driver/bo/DeviceBO.java
Normal file
41
nl-iot/src/main/java/org/nl/iot/core/driver/bo/DeviceBO.java
Normal file
@@ -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;
|
||||
}
|
||||
37
nl-iot/src/main/java/org/nl/iot/core/driver/bo/SiteBO.java
Normal file
37
nl-iot/src/main/java/org/nl/iot/core/driver/bo/SiteBO.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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确定真实的数据类型
|
||||
*/
|
||||
|
||||
@@ -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<String, ModbusMaster> connectMap;
|
||||
private Map<String, PlcConnection> 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<RValue> batchRead(DeviceBO device, List<SiteBO> points) {
|
||||
return batchReadValue(getConnector(device), device, points);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean write(DeviceBO device, WValue wValue) {
|
||||
return writeValue(getConnector(device), device, wValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WResponse> batchWrite(DeviceBO device, List<WValue> wValue) {
|
||||
return batchWriteValue(getConnector(device), wValue);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static List<WResponse> batchWriteValue(PlcConnection modbusMaster, List<WValue> wValues) {
|
||||
List<WResponse> res = new ArrayList<>();
|
||||
PlcWriteRequest.Builder writeRequestBuilder = doBuildWriteRequest(modbusMaster, wValues);
|
||||
PlcWriteRequest writeRequest = writeRequestBuilder.build();
|
||||
// 执行写入(异步+超时控制)
|
||||
CompletableFuture<? extends PlcWriteResponse> 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 连接器
|
||||
* <p>
|
||||
* 该方法用于根据设备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 设备点位值
|
||||
* <p>
|
||||
* 根据点位配置中的功能码(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<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, actualAddress);
|
||||
Boolean coilValue = ModBusTcpUtils.getMasterValue(modbusMaster, coilLocator);
|
||||
return String.valueOf(coilValue);
|
||||
case 2:
|
||||
BaseLocator<Boolean> inputLocator = BaseLocator.inputStatus(slaveId, actualAddress);
|
||||
Boolean inputStatusValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputLocator);
|
||||
return String.valueOf(inputStatusValue);
|
||||
case 3:
|
||||
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, ModBusTcpUtils.getValueType(type));
|
||||
Number inputRegisterValue = ModBusTcpUtils.getMasterValue(modbusMaster, inputRegister);
|
||||
return String.valueOf(inputRegisterValue);
|
||||
default:
|
||||
return "0";
|
||||
// 3. 执行请求
|
||||
PlcReadRequest readRequest = readBuilder.build();
|
||||
CompletableFuture<? extends PlcReadResponse> 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<RValue> batchReadValue(PlcConnection modbusMaster, DeviceBO deviceBO, List<SiteBO> points) {
|
||||
// 1. 解析配置
|
||||
PlcReadRequest.Builder readBuilder = doBuildReadRequest(modbusMaster, points);
|
||||
// 3. 执行请求
|
||||
PlcReadRequest readRequest = readBuilder.build();
|
||||
CompletableFuture<? extends PlcReadResponse> readFuture = readRequest.execute();
|
||||
PlcReadResponse readResponse = readFuture.get(10, TimeUnit.SECONDS);
|
||||
List<RValue> 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<SiteBO> 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<WValue> 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 设备写入点位值
|
||||
* <p>
|
||||
* 根据点位配置中的功能码(functionCode)和偏移量(offset), 将指定值写入 Modbus 设备的相应点位。
|
||||
* 支持的功能码包括:
|
||||
* - 1: 写入线圈状态(Coil Status)
|
||||
* - 3: 写入保持寄存器(Holding Register)
|
||||
* <p>
|
||||
* 对于功能码 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<Number> 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<? extends PlcWriteResponse> writeFuture = writeRequest.execute();
|
||||
PlcWriteResponse coilResponse = writeFuture.get(10, TimeUnit.SECONDS);
|
||||
PlcResponseCode responseCode = coilResponse.getResponseCode(wValue.getPoint().getAlias());
|
||||
return responseCode == PlcResponseCode.OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 数据类型
|
||||
* <p>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
}
|
||||
@@ -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驱动事件,暂时不需要
|
||||
|
||||
/**
|
||||
* 执行读操作
|
||||
* <p>
|
||||
* 该接口用于从指定设备中读取位号的数据。由于设备类型和通信协议的差异, 读取操作可能无法直接执行, 请根据实际情况灵活处理。
|
||||
* <p>
|
||||
* 注意: 读取操作可能会抛出异常, 调用方需做好异常处理。
|
||||
*
|
||||
* @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<RValue> batchRead(DeviceBO device, List<SiteBO> point);
|
||||
|
||||
/**
|
||||
* 执行写操作
|
||||
* <p>
|
||||
* 该接口用于向指定设备中的位号写入数据。由于设备类型和通信协议的差异, 写入操作可能无法直接执行, 请根据实际情况灵活处理。
|
||||
* <p>
|
||||
* 注意: 写入操作可能会抛出异常, 调用方需做好异常处理。
|
||||
*
|
||||
* @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<WResponse> batchWrite(DeviceBO device, List<WValue> wValue);
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user