对接逆变器
This commit is contained in:
		| @ -0,0 +1,44 @@ | |||||||
|  | package org.dromara.tcpfuwu.constant; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Modbus TCP协议常量 | ||||||
|  |  */ | ||||||
|  | public class ModbusConstant { | ||||||
|  |     // Modbus功能码:读取输入寄存器(0x04) | ||||||
|  |     public static final byte FUNC_READ_INPUT_REG = 0x04; | ||||||
|  |     // CRC16校验表(Modbus RTU标准) | ||||||
|  |     public static final Integer[] CRC16_TABLE = { | ||||||
|  |         0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, | ||||||
|  |         0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, | ||||||
|  |         0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, | ||||||
|  |         0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, | ||||||
|  |         0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, | ||||||
|  |         0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, | ||||||
|  |         0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, | ||||||
|  |         0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, | ||||||
|  |         0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, | ||||||
|  |         0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, | ||||||
|  |         0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, | ||||||
|  |         0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, | ||||||
|  |         0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, | ||||||
|  |         0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, | ||||||
|  |         0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, | ||||||
|  |         0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, | ||||||
|  |         0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, | ||||||
|  |         0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, | ||||||
|  |         0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, | ||||||
|  |         0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, | ||||||
|  |         0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, | ||||||
|  |         0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, | ||||||
|  |         0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, | ||||||
|  |         0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, | ||||||
|  |         0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, | ||||||
|  |         0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, | ||||||
|  |         0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, | ||||||
|  |         0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, | ||||||
|  |         0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, | ||||||
|  |         0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, | ||||||
|  |         0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, | ||||||
|  |         0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 | ||||||
|  |     }; | ||||||
|  | } | ||||||
| @ -0,0 +1,16 @@ | |||||||
|  | package org.dromara.tcpfuwu.domain; | ||||||
|  |  | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  |  | ||||||
|  | @Data | ||||||
|  | public class DeviceCache { | ||||||
|  |     private String snCode;  // 设备SN码 | ||||||
|  |     private long createTime;  // 连接创建时间(毫秒) | ||||||
|  |     private long lastHeartbeatTime;  // 最后心跳时间(毫秒) | ||||||
|  |     private boolean isExpired;  // 是否过期 | ||||||
|  |     // 变量值缓存:key=变量名,value=解析后的值(带倍率) | ||||||
|  |     private Map<String, Object> variableValues = new ConcurrentHashMap<>(); | ||||||
|  | } | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | package org.dromara.tcpfuwu.domain; | ||||||
|  |  | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | @Data | ||||||
|  | public class ModbusVariable { | ||||||
|  |     private Long id; | ||||||
|  |     private String snCode;         // 设备SN码 | ||||||
|  |     private Integer slaveId;          // 从机地址 | ||||||
|  |     private Integer funcCode;         // 功能码 | ||||||
|  |     private Integer startRegAddr;  // 起始寄存器地址 | ||||||
|  |     private Integer regQuantity;      // 寄存器数量 | ||||||
|  |     private String variableName;   // 变量名 | ||||||
|  |     private String dataType;       // 数据类型(S16/U16/S32/U32/FLOAT) | ||||||
|  |     private Double multiplier;     // 数据倍率 | ||||||
|  |     private String unit;           // 单位 | ||||||
|  |     private Byte isEnabled;        // 是否启用(1=启用) | ||||||
|  | } | ||||||
| @ -0,0 +1,340 @@ | |||||||
|  | package org.dromara.tcpfuwu.handler; | ||||||
|  |  | ||||||
|  | import org.apache.ibatis.session.SqlSession; | ||||||
|  | import org.apache.ibatis.session.SqlSessionFactory; | ||||||
|  | import org.dromara.shebei.domain.dto.OpsSbLiebiaoDto; | ||||||
|  | import org.dromara.shebei.domain.vo.OpsSbMbBianliangVo; | ||||||
|  | import org.dromara.shebei.service.IOpsSbLiebiaoService; | ||||||
|  | import org.dromara.tcpfuwu.constant.ModbusConstant; | ||||||
|  | import org.dromara.tcpfuwu.domain.DeviceCache; | ||||||
|  | import org.dromara.tcpfuwu.domain.ModbusVariable; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.io.OutputStream; | ||||||
|  | import java.net.Socket; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.nio.ByteOrder; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  | import java.util.concurrent.ScheduledExecutorService; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 设备连接处理类:处理单设备的心跳和Modbus通信 | ||||||
|  |  */ | ||||||
|  | //@Component | ||||||
|  | public class DeviceHandler { | ||||||
|  |     // 配置参数 | ||||||
|  |     private static final long HEARTBEAT_EXPIRE_MS = 30 * 1000; | ||||||
|  |     private static final int HEARTBEAT_LEN = 20; | ||||||
|  |     private static final int SN_CODE_LEN = 10; | ||||||
|  |     private static final int MODBUS_REQUEST_CYCLE = 10; // 请求周期(秒) | ||||||
|  |  | ||||||
|  |     // 依赖注入 | ||||||
|  | //    @Autowired | ||||||
|  |     private final IOpsSbLiebiaoService sbLiebiaoService; | ||||||
|  |  | ||||||
|  |     // 设备信息 | ||||||
|  |     private final Socket clientSocket; | ||||||
|  |     private final String deviceAddr; | ||||||
|  |     private final DeviceCache deviceCache; | ||||||
|  |     private final Map<String, DeviceCache> globalCache; | ||||||
|  |  | ||||||
|  |     // 状态控制 | ||||||
|  |     private final AtomicBoolean isRunning = new AtomicBoolean(true); | ||||||
|  |     private InputStream in; | ||||||
|  |     private OutputStream out; | ||||||
|  |     private ScheduledExecutorService modbusScheduler; | ||||||
|  |  | ||||||
|  |     public DeviceHandler( | ||||||
|  |         Socket clientSocket, | ||||||
|  |         String deviceAddr, | ||||||
|  |         DeviceCache deviceCache, | ||||||
|  |         Map<String, DeviceCache> globalCache, | ||||||
|  |         IOpsSbLiebiaoService sbLiebiaoService // 新增:手动传递Spring服务 | ||||||
|  |     ) { | ||||||
|  |         this.clientSocket = clientSocket; | ||||||
|  |         this.deviceAddr = deviceAddr; | ||||||
|  |         this.deviceCache = deviceCache; | ||||||
|  |         this.globalCache = globalCache; | ||||||
|  |         this.sbLiebiaoService = sbLiebiaoService; // 赋值 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 处理设备通信的主方法 | ||||||
|  |      */ | ||||||
|  |     public void handle() { | ||||||
|  |         try { | ||||||
|  |             // 初始化IO流 | ||||||
|  |             in = clientSocket.getInputStream(); | ||||||
|  |             out = clientSocket.getOutputStream(); | ||||||
|  |  | ||||||
|  |             // 启动心跳接收线程 | ||||||
|  |             startHeartbeatReceiver(); | ||||||
|  |  | ||||||
|  |             // 启动Modbus定时请求线程 | ||||||
|  |             startModbusScheduler(); | ||||||
|  |  | ||||||
|  |             // 阻塞等待线程结束 | ||||||
|  |             synchronized (this) { | ||||||
|  |                 wait(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             System.err.printf("【设备处理异常】地址:%s,原因:%s%n", deviceAddr, e.getMessage()); | ||||||
|  |         } finally { | ||||||
|  |             // 清理资源 | ||||||
|  |             stop(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 启动心跳接收线程 | ||||||
|  |      */ | ||||||
|  |     private void startHeartbeatReceiver() { | ||||||
|  |         new Thread(() -> { | ||||||
|  |             try { | ||||||
|  |                 byte[] buffer = new byte[1024]; | ||||||
|  |                 while (isRunning.get()) { | ||||||
|  |                     int len = in.read(buffer); | ||||||
|  |                     if (len <= 0) { | ||||||
|  |                         System.out.printf("【心跳接收失败】设备:%s,连接断开%n", deviceAddr); | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     byte[] data = Arrays.copyOf(buffer, len); | ||||||
|  |                     if (isHeartbeatData(data)) { | ||||||
|  |                         // 解析SN码 | ||||||
|  |                         String snCode = new String(Arrays.copyOfRange(data, 0, SN_CODE_LEN)).trim(); | ||||||
|  |                         // 更新缓存 | ||||||
|  |                         deviceCache.setSnCode(snCode); | ||||||
|  |                         deviceCache.setLastHeartbeatTime(System.currentTimeMillis()); | ||||||
|  |                         System.out.printf("【心跳更新】设备:%s,SN:%s%n", deviceAddr, snCode); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 System.err.printf("【心跳线程异常】设备:%s,原因:%s%n", deviceAddr, e.getMessage()); | ||||||
|  |             } finally { | ||||||
|  |                 // 心跳线程结束,标记设备离线 | ||||||
|  |                 isRunning.set(false); | ||||||
|  |                 synchronized (DeviceHandler.this) { | ||||||
|  |                     DeviceHandler.this.notify(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, "heartbeat-" + deviceAddr).start(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 启动Modbus定时请求调度器 | ||||||
|  |      */ | ||||||
|  |     private void startModbusScheduler() { | ||||||
|  |         modbusScheduler = Executors.newSingleThreadScheduledExecutor( | ||||||
|  |             r -> new Thread(r, "modbus-" + deviceAddr) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         modbusScheduler.scheduleAtFixedRate(() -> { | ||||||
|  |             if (!isRunning.get() || !clientSocket.isConnected() || clientSocket.isClosed()) { | ||||||
|  |                 stop(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 未获取SN码则等待 | ||||||
|  |             if (deviceCache.getSnCode() == null) { | ||||||
|  |                 System.out.printf("【等待心跳】设备:%s%n", deviceAddr); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 查询变量并处理 | ||||||
|  |             List<ModbusVariable> variables = queryVariables(deviceCache.getSnCode()); | ||||||
|  |             if (variables.isEmpty()) { | ||||||
|  |                 System.out.printf("【无变量配置】SN:%s%n", deviceCache.getSnCode()); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 遍历变量发送请求 | ||||||
|  |             for (ModbusVariable var : variables) { | ||||||
|  |                 try { | ||||||
|  |                     handleVariable(var); | ||||||
|  |                 } catch (Exception e) { | ||||||
|  |                     System.err.printf("【变量处理失败】SN:%s,变量:%s,原因:%s%n", | ||||||
|  |                             deviceCache.getSnCode(), var.getVariableName(), e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         }, 0, MODBUS_REQUEST_CYCLE, TimeUnit.SECONDS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 处理单个变量的请求与解析 | ||||||
|  |      */ | ||||||
|  |     private void handleVariable(ModbusVariable var) throws Exception { | ||||||
|  |         // 生成请求帧 | ||||||
|  |         byte[] request = generateModbusFrame(var); | ||||||
|  |         System.out.printf("【发送请求】SN:%s,变量:%s,帧:%s%n", | ||||||
|  |                 deviceCache.getSnCode(), var.getVariableName(), bytesToHex(request)); | ||||||
|  |  | ||||||
|  |         // 发送请求 | ||||||
|  |         out.write(request); | ||||||
|  |         out.flush(); | ||||||
|  |  | ||||||
|  |         // 接收响应(超时3秒) | ||||||
|  |         byte[] response = receiveResponse(3000); | ||||||
|  |         if (response == null) { | ||||||
|  |             throw new Exception("响应超时"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 验证响应 | ||||||
|  |         if (!validateResponse(response, var)) { | ||||||
|  |             throw new Exception("响应验证失败"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 解析数据 | ||||||
|  |         Object value = parseValue(response, var); | ||||||
|  |         System.out.printf("【解析成功】SN:%s,变量:%s,值:%s %s%n", | ||||||
|  |                 deviceCache.getSnCode(), var.getVariableName(), value, var.getUnit()); | ||||||
|  |  | ||||||
|  |         // 更新缓存 | ||||||
|  |         deviceCache.getVariableValues().put(var.getVariableName(), value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 从数据库查询变量列表 | ||||||
|  |      */ | ||||||
|  |     private List<ModbusVariable> queryVariables(String snCode) { | ||||||
|  |         ArrayList<ModbusVariable> modbusVariables = new ArrayList<>(); | ||||||
|  |         OpsSbLiebiaoDto opsSbLiebiaoDto = sbLiebiaoService.getLiebiaoBianliangList(snCode); | ||||||
|  |         if (!opsSbLiebiaoDto.getSbMbBianliangVos().isEmpty()) { | ||||||
|  |             for (OpsSbMbBianliangVo v : opsSbLiebiaoDto.getSbMbBianliangVos()) { | ||||||
|  |                 ModbusVariable modbusVariable = new ModbusVariable(); | ||||||
|  |                 modbusVariable.setDataType(v.getShujvGeshi()); | ||||||
|  |                 modbusVariable.setVariableName(v.getBlName()); | ||||||
|  |                 modbusVariable.setUnit(v.getBlDanwei()); | ||||||
|  |                 modbusVariable.setSnCode(snCode); | ||||||
|  |                 modbusVariable.setSlaveId(Math.toIntExact(opsSbLiebiaoDto.getSlaveId())); | ||||||
|  |                 modbusVariable.setFuncCode(Integer.parseInt(v.getJicunqiGnm())); | ||||||
|  |                 modbusVariable.setStartRegAddr(Integer.parseInt(v.getJicunqiAdd())); | ||||||
|  |                 switch (v.getShujvGeshi()) { | ||||||
|  |                     case "S16": modbusVariable.setRegQuantity(1); break; | ||||||
|  |                     case "U16": modbusVariable.setRegQuantity(1); break; | ||||||
|  |                     case "S32": modbusVariable.setRegQuantity(2); break; | ||||||
|  |                     case "U32": modbusVariable.setRegQuantity(2); break; | ||||||
|  |                 } | ||||||
|  |                 modbusVariables.add(modbusVariable); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return modbusVariables; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 停止所有资源 | ||||||
|  |      */ | ||||||
|  |     public void stop() { | ||||||
|  |         isRunning.set(false); | ||||||
|  |  | ||||||
|  |         // 关闭Modbus调度器 | ||||||
|  |         if (modbusScheduler != null) { | ||||||
|  |             modbusScheduler.shutdown(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 关闭Socket和流 | ||||||
|  |         try { | ||||||
|  |             if (in != null) in.close(); | ||||||
|  |             if (out != null) out.close(); | ||||||
|  |             if (clientSocket != null && !clientSocket.isClosed()) { | ||||||
|  |                 clientSocket.close(); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             System.err.printf("【资源关闭异常】设备:%s,原因:%s%n", deviceAddr, e.getMessage()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 从全局缓存移除 | ||||||
|  |         globalCache.remove(deviceAddr); | ||||||
|  |         System.out.printf("【设备下线】地址:%s%n", deviceAddr); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ------------------------------ 工具方法 ------------------------------ | ||||||
|  |     private boolean isHeartbeatData(byte[] data) { | ||||||
|  |         if (data.length != HEARTBEAT_LEN) return false; | ||||||
|  |         for (byte b : data) { | ||||||
|  |             if (b < '0' || b > '9') return false; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private byte[] generateModbusFrame(ModbusVariable var) { | ||||||
|  |         ByteBuffer buffer = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN); | ||||||
|  |         buffer.put(var.getSlaveId().byteValue()) | ||||||
|  |               .put(var.getFuncCode().byteValue()) | ||||||
|  |               .putShort(var.getStartRegAddr().shortValue()) | ||||||
|  |               .putShort(var.getRegQuantity().shortValue()); | ||||||
|  |  | ||||||
|  |         byte[] body = buffer.array(); | ||||||
|  |         byte[] crc = calculateCrc16(body); | ||||||
|  |  | ||||||
|  |         byte[] frame = new byte[body.length + crc.length]; | ||||||
|  |         System.arraycopy(body, 0, frame, 0, body.length); | ||||||
|  |         System.arraycopy(crc, 0, frame, body.length, crc.length); | ||||||
|  |         return frame; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private byte[] receiveResponse(int timeoutMs) throws Exception { | ||||||
|  |         long start = System.currentTimeMillis(); | ||||||
|  |         while (System.currentTimeMillis() - start < timeoutMs) { | ||||||
|  |             if (in.available() > 0) { | ||||||
|  |                 byte[] buffer = new byte[1024]; | ||||||
|  |                 int len = in.read(buffer); | ||||||
|  |                 return Arrays.copyOf(buffer, len); | ||||||
|  |             } | ||||||
|  |             Thread.sleep(10); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean validateResponse(byte[] response, ModbusVariable var) { | ||||||
|  |         if (response.length < 5) return false; | ||||||
|  |         if (response[0] != var.getSlaveId() || response[1] != var.getFuncCode()) return false; | ||||||
|  |         if (response[2] != var.getRegQuantity() * 2) return false; | ||||||
|  |  | ||||||
|  |         byte[] body = Arrays.copyOf(response, response.length - 2); | ||||||
|  |         byte[] receivedCrc = Arrays.copyOfRange(response, response.length - 2, response.length); | ||||||
|  |         return Arrays.equals(receivedCrc, calculateCrc16(body)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Object parseValue(byte[] response, ModbusVariable var) { | ||||||
|  |         ByteBuffer buffer = ByteBuffer.wrap(response, 3, response[2]).order(ByteOrder.BIG_ENDIAN); | ||||||
|  |         double rawValue; | ||||||
|  |  | ||||||
|  |         switch (var.getDataType().toUpperCase()) { | ||||||
|  |             case "S16": rawValue = buffer.getShort(); break; | ||||||
|  |             case "U16": rawValue = buffer.getShort() & 0xFFFF; break; | ||||||
|  |             case "S32": rawValue = buffer.getInt(); break; | ||||||
|  |             case "U32": rawValue = buffer.getLong() & 0xFFFFFFFFL; break; | ||||||
|  |             case "FLOAT": rawValue = buffer.getFloat(); break; | ||||||
|  |             default: throw new IllegalArgumentException("不支持的数据类型:" + var.getDataType()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return rawValue * var.getMultiplier(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private byte[] calculateCrc16(byte[] data) { | ||||||
|  |         int crc = 0xFFFF; | ||||||
|  |         for (byte b : data) { | ||||||
|  |             crc = (crc >> 8) ^ ModbusConstant.CRC16_TABLE[(crc ^ (b & 0xFF)) & 0xFF]; | ||||||
|  |         } | ||||||
|  |         return new byte[]{(byte) (crc & 0xFF), (byte) ((crc >> 8) & 0xFF)}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String bytesToHex(byte[] bytes) { | ||||||
|  |         StringBuilder sb = new StringBuilder(); | ||||||
|  |         for (byte b : bytes) { | ||||||
|  |             sb.append(String.format("%02X ", b)); | ||||||
|  |         } | ||||||
|  |         return sb.toString().trim(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,170 @@ | |||||||
|  | package org.dromara.tcpfuwu.server; | ||||||
|  |  | ||||||
|  | import jakarta.annotation.PostConstruct; | ||||||
|  | import jakarta.annotation.PreDestroy; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  |  | ||||||
|  | import org.dromara.shebei.service.IOpsSbLiebiaoService; | ||||||
|  | import org.dromara.tcpfuwu.domain.DeviceCache; | ||||||
|  | import org.dromara.tcpfuwu.handler.DeviceHandler; | ||||||
|  |  | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.beans.factory.annotation.Value; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import java.net.ServerSocket; | ||||||
|  | import java.net.Socket; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.*; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 统一TCP服务器(发送Modbus请求前必须验证心跳状态) | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | @Slf4j | ||||||
|  | public class UnifiedTcpServer { | ||||||
|  |  | ||||||
|  |     @Value("${tcp.server.port:8888}") | ||||||
|  |     private int tcpPort; | ||||||
|  |     @Value("${tcp.server.heartbeat-timeout: 60000}") | ||||||
|  |     private int heartbeatExpireMs; | ||||||
|  |  | ||||||
|  |     // 线程池:处理设备连接 | ||||||
|  |     private ExecutorService clientExecutor; | ||||||
|  |     // 服务器Socket | ||||||
|  |     private ServerSocket serverSocket; | ||||||
|  |     // 设备缓存 | ||||||
|  |     private final Map<String, DeviceCache> deviceCache = new ConcurrentHashMap<>(); | ||||||
|  |     // 缓存清理线程 | ||||||
|  |     private ScheduledExecutorService cacheCleaner; | ||||||
|  |     // 服务运行状态 | ||||||
|  |     private volatile boolean isRunning = false; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private IOpsSbLiebiaoService sbLiebiaoService; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 初始化方法:Spring容器启动后自动调用 | ||||||
|  |      */ | ||||||
|  |     @PostConstruct | ||||||
|  |     public void start() { | ||||||
|  |         try { | ||||||
|  |             // 初始化服务器Socket | ||||||
|  |             serverSocket = new ServerSocket(tcpPort); | ||||||
|  |             // 初始化线程池(处理设备连接) | ||||||
|  |             clientExecutor = Executors.newCachedThreadPool(r -> { | ||||||
|  |                 Thread thread = new Thread(r); | ||||||
|  |                 thread.setName("tcp-client-handler-" + thread.getId()); | ||||||
|  |                 thread.setDaemon(true); // 守护线程,随主线程退出 | ||||||
|  |                 return thread; | ||||||
|  |             }); | ||||||
|  |             // 初始化缓存清理线程 | ||||||
|  |             initCacheCleaner(); | ||||||
|  |  | ||||||
|  |             isRunning = true; | ||||||
|  |             System.out.printf("【TCP服务启动成功】监听端口:%d%n", tcpPort); | ||||||
|  |  | ||||||
|  |             // 启动接受连接的线程 | ||||||
|  |             new Thread(this::acceptConnections, "tcp-acceptor").start(); | ||||||
|  |  | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             System.err.println("【TCP服务启动失败】" + e.getMessage()); | ||||||
|  |             e.printStackTrace(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 接受设备连接的循环 | ||||||
|  |      */ | ||||||
|  |     private void acceptConnections() { | ||||||
|  |         while (isRunning) { | ||||||
|  |             try { | ||||||
|  |                 // 阻塞等待设备连接 | ||||||
|  |                 Socket clientSocket = serverSocket.accept(); | ||||||
|  |                 String deviceAddr = clientSocket.getInetAddress() + ":" + clientSocket.getPort(); | ||||||
|  |                 System.out.printf("%n【设备上线】地址:%s%n", deviceAddr); | ||||||
|  |  | ||||||
|  |                 // 初始化设备缓存 | ||||||
|  |                 DeviceCache cache = new DeviceCache(); | ||||||
|  |                 cache.setCreateTime(System.currentTimeMillis()); | ||||||
|  |                 deviceCache.put(deviceAddr, cache); | ||||||
|  |  | ||||||
|  |                 // 提交设备处理任务 | ||||||
|  |                 clientExecutor.submit(() -> | ||||||
|  |                     new DeviceHandler(clientSocket, deviceAddr, cache, deviceCache,sbLiebiaoService).handle() | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 if (isRunning) { // 非关闭状态下的异常 | ||||||
|  |                     System.err.println("【连接接受异常】" + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 初始化缓存清理线程 | ||||||
|  |      */ | ||||||
|  |     private void initCacheCleaner() { | ||||||
|  |         cacheCleaner = Executors.newSingleThreadScheduledExecutor(r -> { | ||||||
|  |             Thread thread = new Thread(r); | ||||||
|  |             thread.setName("cache-cleaner"); | ||||||
|  |             thread.setDaemon(true); | ||||||
|  |             return thread; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // 每20秒清理一次过期设备 | ||||||
|  |         cacheCleaner.scheduleAtFixedRate(() -> { | ||||||
|  |             long currentTime = System.currentTimeMillis(); | ||||||
|  |             for (Map.Entry<String, DeviceCache> e : deviceCache.entrySet()) { | ||||||
|  |                 String deviceAddr = e.getKey(); | ||||||
|  |                 DeviceCache cache = e.getValue(); | ||||||
|  |                 // 判断是否过期:未收到过心跳 或 超过过期时间 | ||||||
|  |                 boolean isExpired = cache.getLastHeartbeatTime() == 0 | ||||||
|  |                     ? (currentTime - cache.getCreateTime() > heartbeatExpireMs) | ||||||
|  |                     : (currentTime - cache.getLastHeartbeatTime() > heartbeatExpireMs); | ||||||
|  |  | ||||||
|  |                 if (isExpired && !cache.isExpired()) { | ||||||
|  |                     cache.setExpired(true); | ||||||
|  |                     deviceCache.remove(deviceAddr); | ||||||
|  |                     System.out.printf("【设备过期】设备地址:%s,已清理缓存%n", deviceAddr); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, 0, 20, TimeUnit.SECONDS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 销毁方法:Spring容器关闭前自动调用 | ||||||
|  |      */ | ||||||
|  |     @PreDestroy | ||||||
|  |     public void stop() { | ||||||
|  |         isRunning = false; | ||||||
|  |         System.out.println("【TCP服务开始关闭】"); | ||||||
|  |  | ||||||
|  |         // 关闭服务器Socket | ||||||
|  |         try { | ||||||
|  |             if (serverSocket != null && !serverSocket.isClosed()) { | ||||||
|  |                 serverSocket.close(); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             System.err.println("【关闭ServerSocket异常】" + e.getMessage()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 关闭线程池 | ||||||
|  |         if (clientExecutor != null) { | ||||||
|  |             clientExecutor.shutdown(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 关闭缓存清理线程 | ||||||
|  |         if (cacheCleaner != null) { | ||||||
|  |             cacheCleaner.shutdown(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 清理设备缓存 | ||||||
|  |         deviceCache.clear(); | ||||||
|  |  | ||||||
|  |         System.out.println("【TCP服务已关闭】"); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,45 @@ | |||||||
|  | package org.dromara.tcpfuwu.starter; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.dromara.tcpfuwu.server.UnifiedTcpServer; | ||||||
|  | import org.springframework.boot.CommandLineRunner; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | import javax.annotation.PreDestroy; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Spring启动时自动初始化TCP服务器,关闭时释放资源 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | @Slf4j | ||||||
|  | public class TcpServersStarter implements CommandLineRunner { | ||||||
|  |  | ||||||
|  |     private final UnifiedTcpServer unifiedTcpServer; | ||||||
|  |  | ||||||
|  |     // 构造注入两个TCP服务器Bean | ||||||
|  |     public TcpServersStarter(  UnifiedTcpServer unifiedTcpServer) { | ||||||
|  |         this.unifiedTcpServer = unifiedTcpServer; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Spring Boot启动后执行(CommandLineRunner接口方法) | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void run(String... args) throws Exception { | ||||||
|  |         log.info("【TCP服务器启动器】开始初始化Modbus和逆变器心跳服务器..."); | ||||||
|  |         // 启动两个服务器(独立线程,不阻塞Spring主线程) | ||||||
|  |         unifiedTcpServer.start(); | ||||||
|  |         log.info("【TCP服务器启动器】所有TCP服务器初始化完成"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Spring容器销毁前执行(释放资源) | ||||||
|  |      */ | ||||||
|  |     @PreDestroy | ||||||
|  |     public void stopTcpServers() { | ||||||
|  |         log.info("【TCP服务器启动器】开始关闭所有TCP服务器..."); | ||||||
|  |         unifiedTcpServer.stop(); | ||||||
|  |         log.info("【TCP服务器启动器】所有TCP服务器已关闭"); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,45 @@ | |||||||
|  | package org.dromara.tcpfuwu.util; | ||||||
|  |  | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 字节数组处理工具类 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class ByteUtils { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 字节数组转16进制字符串(空格分隔) | ||||||
|  |      */ | ||||||
|  |     public String bytesToHex(byte[] bytes) { | ||||||
|  |         if (bytes == null || bytes.length == 0) { | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  |         StringBuilder sb = new StringBuilder(); | ||||||
|  |         for (byte b : bytes) { | ||||||
|  |             sb.append(String.format("%02X ", b)); | ||||||
|  |         } | ||||||
|  |         return sb.toString().trim(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 字节数组转ASCII字符串(不可打印字符用[0xXX]表示) | ||||||
|  |      */ | ||||||
|  |     public String bytesToAsciiString(byte[] bytes) { | ||||||
|  |         if (bytes == null || bytes.length == 0) { | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  |         StringBuilder sb = new StringBuilder(); | ||||||
|  |         for (byte b : bytes) { | ||||||
|  |             // 可打印ASCII范围:32(空格)~126(~) | ||||||
|  |             if (b >= 32 && b <= 126) { | ||||||
|  |                 sb.append((char) b); | ||||||
|  |             } else { | ||||||
|  |                 sb.append("[0x").append(String.format("%02X", b)).append("]"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return sb.toString(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user