集成modbus连接本地设备读取数据代码-配置文件方式读取;

PCS开关机功能通过modbus连接设备发送控制命令;
This commit is contained in:
zq
2025-12-19 11:38:04 +08:00
parent 16d414f092
commit ff487f1b05
26 changed files with 1286 additions and 216 deletions

View File

@ -194,6 +194,8 @@ mqtt:
connection-timeout: 15
keep-alive-interval: 30
automatic-reconnect: true
topic: 021_DDS_01_UP
siteId: 021_DDS_01
modbus:
pool:

View File

@ -194,6 +194,8 @@ mqtt:
connection-timeout: 15
keep-alive-interval: 30
automatic-reconnect: true
topic:
siteId:
modbus:
pool:

View File

@ -196,6 +196,8 @@ mqtt:
connection-timeout: 15
keep-alive-interval: 30
automatic-reconnect: true
topic:
siteId:
modbus:
pool:

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="../logs" />
<property name="log.path" value="/etc/xzzn/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

View File

@ -119,6 +119,12 @@
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- modbus -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,61 @@
package com.xzzn.common.core.modbus;
import com.serotonin.modbus4j.ModbusMaster;
import com.xzzn.common.core.modbus.domain.DeviceConfig;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PreDestroy;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class Modbus4jConnectionManager {
private static final Logger logger = LoggerFactory.getLogger(Modbus4jConnectionManager.class);
private final Map<String, GenericObjectPool<ModbusMaster>> connectionPools = new ConcurrentHashMap<>();
public ModbusMaster borrowMaster(DeviceConfig config) throws Exception {
String poolKey = getPoolKey(config);
GenericObjectPool<ModbusMaster> pool = connectionPools.computeIfAbsent(poolKey, key -> {
PooledModbusMasterFactory factory = new PooledModbusMasterFactory(config.getHost(), config.getPort());
GenericObjectPoolConfig<ModbusMaster> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(5); // 池中最大连接数
poolConfig.setMinIdle(1); // 最小空闲连接数
poolConfig.setTestOnBorrow(true); // 借用时测试连接有效性
return new GenericObjectPool<>(factory, poolConfig);
});
return pool.borrowObject();
}
public void returnMaster(DeviceConfig config, ModbusMaster master) {
if (master == null) {
return;
}
String poolKey = getPoolKey(config);
GenericObjectPool<ModbusMaster> pool = connectionPools.get(poolKey);
if (pool != null) {
pool.returnObject(master);
}
}
private String getPoolKey(DeviceConfig config) {
return config.getHost() + ":" + config.getPort();
}
@PreDestroy
public void shutdown() {
connectionPools.values().forEach(GenericObjectPool::close);
}
}

View File

@ -0,0 +1,384 @@
package com.xzzn.common.core.modbus;
import com.serotonin.modbus4j.BatchRead;
import com.serotonin.modbus4j.BatchResults;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.locator.BaseLocator;
import com.serotonin.modbus4j.msg.WriteCoilRequest;
import com.serotonin.modbus4j.msg.WriteCoilResponse;
import com.serotonin.modbus4j.msg.WriteCoilsRequest;
import com.serotonin.modbus4j.msg.WriteCoilsResponse;
import com.serotonin.modbus4j.msg.WriteRegisterRequest;
import com.serotonin.modbus4j.msg.WriteRegisterResponse;
import com.serotonin.modbus4j.msg.WriteRegistersRequest;
import com.serotonin.modbus4j.msg.WriteRegistersResponse;
import com.xzzn.common.core.modbus.domain.DeviceConfig;
import com.xzzn.common.core.modbus.domain.TagConfig;
import com.xzzn.common.core.modbus.domain.WriteTagConfig;
import com.xzzn.common.core.redis.RedisCache;
import com.xzzn.common.enums.ModBusType;
import com.xzzn.common.enums.RegisterType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import static com.xzzn.common.enums.RegisterType.COIL;
import static com.xzzn.common.enums.RegisterType.DISCRETE_INPUT;
@Service
public class ModbusProcessor {
private static final Logger logger = LoggerFactory.getLogger(ModbusProcessor.class);
@Autowired
private RedisCache redisCache;
@Autowired
private Modbus4jConnectionManager connectionManager;
public boolean writeDataToDevice(DeviceConfig config) {
ModbusMaster master = null;
boolean result = true;
try {
master = connectionManager.borrowMaster(config);
// 设置了Modbus通信的超时时间为3000毫秒3秒。当主设备与从设备通信时若在3秒内未收到响应则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
master.setTimeout(5000);
writeTagValue(master, config, config.getWriteTags());
} catch (Exception e) {
logger.error("Failed to borrow connection or write to devices '{}'", config.getDeviceName(), e);
result = false;
}
finally {
// 关键:无论成功与否,都必须将连接归还到池中
if (master != null) {
connectionManager.returnMaster(config, master);
}
}
return result;
}
public void writeTagValue(ModbusMaster master, DeviceConfig config, List<WriteTagConfig> tags) {
tags.forEach(tag -> {
Map<Integer, RegisterType> type = ModBusType.REGISTER_TYPE;
int firstDigit = Integer.parseInt(tag.getAddress().substring(0, 1));
int address = 0;
int addressLength = tag.getAddress().length();
int exp = (int) Math.pow(10, addressLength-1);
if (firstDigit != 0){
int digit = Integer.parseInt(tag.getAddress());
address = digit % (exp);
}else {
address = Integer.parseInt(tag.getAddress());
}
RegisterType registerType = type.get(firstDigit);
logger.info("Register type: {}, address: {}, firstDigit: {}", registerType, tag.getAddress(), firstDigit);
switch (registerType) {
case COIL: {
writeCoilRequest(master, config.getSlaveId(), address, Boolean.parseBoolean(String.valueOf(tag.getValue())));
break;
}
case HOLDING_REGISTER: {
writeRegisterRequest(master, config.getSlaveId(), address, Integer.parseInt(String.valueOf(tag.getValue())));
break;
}
default:
logger.error("Unsupported register type: {}", registerType);
break;
}
});
}
public static void writeCoilRequest(ModbusMaster master, int slaveId, int address, boolean value) {
try {
WriteCoilRequest request = new WriteCoilRequest(slaveId, address, value);
WriteCoilResponse response = (WriteCoilResponse)master.send(request);
if (response.isException()) {
logger.info("Write coil failed: " + response.getExceptionMessage());
} else {
logger.info("Write coil successful");
}
} catch (Exception e) {
logger.error("Failed to write coil value '{}' to address '{}'", value, address, e);
}
}
public static void writeCoilsRequest(ModbusMaster master, int slaveId, int address, boolean[] values) {
try {
WriteCoilsRequest request = new WriteCoilsRequest(slaveId, address, values);
WriteCoilsResponse response = (WriteCoilsResponse)master.send(request);
if (response.isException()) {
logger.info("Write coils failed: " + response.getExceptionMessage());
} else {
logger.info("Write coils successful");
}
} catch (Exception e) {
logger.error("Failed to write coils value '{}' to address '{}'", values, address, e);
}
}
public static void writeRegisterRequest(ModbusMaster master, int slaveId, int address, int value) {
try {
WriteRegisterRequest request = new WriteRegisterRequest(slaveId, address, value);
WriteRegisterResponse response = (WriteRegisterResponse)master.send(request);
if (response.isException()) {
logger.info("Write register failed: " + response.getExceptionMessage());
} else {
logger.info("Write register successful");
}
} catch (Exception e) {
logger.error("Failed to write register value '{}' to address '{}'", value, address, e);
}
}
public static void writeRegistersRequest(ModbusMaster master, int slaveId, int address, short[] values) {
try {
WriteRegistersRequest request = new WriteRegistersRequest(slaveId, address, values);
WriteRegistersResponse response = (WriteRegistersResponse)master.send(request);
if (response.isException()) {
logger.info("Write registers failed: " + response.getExceptionMessage());
} else {
logger.info("Write registers successful");
}
} catch (Exception e) {
logger.error("Failed to write registers value '{}' to address '{}'", values, address, e);
}
}
public static void setCoilValue(ModbusMaster master, int slaveId, int address, int value) {
// 写入单个线圈(从站ID, 地址, 布尔值)
BaseLocator<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, address);
try {
// 写入布尔值到线圈
master.setValue(coilLocator, value);
logger.info("set coil successful");
} catch (ModbusTransportException | ErrorResponseException e) {
logger.error("Failed to set coil value '{}' to address '{}'", value, address, e);
}
}
public static void setRegisterValue(ModbusMaster master, int slaveId, int address, int value) {
// 写入单个保持寄存器(从站, 地址, 16位整数)
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, address, DataType.TWO_BYTE_INT_UNSIGNED);
try {
// 写入整数值到保持寄存器
master.setValue(holdingLocator, value);
logger.info("set register successful");
} catch (ModbusTransportException | ErrorResponseException e) {
logger.error("Failed to set register value '{}' to address '{}'", value, address, e);
}
}
public Map<String, Object> readDataFromDevice(DeviceConfig config, ModbusMaster master) {
Map<String, Object> deviceData = new HashMap<>();
// ModbusMaster master = null; // 将master的声明提前
try {
master = connectionManager.borrowMaster(config);
// 设置了Modbus通信的超时时间为3000毫秒3秒。当主设备与从设备通信时若在3秒内未收到响应则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
master.setTimeout(5000);
BatchResults<String> results = readTagValues(master, config.getSlaveId(), config.getTags());
for (TagConfig tag : config.getTags()) {
if (Objects.equals(tag.getDataType(), "FOUR_BYTE_FLOAT_DBCA")){
Object value = results.getValue(tag.getKey());
value = convertValueToFloat(value);
deviceData.put(tag.getKey(), value);
}else {
deviceData.put(tag.getKey(), results.getValue(tag.getKey()));
}
// try {
// Object value = readTagValue(master, config.getSlaveId(), tag);
// if (value != null) {
// deviceData.put(tag.getKey(), value);
// }
// } catch (Exception e) {
// logger.error("Failed to read tag '{}' from devices '{}'", tag.getKey(), config.getDeviceName(), e);
// }
}
} catch (Exception e) {
logger.error("Failed to borrow connection or read from devices '{}'", config.getDeviceName(), e);
}
finally {
// 关键:无论成功与否,都必须将连接归还到池中
if (master != null) {
connectionManager.returnMaster(config, master);
}
}
String deviceNumber = config.getDeviceNumber();
redisCache.setCacheObject(deviceNumber, deviceData);
return deviceData;
}
private Object readTagValue(ModbusMaster master, int slaveId, TagConfig tag) throws Exception {
Object value;
Map<Integer, RegisterType> type = ModBusType.REGISTER_TYPE;
Map<String, Integer> DATA_LENGTH = ModBusType.LENGTH;
int firstDigit = Integer.parseInt(tag.getAddress().substring(0, 1));
int address = 0;
int addressLength = tag.getAddress().length();
int exp = (int) Math.pow(10, addressLength-1);
if (firstDigit != 0){
int digit = Integer.parseInt(tag.getAddress());
address = digit % (exp);
}else {
address = Integer.parseInt(tag.getAddress());
}
RegisterType registerType = type.get(firstDigit);
int dataLength = DATA_LENGTH.get(tag.getDataType());
// 增加配置校验和警告
if ((Objects.equals(registerType, COIL) || Objects.equals(registerType, DISCRETE_INPUT))) {
logger.warn("Configuration warning for tag '{}': ModBusType is {} but DataType is {}. DataType should be BOOLEAN. Proceeding with BOOLEAN read.",
tag.getKey(), registerType, tag.getDataType());
}
try {
switch (registerType) {
case COIL: {
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, address);
value = master.getValue(loc);
logger.info("读取线圈{}成功: {}",tag.getAddress(), value);
break;
}
case DISCRETE_INPUT: {
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, address);
value = master.getValue(loc);
logger.info("读取输入{}成功: {}",tag.getAddress(), value);
break;
}
case HOLDING_REGISTER: {
if (dataLength == 28){
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, address, 4);
value = master.getValue(locator);
//将 value 转换为二进制数据
value = convertValueToFloat(value);
}else{
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, address, dataLength);
value = master.getValue(locator);
}
logger.info("读取保持寄存器{}成功: {}",tag.getAddress(), value);
break;
}
case INPUT_REGISTER: {
BaseLocator<Number> locator = BaseLocator.inputRegister(slaveId, address, dataLength);
value= master.getValue(locator);
logger.info("读取输入寄存器{}成功: {}",tag.getAddress(), value);
break;
}
default:
logger.error("Unsupported register type: {}", registerType);
value = null;
break;
}
}catch (Exception e){
logger.error("Failed to read tag '{}'", tag.getKey(), e);
value = null;
}
return value;
}
private BatchResults<String> readTagValues(ModbusMaster master, int slaveId, List<TagConfig> tags) throws Exception {
try {
BatchRead<String> batch = new BatchRead<>();
tags.forEach(tag -> {
Map<Integer, RegisterType> type = ModBusType.REGISTER_TYPE;
Map<String, Integer> DATA_LENGTH = ModBusType.LENGTH;
int firstDigit = Integer.parseInt(tag.getAddress().substring(0, 1));
int address = 0;
int addressLength = tag.getAddress().length();
int exp = (int) Math.pow(10, addressLength-1);
if (firstDigit != 0){
int digit = Integer.parseInt(tag.getAddress());
address = digit % (exp);
}else {
address = Integer.parseInt(tag.getAddress());
}
RegisterType registerType = type.get(firstDigit);
int dataLength = DATA_LENGTH.get(tag.getDataType());
switch (registerType) {
case COIL: {
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, address);
batch.addLocator(tag.getKey(), loc);
break;
}
case DISCRETE_INPUT: {
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, address);
batch.addLocator(tag.getKey(), loc);
break;
}
case HOLDING_REGISTER: {
logger.info("HOLDING_REGISTER: {}",tag.getAddress());
if (dataLength == 28){
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, address, 4);
batch.addLocator(tag.getKey(), locator);
}else {
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, address, dataLength);
batch.addLocator(tag.getKey(), loc);
}
break;
}
case INPUT_REGISTER: {
logger.info("INPUT_REGISTER: {}",tag.getAddress());
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, address, dataLength);
batch.addLocator(tag.getKey(), loc);
break;
}
}
});
BatchResults<String> results = master.send(batch);
for (TagConfig tag : tags){
if (tag.getBit()!=null){
logger.info("批处理读取寄存器成功: {}",tag.getAddress() +"(" + tag.getBit() + "):" + results.getValue(tag.getKey()));
}else {
logger.info("批处理读取寄存器成功: {}",tag.getAddress() + ":" + results.getValue(tag.getKey()));
}
}
return results;
}catch (Exception e){
logger.error("Failed to read master '{}'", slaveId, e);
throw new Exception(e);
}
}
public static float convertValueToFloat(Object value) {
if (!(value instanceof Number)) {
throw new IllegalArgumentException("Input must be a Number");
}
int intValue = ((Number) value).intValue();
String binaryData = Integer.toBinaryString(intValue);
// 补足32位二进制字符串
if (binaryData.length() < 32) {
binaryData = String.format("%32s", binaryData).replace(' ', '0');
}
// 分割为4个字节并重组为 dcba 格式
byte[] reorderedBytes = new byte[4];
for (int i = 0; i < 4; i++) {
int start = i * 8;
String byteStr = binaryData.substring(start, start + 8);
reorderedBytes[3 - i] = (byte) Integer.parseInt(byteStr, 2);
}
// 使用 ByteBuffer 提高可读性和安全性
return Float.intBitsToFloat(
java.nio.ByteBuffer.wrap(reorderedBytes).getInt()
);
}
}

View File

@ -0,0 +1,58 @@
package com.xzzn.common.core.modbus;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.ip.IpParameters;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
public class PooledModbusMasterFactory extends BasePooledObjectFactory<ModbusMaster> {
private final String host;
private final int port;
public PooledModbusMasterFactory(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public ModbusMaster create() throws Exception {
IpParameters params = new IpParameters();
params.setHost(this.host);
params.setPort(this.port);
params.setEncapsulated(false);
ModbusMaster master = new ModbusFactory().createTcpMaster(params, true);
master.init();
return master;
}
@Override
public PooledObject<ModbusMaster> wrap(ModbusMaster master) {
return new DefaultPooledObject<>(master);
}
@Override
public void destroyObject(PooledObject<ModbusMaster> p) throws Exception {
if (p.getObject() != null) {
p.getObject().destroy();
}
super.destroyObject(p);
}
@Override
public boolean validateObject(PooledObject<ModbusMaster> p) {
// testConnectivity() 方法在 modbus4j 中不存在。
// 一个真正的验证需要执行一次读操作,但这需要 slaveId而工厂是通用的不知道 slaveId。
// 因此,我们使用一个较弱的验证,只检查 master 对象是否已初始化。
// 这无法检测到被远程设备断开的连接,错误将在实际读取操作中被捕获。
if (p.getObject() == null) {
return false;
}
return p.getObject().isInitialized();
}
}

View File

@ -0,0 +1,91 @@
package com.xzzn.common.core.modbus.domain;
import java.util.List;
public class DeviceConfig {
private String deviceNumber;
private String deviceName;
private String host;
private int port;
private long time;
private int slaveId;
private boolean enabled;
private List<TagConfig> tags;
private List<WriteTagConfig> writeTags;
public String getDeviceNumber() {
return deviceNumber;
}
public void setDeviceNumber(String deviceNumber) {
this.deviceNumber = deviceNumber;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public int getSlaveId() {
return slaveId;
}
public void setSlaveId(int slaveId) {
this.slaveId = slaveId;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<TagConfig> getTags() {
return tags;
}
public void setTags(List<TagConfig> tags) {
this.tags = tags;
}
public List<WriteTagConfig> getWriteTags() {
return writeTags;
}
public void setWriteTags(List<WriteTagConfig> writeTags) {
this.writeTags = writeTags;
}
}

View File

@ -0,0 +1,78 @@
package com.xzzn.common.core.modbus.domain;
public class TagConfig {
private String key;
private String des;
private String address;
private String dataType;
private float a;
private float k;
private float b;
private Integer bit;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public float getA() {
return a;
}
public void setA(float a) {
this.a = a;
}
public float getK() {
return k;
}
public void setK(float k) {
this.k = k;
}
public float getB() {
return b;
}
public void setB(float b) {
this.b = b;
}
public Integer getBit() {
return bit;
}
public void setBit(Integer bit) {
this.bit = bit;
}
}

View File

@ -0,0 +1,22 @@
package com.xzzn.common.core.modbus.domain;
public class WriteTagConfig {
private Object value;
private String address;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}

View File

@ -0,0 +1,57 @@
package com.xzzn.common.enums;
import com.serotonin.modbus4j.code.DataType;
import java.util.HashMap;
import java.util.Map;
public class ModBusType {
public static final Map<Integer, RegisterType> REGISTER_TYPE = new HashMap<Integer, RegisterType>();
public static final Map<String, Integer> LENGTH = new HashMap<String, Integer>();
static {
REGISTER_TYPE.put(0, RegisterType.COIL);
REGISTER_TYPE.put(1, RegisterType.DISCRETE_INPUT);
REGISTER_TYPE.put(4, RegisterType.HOLDING_REGISTER);
REGISTER_TYPE.put(3, RegisterType.INPUT_REGISTER);
LENGTH.put("BINARY",DataType.BINARY);
LENGTH.put("TWO_BYTE_INT_UNSIGNED",DataType.TWO_BYTE_INT_UNSIGNED);
LENGTH.put("TWO_BYTE_INT_SIGNED",DataType.TWO_BYTE_INT_SIGNED);
LENGTH.put("TWO_BYTE_INT_UNSIGNED_SWAPPED",DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED);
LENGTH.put("TWO_BYTE_INT_SIGNED_SWAPPED",DataType.TWO_BYTE_INT_SIGNED_SWAPPED);
LENGTH.put("FOUR_BYTE_INT_UNSIGNED",DataType.FOUR_BYTE_INT_UNSIGNED);
LENGTH.put("FOUR_BYTE_INT_SIGNED",DataType.FOUR_BYTE_INT_SIGNED);
LENGTH.put("FOUR_BYTE_INT_UNSIGNED_SWAPPED",DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED);
LENGTH.put("FOUR_BYTE_INT_SIGNED_SWAPPED",DataType.FOUR_BYTE_INT_SIGNED_SWAPPED);
LENGTH.put("FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED",DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED);
LENGTH.put("FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED",DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED);
LENGTH.put("FOUR_BYTE_FLOAT",DataType.FOUR_BYTE_FLOAT);
LENGTH.put("FOUR_BYTE_FLOAT_SWAPPED",DataType.FOUR_BYTE_FLOAT_SWAPPED);
LENGTH.put("FOUR_BYTE_FLOAT_SWAPPED_INVERTED",DataType.FOUR_BYTE_FLOAT_SWAPPED_INVERTED);
LENGTH.put("FOUR_BYTE_FLOAT_INVERTED",DataType.FOUR_BYTE_FLOAT_INVERTED);
LENGTH.put("FOUR_BYTE_FLOAT_SWAPPED_SWAPPED",DataType.FOUR_BYTE_FLOAT_SWAPPED_SWAPPED);
LENGTH.put("EIGHT_BYTE_INT_UNSIGNED",DataType.EIGHT_BYTE_INT_UNSIGNED);
LENGTH.put("EIGHT_BYTE_INT_SIGNED",DataType.EIGHT_BYTE_INT_SIGNED);
LENGTH.put("EIGHT_BYTE_INT_UNSIGNED_SWAPPED",DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED);
LENGTH.put("EIGHT_BYTE_INT_SIGNED_SWAPPED",DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED);
LENGTH.put("EIGHT_BYTE_FLOAT",DataType.EIGHT_BYTE_FLOAT);
LENGTH.put("EIGHT_BYTE_FLOAT_SWAPPED",DataType.EIGHT_BYTE_FLOAT_SWAPPED);
LENGTH.put("TWO_BYTE_BCD",DataType.TWO_BYTE_BCD);
LENGTH.put("FOUR_BYTE_BCD",DataType.FOUR_BYTE_BCD);
LENGTH.put("FOUR_BYTE_BCD_SWAPPED",DataType.FOUR_BYTE_BCD_SWAPPED);
LENGTH.put("CHAR",DataType.CHAR);
LENGTH.put("VARCHAR",DataType.VARCHAR);
LENGTH.put("FOUR_BYTE_FLOAT_DBCA",28);
}
}

View File

@ -0,0 +1,45 @@
package com.xzzn.common.enums;
public enum PcsControlCommand {
TOTAL_START(0x0004, "总运行"),
TOTAL_STOP(0x0010, "总停机"),
TOTAL_RESET(0x0080, "总复位"),
// CLUSTER1_START(0x1004, "电池簇1运行"),
// CLUSTER1_STOP(0x1010, "电池簇1停机"),
// CLUSTER1_RESET(0x1080, "电池簇1复位"),
// CLUSTER2_START(0x2004, "电池簇2运行"),
// CLUSTER2_STOP(0x2010, "电池簇2停机"),
// CLUSTER2_RESET(0x2080, "电池簇2复位"),
// CLUSTER3_START(0x3004, "电池簇3运行"),
// CLUSTER3_STOP(0x3010, "电池簇3停机"),
// CLUSTER3_RESET(0x3080, "电池簇3复位"),
// CLUSTER4_START(0x4004, "电池簇4运行"),
// CLUSTER4_STOP(0x4010, "电池簇4停机"),
// CLUSTER4_RESET(0x4080, "电池簇4复位"),
;
private final int code;
private final String description;
PcsControlCommand(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static PcsControlCommand fromDeviceStatus(String deviceStatus) {
if (DeviceRunningStatus.RUNNING.getCode().equals(deviceStatus)) {
return PcsControlCommand.TOTAL_START;
} else if (DeviceRunningStatus.SHUTDOWN.getCode().equals(deviceStatus) ) {
return PcsControlCommand.TOTAL_STOP;
}
return null;
}
}

View File

@ -0,0 +1,8 @@
package com.xzzn.common.enums;
public enum RegisterType {
COIL,
DISCRETE_INPUT,
HOLDING_REGISTER,
INPUT_REGISTER
}

View File

@ -0,0 +1,34 @@
package com.xzzn.quartz.config;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTask {
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
private final Map<String, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>();
public void startTask(String deviceId, Runnable task, long period) {
stopTask(deviceId); // 如果已有同ID任务在运行先停止
ScheduledFuture<?> future = executor.scheduleAtFixedRate(task, 0, period, TimeUnit.MILLISECONDS);
futureMap.put(deviceId, future);
}
public void stopTask(String deviceId) {
ScheduledFuture<?> future = futureMap.get(deviceId);
if (future != null && !future.isDone()) {
future.cancel(true);
}
futureMap.remove(deviceId);
}
}

View File

@ -1,179 +1,204 @@
package com.xzzn.quartz.task;
import com.xzzn.common.enums.DeviceRunningStatus;
import com.xzzn.ems.domain.EmsDeviceChangeLog;
import com.xzzn.ems.domain.EmsDevicesSetting;
import com.xzzn.ems.mapper.EmsDeviceChangeLogMapper;
import com.xzzn.ems.mapper.EmsDevicesSettingMapper;
import com.xzzn.ems.mapper.EmsMqttMessageMapper;
import com.xzzn.ems.service.impl.EmsDeviceSettingServiceImpl;
import com.xzzn.framework.manager.ModbusConnectionManager;
import com.xzzn.framework.manager.ModbusConnectionWrapper;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.serotonin.modbus4j.ModbusMaster;
import com.xzzn.common.constant.RedisKeyConstants;
import com.xzzn.common.core.modbus.ModbusProcessor;
import com.xzzn.common.core.modbus.domain.DeviceConfig;
import com.xzzn.common.core.redis.RedisCache;
import com.xzzn.common.utils.DateUtils;
import com.xzzn.ems.service.IEmsAlarmRecordsService;
import com.xzzn.ems.service.impl.DeviceDataProcessServiceImpl;
import com.xzzn.framework.manager.MqttLifecycleManager;
import com.xzzn.framework.web.service.ModbusService;
import com.xzzn.framework.web.service.MqttPublisher;
import com.xzzn.quartz.config.ScheduledTask;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 轮询设备-通过modbus协议读取数据
*/
@Component("modbusPoller")
public class ModbusPoller {
private static final Logger logger = LoggerFactory.getLogger(ModbusPoller.class);
private static final Logger log = LoggerFactory.getLogger(ModbusPoller.class);
private final MqttLifecycleManager mqttLifecycleManager;
private final ScheduledTask scheduledTask;
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private ModbusConnectionManager connectionManager;
private ModbusProcessor modbusProcessor;
@Autowired
private ModbusService modbusService;
private IEmsAlarmRecordsService iEmsAlarmRecordsService;
@Autowired
private EmsDevicesSettingMapper deviceRepo;
private DeviceDataProcessServiceImpl deviceDataProcessServiceImpl;
@Autowired
private EmsMqttMessageMapper emsMqttMessageMapper;
private RedisCache redisCache;
@Autowired
private EmsDeviceSettingServiceImpl emsDeviceSettingServiceImpl;
@Autowired
private EmsDeviceChangeLogMapper emsDeviceChangeLogMapper;
private MqttPublisher mqttPublisher;
@Value("${mqtt.topic}")
private String topic;
@Value("${mqtt.siteId}")
private String siteId;
@Autowired
public ModbusPoller(MqttLifecycleManager mqttLifecycleManager) {
public ModbusPoller(MqttLifecycleManager mqttLifecycleManager, ScheduledTask scheduledTask) {
this.mqttLifecycleManager = mqttLifecycleManager;
this.scheduledTask = scheduledTask;
}
public void pollAllDevices() {
logger.info("开始执行Modbus设备轮询...");
EmsDevicesSetting selectEntity = new EmsDevicesSetting();
selectEntity.setDeviceStatus(DeviceRunningStatus.RUNNING.getCode());
List<EmsDevicesSetting> activeDevices = deviceRepo.selectEmsDevicesSettingList(selectEntity);
EmsDevicesSetting device = activeDevices.get(0);
try {
//pollSingleDevice(device);
} catch (Exception e) {
logger.error("调度设备{}任务失败", device.getId(), e);
}
/*activeDevices.forEach(device -> {
try {
CompletableFuture.runAsync(() -> pollSingleDevice(device))
.exceptionally(e -> {
logger.error("设备{}轮询异常", device.getId(), e);
return null;
});
} catch (Exception e) {
logger.error("调度设备{}任务失败", device.getId(), e);
}
});*/
}
private void pollSingleDevice(EmsDevicesSetting device) {
logger.debug("开始轮询设备: {}", device.getSiteId(), device.getDeviceName(), device.getId());
ModbusConnectionWrapper wrapper = null;
try {
// 获取连接
wrapper = connectionManager.getConnection(device);
if(wrapper == null || !wrapper.isActive()){
logger.error("轮询设备{}连接失败: {}", device.getId());
Path devicesDir = Paths.get(System.getProperty("user.dir"), "devices");
if (!Files.exists(devicesDir)) {
log.error("Devices目录不存在: {}", devicesDir);
return;
}
// 读取保持寄存器
int[] data = modbusService.readHoldingRegisters(
wrapper.getConnection(),
1, //从站ID
10 // 寄存器数量
);
// 处理读取到的数据
processData(device, data);
List<Path> jsonFiles = null;
try {
jsonFiles = Files.list(devicesDir)
.filter(path -> path.toString().endsWith(".json"))
.collect(Collectors.toList());
} catch (IOException e) {
log.error("modbusPoller.loadConfigs 获取设备配置文件失败: {}", devicesDir, e);
return;
}
// 按 host:port 分组
Map<String, List<DeviceConfig>> groupedConfigs = new HashMap<>();
for (Path filePath : jsonFiles) {
DeviceConfig config = null;
try {
config = objectMapper.readValue(filePath.toFile(), DeviceConfig.class);
} catch (IOException e) {
log.error("modbusPoller.loadConfigs 解析设备配置文件失败: {}", filePath, e);
continue;
}
if (config.isEnabled()) {
String key = config.getHost() + ":" + config.getPort();
groupedConfigs.computeIfAbsent(key, k -> new ArrayList<>()).add(config);
}
}
// 为每个 host:port 启动一个任务
for (Map.Entry<String, List<DeviceConfig>> entry : groupedConfigs.entrySet()) {
String groupKey = entry.getKey();
List<DeviceConfig> configs = entry.getValue();
// 取其中一个配置的时间间隔作为该组任务的执行周期
long interval = configs.get(0).getTime();
scheduledTask.startTask(groupKey, () -> {
for (DeviceConfig config : configs) {
try {
scheduledStart(config, null);
} catch (Exception e) {
logger.error("轮询设备{}失败: {}", device.getId(), e.getMessage());
// 标记连接为无效
if (wrapper != null) {
wrapper.close();
connectionManager.removeConnection(Integer.parseInt(device.getDeviceId()));
log.error("采集设备数据异常: {}", config.getDeviceName(), e);
}
// 设备轮询不到修改运行状态
String beforeStatus = device.getDeviceStatus();
device.setDeviceStatus(DeviceRunningStatus.SHUTDOWN.getCode());
emsDeviceSettingServiceImpl.updateDevice(device);
// 轮询设备,设备状态变更日志
EmsDeviceChangeLog log = createLogEntity(beforeStatus,device);
emsDeviceChangeLogMapper.insertEmsDeviceChangeLog(log);
throw new RuntimeException("轮询设备失败", e);
}
}, interval);
}
}
// 处理获取到的数据
private void processData(EmsDevicesSetting device, int[] data) throws MqttException {
String beforeStatus = device.getDeviceStatus();
Boolean error = true;
if (data == null || data.length == 0) {
logger.warn("设备{}返回空数据", device.getId());
// 设备读取不到-设置设备故障
device.setDeviceStatus(DeviceRunningStatus.FAULT.getCode());
error = false;
public void scheduledStart(DeviceConfig config, ModbusMaster master) {
try {
if (config.isEnabled()) {
log.info("Reading data from devices: {}", config.getDeviceName());
Map<String, Object> data = modbusProcessor.readDataFromDevice(config, master);
// 在这里处理采集到的数据
config.getTags().forEach(tag -> {
Object rawValue = data.get(tag.getKey());
if (rawValue != null) {
float value = 0;
if (rawValue instanceof Number) {
value = ((Number) rawValue).floatValue(); // 安全地转换为 float
} else {
// 恢复设备状态 - 运行
device.setDeviceStatus(DeviceRunningStatus.RUNNING.getCode());
log.error("tag:{},无法将数据转换为数字: {}", tag.getKey(), rawValue);
}
emsDeviceSettingServiceImpl.updateDevice(device);
// 轮询设备,设备状态变更日志
EmsDeviceChangeLog log = createLogEntity(beforeStatus,device);
emsDeviceChangeLogMapper.insertEmsDeviceChangeLog(log);
// 错误数据-不处理直接返回
if (!error) {
value = tag.getA() * value * value + tag.getK() * value + tag.getB();
int intValue = (int) value;
if (tag.getBit() != null) {
log.info("tag:{},bit:{},value:{}", tag.getKey(), tag.getBit(), value);
String binary = Integer.toBinaryString(intValue);
data.put(tag.getKey(), binary);
} else {
data.put(tag.getKey(), value);
}
} else {
data.put(tag.getKey(), rawValue);
log.warn("tag:{},数据为空: {}", tag.getKey(), rawValue);
}
});
log.info("Data from {}: {}", config.getDeviceName(), data);
String deviceNumber = config.getDeviceNumber();
//处理数据并发送MQTT消息、保存Redis数据和数据入库
processingData(data, deviceNumber);
}
} catch (Exception e) {
log.error("设备数据采集异常: {}", config.getDeviceName(), e);
}
}
private void processingData(Map<String, Object> data, String deviceNumber) {
if (data == null || data.size() == 0) {
// 添加设备告警
iEmsAlarmRecordsService.addEmptyDataAlarmRecord(siteId, deviceNumber);
return;
}
// 数据处理逻辑
StringBuilder sb = new StringBuilder();
sb.append("设备[").append(device.getDeviceName()).append("]数据: ");
for (int i = 0; i < data.length; i++) {
sb.append("R").append(i).append("=").append(data[i]).append(" ");
Long timestamp = System.currentTimeMillis();
JSONObject json = new JSONObject();
json.put("Data", data);
json.put("timestamp", timestamp);
json.put("Device", deviceNumber);
sendMqttMsg(json);
saveRedisData(json, deviceNumber);
saveDataToDatabase(data, deviceNumber, timestamp);
}
String message = sb.toString();
logger.info(sb.toString());
/*
String siteId = device.getSiteId();
if (siteId.startsWith("021_DDS")) {
dDSDataProcessService.handleDdsData(message);
} else if (siteId.startsWith("021_FXX")) {
fXXDataProcessService.handleFxData(message);
}*/
// 测试发送mqtt
/* EmsMqttMessage msg = emsMqttMessageMapper.selectEmsMqttMessageById(1L);
String dataJson = msg.getMqttMessage();
String topic = msg.getMqttTopic();
logger.info("topic" + topic);
logger.info("dataJson" + dataJson);
// 将设备数据下发到mqtt服务器上
mqttLifecycleManager.publish(topic, dataJson, 0);*/
public void sendMqttMsg(JSONObject json) {
try {
mqttPublisher.publish(topic, Collections.singletonList(json).toString(), 0);
} catch (MqttException e) {
log.error("MQTT消息发布失败: {}", json.toJSONString(), e);
}
log.info("已发送数据: {}", json.toJSONString());
}
private EmsDeviceChangeLog createLogEntity(String beforeStatus, EmsDevicesSetting device) {
EmsDeviceChangeLog log = new EmsDeviceChangeLog();
log.setLogId(UUID.randomUUID().toString());
log.setLogTime(new Date());
log.setSiteId(device.getSiteId());
log.setDeviceId(device.getDeviceId());
log.setBeforeStatus(beforeStatus);
log.setAfterStatus(device.getDeviceStatus());
log.setCreateBy("sys");
log.setCreateTime(new Date());
return log;
public void saveRedisData(JSONObject obj, String deviceNumber) {
try {
// 存放mqtt原始每个设备最晚一次数据便于后面点位获取数据
redisCache.setCacheObject(RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceNumber, obj);
// 存放每次同步数据,失效时间(同同步时间)-用于判断是否正常同步数据和保护策略查询
redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA_ALARM + siteId + "_" + deviceNumber, obj, 1, TimeUnit.MINUTES);
log.info("数据已成功存储在Redis: {}", deviceNumber);
} catch (Exception e) {
log.error("无法在设备的Redis中存储数据: {}", deviceNumber, e);
}
}
private void saveDataToDatabase(Map<String, Object> data, String deviceNumber, Long timestamp) {
deviceDataProcessServiceImpl.processingDeviceData(siteId, deviceNumber, JSON.toJSONString(data), DateUtils.convertUpdateTime(timestamp));
}
}

View File

@ -1,12 +1,14 @@
package com.xzzn.ems.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xzzn.common.annotation.Excel;
import com.xzzn.common.core.domain.BaseEntity;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xzzn.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.xzzn.common.annotation.Excel;
/**
* 单体电池实时数据对象 ems_battery_data
@ -70,6 +72,10 @@ public class EmsBatteryData extends BaseEntity
@Excel(name = "单体电池内阻")
private BigDecimal interResistance;
/** 单体电池电流 */
@Excel(name = "单体电池电流")
private BigDecimal current;
public void setId(Long id)
{
this.id = id;
@ -200,6 +206,14 @@ public class EmsBatteryData extends BaseEntity
return interResistance;
}
public BigDecimal getCurrent() {
return current;
}
public void setCurrent(BigDecimal current) {
this.current = current;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@ -221,6 +235,7 @@ public class EmsBatteryData extends BaseEntity
.append("deviceId", getDeviceId())
.append("clusterDeviceId", getClusterDeviceId())
.append("interResistance", getInterResistance())
.append("current", getCurrent())
.toString();
}
}

View File

@ -1,12 +1,14 @@
package com.xzzn.ems.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xzzn.common.annotation.Excel;
import com.xzzn.common.core.domain.BaseEntity;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xzzn.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.xzzn.common.annotation.Excel;
/**
* PCS数据对象 ems_pcs_data
@ -34,8 +36,8 @@ public class EmsPcsData extends BaseEntity
@Excel(name = "并网状态0-并网 1-未并网")
private String gridStatus;
/** 设备状态0-在线 1-离线 2-维修中 */
@Excel(name = "设备状态0-在线 1-离线 2-维修中")
/** 设备运行状态0-离线、1-待机、2-运行、3-故障、4-停机 */
@Excel(name = "设备运行状态0-离线、1-待机、2-运行、3-故障、4-停机")
private String deviceStatus;
/** 控制模式0-远程 1-本地 */
@ -234,6 +236,22 @@ public class EmsPcsData extends BaseEntity
@Excel(name = "4#模块IGBT最高温")
private BigDecimal module4Temp;
/** 电池簇1PCS有功功率给定 */
@Excel(name = "电池簇1PCS有功功率给定")
private BigDecimal cluster1ActivePower;
/** 电池簇2PCS有功功率给定 */
@Excel(name = "电池簇2PCS有功功率给定")
private BigDecimal cluster2ActivePower;
/** 电池簇3PCS有功功率给定 */
@Excel(name = "电池簇3PCS有功功率给定")
private BigDecimal cluster3ActivePower;
/** 电池簇4PCS有功功率给定 */
@Excel(name = "电池簇4PCS有功功率给定")
private BigDecimal cluster4ActivePower;
public void setId(Long id)
{
this.id = id;
@ -774,6 +792,38 @@ public class EmsPcsData extends BaseEntity
return module4Temp;
}
public BigDecimal getCluster1ActivePower() {
return cluster1ActivePower;
}
public void setCluster1ActivePower(BigDecimal cluster1ActivePower) {
this.cluster1ActivePower = cluster1ActivePower;
}
public BigDecimal getCluster2ActivePower() {
return cluster2ActivePower;
}
public void setCluster2ActivePower(BigDecimal cluster2ActivePower) {
this.cluster2ActivePower = cluster2ActivePower;
}
public BigDecimal getCluster3ActivePower() {
return cluster3ActivePower;
}
public void setCluster3ActivePower(BigDecimal cluster3ActivePower) {
this.cluster3ActivePower = cluster3ActivePower;
}
public BigDecimal getCluster4ActivePower() {
return cluster4ActivePower;
}
public void setCluster4ActivePower(BigDecimal cluster4ActivePower) {
this.cluster4ActivePower = cluster4ActivePower;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@ -836,6 +886,10 @@ public class EmsPcsData extends BaseEntity
.append("module2Temp", getModule2Temp())
.append("module3Temp", getModule3Temp())
.append("module4Temp", getModule4Temp())
.append("cluster1ActivePower", getCluster1ActivePower())
.append("cluster2ActivePower", getCluster2ActivePower())
.append("cluster3ActivePower", getCluster3ActivePower())
.append("cluster4ActivePower", getCluster4ActivePower())
.toString();
}
}

View File

@ -1,5 +1,7 @@
package com.xzzn.ems.domain.vo;
import java.util.List;
import javax.validation.constraints.NotBlank;
/**
@ -22,6 +24,10 @@ public class DeviceUpdateRequest {
/** 设备类型 */
private String deviceCategory;
/** 设备点位数据匹配表字段 */
private List<String> matchFields;
public String getSiteId() {
return siteId;
}
@ -53,4 +59,12 @@ public class DeviceUpdateRequest {
public void setDeviceCategory(String deviceCategory) {
this.deviceCategory = deviceCategory;
}
public List<String> getMatchFields() {
return matchFields;
}
public void setMatchFields(List<String> matchFields) {
this.matchFields = matchFields;
}
}

View File

@ -1,15 +1,15 @@
package com.xzzn.ems.mapper;
import com.xzzn.ems.domain.EmsPointMatch;
import com.xzzn.ems.domain.vo.DevicePointMatchExportVo;
import com.xzzn.ems.domain.vo.DeviceUpdateRequest;
import com.xzzn.ems.domain.vo.GeneralQueryDataVo;
import com.xzzn.ems.domain.vo.PointQueryResponse;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.xzzn.ems.domain.EmsPointMatch;
import com.xzzn.ems.domain.vo.DevicePointMatchExportVo;
import com.xzzn.ems.domain.vo.DevicePointMatchVo;
import com.xzzn.ems.domain.vo.DeviceUpdateRequest;
import com.xzzn.ems.domain.vo.GeneralQueryDataVo;
import com.xzzn.ems.domain.vo.PointQueryResponse;
import org.apache.ibatis.annotations.Param;
/**
@ -171,5 +171,5 @@ public interface EmsPointMatchMapper
int getDevicePointAlarmNum(@Param("siteId") String siteId, @Param("deviceId") String deviceId, @Param("deviceCategory") String deviceCategory);
EmsPointMatch selectDeviceStatusPoint(@Param("request") DeviceUpdateRequest request);
List<EmsPointMatch> selectDeviceStatusPoint(@Param("request") DeviceUpdateRequest request);
}

View File

@ -161,6 +161,12 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i
// 存放每次同步数据,失效时间(同同步时间)-用于判断是否正常同步数据
redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA + siteId + "_" + deviceId, obj, 1, TimeUnit.MINUTES);
// 处理设备数据
processingDeviceData(siteId, deviceId, jsonData, dataUpdateTime);
}
}
public void processingDeviceData(String siteId, String deviceId, String jsonData, Date dataUpdateTime) {
if (deviceId.contains(SiteDevice.BMSD.name())) {
batteryStackDataProcess(siteId, deviceId, jsonData, dataUpdateTime);
batteryGroupDataProcess(siteId, deviceId, jsonData);
@ -188,7 +194,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i
emsDataProcess(siteId, deviceId, jsonData, dataUpdateTime);
}
}
}
private void emsDataProcess(String siteId, String deviceId, String jsonData, Date dataUpdateTime) {
//EMS
@ -249,14 +254,14 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i
}
private void meteXFProcess(String siteId, String deviceId, String dataJson, Date dataUpdateTime) {
public void meteXFProcess(String siteId, String deviceId, String dataJson, Date dataUpdateTime) {
//消防
Map<String, Object> obj = JSON.parseObject(dataJson, new TypeReference<Map<String, Object>>() {
});
// 点位匹配数据
List<EmsPointMatch> pointMatchList = devicePointMatchDataProcessor.getDevicePointMatch(siteId, deviceId, DeviceMatchTable.XF.getCode());
if (CollectionUtils.isEmpty(pointMatchList)) {
log.info("未找到匹配的点位数据,无法处理动环数据siteId: " + siteId + "deviceId: " + deviceId);
log.info("未找到匹配的点位数据,无法处理消防数据siteId: " + siteId + "deviceId: " + deviceId);
return;
}
Map<String, List<EmsPointEnumMatch>> pointEnumMatchMap = devicePointMatchDataProcessor.getPointEnumMatchMap(siteId, DeviceMatchTable.XF.getCode());
@ -1415,7 +1420,16 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i
redisCache.setCacheObject(RedisKeyConstants.ORIGINAL_MQTT_DATA_ALARM + siteId + "_" + deviceId, obj);
// 存放每次同步数据,失效时间(同同步时间)-用于判断是否正常同步数据
redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA_ALARM + siteId + "_" + deviceId, obj, 1, TimeUnit.MINUTES);
// 处理设备数据,根据不同设备类型进行不同的数据处理
String deviceCategory = processingDeviceAlarmData(siteId, deviceId, jsonData, dataUpdateTime);
if (StringUtils.isEmpty(deviceCategory)) {
// 处理告警信息
alarmDataProcess(siteId, deviceId, jsonData, alarmMatchInfo, deviceCategory);
}
}
}
public String processingDeviceAlarmData(String siteId, String deviceId, String jsonData, Date dataUpdateTime) {
String deviceCategory = "";
if (deviceId.contains(SiteDevice.ZSLQ.name())) {
coolingAlarmDataProcess(siteId, deviceId, jsonData, dataUpdateTime);
@ -1429,11 +1443,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i
deviceCategory = DeviceCategory.PCS.getCode();
pcsAlarmDataProcess(siteId, deviceId, jsonData, dataUpdateTime);
}
if (StringUtils.isEmpty(deviceCategory)) {
// 处理告警信息
alarmDataProcess(siteId, deviceId, jsonData, alarmMatchInfo, deviceCategory);
}
}
return deviceCategory;
}
private void pcsAlarmDataProcess(String siteId, String deviceId, String jsonData, Date dataUpdateTime) {

View File

@ -4,15 +4,19 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.xzzn.common.constant.RedisKeyConstants;
import com.xzzn.common.core.modbus.ModbusProcessor;
import com.xzzn.common.core.modbus.domain.DeviceConfig;
import com.xzzn.common.core.modbus.domain.WriteTagConfig;
import com.xzzn.common.core.redis.RedisCache;
import com.xzzn.common.enums.DeviceCategory;
import com.xzzn.common.enums.DeviceRunningStatus;
import com.xzzn.common.enums.DeviceType;
import com.xzzn.common.enums.PcsControlCommand;
import com.xzzn.common.enums.PointType;
import com.xzzn.common.enums.SiteEnum;
import com.xzzn.common.exception.ServiceException;
import com.xzzn.common.utils.DateUtils;
import com.xzzn.common.utils.StringUtils;
import com.xzzn.common.utils.file.ImageUtils;
import com.xzzn.ems.domain.EmsDevicesSetting;
import com.xzzn.ems.domain.EmsPointMatch;
import com.xzzn.ems.domain.vo.DeviceUpdateRequest;
@ -23,15 +27,22 @@ import com.xzzn.ems.mapper.EmsDevicesSettingMapper;
import com.xzzn.ems.mapper.EmsPointMatchMapper;
import com.xzzn.ems.service.IEmsDeviceSettingService;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.util.CollectionUtils;
/**
* 站点信息 服务层实现
@ -43,6 +54,8 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService
private static final Logger log = LoggerFactory.getLogger(EmsDeviceSettingServiceImpl.class);
private static final String DDS_SITE_ID = "021_DDS_01";
private static final String DEVICE_STATUS_FIELD = "device_status";
private static final List<String> CLUSTER_ACTIVE_POWER_FIELDS = Arrays.asList("cluster1_active_power", "cluster2_active_power", "cluster3_active_power", "cluster4_active_power");
@Autowired
private EmsDevicesSettingMapper emsDevicesMapper;
@ -54,6 +67,8 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService
private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper;
@Autowired
private EmsBatteryClusterServiceImpl emsBatteryClusterServiceImpl;
@Autowired
private ModbusProcessor modbusProcessor;
/**
* 获取设备详细信息
@ -486,11 +501,13 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService
throw new ServiceException("未找到对应设备配置信息");
}
// 查询设备配置的设备状态对应点位
// request.setDeviceCategory(DeviceCategory.PCS.getCode());
// EmsPointMatch pointMatch = emsPointMatchMapper.selectDeviceStatusPoint(request);
// if (pointMatch == null) {
// throw new ServiceException("未找到对应设备状态点位");
// }
CLUSTER_ACTIVE_POWER_FIELDS.add(DEVICE_STATUS_FIELD);
request.setMatchFields(CLUSTER_ACTIVE_POWER_FIELDS);
request.setDeviceCategory(DeviceCategory.PCS.getCode());
List<EmsPointMatch> pointMatchList = emsPointMatchMapper.selectDeviceStatusPoint(request);
if (CollectionUtils.isEmpty(pointMatchList) || pointMatchList.stream().noneMatch(point -> DEVICE_STATUS_FIELD.equals(point.getMatchField()))) {
throw new ServiceException("未找到对应设备状态点位");
}
if (DeviceRunningStatus.getShutdownCodeList().contains(request.getDeviceStatus())) {
// 开机逻辑
device.setDeviceStatus(DeviceRunningStatus.RUNNING.getCode());
@ -500,10 +517,50 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService
} else {
return false;
}
// TODO 调用Modbus向设备发送指
// 根据设备类型和请求状态确定控制命
PcsControlCommand command = PcsControlCommand.fromDeviceStatus(device.getDeviceStatus());
if (command == null) {
throw new ServiceException("不支持的设备状态操作");
}
// 调用Modbus向设备发送指令
DeviceConfig deviceConfig = getDeviceConfig(device);
deviceConfig.setWriteTags(getWriteTags(device, pointMatchList, command));
log.info("设备控制指令发送数据: {}", JSON.toJSONString(deviceConfig));
boolean result = modbusProcessor.writeDataToDevice(deviceConfig);
if (!result) {
throw new ServiceException("设备控制指令发送失败");
}
device.setUpdatedAt(DateUtils.getNowDate());
emsDevicesMapper.updateEmsDevicesSetting(device);
return true;
}
public DeviceConfig getDeviceConfig(EmsDevicesSetting device) {
DeviceConfig deviceConfig = new DeviceConfig();
deviceConfig.setDeviceNumber(device.getDeviceId());
deviceConfig.setDeviceName(device.getDeviceName());
deviceConfig.setSlaveId(device.getSlaveId().intValue());
if (DeviceType.TCP.name().equals(device.getDeviceType())) {
deviceConfig.setHost(device.getIpAddress());
deviceConfig.setPort(device.getIpPort().intValue());
}
return deviceConfig;
}
public List<WriteTagConfig> getWriteTags(EmsDevicesSetting device, List<EmsPointMatch> pointMatchList, PcsControlCommand command) {
List<WriteTagConfig> writeTags = new ArrayList<>();
for (EmsPointMatch pointMatch : pointMatchList) {
WriteTagConfig writeTag = new WriteTagConfig();
writeTag.setAddress(pointMatch.getIpAddress());
if (DEVICE_STATUS_FIELD.equals(pointMatch.getMatchField())) {
writeTag.setValue(command.getCode());
} else {
// 电池簇PCS有功功率给定置0
writeTag.setValue(0);
}
writeTags.add(writeTag);
}
return writeTags;
}
}

View File

@ -23,11 +23,12 @@
<result property="deviceId" column="device_id" />
<result property="clusterDeviceId" column="cluster_device_id" />
<result property="interResistance" column="inter_resistance" />
<result property="current" column="current" />
<result property="dayTime" column="day_time" />
</resultMap>
<sql id="selectEmsBatteryDataDayVo">
select id, battery_pack, battery_cluster, battery_cell_id, voltage, temperature, soc, soh, data_timestamp, create_by, create_time, update_by, update_time, remark, site_id, device_id, cluster_device_id, inter_resistance, day_time from ems_battery_data_day
select id, battery_pack, battery_cluster, battery_cell_id, voltage, temperature, soc, soh, data_timestamp, create_by, create_time, update_by, update_time, remark, site_id, device_id, cluster_device_id, inter_resistance, current, day_time from ems_battery_data_day
</sql>
<select id="selectEmsBatteryDataDayList" parameterType="EmsBatteryDataDay" resultMap="EmsBatteryDataDayResult">
@ -45,6 +46,7 @@
<if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if>
<if test="clusterDeviceId != null and clusterDeviceId != ''"> and cluster_device_id = #{clusterDeviceId}</if>
<if test="interResistance != null "> and inter_resistance = #{interResistance}</if>
<if test="current != null "> and current = #{current}</if>
<if test="dayTime != null "> and day_time = #{dayTime}</if>
</where>
</select>
@ -74,6 +76,7 @@
<if test="deviceId != null">device_id,</if>
<if test="clusterDeviceId != null">cluster_device_id,</if>
<if test="interResistance != null">inter_resistance,</if>
<if test="current != null">current,</if>
<if test="dayTime != null">day_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
@ -118,6 +121,7 @@
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="clusterDeviceId != null">cluster_device_id = #{clusterDeviceId},</if>
<if test="interResistance != null">inter_resistance = #{interResistance},</if>
<if test="current != null">current = #{current},</if>
<if test="dayTime != null">day_time = #{dayTime},</if>
</trim>
where id = #{id}
@ -164,6 +168,7 @@
device_id,
cluster_device_id,
inter_resistance,
current,
day_time
) VALUES
<foreach collection="list" item="item" separator=",">
@ -184,6 +189,7 @@
#{item.deviceId},
#{item.clusterDeviceId},
#{item.interResistance},
#{item.current},
#{item.dayTime}
)
</foreach>
@ -195,6 +201,7 @@
soc = IF(VALUES(temperature) > temperature, VALUES(soc), soc),
soh = IF(VALUES(temperature) > temperature, VALUES(soh), soh),
inter_resistance = IF(VALUES(temperature) > temperature, VALUES(inter_resistance), inter_resistance),
current = IF(VALUES(temperature) > temperature, VALUES(current), current),
update_by = IF(VALUES(temperature) > temperature, VALUES(update_by), update_by),
temperature = IF(VALUES(temperature) > temperature, VALUES(temperature), temperature)
</insert>

View File

@ -64,10 +64,22 @@
<result property="module2Temp" column="module2_temp" />
<result property="module3Temp" column="module3_temp" />
<result property="module4Temp" column="module4_temp" />
<result property="cluster1ActivePower" column="cluster1_active_power" />
<result property="cluster2ActivePower" column="cluster2_active_power" />
<result property="cluster3ActivePower" column="cluster3_active_power" />
<result property="cluster4ActivePower" column="cluster4_active_power" />
</resultMap>
<sql id="selectEmsPcsDataVo">
select id, data_update_time, work_status, grid_status, device_status, control_mode, total_active_power, daily_ac_charge_energy, a_phase_voltage, a_phase_current, total_reactive_power, daily_ac_discharge_energy, b_phase_voltage, b_phase_current, total_apparent_power, pcs_module_temperature, c_phase_voltage, c_phase_current, total_power_factor, pcs_environment_temperature, ac_frequency, branch_status, discharge_status, dc_power, dc_voltage, dc_current, create_by, create_time, update_by, update_time, remark, site_id, device_id, date_month, date_day, total_ac_charge_energy, total_ac_discharge_energy, ac_charge_active_power, ac_capacitive_reactive_power, ac_discharge_active_power, ac_inductive_reactive_power, max_capacitive_power_capacity, max_inductive_power_capacity, max_charge_power_capacity, max_discharge_power_capacity, ac_switch_status, dc_switch_status, remote_control_status, sys_u_current, sys_v_current, sys_w_current, dw_frequency, u_temperature, v_temperature, w_temperature, module1_temp, module2_temp, module3_temp, module4_temp from ems_pcs_data
select id, data_update_time, work_status, grid_status, device_status, control_mode, total_active_power, daily_ac_charge_energy,
a_phase_voltage, a_phase_current, total_reactive_power, daily_ac_discharge_energy, b_phase_voltage, b_phase_current,
total_apparent_power, pcs_module_temperature, c_phase_voltage, c_phase_current, total_power_factor, pcs_environment_temperature,
ac_frequency, branch_status, discharge_status, dc_power, dc_voltage, dc_current, create_by, create_time, update_by, update_time,
remark, site_id, device_id, date_month, date_day, total_ac_charge_energy, total_ac_discharge_energy, ac_charge_active_power,
ac_capacitive_reactive_power, ac_discharge_active_power, ac_inductive_reactive_power, max_capacitive_power_capacity,
max_inductive_power_capacity, max_charge_power_capacity, max_discharge_power_capacity, ac_switch_status, dc_switch_status,
remote_control_status, sys_u_current, sys_v_current, sys_w_current, dw_frequency, u_temperature, v_temperature, w_temperature,
module1_temp, module2_temp, module3_temp, module4_temp, cluster1_active_power, cluster2_active_power, cluster3_active_power, cluster4_active_power from ems_pcs_data
</sql>
<select id="selectEmsPcsDataList" parameterType="EmsPcsData" resultMap="EmsPcsDataResult">
@ -126,6 +138,10 @@
<if test="module2Temp != null "> and module2_temp = #{module2Temp}</if>
<if test="module3Temp != null "> and module3_temp = #{module3Temp}</if>
<if test="module4Temp != null "> and module4_temp = #{module4Temp}</if>
<if test="cluster1ActivePower != null "> and cluster1_active_power = #{cluster1ActivePower}</if>
<if test="cluster2ActivePower != null "> and cluster2_active_power = #{cluster2ActivePower}</if>
<if test="cluster3ActivePower != null "> and cluster3_active_power = #{cluster3ActivePower}</if>
<if test="cluster4ActivePower != null "> and cluster4_active_power = #{cluster4ActivePower}</if>
</where>
</select>
@ -195,6 +211,10 @@
<if test="module2Temp != null">module2_temp,</if>
<if test="module3Temp != null">module3_temp,</if>
<if test="module4Temp != null">module4_temp,</if>
<if test="cluster1ActivePower != null">cluster1_active_power,</if>
<if test="cluster2ActivePower != null">cluster2_active_power,</if>
<if test="cluster3ActivePower != null">cluster3_active_power,</if>
<if test="cluster4ActivePower != null">cluster4_active_power,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="dataUpdateTime != null">#{dataUpdateTime},</if>
@ -255,6 +275,10 @@
<if test="module2Temp != null">#{module2Temp},</if>
<if test="module3Temp != null">#{module3Temp},</if>
<if test="module4Temp != null">#{module4Temp},</if>
<if test="cluster1ActivePower != null">#{cluster1ActivePower},</if>
<if test="cluster2ActivePower != null">#{cluster2ActivePower},</if>
<if test="cluster3ActivePower != null">#{cluster3ActivePower},</if>
<if test="cluster4ActivePower != null">#{cluster4ActivePower},</if>
</trim>
</insert>
@ -319,6 +343,10 @@
<if test="module2Temp != null">module2_temp = #{module2Temp},</if>
<if test="module3Temp != null">module3_temp = #{module3Temp},</if>
<if test="module4Temp != null">module4_temp = #{module4Temp},</if>
<if test="cluster1ActivePower != null">cluster1_active_power = #{cluster1ActivePower},</if>
<if test="cluster2ActivePower != null">cluster2_active_power = #{cluster2ActivePower},</if>
<if test="cluster3ActivePower != null">cluster3_active_power = #{cluster3ActivePower},</if>
<if test="cluster4ActivePower != null">cluster4_active_power = #{cluster4ActivePower},</if>
</trim>
where id = #{id}
</update>

View File

@ -541,12 +541,15 @@
and device_id = #{deviceId}
and device_category = #{deviceCategory}
</select>
<select id="selectDeviceStatusPoint" resultType="EmsPointMatch" parameterType="com.xzzn.ems.domain.vo.DeviceUpdateRequest">
<select id="selectDeviceStatusPoint" parameterType="com.xzzn.ems.domain.vo.DeviceUpdateRequest" resultMap="EmsPointMatchResult" >
<include refid="selectEmsPointMatchVo"/>
where site_id = #{request.siteId}
and device_id = #{request.deviceId}
and device_category = #{request.deviceCategory}
and match_field = 'device_status'
and match_field in
<foreach collection="list" item="matchField" open="(" separator="," close=")">
#{matchField}
</foreach>
</select>
</mapper>

View File

@ -171,6 +171,12 @@
<artifactId>j2mod</artifactId>
<version>3.1.0</version> <!-- 提供TCPMasterConnection、SerialConnection等类 -->
</dependency>
<!-- modbus4j -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Resilience4j 断路器核心 -->
<dependency>
<groupId>io.github.resilience4j</groupId>