| | |
| | | <artifactId>acs-common</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.ghgande</groupId> |
| | | <artifactId>j2mod</artifactId> |
| | | <version>3.2.1</version> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | <build> |
| | | <plugins> |
| | | <!-- <plugin>--> |
| | | <!-- <groupId>org.apache.maven.plugins</groupId>--> |
| | | <!-- <artifactId>maven-compiler-plugin</artifactId>--> |
| | | <!-- <configuration>--> |
| | | <!-- <source>${java.version}</source>--> |
| | | <!-- <source>${java.version}</source>--> |
| | | <!-- </configuration>--> |
| | | <!-- </plugin>--> |
| | | <!-- <plugin>--> |
| | | <!-- <groupId>org.apache.maven.plugins</groupId>--> |
| | | <!-- <artifactId>maven-compiler-plugin</artifactId>--> |
| | | <!-- <configuration>--> |
| | | <!-- <source>${java.version}</source>--> |
| | | <!-- <source>${java.version}</source>--> |
| | | <!-- </configuration>--> |
| | | <!-- </plugin>--> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-resources-plugin</artifactId> |
| | |
| | | package com.zy.acs.charge; |
| | | |
| | | import com.zy.acs.charge.model.ChargerStatus; |
| | | |
| | | /** |
| | | * 所有充电桩都实现这个接口 |
| | | */ |
| | | public interface ChargeCoreService { |
| | | |
| | | boolean connect() throws Exception; |
| | | |
| | | void disconnect(); |
| | | |
| | | boolean isConnected(); |
| | | |
| | | boolean startCharging() throws Exception; |
| | | |
| | | boolean stopCharging() throws Exception; |
| | | |
| | | ChargerStatus getStatus() throws Exception; |
| | | |
| | | boolean setVoltage(int voltageDecivolts) throws Exception; // 单位0.1V |
| | | |
| | | boolean setCurrent(int currentDecamperes) throws Exception; // 单位0.1A |
| | | |
| | | boolean clearFault() throws Exception; |
| | | } |
| New file |
| | |
| | | package com.zy.acs.charge.constant; |
| | | |
| | | /** |
| | | * |
| | | */ |
| | | public enum AiPowerChargerCoilEnum { |
| | | |
| | | |
| | | WORK_STATUS(100, 1, "工作状态"), |
| | | OVERHEAT_STATUS(101, 1, "过热状态"), |
| | | FAULT_STATUS(102, 1, "异常状态"), |
| | | CV_CC_STATUS(103, 1, "恒压恒流状态"), |
| | | BATTERY_CONNECTED(104, 1, "电池接入状态"), |
| | | FAN_STATUS(105, 1, "散热风扇状态"), |
| | | PLATE_OVERHEAT(106, 1, "板过热状态"), |
| | | BRUSH_PRESSED(107, 1, "刷块压紧"), |
| | | AGV_REPORTED_IN_POSITION(108, 3, "AGV车报告到位"), |
| | | CHARGER_CONFIRMED_AGV_READY(109, 1, "充电机确认AGV就位"), |
| | | FORWARD_RELAY_ONLINE(110, 1, "在线正继电器状态(前进到位)"), |
| | | BACKWARD_RELAY_OFFLINE(111, 1, "离线正继电器状态(后退到位)"), |
| | | BMS_CHARGE_ENABLE(112, 1, "BMS电池充电允许"), |
| | | COMM_RS485_STATUS(113, 1, "与主控板RS485通讯状态"), |
| | | COMM_CAN_STATUS(114, 1, "与电源模块CAN通讯状态"), |
| | | COMM_BMS_STATUS(115, 1, "与电池BMS通信状态"); |
| | | |
| | | |
| | | // 寄存器地址 |
| | | private Integer addr; |
| | | |
| | | // 1只读,2只写,3读写 |
| | | private Integer type; |
| | | |
| | | // 名称 |
| | | private String des; |
| | | |
| | | |
| | | AiPowerChargerCoilEnum(int addr, int type, String des) { |
| | | this.addr = addr; |
| | | this.type = type; |
| | | this.des = des; |
| | | } |
| | | |
| | | |
| | | public Integer getAddr() { |
| | | return addr; |
| | | } |
| | | |
| | | public Integer getType() { |
| | | return type; |
| | | } |
| | | |
| | | public String getDes() { |
| | | return des; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.charge.constant; |
| | | |
| | | public enum AiPowerChargerRegisterEnum { |
| | | // 寄存器地址 100~116 |
| | | CHARGE_VOLTAGE(100, 3, "充电机充电电压", 0.1), // 单位0.1V |
| | | CHARGE_CURRENT(101, 3, "充电机充电电流", 0.1), // 单位0.1A |
| | | CHARGE_TIME(102, 1, "充电机充电时间", 1.0), // 单位1分钟 |
| | | CHARGE_CAPACITY(103, 1, "充电机充电电量", 0.1), // 单位0.1Ah |
| | | CHARGE_ENERGY(104, 1, "充电机充电能量", 0.1), // 单位0.1kWh |
| | | BMS_CELL_MAX_VOLTAGE(105, 1, "BMS电池单体最高电压", 1.0), // 单位1mV |
| | | BMS_CELL_MIN_VOLTAGE(106, 1, "BMS电池单体最低电压", 1.0), // 单位1mV |
| | | BMS_PACK_VOLTAGE(107, 1, "BMS电池组电压", 0.1), // 单位0.1V |
| | | BMS_VOLTAGE_DEMAND(108, 1, "BMS电压需求", 0.1), // 单位0.1V |
| | | BMS_CURRENT_DEMAND(109, 1, "BMS电流需求", 0.1), // 单位0.1A |
| | | CHARGER_TEMP(110, 1, "充电机机内温度", 1.0), // 单位1℃ |
| | | BMS_SOC(111, 3, "BMS剩余容量(SOC)", 0.1), // 单位0.1% |
| | | BMS_END_FLAG(112, 1, "BMS充电结束标识", 1.0), |
| | | /** |
| | | * 1:充电过压 |
| | | * 2:放电告警 |
| | | * 3:电池过温 |
| | | * 4:电量过低 |
| | | * 5:电压断线 |
| | | * 6:充电过流 |
| | | * 7:电压过低 |
| | | * 8:电压过高 9: 其它故障 |
| | | * 0:正常 |
| | | */ |
| | | |
| | | CHARGER_ID(113, 1, "充电机编号", 1.0), // 单位1号 |
| | | CHARGER_FAULT(114, 1, "充电机故障", 1.0), // 故障列表值 |
| | | CHARGE_MODE(115, 1, "充电机充电模式", 1.0), // 0自动,1手动 |
| | | SCHEDULE_FLAG(116, 3, "调度标识", 1.0); // 0正常,1清故障,2完成退回,3恢复待机 |
| | | |
| | | private final Integer addr; |
| | | private final Integer type; |
| | | private final String des; |
| | | private final Double raw; // 单位转换因子(原始值 × raw = 实际物理量) |
| | | |
| | | AiPowerChargerRegisterEnum(Integer addr, Integer type, String des, Double raw) { |
| | | this.addr = addr; |
| | | this.type = type; |
| | | this.des = des; |
| | | this.raw = raw; |
| | | } |
| | | |
| | | public Integer getAddr() { |
| | | return addr; |
| | | } |
| | | |
| | | public Integer getType() { |
| | | return type; |
| | | } |
| | | |
| | | public String getDes() { |
| | | return des; |
| | | } |
| | | |
| | | public Double getRaw() { |
| | | return raw; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.charge.impl; |
| | | |
| | | import com.zy.acs.charge.ChargeCoreService; |
| | | import com.zy.acs.charge.constant.AiPowerChargerCoilEnum; |
| | | import com.zy.acs.charge.constant.AiPowerChargerRegisterEnum; |
| | | import com.zy.acs.charge.model.ChargerStatus; |
| | | import com.zy.acs.charge.protocol.ModbusAdapter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.context.annotation.Primary; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | /** |
| | | * 爱普拉AGV锂电智能充电机调度 |
| | | */ |
| | | @Service |
| | | @Primary |
| | | @Slf4j |
| | | public class AiPowerChargeServiceImpl implements ChargeCoreService { |
| | | |
| | | @Autowired |
| | | private ModbusAdapter modbusAdapter; |
| | | |
| | | @Override |
| | | public boolean connect() throws Exception { |
| | | modbusAdapter.connect(); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void disconnect() { |
| | | modbusAdapter.disconnect(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isConnected() { |
| | | // 可以通过尝试读取一个寄存器来判断,但为了简单,这里直接返回连接状态(需改造 ModbusAdapter 暴露状态) |
| | | // 可在 ModbusAdapter 中添加 isConnected() 方法 |
| | | return false; // 待实现 |
| | | } |
| | | |
| | | @Override |
| | | public boolean startCharging() throws Exception { |
| | | // AGV车报告到位 -> true |
| | | modbusAdapter.writeCoil(AiPowerChargerCoilEnum.AGV_REPORTED_IN_POSITION.getAddr(), true); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean stopCharging() throws Exception { |
| | | modbusAdapter.writeCoil(AiPowerChargerCoilEnum.AGV_REPORTED_IN_POSITION.getAddr(), false); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public ChargerStatus getStatus() throws Exception { |
| | | ChargerStatus status = new ChargerStatus(); |
| | | |
| | | // ---------- 批量读取线圈(地址100~115共16个) ---------- |
| | | boolean[] coils = modbusAdapter.readCoils(100, 16); |
| | | status.setWorking(coils[0]); // 地址100 |
| | | status.setOverheat(coils[1]); // 101 |
| | | status.setFault(coils[2]); // 102 |
| | | status.setCvMode(coils[3]); // 103 |
| | | status.setBatteryConnected(coils[4]); // 104 |
| | | status.setFanAbnormal(coils[5]); // 105 |
| | | status.setPlateOverheat(coils[6]); // 106 |
| | | status.setBrushPressed(coils[7]); // 107 |
| | | status.setAgvReportedInPosition(coils[8]); // 108 |
| | | status.setChargerConfirmedAgvReady(coils[9]); // 109 |
| | | status.setForwardRelayOnline(coils[10]); // 110 |
| | | status.setBackwardRelayOffline(coils[11]); // 111 |
| | | status.setBmsChargeEnable(coils[12]); // 112 |
| | | status.setCommRs485Success(!coils[13]); // 113(注意:0=成功,1=超时) |
| | | status.setCommCanSuccess(!coils[14]); // 114 |
| | | status.setCommBmsSuccess(!coils[15]); // 115 |
| | | |
| | | // ---------- 批量读取保持寄存器(地址100~116共17个) ---------- |
| | | int[] regs = modbusAdapter.readHoldingRegisters(100, 17); |
| | | status.setVoltage(regs[0] * AiPowerChargerRegisterEnum.CHARGE_VOLTAGE.getRaw()); |
| | | status.setCurrent(regs[1] * AiPowerChargerRegisterEnum.CHARGE_CURRENT.getRaw()); |
| | | status.setChargingTime(regs[2]); |
| | | status.setCapacity(regs[3] * AiPowerChargerRegisterEnum.CHARGE_CAPACITY.getRaw()); |
| | | status.setEnergy(regs[4] * AiPowerChargerRegisterEnum.CHARGE_ENERGY.getRaw()); |
| | | status.setCellMaxVoltage(regs[5]); |
| | | status.setCellMinVoltage(regs[6]); |
| | | status.setPackVoltage(regs[7] * AiPowerChargerRegisterEnum.BMS_PACK_VOLTAGE.getRaw()); |
| | | status.setVoltageDemand(regs[8] * AiPowerChargerRegisterEnum.BMS_VOLTAGE_DEMAND.getRaw()); |
| | | status.setCurrentDemand(regs[9] * AiPowerChargerRegisterEnum.BMS_CURRENT_DEMAND.getRaw()); |
| | | status.setTemperature(regs[10]); |
| | | status.setSoc(regs[11] * AiPowerChargerRegisterEnum.BMS_SOC.getRaw()); |
| | | status.setEndFlag(regs[12]); |
| | | status.setChargerId(regs[13]); |
| | | status.setFaultCode(regs[14]); |
| | | status.setChargeMode(regs[15]); |
| | | status.setScheduleFlag(regs[16]); |
| | | |
| | | return status; |
| | | } |
| | | |
| | | @Override |
| | | public boolean setVoltage(int voltageDecivolts) throws Exception { |
| | | // BMS无时才允许写入,这里假设允许 |
| | | modbusAdapter.writeHoldingRegister(AiPowerChargerRegisterEnum.CHARGE_VOLTAGE.getAddr(), voltageDecivolts); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean setCurrent(int currentDecamperes) throws Exception { |
| | | modbusAdapter.writeHoldingRegister(AiPowerChargerRegisterEnum.CHARGE_CURRENT.getAddr(), currentDecamperes); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean clearFault() throws Exception { |
| | | // 写入调度标识地址为1:清除故障 |
| | | modbusAdapter.writeHoldingRegister(AiPowerChargerRegisterEnum.SCHEDULE_FLAG.getAddr(), 1); |
| | | return true; |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | //package com.zy.acs.charge.impl; |
| | | // |
| | | //import com.zy.acs.charge.ChargeCoreService; |
| | | //import org.springframework.stereotype.Service; |
| | | // |
| | | ///** |
| | | // * 其他类型充电桩,实现接口即可 |
| | | // */ |
| | | //@Service |
| | | //public class OtherChargeServiceImpl implements ChargeCoreService { |
| | | // |
| | | //} |
| New file |
| | |
| | | package com.zy.acs.charge.model; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class ChargerStatus implements Serializable { |
| | | // 线圈状态 |
| | | private boolean working; |
| | | private boolean overheat; |
| | | private boolean fault; |
| | | private boolean cvMode; |
| | | private boolean batteryConnected; |
| | | private boolean fanAbnormal; |
| | | private boolean plateOverheat; |
| | | private boolean brushPressed; |
| | | private boolean agvReportedInPosition; |
| | | private boolean chargerConfirmedAgvReady; |
| | | private boolean forwardRelayOnline; |
| | | private boolean backwardRelayOffline; |
| | | private boolean bmsChargeEnable; |
| | | private boolean commRs485Success; |
| | | private boolean commCanSuccess; |
| | | private boolean commBmsSuccess; |
| | | |
| | | // 寄存器数据 |
| | | private double voltage; // V |
| | | private double current; // A |
| | | private int chargingTime; // 分钟 |
| | | private double capacity; // Ah |
| | | private double energy; // kWh |
| | | private int cellMaxVoltage; // mV |
| | | private int cellMinVoltage; // mV |
| | | private double packVoltage; // V |
| | | private double voltageDemand; // V |
| | | private double currentDemand; // A |
| | | private int temperature; // ℃ |
| | | private double soc; // % |
| | | private int endFlag; // 充电结束标识 |
| | | private int chargerId; |
| | | private int faultCode; |
| | | private int chargeMode; // 0自动,1手动 |
| | | private int scheduleFlag; // 调度标识 |
| | | } |
| New file |
| | |
| | | package com.zy.acs.charge.protocol; |
| | | |
| | | import com.ghgande.j2mod.modbus.ModbusException; |
| | | import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; |
| | | import com.ghgande.j2mod.modbus.msg.*; |
| | | import com.ghgande.j2mod.modbus.net.TCPMasterConnection; |
| | | import com.ghgande.j2mod.modbus.procimg.SimpleRegister; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import javax.annotation.PreDestroy; |
| | | import java.net.InetAddress; |
| | | import java.net.UnknownHostException; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 读取Modbus 协议工具类 |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | public class ModbusAdapter { |
| | | |
| | | @Value("${charger.modbus.host}") |
| | | private String host; |
| | | |
| | | @Value("${charger.modbus.port}") |
| | | private int port; |
| | | |
| | | @Value("${charger.modbus.unit-id}") |
| | | private int unitId; |
| | | |
| | | @Value("${charger.modbus.connect-timeout:3000}") |
| | | private int connectTimeout; |
| | | |
| | | @Value("${charger.modbus.transaction-timeout:5000}") |
| | | private int transactionTimeout; |
| | | |
| | | @Value("${charger.modbus.heartbeat.enabled:false}") |
| | | private boolean heartbeatEnabled; |
| | | |
| | | @Value("${charger.modbus.heartbeat.interval:30000}") |
| | | private long heartbeatInterval; |
| | | |
| | | private TCPMasterConnection connection; |
| | | private ModbusTCPTransaction transaction; |
| | | private volatile boolean connected = false; |
| | | private ScheduledExecutorService heartbeatScheduler; |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | try { |
| | | InetAddress address = InetAddress.getByName(host); |
| | | connection = new TCPMasterConnection(address); |
| | | connection.setPort(port); |
| | | connection.setTimeout(connectTimeout); |
| | | } catch (UnknownHostException e) { |
| | | throw new RuntimeException("Invalid Modbus host: " + host, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 建立连接 |
| | | */ |
| | | public synchronized void connect() throws Exception { |
| | | if (connected) { |
| | | return; |
| | | } |
| | | if (connection == null) { |
| | | throw new IllegalStateException("Modbus connection not initialized"); |
| | | } |
| | | connection.connect(); |
| | | transaction = new ModbusTCPTransaction(connection); |
| | | connected = true; |
| | | log.info("Modbus TCP connected to {}:{}", host, port); |
| | | |
| | | if (heartbeatEnabled) { |
| | | startHeartbeat(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 断开连接 |
| | | */ |
| | | public synchronized void disconnect() { |
| | | if (heartbeatScheduler != null && !heartbeatScheduler.isShutdown()) { |
| | | heartbeatScheduler.shutdownNow(); |
| | | } |
| | | if (connection != null && connection.isConnected()) { |
| | | connection.close(); |
| | | } |
| | | connected = false; |
| | | log.info("Modbus TCP disconnected"); |
| | | } |
| | | |
| | | /** |
| | | * 确保连接可用(内部自动重连) |
| | | */ |
| | | private void ensureConnected() throws Exception { |
| | | if (!connected) { |
| | | connect(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 读取线圈(功能码01) |
| | | */ |
| | | public boolean readCoil(int address) throws Exception { |
| | | return executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | ReadCoilsRequest request = new ReadCoilsRequest(address, 1); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | ReadCoilsResponse response = (ReadCoilsResponse) transaction.getResponse(); |
| | | return response.getCoilStatus(0); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 批量读取线圈(功能码01) |
| | | * |
| | | * @param startAddress 起始地址 |
| | | * @param quantity 数量(1~2000) |
| | | * @return boolean数组,索引从0开始对应起始地址的线圈 |
| | | */ |
| | | public boolean[] readCoils(int startAddress, int quantity) throws Exception { |
| | | return executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | ReadCoilsRequest request = new ReadCoilsRequest(startAddress, quantity); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | ReadCoilsResponse response = (ReadCoilsResponse) transaction.getResponse(); |
| | | boolean[] result = new boolean[quantity]; |
| | | for (int i = 0; i < quantity; i++) { |
| | | result[i] = response.getCoilStatus(i); |
| | | } |
| | | return result; |
| | | }); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 写入单个线圈(功能码05) |
| | | */ |
| | | public void writeCoil(int address, boolean state) throws Exception { |
| | | executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | WriteCoilRequest request = new WriteCoilRequest(address, state); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | return null; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 读取保持寄存器(功能码03) |
| | | */ |
| | | public int readHoldingRegister(int address) throws Exception { |
| | | return executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(address, 1); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) transaction.getResponse(); |
| | | return response.getRegister(0).getValue(); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 写入单个寄存器(功能码06) |
| | | */ |
| | | public void writeHoldingRegister(int address, int value) throws Exception { |
| | | executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | WriteSingleRegisterRequest request = new WriteSingleRegisterRequest(address, new SimpleRegister(value)); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | return null; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 批量读取多个寄存器 |
| | | */ |
| | | public int[] readHoldingRegisters(int address, int quantity) throws Exception { |
| | | return executeWithRetry(() -> { |
| | | ensureConnected(); |
| | | ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(address, quantity); |
| | | request.setUnitID(unitId); |
| | | transaction.setRequest(request); |
| | | transaction.execute(); |
| | | ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) transaction.getResponse(); |
| | | int[] values = new int[quantity]; |
| | | for (int i = 0; i < quantity; i++) { |
| | | values[i] = response.getRegister(i).getValue(); |
| | | } |
| | | return values; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 带重试的执行器(自动处理网络异常并重连重试) |
| | | */ |
| | | private <T> T executeWithRetry(Operation<T> operation) throws Exception { |
| | | Exception lastException = null; |
| | | int maxRetries = 3; |
| | | for (int i = 0; i < maxRetries; i++) { |
| | | try { |
| | | return operation.execute(); |
| | | } catch (Exception e) { |
| | | lastException = e; |
| | | if (isNetworkException(e)) { |
| | | log.warn("Modbus network error, retrying {}/{}", i + 1, maxRetries); |
| | | tryReconnect(); |
| | | } else { |
| | | // 非网络异常不重试 |
| | | throw e; |
| | | } |
| | | try { |
| | | Thread.sleep(1000); |
| | | } catch (InterruptedException ignored) { |
| | | Thread.currentThread().interrupt(); |
| | | throw new Exception("Retry interrupted", e); |
| | | } |
| | | } |
| | | } |
| | | throw new Exception("Modbus operation failed after " + maxRetries + " retries", lastException); |
| | | } |
| | | |
| | | private boolean isNetworkException(Exception e) { |
| | | return e instanceof java.net.SocketTimeoutException || |
| | | e instanceof java.io.IOException || |
| | | (e instanceof ModbusException && e.getMessage().toLowerCase().contains("timeout")); |
| | | } |
| | | |
| | | private synchronized void tryReconnect() { |
| | | try { |
| | | disconnect(); |
| | | connect(); |
| | | } catch (Exception e) { |
| | | log.error("Reconnect failed", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 心跳保活 |
| | | */ |
| | | private void startHeartbeat() { |
| | | heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(); |
| | | heartbeatScheduler.scheduleAtFixedRate(() -> { |
| | | try { |
| | | // 读取一个无副作用的寄存器,如充电机编号(地址113) |
| | | int chargerId = readHoldingRegister(113); |
| | | log.debug("Heartbeat success, chargerId={}", chargerId); |
| | | } catch (Exception e) { |
| | | log.warn("Heartbeat failed", e); |
| | | tryReconnect(); |
| | | } |
| | | }, heartbeatInterval, heartbeatInterval, TimeUnit.MILLISECONDS); |
| | | } |
| | | |
| | | @PreDestroy |
| | | public void destroy() { |
| | | disconnect(); |
| | | } |
| | | |
| | | @FunctionalInterface |
| | | private interface Operation<T> { |
| | | T execute() throws Exception; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.charge.protocol; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | |
| | | @Component |
| | | @Slf4j |
| | | public class ModbusConnectionManager { |
| | | |
| | | @Autowired |
| | | private ModbusAdapter modbusAdapter; |
| | | |
| | | @Value("${charger.modbus.retry.max-attempts:3}") |
| | | private int maxRetry; |
| | | |
| | | @Value("${charger.modbus.retry.initial-delay:1000}") |
| | | private long initialDelay; |
| | | |
| | | @PostConstruct |
| | | public void startAsyncConnect() { |
| | | // 启动异步线程尝试连接 |
| | | new Thread(() -> { |
| | | for (int i = 0; i < maxRetry; i++) { |
| | | try { |
| | | modbusAdapter.connect(); |
| | | log.info("Modbus connection established after {} attempts", i + 1); |
| | | // 可选:启动心跳 |
| | | startHeartbeat(); |
| | | return; |
| | | } catch (Exception e) { |
| | | log.warn("Modbus connection attempt {} failed: {}", i + 1, e.getMessage()); |
| | | if (i < maxRetry - 1) { |
| | | try { |
| | | Thread.sleep(initialDelay); |
| | | } catch (InterruptedException ignored) { |
| | | } |
| | | } |
| | | } |
| | | } |
| | | log.error("Failed to connect to Modbus charger after {} attempts", maxRetry); |
| | | }).start(); |
| | | } |
| | | |
| | | private void startHeartbeat() { |
| | | // 实现心跳 |
| | | } |
| | | |
| | | } |
| | |
| | | swagger-base-package: com.zy.acs |
| | | swagger-title: RCS API文档 |
| | | swagger-version: 1.0 |
| | | token-key: KUHSMcYQ4lePt3r6bckz0P13cBJyoonYqInThvQlUnbsFCIcCcZZAbWZ6UNFztYNYPhGdy6eyb8WdIz8FU2Cz396TyTJk3NI2rtXMHBOehRb4WWJ4MdYVVg2oWPyqRQ2 |
| | | token-key: KUHSMcYQ4lePt3r6bckz0P13cBJyoonYqInThvQlUnbsFCIcCcZZAbWZ6UNFztYNYPhGdy6eyb8WdIz8FU2Cz396TyTJk3NI2rtXMHBOehRb4WWJ4MdYVVg2oWPyqRQ2 |
| | | |
| | | charger: |
| | | modbus: |
| | | host: 192.168.1.100 |
| | | port: 502 |
| | | unit-id: 1 |
| | | connect-timeout: 3000 # 连接超时(毫秒) |
| | | transaction-timeout: 5000 # 读写超时(毫秒) |
| | | retry: |
| | | max-attempts: 3 # 启动时最大重试次数 |
| | | initial-delay: 1000 # 重试间隔(毫秒) |
| | | heartbeat: |
| | | enabled: true |
| | | interval: 30000 # 心跳间隔(毫秒) |