|
|
@@ -0,0 +1,721 @@
|
|
|
+/*
|
|
|
+ * 文 件 名: MockPileClient
|
|
|
+ * 版 权: 华设设计集团股份有限公司
|
|
|
+ * 描 述: Mock充电桩客户端
|
|
|
+ * 修 改 人: lvwenbin
|
|
|
+ * 修改时间: 2025/12/23
|
|
|
+ */
|
|
|
+package com.ruoyi.ems.charging.mock.simulator;
|
|
|
+
|
|
|
+import com.ruoyi.ems.charging.core.ChargingPileEncoder;
|
|
|
+import com.ruoyi.ems.charging.mock.config.MockPileConfig;
|
|
|
+import com.ruoyi.ems.charging.mock.config.MockPileConfig.GunConfig;
|
|
|
+import com.ruoyi.ems.charging.mock.config.MockPileConfig.PileHostConfig;
|
|
|
+import com.ruoyi.ems.charging.mock.handler.MockClientDecoder;
|
|
|
+import com.ruoyi.ems.charging.mock.handler.MockClientHandler;
|
|
|
+import com.ruoyi.ems.charging.mock.model.MockGunState;
|
|
|
+import com.ruoyi.ems.charging.model.req.*;
|
|
|
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
|
|
|
+import com.ruoyi.ems.charging.utils.ByteUtils;
|
|
|
+import io.netty.bootstrap.Bootstrap;
|
|
|
+import io.netty.channel.*;
|
|
|
+import io.netty.channel.nio.NioEventLoopGroup;
|
|
|
+import io.netty.channel.socket.SocketChannel;
|
|
|
+import io.netty.channel.socket.nio.NioSocketChannel;
|
|
|
+import io.netty.handler.timeout.IdleStateHandler;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Mock充电桩客户端
|
|
|
+ * 模拟单个充电桩主机的行为,包括登录、心跳、数据上报、充电模拟等
|
|
|
+ *
|
|
|
+ * @author lvwenbin
|
|
|
+ * @version [版本号, 2025/12/23]
|
|
|
+ */
|
|
|
+public class MockPileClient {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(MockPileClient.class);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 主机配置
|
|
|
+ */
|
|
|
+ private final PileHostConfig config;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全局配置
|
|
|
+ */
|
|
|
+ private final MockPileConfig globalConfig;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 事件循环组
|
|
|
+ */
|
|
|
+ private final EventLoopGroup eventLoopGroup;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否共享EventLoopGroup
|
|
|
+ */
|
|
|
+ private final boolean sharedEventLoopGroup;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通道
|
|
|
+ */
|
|
|
+ private volatile Channel channel;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否已登录
|
|
|
+ */
|
|
|
+ private final AtomicBoolean loggedIn = new AtomicBoolean(false);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否运行中
|
|
|
+ */
|
|
|
+ private final AtomicBoolean running = new AtomicBoolean(false);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 序列号计数器
|
|
|
+ */
|
|
|
+ private final AtomicInteger sequenceNo = new AtomicInteger(0);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 枪状态映射 - 枪号 -> 状态
|
|
|
+ */
|
|
|
+ private final Map<String, MockGunState> gunStateMap = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调度线程池
|
|
|
+ */
|
|
|
+ private ScheduledExecutorService scheduler;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 心跳任务
|
|
|
+ */
|
|
|
+ private ScheduledFuture<?> heartbeatTask;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 数据上报任务
|
|
|
+ */
|
|
|
+ private ScheduledFuture<?> reportTask;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 充电模拟任务
|
|
|
+ */
|
|
|
+ private ScheduledFuture<?> chargingSimulatorTask;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 充电更新任务
|
|
|
+ */
|
|
|
+ private ScheduledFuture<?> chargingUpdateTask;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构造函数
|
|
|
+ *
|
|
|
+ * @param config 主机配置
|
|
|
+ * @param globalConfig 全局配置
|
|
|
+ */
|
|
|
+ public MockPileClient(PileHostConfig config, MockPileConfig globalConfig) {
|
|
|
+ this(config, globalConfig, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构造函数
|
|
|
+ *
|
|
|
+ * @param config 主机配置
|
|
|
+ * @param globalConfig 全局配置
|
|
|
+ * @param eventLoopGroup 事件循环组(可共享)
|
|
|
+ */
|
|
|
+ public MockPileClient(PileHostConfig config, MockPileConfig globalConfig, EventLoopGroup eventLoopGroup) {
|
|
|
+ this.config = config;
|
|
|
+ this.globalConfig = globalConfig;
|
|
|
+
|
|
|
+ if (eventLoopGroup != null) {
|
|
|
+ this.eventLoopGroup = eventLoopGroup;
|
|
|
+ this.sharedEventLoopGroup = true;
|
|
|
+ } else {
|
|
|
+ this.eventLoopGroup = new NioEventLoopGroup(1);
|
|
|
+ this.sharedEventLoopGroup = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化枪状态
|
|
|
+ initGunStates();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化枪状态
|
|
|
+ */
|
|
|
+ private void initGunStates() {
|
|
|
+ for (GunConfig gunConfig : config.getGuns()) {
|
|
|
+ MockGunState gunState = new MockGunState(
|
|
|
+ config.getPileCode(),
|
|
|
+ gunConfig.getGunNo(),
|
|
|
+ gunConfig.getRatedPower()
|
|
|
+ );
|
|
|
+ gunStateMap.put(gunConfig.getGunNo(), gunState);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果配置的枪数量大于实际配置的枪,补充默认枪
|
|
|
+ for (int i = gunStateMap.size() + 1; i <= config.getGunCount(); i++) {
|
|
|
+ String gunNo = String.valueOf(i);
|
|
|
+ MockGunState gunState = new MockGunState(config.getPileCode(), gunNo, 120);
|
|
|
+ gunStateMap.put(gunNo, gunState);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动客户端
|
|
|
+ */
|
|
|
+ public void start() {
|
|
|
+ if (running.compareAndSet(false, true)) {
|
|
|
+ log.info("[{}] 启动Mock充电桩客户端...", getPileCode());
|
|
|
+
|
|
|
+ scheduler = Executors.newScheduledThreadPool(2, r -> {
|
|
|
+ Thread t = new Thread(r, "mock-pile-" + getPileCode());
|
|
|
+ t.setDaemon(true);
|
|
|
+ return t;
|
|
|
+ });
|
|
|
+
|
|
|
+ connect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 连接服务器
|
|
|
+ */
|
|
|
+ private void connect() {
|
|
|
+ if (!running.get()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("[{}] 正在连接服务器 {}:{}", getPileCode(),
|
|
|
+ globalConfig.getServerHost(), globalConfig.getServerPort());
|
|
|
+
|
|
|
+ Bootstrap bootstrap = new Bootstrap();
|
|
|
+ bootstrap.group(eventLoopGroup)
|
|
|
+ .channel(NioSocketChannel.class)
|
|
|
+ .option(ChannelOption.SO_KEEPALIVE, true)
|
|
|
+ .option(ChannelOption.TCP_NODELAY, true)
|
|
|
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
|
|
|
+ .handler(new ChannelInitializer<SocketChannel>() {
|
|
|
+ @Override
|
|
|
+ protected void initChannel(SocketChannel ch) {
|
|
|
+ ChannelPipeline pipeline = ch.pipeline();
|
|
|
+
|
|
|
+ // 空闲检测
|
|
|
+ pipeline.addLast("idleStateHandler",
|
|
|
+ new IdleStateHandler(0, 30, 0, TimeUnit.SECONDS));
|
|
|
+
|
|
|
+ // 解码器
|
|
|
+ pipeline.addLast("decoder", new MockClientDecoder());
|
|
|
+
|
|
|
+ // 编码器
|
|
|
+ pipeline.addLast("encoder", new ChargingPileEncoder());
|
|
|
+
|
|
|
+ // 业务处理器
|
|
|
+ pipeline.addLast("handler", new MockClientHandler(MockPileClient.this));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ bootstrap.connect(globalConfig.getServerHost(), globalConfig.getServerPort())
|
|
|
+ .addListener((ChannelFutureListener) future -> {
|
|
|
+ if (future.isSuccess()) {
|
|
|
+ log.info("[{}] 连接服务器成功", getPileCode());
|
|
|
+ } else {
|
|
|
+ log.warn("[{}] 连接服务器失败: {}", getPileCode(), future.cause().getMessage());
|
|
|
+ scheduleReconnect();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安排重连
|
|
|
+ */
|
|
|
+ private void scheduleReconnect() {
|
|
|
+ if (!running.get()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int delaySeconds = globalConfig.getReconnectIntervalSeconds();
|
|
|
+ log.info("[{}] {}秒后尝试重连...", getPileCode(), delaySeconds);
|
|
|
+
|
|
|
+ scheduler.schedule(this::connect, delaySeconds, TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 连接建立回调
|
|
|
+ */
|
|
|
+ public void onConnected(Channel channel) {
|
|
|
+ this.channel = channel;
|
|
|
+ this.loggedIn.set(false);
|
|
|
+
|
|
|
+ // 发送登录认证
|
|
|
+ sendLogin();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 连接断开回调
|
|
|
+ */
|
|
|
+ public void onDisconnected() {
|
|
|
+ this.channel = null;
|
|
|
+ this.loggedIn.set(false);
|
|
|
+
|
|
|
+ // 停止定时任务
|
|
|
+ stopScheduledTasks();
|
|
|
+
|
|
|
+ // 安排重连
|
|
|
+ scheduleReconnect();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 登录成功回调
|
|
|
+ */
|
|
|
+ public void onLoginSuccess() {
|
|
|
+ this.loggedIn.set(true);
|
|
|
+
|
|
|
+ // 启动定时任务
|
|
|
+ startScheduledTasks();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 登录失败回调
|
|
|
+ */
|
|
|
+ public void onLoginFailed() {
|
|
|
+ this.loggedIn.set(false);
|
|
|
+
|
|
|
+ // 关闭连接,触发重连
|
|
|
+ if (channel != null && channel.isActive()) {
|
|
|
+ channel.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 心跳应答回调
|
|
|
+ */
|
|
|
+ public void onHeartbeatResponse() {
|
|
|
+ // 心跳正常,可以在这里做一些统计
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送登录认证
|
|
|
+ */
|
|
|
+ private void sendLogin() {
|
|
|
+ LoginReqFrame frame = new LoginReqFrame();
|
|
|
+ frame.setSequenceNo(nextSequenceNo());
|
|
|
+ frame.setPileCode(config.getPileCode());
|
|
|
+ frame.setPileType(config.getPileType());
|
|
|
+ frame.setGunCount(config.getGunCount());
|
|
|
+ frame.setProtocolVersion(config.getProtocolVersion());
|
|
|
+ frame.setSoftwareVersion(config.getSoftwareVersion());
|
|
|
+ frame.setNetworkType(config.getNetworkType());
|
|
|
+ frame.setSimCard("00000000000000000000");
|
|
|
+ frame.setCarrier((byte) 0x04);
|
|
|
+
|
|
|
+ sendFrame(frame);
|
|
|
+ log.info("[{}] 发送登录认证: gunCount={}", getPileCode(), config.getGunCount());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送心跳
|
|
|
+ */
|
|
|
+ private void sendHeartbeat() {
|
|
|
+ if (!loggedIn.get() || channel == null || !channel.isActive()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为每个枪发送心跳
|
|
|
+ for (MockGunState gunState : gunStateMap.values()) {
|
|
|
+ HeartbeatReqFrame frame = new HeartbeatReqFrame();
|
|
|
+ frame.setSequenceNo(nextSequenceNo());
|
|
|
+ frame.setPileCode(config.getPileCode());
|
|
|
+ frame.setGunNo(String.format("%02d", Integer.parseInt(gunState.getGunNo())));
|
|
|
+ frame.setGunStatus((byte) 0x00); // 正常
|
|
|
+
|
|
|
+ sendFrame(frame);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.debug("[{}] 发送心跳, 枪数量: {}", getPileCode(), gunStateMap.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送实时数据
|
|
|
+ */
|
|
|
+ private void sendRealtimeData() {
|
|
|
+ if (!loggedIn.get() || channel == null || !channel.isActive()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (MockGunState gunState : gunStateMap.values()) {
|
|
|
+ RealtimeDataFrame frame = buildRealtimeDataFrame(gunState);
|
|
|
+ sendFrame(frame);
|
|
|
+
|
|
|
+ log.debug("[{}] 发送实时数据: gun={}, status={}, power={:.1f}kW, energy={:.4f}kWh",
|
|
|
+ getPileCode(), gunState.getGunNo(),
|
|
|
+ gunState.isCharging() ? "充电中" : "空闲",
|
|
|
+ gunState.getOutputPower(), gunState.getChargingEnergy());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送充电握手
|
|
|
+ */
|
|
|
+ private void sendChargingHandshake(MockGunState gunState) {
|
|
|
+ ChargingHandshakeFrame frame = new ChargingHandshakeFrame();
|
|
|
+ frame.setSequenceNo(nextSequenceNo());
|
|
|
+ frame.setTransactionNo(gunState.getTransactionNo());
|
|
|
+ frame.setPileCode(gunState.getPileCode());
|
|
|
+ frame.setGunNo(String.format("%02d", Integer.parseInt(gunState.getGunNo())));
|
|
|
+ frame.setBmsProtocolVersion(new byte[]{0x01, 0x01, 0x00});
|
|
|
+ frame.setBmsBatteryType(gunState.getBatteryType());
|
|
|
+ frame.setBmsRatedCapacity((int) (gunState.getRatedCapacity() * 10));
|
|
|
+ frame.setBmsRatedVoltage((int) (gunState.getRatedVoltage() * 10));
|
|
|
+ frame.setBmsManufacturer(new byte[]{0x54, 0x45, 0x53, 0x54}); // TEST
|
|
|
+ frame.setBmsBatterySerialNo(new byte[4]);
|
|
|
+ frame.setBmsProductionYear(2023 - 1985);
|
|
|
+ frame.setBmsProductionMonth(6);
|
|
|
+ frame.setBmsProductionDay(15);
|
|
|
+ frame.setBmsChargingCount(100);
|
|
|
+ frame.setBmsOwnership((byte) 0x01);
|
|
|
+ frame.setReserved((byte) 0x00);
|
|
|
+ frame.setVin(gunState.getVin());
|
|
|
+ frame.setBmsSoftwareVersion(new byte[8]);
|
|
|
+
|
|
|
+ sendFrame(frame);
|
|
|
+ log.info("[{}] 发送充电握手: gun={}, vin={}", getPileCode(), gunState.getGunNo(), gunState.getVin());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送充电结束
|
|
|
+ */
|
|
|
+ private void sendChargingEnd(MockGunState gunState) {
|
|
|
+ ChargingEndFrame frame = new ChargingEndFrame();
|
|
|
+ frame.setSequenceNo(nextSequenceNo());
|
|
|
+ frame.setTransactionNo(gunState.getTransactionNo());
|
|
|
+ frame.setPileCode(gunState.getPileCode());
|
|
|
+ frame.setGunNo(String.format("%02d", Integer.parseInt(gunState.getGunNo())));
|
|
|
+ frame.setBmsFinalSoc(gunState.getSoc());
|
|
|
+ frame.setBmsMinCellVoltage((int) (gunState.getRatedVoltage() / 100 * 0.95 * 100)); // 模拟最低单体电压
|
|
|
+ frame.setBmsMaxCellVoltage((int) (gunState.getRatedVoltage() / 100 * 1.05 * 100)); // 模拟最高单体电压
|
|
|
+ frame.setBmsMinTemp(gunState.getBatteryMaxTemp() - 5 + 50); // 偏移量+50
|
|
|
+ frame.setBmsMaxTemp(gunState.getBatteryMaxTemp() + 50);
|
|
|
+ frame.setTotalChargingTime(gunState.getChargingTime());
|
|
|
+ frame.setOutputEnergy((int) (gunState.getChargingEnergy() * 10)); // 0.1kWh/位
|
|
|
+ frame.setChargerNo(1);
|
|
|
+
|
|
|
+ sendFrame(frame);
|
|
|
+ log.info("[{}] 发送充电结束: gun={}, energy={:.2f}kWh, time={}min, soc={}%",
|
|
|
+ getPileCode(), gunState.getGunNo(),
|
|
|
+ gunState.getChargingEnergy(), gunState.getChargingTime(), gunState.getSoc());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建实时数据帧
|
|
|
+ */
|
|
|
+ private RealtimeDataFrame buildRealtimeDataFrame(MockGunState gunState) {
|
|
|
+ RealtimeDataFrame frame = new RealtimeDataFrame();
|
|
|
+ frame.setSequenceNo(nextSequenceNo());
|
|
|
+
|
|
|
+ // 基础信息
|
|
|
+ frame.setTransactionNo(gunState.getTransactionNo() != null ?
|
|
|
+ gunState.getTransactionNo() : "00000000000000000000000000000000");
|
|
|
+ frame.setPileCode(gunState.getPileCode());
|
|
|
+ frame.setGunNo(String.format("%02d", Integer.parseInt(gunState.getGunNo())));
|
|
|
+
|
|
|
+ // 状态信息
|
|
|
+ frame.setStatus(gunState.getStatus());
|
|
|
+ frame.setGunReturned(gunState.getGunReturned());
|
|
|
+ frame.setGunConnected(gunState.isGunConnected() ?
|
|
|
+ ProtocolConstants.GUN_CONNECTED : ProtocolConstants.GUN_NOT_CONNECTED);
|
|
|
+
|
|
|
+ // 输出参数 (保留一位小数)
|
|
|
+ frame.setOutputVoltage((int) (gunState.getOutputVoltage() * 10));
|
|
|
+ frame.setOutputCurrent((int) (gunState.getOutputCurrent() * 10));
|
|
|
+
|
|
|
+ // 温度 (偏移量+50)
|
|
|
+ frame.setGunTemperature(gunState.getGunTemperature() + 50);
|
|
|
+ frame.setBatteryMaxTemp(gunState.getBatteryMaxTemp() + 50);
|
|
|
+
|
|
|
+ // 枪线编码
|
|
|
+ frame.setGunLineCode(new byte[8]);
|
|
|
+
|
|
|
+ // SOC和充电时间
|
|
|
+ frame.setSoc(gunState.getSoc());
|
|
|
+ frame.setChargingTime(gunState.getChargingTime());
|
|
|
+ frame.setRemainingTime(gunState.getRemainingTime());
|
|
|
+
|
|
|
+ // 能量 (保留四位小数)
|
|
|
+ frame.setChargingEnergy((long) (gunState.getChargingEnergy() * 10000));
|
|
|
+ frame.setLossCorrectedEnergy((long) (gunState.getChargingEnergy() * 10000));
|
|
|
+
|
|
|
+ // 金额 (保留四位小数)
|
|
|
+ frame.setChargedAmount((long) (gunState.getChargedAmount() * 10000));
|
|
|
+
|
|
|
+ // 故障码
|
|
|
+ frame.setHardwareFault(gunState.getHardwareFault());
|
|
|
+
|
|
|
+ return frame;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动定时任务
|
|
|
+ */
|
|
|
+ private void startScheduledTasks() {
|
|
|
+ // 心跳任务
|
|
|
+ heartbeatTask = scheduler.scheduleAtFixedRate(
|
|
|
+ this::sendHeartbeat,
|
|
|
+ 0,
|
|
|
+ globalConfig.getHeartbeatIntervalSeconds(),
|
|
|
+ TimeUnit.SECONDS
|
|
|
+ );
|
|
|
+
|
|
|
+ // 数据上报任务 (使用充电时的上报间隔)
|
|
|
+ reportTask = scheduler.scheduleAtFixedRate(
|
|
|
+ this::sendRealtimeData,
|
|
|
+ 1,
|
|
|
+ globalConfig.getChargingReportIntervalSeconds(),
|
|
|
+ TimeUnit.SECONDS
|
|
|
+ );
|
|
|
+
|
|
|
+ // 充电更新任务 (每秒更新一次充电状态)
|
|
|
+ chargingUpdateTask = scheduler.scheduleAtFixedRate(
|
|
|
+ this::updateChargingStates,
|
|
|
+ 1,
|
|
|
+ 1,
|
|
|
+ TimeUnit.SECONDS
|
|
|
+ );
|
|
|
+
|
|
|
+ // 充电模拟任务
|
|
|
+ if (globalConfig.isAutoChargingEnabled()) {
|
|
|
+ chargingSimulatorTask = scheduler.scheduleAtFixedRate(
|
|
|
+ this::simulateChargingCycle,
|
|
|
+ 10, // 10秒后开始第一次
|
|
|
+ globalConfig.getChargingCycleSeconds(),
|
|
|
+ TimeUnit.SECONDS
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("[{}] 定时任务已启动: 心跳间隔={}s, 数据上报间隔={}s, 充电周期={}s",
|
|
|
+ getPileCode(),
|
|
|
+ globalConfig.getHeartbeatIntervalSeconds(),
|
|
|
+ globalConfig.getChargingReportIntervalSeconds(),
|
|
|
+ globalConfig.getChargingCycleSeconds());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止定时任务
|
|
|
+ */
|
|
|
+ private void stopScheduledTasks() {
|
|
|
+ if (heartbeatTask != null) {
|
|
|
+ heartbeatTask.cancel(false);
|
|
|
+ heartbeatTask = null;
|
|
|
+ }
|
|
|
+ if (reportTask != null) {
|
|
|
+ reportTask.cancel(false);
|
|
|
+ reportTask = null;
|
|
|
+ }
|
|
|
+ if (chargingSimulatorTask != null) {
|
|
|
+ chargingSimulatorTask.cancel(false);
|
|
|
+ chargingSimulatorTask = null;
|
|
|
+ }
|
|
|
+ if (chargingUpdateTask != null) {
|
|
|
+ chargingUpdateTask.cancel(false);
|
|
|
+ chargingUpdateTask = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新所有枪的充电状态
|
|
|
+ */
|
|
|
+ private void updateChargingStates() {
|
|
|
+ for (MockGunState gunState : gunStateMap.values()) {
|
|
|
+ if (gunState.isCharging()) {
|
|
|
+ gunState.updateChargingProgress(1); // 每秒更新一次
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 模拟充电周期
|
|
|
+ * 随机选择一把空闲的枪开始充电
|
|
|
+ */
|
|
|
+ private void simulateChargingCycle() {
|
|
|
+ if (!loggedIn.get()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找空闲的枪
|
|
|
+ List<MockGunState> idleGuns = new ArrayList<>();
|
|
|
+ for (MockGunState gunState : gunStateMap.values()) {
|
|
|
+ if (gunState.isIdle()) {
|
|
|
+ idleGuns.add(gunState);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (idleGuns.isEmpty()) {
|
|
|
+ log.debug("[{}] 所有枪都在充电中,跳过本次充电模拟", getPileCode());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 随机选择一把枪开始充电
|
|
|
+ MockGunState selectedGun = idleGuns.get(new Random().nextInt(idleGuns.size()));
|
|
|
+
|
|
|
+ log.info("[{}] 开始模拟充电: gun={}", getPileCode(), selectedGun.getGunNo());
|
|
|
+
|
|
|
+ // 开始充电
|
|
|
+ selectedGun.startCharging();
|
|
|
+
|
|
|
+ // 发送充电握手
|
|
|
+ sendChargingHandshake(selectedGun);
|
|
|
+
|
|
|
+ // 安排充电结束
|
|
|
+ scheduler.schedule(() -> {
|
|
|
+ if (selectedGun.isCharging()) {
|
|
|
+ // 发送充电结束
|
|
|
+ sendChargingEnd(selectedGun);
|
|
|
+
|
|
|
+ // 停止充电
|
|
|
+ selectedGun.stopCharging();
|
|
|
+
|
|
|
+ log.info("[{}] 充电模拟结束: gun={}", getPileCode(), selectedGun.getGunNo());
|
|
|
+ }
|
|
|
+ }, globalConfig.getChargingDurationSeconds(), TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动开始指定枪的充电
|
|
|
+ */
|
|
|
+ public void startCharging(String gunNo) {
|
|
|
+ MockGunState gunState = gunStateMap.get(gunNo);
|
|
|
+ if (gunState == null) {
|
|
|
+ log.warn("[{}] 未找到枪号: {}", getPileCode(), gunNo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (gunState.isCharging()) {
|
|
|
+ log.warn("[{}] 枪[{}]已在充电中", getPileCode(), gunNo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ gunState.startCharging();
|
|
|
+ sendChargingHandshake(gunState);
|
|
|
+
|
|
|
+ log.info("[{}] 手动开始充电: gun={}", getPileCode(), gunNo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动停止指定枪的充电
|
|
|
+ */
|
|
|
+ public void stopCharging(String gunNo) {
|
|
|
+ MockGunState gunState = gunStateMap.get(gunNo);
|
|
|
+ if (gunState == null) {
|
|
|
+ log.warn("[{}] 未找到枪号: {}", getPileCode(), gunNo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!gunState.isCharging()) {
|
|
|
+ log.warn("[{}] 枪[{}]未在充电", getPileCode(), gunNo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ sendChargingEnd(gunState);
|
|
|
+ gunState.stopCharging();
|
|
|
+
|
|
|
+ log.info("[{}] 手动停止充电: gun={}", getPileCode(), gunNo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送帧
|
|
|
+ */
|
|
|
+ private void sendFrame(com.ruoyi.ems.charging.model.BaseFrame frame) {
|
|
|
+ if (channel != null && channel.isActive()) {
|
|
|
+ channel.writeAndFlush(frame);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取下一个序列号
|
|
|
+ */
|
|
|
+ private int nextSequenceNo() {
|
|
|
+ return sequenceNo.getAndIncrement() & 0xFFFF;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止客户端
|
|
|
+ */
|
|
|
+ public void stop() {
|
|
|
+ if (running.compareAndSet(true, false)) {
|
|
|
+ log.info("[{}] 停止Mock充电桩客户端...", getPileCode());
|
|
|
+
|
|
|
+ stopScheduledTasks();
|
|
|
+
|
|
|
+ if (channel != null && channel.isActive()) {
|
|
|
+ channel.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (scheduler != null && !scheduler.isShutdown()) {
|
|
|
+ scheduler.shutdownNow();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sharedEventLoopGroup) {
|
|
|
+ eventLoopGroup.shutdownGracefully();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取桩编号
|
|
|
+ */
|
|
|
+ public String getPileCode() {
|
|
|
+ return config.getPileCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取枪状态
|
|
|
+ */
|
|
|
+ public MockGunState getGunState(String gunNo) {
|
|
|
+ // 尝试直接匹配
|
|
|
+ MockGunState state = gunStateMap.get(gunNo);
|
|
|
+ if (state != null) {
|
|
|
+ return state;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试去掉前导零匹配
|
|
|
+ try {
|
|
|
+ int gunNoInt = Integer.parseInt(gunNo);
|
|
|
+ return gunStateMap.get(String.valueOf(gunNoInt));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取所有枪状态
|
|
|
+ */
|
|
|
+ public Collection<MockGunState> getAllGunStates() {
|
|
|
+ return gunStateMap.values();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否已登录
|
|
|
+ */
|
|
|
+ public boolean isLoggedIn() {
|
|
|
+ return loggedIn.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否运行中
|
|
|
+ */
|
|
|
+ public boolean isRunning() {
|
|
|
+ return running.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否已连接
|
|
|
+ */
|
|
|
+ public boolean isConnected() {
|
|
|
+ return channel != null && channel.isActive();
|
|
|
+ }
|
|
|
+}
|