learshaw 3 месяцев назад
Родитель
Сommit
f5872eb40d
43 измененных файлов с 8168 добавлено и 26 удалено
  1. 6 0
      ems/ems-cloud/ems-dev-adapter/pom.xml
  2. 0 2
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/EmsDevAdpApplication.java
  3. 217 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/config/ChargingPileExecutorConfig.java
  4. 138 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/config/ChargingPileServer.java
  5. 72 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileChannelInitializer.java
  6. 139 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileDecoder.java
  7. 52 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileEncoder.java
  8. 367 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileSessionManager.java
  9. 64 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/IdleStateEventHandler.java
  10. 204 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handler/ChargingPileMessageHandler.java
  11. 172 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/MockPileClientStarter.java
  12. 102 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/config/MockPileAutoConfiguration.java
  13. 403 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/config/MockPileConfig.java
  14. 178 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/handler/MockClientDecoder.java
  15. 217 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/handler/MockClientHandler.java
  16. 545 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/model/MockGunState.java
  17. 721 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/simulator/MockPileClient.java
  18. 301 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/simulator/MockPileClientManager.java
  19. 208 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/BaseFrame.java
  20. 174 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/ChargingSession.java
  21. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/RealtimeDataCache.java
  22. 173 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/ChargingEndFrame.java
  23. 228 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/ChargingHandshakeFrame.java
  24. 95 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/HeartbeatReqFrame.java
  25. 163 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/LoginReqFrame.java
  26. 333 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/RealtimeDataFrame.java
  27. 68 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/WorkParamSetRespFrame.java
  28. 89 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/HeartbeatRespFrame.java
  29. 104 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/LoginRespFrame.java
  30. 81 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/ReadRealtimeDataFrame.java
  31. 127 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/WorkParamSetFrame.java
  32. 230 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/protocol/FrameParser.java
  33. 500 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/protocol/ProtocolConstants.java
  34. 292 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/service/ChargingDataService.java
  35. 214 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/service/PowerControlService.java
  36. 456 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/utils/ByteUtils.java
  37. 135 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/utils/CRC16Utils.java
  38. 217 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/controller/MockPileController.java
  39. 37 15
      ems/ems-cloud/ems-dev-adapter/src/main/resources/application-local.yml
  40. 43 4
      ems/ems-cloud/ems-dev-adapter/src/main/resources/application-prod-ct.yml
  41. 218 0
      ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/ProtocolParserTest.java
  42. 15 1
      ems/sql/ems_init_data_ctfwq.sql
  43. 4 4
      ems/sql/ems_sys_data.sql

+ 6 - 0
ems/ems-cloud/ems-dev-adapter/pom.xml

@@ -118,6 +118,12 @@
             <version>4.13.2</version>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.77.Final</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 0 - 2
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/EmsDevAdpApplication.java

@@ -1,11 +1,9 @@
 package com.ruoyi.ems;
 
 import com.ruoyi.common.security.annotation.EnableCustomConfig;
-import com.ruoyi.common.security.annotation.EnableRyFeignClients;
 import com.ruoyi.common.swagger.annotation.EnableCustomSwagger2;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 

+ 217 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/config/ChargingPileExecutorConfig.java

@@ -0,0 +1,217 @@
+/*
+ * 文 件 名:  ChargingPileExecutorConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩线程池配置
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 充电桩线程池配置
+ * 读取yml中的executor配置并创建线程池Bean
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@Configuration
+@EnableAsync
+@ConfigurationProperties(prefix = "charging-pile.executor")
+public class ChargingPileExecutorConfig {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileExecutorConfig.class);
+
+    /**
+     * Boss组线程池配置
+     */
+    private ExecutorProperties bossGroup = new ExecutorProperties();
+
+    /**
+     * Worker组线程池配置
+     */
+    private ExecutorProperties workerGroup = new ExecutorProperties();
+
+    /**
+     * 消息处理线程池配置
+     */
+    private ExecutorProperties msgExec = new ExecutorProperties();
+
+    /**
+     * 消息处理线程池Bean
+     * 对应 @Async("msgExecExecutor") 注解
+     */
+    @Bean("msgExecExecutor")
+    public Executor msgExecExecutor() {
+        log.info("初始化充电桩消息处理线程池 msgExecExecutor");
+        log.info("配置: corePoolSize={}, maxPoolSize={}, queueCapacity={}, namePrefix={}",
+                msgExec.getCorePoolSize(), msgExec.getMaxPoolSize(),
+                msgExec.getQueueCapacity(), msgExec.getNamePrefix());
+
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        // 核心线程数
+        executor.setCorePoolSize(msgExec.getCorePoolSize());
+
+        // 最大线程数
+        executor.setMaxPoolSize(msgExec.getMaxPoolSize());
+
+        // 队列容量
+        executor.setQueueCapacity(msgExec.getQueueCapacity());
+
+        // 线程名前缀
+        executor.setThreadNamePrefix(msgExec.getNamePrefix());
+
+        // 线程空闲时间(秒)
+        executor.setKeepAliveSeconds(60);
+
+        // 拒绝策略:由调用线程执行
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        // 等待所有任务完成后再关闭
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+
+        // 等待时间(秒)
+        executor.setAwaitTerminationSeconds(30);
+
+        executor.initialize();
+
+        return executor;
+    }
+
+    /**
+     * Boss组线程池Bean (可选,用于Netty以外的场景)
+     */
+    @Bean("bossExecutor")
+    public Executor bossExecutor() {
+        log.info("初始化Boss线程池 bossExecutor");
+
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(bossGroup.getCorePoolSize());
+        executor.setMaxPoolSize(bossGroup.getMaxPoolSize());
+        executor.setQueueCapacity(bossGroup.getQueueCapacity());
+        executor.setThreadNamePrefix(bossGroup.getNamePrefix());
+        executor.setKeepAliveSeconds(60);
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(30);
+        executor.initialize();
+
+        return executor;
+    }
+
+    /**
+     * Worker组线程池Bean (可选,用于Netty以外的场景)
+     */
+    @Bean("workerExecutor")
+    public Executor workerExecutor() {
+        log.info("初始化Worker线程池 workerExecutor");
+
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(workerGroup.getCorePoolSize());
+        executor.setMaxPoolSize(workerGroup.getMaxPoolSize());
+        executor.setQueueCapacity(workerGroup.getQueueCapacity());
+        executor.setThreadNamePrefix(workerGroup.getNamePrefix());
+        executor.setKeepAliveSeconds(60);
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(30);
+        executor.initialize();
+
+        return executor;
+    }
+
+    // ==================== Getters and Setters ====================
+
+    public ExecutorProperties getBossGroup() {
+        return bossGroup;
+    }
+
+    public void setBossGroup(ExecutorProperties bossGroup) {
+        this.bossGroup = bossGroup;
+    }
+
+    public ExecutorProperties getWorkerGroup() {
+        return workerGroup;
+    }
+
+    public void setWorkerGroup(ExecutorProperties workerGroup) {
+        this.workerGroup = workerGroup;
+    }
+
+    public ExecutorProperties getMsgExec() {
+        return msgExec;
+    }
+
+    public void setMsgExec(ExecutorProperties msgExec) {
+        this.msgExec = msgExec;
+    }
+
+    /**
+     * 线程池属性配置类
+     */
+    public static class ExecutorProperties {
+
+        /**
+         * 核心线程数
+         */
+        private int corePoolSize = 4;
+
+        /**
+         * 最大线程数
+         */
+        private int maxPoolSize = 8;
+
+        /**
+         * 队列容量
+         */
+        private int queueCapacity = 1000;
+
+        /**
+         * 线程名前缀
+         */
+        private String namePrefix = "pool-";
+
+        public int getCorePoolSize() {
+            return corePoolSize;
+        }
+
+        public void setCorePoolSize(int corePoolSize) {
+            this.corePoolSize = corePoolSize;
+        }
+
+        public int getMaxPoolSize() {
+            return maxPoolSize;
+        }
+
+        public void setMaxPoolSize(int maxPoolSize) {
+            this.maxPoolSize = maxPoolSize;
+        }
+
+        public int getQueueCapacity() {
+            return queueCapacity;
+        }
+
+        public void setQueueCapacity(int queueCapacity) {
+            this.queueCapacity = queueCapacity;
+        }
+
+        public String getNamePrefix() {
+            return namePrefix;
+        }
+
+        public void setNamePrefix(String namePrefix) {
+            this.namePrefix = namePrefix;
+        }
+    }
+}

+ 138 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/config/ChargingPileServer.java

@@ -0,0 +1,138 @@
+/*
+ * 文 件 名:  ChargingPileServer
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩TCP服务器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.config;
+
+import com.ruoyi.ems.charging.core.ChargingPileChannelInitializer;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+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 javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+/**
+ * 充电桩TCP服务器
+ * 作为服务端监听充电桩的连接请求
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Component
+public class ChargingPileServer {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileServer.class);
+
+    @Value("${adapter.charging-pile.server.port:8234}")
+    private int port;
+
+    @Value("${adapter.charging-pile.server.boss-threads:1}")
+    private int bossThreads;
+
+    @Value("${adapter.charging-pile.server.worker-threads:4}")
+    private int workerThreads;
+
+    @Autowired
+    private ChargingPileChannelInitializer channelInitializer;
+
+    private EventLoopGroup bossGroup;
+
+    private EventLoopGroup workerGroup;
+
+    private Channel serverChannel;
+
+    /**
+     * 启动服务器
+     */
+    @PostConstruct
+    public void start() {
+        new Thread(this::doStart, "charging-pile-server").start();
+    }
+
+    private void doStart() {
+        try {
+            bossGroup = new NioEventLoopGroup(bossThreads);
+            workerGroup = new NioEventLoopGroup(workerThreads);
+
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
+                .option(ChannelOption.SO_BACKLOG, 1024).option(ChannelOption.SO_REUSEADDR, true)
+                .childOption(ChannelOption.SO_KEEPALIVE, true).childOption(ChannelOption.TCP_NODELAY, true)
+                .childOption(ChannelOption.SO_RCVBUF, 65536).childOption(ChannelOption.SO_SNDBUF, 65536)
+                .childHandler(channelInitializer);
+
+            ChannelFuture future = bootstrap.bind(port).sync();
+            serverChannel = future.channel();
+
+            log.info("充电桩TCP服务器启动成功,监听端口: {}", port);
+
+            // 阻塞直到服务器通道关闭
+            serverChannel.closeFuture().sync();
+
+        }
+        catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("充电桩TCP服务器启动被中断", e);
+        }
+        catch (Exception e) {
+            log.error("充电桩TCP服务器启动失败", e);
+        }
+        finally {
+            shutdown();
+        }
+    }
+
+    /**
+     * 关闭服务器
+     */
+    @PreDestroy
+    public void shutdown() {
+        log.info("正在关闭充电桩TCP服务器...");
+
+        if (serverChannel != null) {
+            try {
+                serverChannel.close().sync();
+            }
+            catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+
+        if (bossGroup != null) {
+            bossGroup.shutdownGracefully();
+        }
+
+        if (workerGroup != null) {
+            workerGroup.shutdownGracefully();
+        }
+
+        log.info("充电桩TCP服务器已关闭");
+    }
+
+    /**
+     * 获取服务器是否运行中
+     */
+    public boolean isRunning() {
+        return serverChannel != null && serverChannel.isActive();
+    }
+
+    /**
+     * 获取监听端口
+     */
+    public int getPort() {
+        return port;
+    }
+}

+ 72 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileChannelInitializer.java

@@ -0,0 +1,72 @@
+/*
+ * 文 件 名:  ChargingPileChannelInitializer
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩通道初始化器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.core;
+
+import com.ruoyi.ems.charging.handler.ChargingPileMessageHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 充电桩通道初始化器
+ * 配置Netty Channel Pipeline
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Component
+public class ChargingPileChannelInitializer extends ChannelInitializer<SocketChannel> {
+    
+    /**
+     * 读空闲超时时间(秒) - 3次心跳间隔
+     */
+    private static final int READER_IDLE_TIME = 35;
+    
+    /**
+     * 写空闲超时时间(秒)
+     */
+    private static final int WRITER_IDLE_TIME = 0;
+    
+    /**
+     * 读写空闲超时时间(秒)
+     */
+    private static final int ALL_IDLE_TIME = 0;
+    
+    @Autowired
+    private ChargingPileMessageHandler messageHandler;
+    
+    @Autowired
+    private IdleStateEventHandler idleStateEventHandler;
+    
+    @Override
+    protected void initChannel(SocketChannel ch) throws Exception {
+        ChannelPipeline pipeline = ch.pipeline();
+        
+        // 日志处理器(调试用,生产环境可移除)
+        // pipeline.addLast("logging", new LoggingHandler(LogLevel.DEBUG));
+        
+        // 空闲状态检测处理器
+        pipeline.addLast("idleStateHandler", 
+            new IdleStateHandler(READER_IDLE_TIME, WRITER_IDLE_TIME, ALL_IDLE_TIME, TimeUnit.SECONDS));
+        pipeline.addLast("idleEventHandler", idleStateEventHandler);
+        
+        // 协议解码器
+        pipeline.addLast("decoder", new ChargingPileDecoder());
+        
+        // 协议编码器
+        pipeline.addLast("encoder", new ChargingPileEncoder());
+        
+        // 业务消息处理器
+        pipeline.addLast("messageHandler", messageHandler);
+    }
+}

+ 139 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileDecoder.java

@@ -0,0 +1,139 @@
+/*
+ * 文 件 名:  ChargingPileDecoder
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩协议解码器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.core;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.FrameParser;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * 充电桩协议解码器
+ * 负责从TCP流中提取完整的协议帧并解码
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public class ChargingPileDecoder extends ByteToMessageDecoder {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileDecoder.class);
+
+    /**
+     * 最小帧长度: 起始标志(1) + 数据长度(1) + 序列号(2) + 加密标志(1) + 帧类型(1) + CRC(2) = 8
+     */
+    private static final int MIN_FRAME_LENGTH = 8;
+
+    /**
+     * 最大帧长度: 起始标志(1) + 数据长度(1) + 数据域(200) + CRC(2) = 204
+     */
+    private static final int MAX_FRAME_LENGTH = 204;
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        // 循环处理,直到没有完整帧可读
+        while (in.readableBytes() >= MIN_FRAME_LENGTH) {
+            // 标记当前位置
+            in.markReaderIndex();
+
+            // 查找起始标志
+            int startIndex = findStartFlag(in);
+            if (startIndex < 0) {
+                // 没有找到起始标志,丢弃所有数据
+                log.debug("未找到起始标志,丢弃{}字节数据", in.readableBytes());
+                in.skipBytes(in.readableBytes());
+                return;
+            }
+
+            // 跳过起始标志之前的数据
+            if (startIndex > 0) {
+                log.debug("丢弃起始标志之前的{}字节数据", startIndex);
+                in.skipBytes(startIndex);
+            }
+
+            // 检查剩余数据是否足够读取帧头
+            if (in.readableBytes() < 2) {
+                return;
+            }
+
+            // 读取起始标志和数据长度
+            in.markReaderIndex();
+            byte startFlag = in.readByte();
+            int dataLength = in.readByte() & 0xFF;
+
+            // 验证数据长度
+            if (dataLength > ProtocolConstants.MAX_DATA_LENGTH) {
+                log.warn("数据长度超限: {}, 最大允许: {}", dataLength, ProtocolConstants.MAX_DATA_LENGTH);
+                // 跳过这个错误的起始标志,继续查找下一个
+                in.resetReaderIndex();
+                in.skipBytes(1);
+                continue;
+            }
+
+            // 计算完整帧长度
+            int frameLength = 1 + 1 + dataLength + 2; // 起始标志 + 数据长度 + 数据域 + CRC
+
+            // 检查数据是否足够
+            in.resetReaderIndex();
+            if (in.readableBytes() < frameLength) {
+                // 数据不完整,等待更多数据
+                log.debug("数据不完整,期望长度: {}, 实际可读: {}", frameLength, in.readableBytes());
+                return;
+            }
+
+            // 读取完整帧数据
+            byte[] frameData = new byte[frameLength];
+            in.readBytes(frameData);
+
+            // 调试日志
+            if (log.isDebugEnabled()) {
+                log.debug("收到原始数据[{}字节]: {}", frameData.length, ByteUtils.bytesToHex(frameData));
+            }
+
+            // 解析帧
+            BaseFrame frame = FrameParser.parse(frameData);
+            if (frame != null) {
+                out.add(frame);
+                log.debug("解码成功: {}", frame);
+            }
+            else {
+                log.warn("帧解析失败,丢弃数据: {}", ByteUtils.bytesToHex(frameData));
+            }
+        }
+    }
+
+    /**
+     * 查找起始标志位置
+     *
+     * @param buf ByteBuf
+     * @return 起始标志相对于当前读取位置的偏移量,未找到返回-1
+     */
+    private int findStartFlag(ByteBuf buf) {
+        int readerIndex = buf.readerIndex();
+        int readableBytes = buf.readableBytes();
+
+        for (int i = 0; i < readableBytes; i++) {
+            if (buf.getByte(readerIndex + i) == ProtocolConstants.START_FLAG) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("解码器异常: {}", cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 52 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileEncoder.java

@@ -0,0 +1,52 @@
+/*
+ * 文 件 名:  ChargingPileEncoder
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩协议编码器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.core;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 充电桩协议编码器
+ * 负责将协议帧对象编码为字节流
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public class ChargingPileEncoder extends MessageToByteEncoder<BaseFrame> {
+    
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileEncoder.class);
+    
+    @Override
+    protected void encode(ChannelHandlerContext ctx, BaseFrame frame, ByteBuf out) throws Exception {
+        try {
+            // 编码帧
+            byte[] data = frame.encode();
+            out.writeBytes(data);
+            
+            // 调试日志
+            if (log.isDebugEnabled()) {
+                log.debug("编码发送[{}字节]: {} -> {}", 
+                    data.length, frame, ByteUtils.bytesToHex(data));
+            }
+        } catch (Exception e) {
+            log.error("编码失败: {} - {}", frame, e.getMessage(), e);
+            throw e;
+        }
+    }
+    
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("编码器异常: {}", cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 367 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileSessionManager.java

@@ -0,0 +1,367 @@
+/*
+ * 文 件 名:  ChargingPileSessionManager
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩会话管理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ * 修改内容:  添加会话清理机制,防止内存泄漏
+ */
+package com.ruoyi.ems.charging.core;
+
+import io.netty.channel.Channel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 充电桩会话管理器
+ * 管理已连接的充电桩会话,包括通道映射、心跳超时检测等
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@Component
+public class ChargingPileSessionManager {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileSessionManager.class);
+
+    /**
+     * 离线会话保留时间(小时) - 超过此时间的离线会话将被清理
+     */
+    private static final int OFFLINE_SESSION_EXPIRE_HOURS = 24;
+
+    /**
+     * 桩编号 -> 会话信息
+     */
+    private final Map<String, PileSession> pileSessionMap = new ConcurrentHashMap<>();
+
+    /**
+     * Channel ID -> 桩编号
+     */
+    private final Map<String, String> channelPileMap = new ConcurrentHashMap<>();
+
+    /**
+     * 注册充电桩会话
+     *
+     * @param pileCode 桩编号
+     * @param channel  通道
+     * @param gunCount 枪数量
+     */
+    public void registerSession(String pileCode, Channel channel, int gunCount) {
+        // 清理旧会话
+        PileSession oldSession = pileSessionMap.get(pileCode);
+        if (oldSession != null && oldSession.getChannel() != null && oldSession.getChannel() != channel) {
+            log.info("充电桩[{}]重新连接,关闭旧连接", pileCode);
+            try {
+                String oldChannelId = oldSession.getChannel().id().asLongText();
+                channelPileMap.remove(oldChannelId);
+                oldSession.getChannel().close();
+            }
+            catch (Exception e) {
+                log.warn("关闭旧连接异常", e);
+            }
+        }
+
+        // 创建新会话
+        PileSession session = new PileSession();
+        session.setPileCode(pileCode);
+        session.setChannel(channel);
+        session.setGunCount(gunCount);
+        session.setLoginTime(LocalDateTime.now());
+        session.setLastHeartbeatTime(LocalDateTime.now());
+        session.setOnline(true);
+
+        pileSessionMap.put(pileCode, session);
+        channelPileMap.put(channel.id().asLongText(), pileCode);
+
+        log.info("充电桩[{}]会话注册成功,枪数量: {}, 通道: {}, 当前在线桩数: {}",
+            pileCode, gunCount, channel.remoteAddress(), getOnlineCount());
+    }
+
+    /**
+     * 注销充电桩会话
+     *
+     * @param channel 通道
+     */
+    public void unregisterSession(Channel channel) {
+        String channelId = channel.id().asLongText();
+        String pileCode = channelPileMap.remove(channelId);
+        if (pileCode != null) {
+            PileSession session = pileSessionMap.get(pileCode);
+            if (session != null && session.getChannel() == channel) {
+                session.setOnline(false);
+                session.setChannel(null);
+                session.setOfflineTime(LocalDateTime.now()); // 记录下线时间
+                log.info("充电桩[{}]会话注销,当前在线桩数: {}", pileCode, getOnlineCount());
+            }
+        }
+    }
+
+    /**
+     * 更新心跳时间
+     *
+     * @param pileCode 桩编号
+     */
+    public void updateHeartbeat(String pileCode) {
+        PileSession session = pileSessionMap.get(pileCode);
+        if (session != null) {
+            session.setLastHeartbeatTime(LocalDateTime.now());
+        }
+    }
+
+    /**
+     * 获取会话
+     *
+     * @param pileCode 桩编号
+     * @return 会话信息
+     */
+    public PileSession getSession(String pileCode) {
+        return pileSessionMap.get(pileCode);
+    }
+
+    /**
+     * 根据Channel获取桩编号
+     *
+     * @param channel 通道
+     * @return 桩编号
+     */
+    public String getPileCodeByChannel(Channel channel) {
+        return channelPileMap.get(channel.id().asLongText());
+    }
+
+    /**
+     * 获取通道
+     *
+     * @param pileCode 桩编号
+     * @return 通道
+     */
+    public Channel getChannel(String pileCode) {
+        PileSession session = pileSessionMap.get(pileCode);
+        return session != null ? session.getChannel() : null;
+    }
+
+    /**
+     * 检查是否在线
+     *
+     * @param pileCode 桩编号
+     * @return 是否在线
+     */
+    public boolean isOnline(String pileCode) {
+        PileSession session = pileSessionMap.get(pileCode);
+        return session != null && session.isOnline() && session.getChannel() != null && session.getChannel().isActive();
+    }
+
+    /**
+     * 获取所有在线会话
+     *
+     * @return 会话集合
+     */
+    public Collection<PileSession> getAllOnlineSessions() {
+        List<PileSession> onlineSessions = new ArrayList<>();
+        for (PileSession session : pileSessionMap.values()) {
+            if (session.isOnline() && session.getChannel() != null && session.getChannel().isActive()) {
+                onlineSessions.add(session);
+            }
+        }
+        return onlineSessions;
+    }
+
+    /**
+     * 获取所有会话(包括离线的)
+     *
+     * @return 会话集合
+     */
+    public Collection<PileSession> getAllSessions() {
+        return pileSessionMap.values();
+    }
+
+    /**
+     * 获取在线充电桩数量
+     *
+     * @return 数量
+     */
+    public int getOnlineCount() {
+        int count = 0;
+        for (PileSession session : pileSessionMap.values()) {
+            if (session.isOnline() && session.getChannel() != null && session.getChannel().isActive()) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * 获取总会话数量(包括离线的)
+     *
+     * @return 数量
+     */
+    public int getTotalSessionCount() {
+        return pileSessionMap.size();
+    }
+
+    /**
+     * 定期清理过期的离线会话 - 每小时执行一次
+     * 防止内存泄漏
+     */
+    @Scheduled(fixedRate = 3600000) // 1小时
+    public void cleanExpiredSessions() {
+        LocalDateTime expireTime = LocalDateTime.now().minus(OFFLINE_SESSION_EXPIRE_HOURS, ChronoUnit.HOURS);
+        int cleanedCount = 0;
+
+        Iterator<Map.Entry<String, PileSession>> iterator = pileSessionMap.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<String, PileSession> entry = iterator.next();
+            PileSession session = entry.getValue();
+
+            // 清理已离线且超过保留时间的会话
+            if (!session.isOnline() && session.getOfflineTime() != null
+                && session.getOfflineTime().isBefore(expireTime)) {
+                iterator.remove();
+                cleanedCount++;
+                log.debug("清理过期离线会话 - 桩号: {}, 离线时间: {}",
+                    session.getPileCode(), session.getOfflineTime());
+            }
+        }
+
+        if (cleanedCount > 0) {
+            log.info("会话清理完成 - 清理过期离线会话: {}个, 剩余会话: {}个, 在线: {}个",
+                cleanedCount, pileSessionMap.size(), getOnlineCount());
+        }
+    }
+
+    /**
+     * 手动移除会话
+     *
+     * @param pileCode 桩编号
+     */
+    public void removeSession(String pileCode) {
+        PileSession session = pileSessionMap.remove(pileCode);
+        if (session != null && session.getChannel() != null) {
+            channelPileMap.remove(session.getChannel().id().asLongText());
+            if (session.getChannel().isActive()) {
+                session.getChannel().close();
+            }
+            log.info("手动移除会话 - 桩号: {}", pileCode);
+        }
+    }
+
+    /**
+     * 充电桩会话信息
+     */
+    public static class PileSession {
+        /**
+         * 桩编号
+         */
+        private String pileCode;
+
+        /**
+         * 通道
+         */
+        private Channel channel;
+
+        /**
+         * 枪数量
+         */
+        private int gunCount;
+
+        /**
+         * 登录时间
+         */
+        private LocalDateTime loginTime;
+
+        /**
+         * 最后心跳时间
+         */
+        private LocalDateTime lastHeartbeatTime;
+
+        /**
+         * 离线时间
+         */
+        private LocalDateTime offlineTime;
+
+        /**
+         * 是否在线
+         */
+        private boolean online;
+
+        /**
+         * 序列号计数器
+         */
+        private int sequenceNo = 0;
+
+        /**
+         * 获取下一个序列号
+         */
+        public synchronized int nextSequenceNo() {
+            sequenceNo = (sequenceNo + 1) & 0xFFFF;
+            return sequenceNo;
+        }
+
+        // Getters and Setters
+        public String getPileCode() {
+            return pileCode;
+        }
+
+        public void setPileCode(String pileCode) {
+            this.pileCode = pileCode;
+        }
+
+        public Channel getChannel() {
+            return channel;
+        }
+
+        public void setChannel(Channel channel) {
+            this.channel = channel;
+        }
+
+        public int getGunCount() {
+            return gunCount;
+        }
+
+        public void setGunCount(int gunCount) {
+            this.gunCount = gunCount;
+        }
+
+        public LocalDateTime getLoginTime() {
+            return loginTime;
+        }
+
+        public void setLoginTime(LocalDateTime loginTime) {
+            this.loginTime = loginTime;
+        }
+
+        public LocalDateTime getLastHeartbeatTime() {
+            return lastHeartbeatTime;
+        }
+
+        public void setLastHeartbeatTime(LocalDateTime lastHeartbeatTime) {
+            this.lastHeartbeatTime = lastHeartbeatTime;
+        }
+
+        public LocalDateTime getOfflineTime() {
+            return offlineTime;
+        }
+
+        public void setOfflineTime(LocalDateTime offlineTime) {
+            this.offlineTime = offlineTime;
+        }
+
+        public boolean isOnline() {
+            return online;
+        }
+
+        public void setOnline(boolean online) {
+            this.online = online;
+        }
+    }
+}

+ 64 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/IdleStateEventHandler.java

@@ -0,0 +1,64 @@
+/*
+ * 文 件 名:  IdleStateEventHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  空闲状态事件处理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.core;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 空闲状态事件处理器
+ * 用于检测心跳超时,关闭无响应的连接
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Component
+@ChannelHandler.Sharable
+public class IdleStateEventHandler extends ChannelInboundHandlerAdapter {
+    
+    private static final Logger log = LoggerFactory.getLogger(IdleStateEventHandler.class);
+    
+    @Autowired
+    private ChargingPileSessionManager sessionManager;
+    
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof IdleStateEvent) {
+            IdleStateEvent event = (IdleStateEvent) evt;
+            
+            if (event.state() == IdleState.READER_IDLE) {
+                // 读空闲,说明长时间没有收到数据
+                String pileCode = sessionManager.getPileCodeByChannel(ctx.channel());
+                log.warn("充电桩[{}]心跳超时,关闭连接: {}", 
+                    pileCode != null ? pileCode : "未知",
+                    ctx.channel().remoteAddress());
+                
+                ctx.close();
+            }
+        } else {
+            super.userEventTriggered(ctx, evt);
+        }
+    }
+    
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        String pileCode = sessionManager.getPileCodeByChannel(ctx.channel());
+        log.error("充电桩[{}]连接异常: {} - {}", 
+            pileCode != null ? pileCode : "未知",
+            ctx.channel().remoteAddress(),
+            cause.getMessage());
+        ctx.close();
+    }
+}

+ 204 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handler/ChargingPileMessageHandler.java

@@ -0,0 +1,204 @@
+/*
+ * 文 件 名:  ChargingPileMessageHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩消息处理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.handler;
+
+import com.ruoyi.ems.charging.core.ChargingPileSessionManager;
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.req.ChargingEndFrame;
+import com.ruoyi.ems.charging.model.req.ChargingHandshakeFrame;
+import com.ruoyi.ems.charging.model.req.HeartbeatReqFrame;
+import com.ruoyi.ems.charging.model.req.LoginReqFrame;
+import com.ruoyi.ems.charging.model.req.RealtimeDataFrame;
+import com.ruoyi.ems.charging.model.req.WorkParamSetRespFrame;
+import com.ruoyi.ems.charging.model.resp.HeartbeatRespFrame;
+import com.ruoyi.ems.charging.model.resp.LoginRespFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.service.ChargingDataService;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 充电桩消息处理器
+ * 处理充电桩上送的各类协议消息
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Component
+@ChannelHandler.Sharable
+public class ChargingPileMessageHandler extends SimpleChannelInboundHandler<BaseFrame> {
+    
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileMessageHandler.class);
+    
+    @Autowired
+    private ChargingPileSessionManager sessionManager;
+    
+    @Autowired
+    private ChargingDataService chargingDataService;
+    
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, BaseFrame frame) throws Exception {
+        byte frameType = frame.getFrameType();
+        
+        switch (frameType) {
+            case ProtocolConstants.FRAME_TYPE_LOGIN_REQ:
+                handleLoginRequest(ctx, (LoginReqFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_HEARTBEAT_REQ:
+                handleHeartbeatRequest(ctx, (HeartbeatReqFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_REALTIME_DATA:
+                handleRealtimeData(ctx, (RealtimeDataFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_CHARGING_HANDSHAKE:
+                handleChargingHandshake(ctx, (ChargingHandshakeFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_CHARGING_END:
+                handleChargingEnd(ctx, (ChargingEndFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET_RESP:
+                handleWorkParamSetResp(ctx, (WorkParamSetRespFrame) frame);
+                break;
+            default:
+                log.info("收到未处理的帧类型: 0x{}", String.format("%02X", frameType & 0xFF));
+                break;
+        }
+    }
+    
+    /**
+     * 处理登录认证请求 (0x01)
+     */
+    private void handleLoginRequest(ChannelHandlerContext ctx, LoginReqFrame req) {
+        log.info("收到登录认证请求: {}", req);
+        
+        String pileCode = req.getPileCode();
+        int gunCount = req.getGunCount();
+        
+        // 验证桩信息(这里简化处理,实际应查询数据库验证)
+        boolean valid = validatePile(pileCode);
+        
+        // 发送登录应答
+        LoginRespFrame resp;
+        if (valid) {
+            // 注册会话
+            sessionManager.registerSession(pileCode, ctx.channel(), gunCount);
+            resp = LoginRespFrame.success(pileCode, req.getSequenceNo());
+            log.info("充电桩[{}]登录成功,枪数量: {}", pileCode, gunCount);
+        } else {
+            resp = LoginRespFrame.fail(pileCode, req.getSequenceNo());
+            log.warn("充电桩[{}]登录失败,验证未通过", pileCode);
+        }
+        
+        ctx.writeAndFlush(resp);
+        
+        // 通知业务层
+        chargingDataService.onPileLogin(req, valid);
+    }
+    
+    /**
+     * 处理心跳请求 (0x03)
+     */
+    private void handleHeartbeatRequest(ChannelHandlerContext ctx, HeartbeatReqFrame req) {
+        log.debug("收到心跳请求: {}", req);
+        
+        String pileCode = req.getPileCode();
+        
+        // 更新心跳时间
+        sessionManager.updateHeartbeat(pileCode);
+        
+        // 发送心跳应答
+        HeartbeatRespFrame resp = HeartbeatRespFrame.create(pileCode, req.getGunNo(), req.getSequenceNo());
+        ctx.writeAndFlush(resp);
+        
+        // 如果枪状态异常,通知业务层
+        if (!req.isNormal()) {
+            log.warn("充电桩[{}]枪[{}]状态异常: 故障", pileCode, req.getGunNo());
+            chargingDataService.onGunFault(pileCode, req.getGunNo());
+        }
+    }
+    
+    /**
+     * 处理实时监测数据 (0x13)
+     * 这是能耗平台最核心的数据处理
+     */
+    private void handleRealtimeData(ChannelHandlerContext ctx, RealtimeDataFrame data) {
+        log.info("收到实时监测数据: {}", data);
+        
+        // 更新心跳时间
+        sessionManager.updateHeartbeat(data.getPileCode());
+        
+        // 处理实时数据
+        chargingDataService.processRealtimeData(data);
+    }
+    
+    /**
+     * 处理充电握手 (0x15)
+     */
+    private void handleChargingHandshake(ChannelHandlerContext ctx, ChargingHandshakeFrame handshake) {
+        log.info("收到充电握手: {}", handshake);
+        
+        // 通知业务层
+        chargingDataService.onChargingHandshake(handshake);
+    }
+    
+    /**
+     * 处理充电结束 (0x19)
+     */
+    private void handleChargingEnd(ChannelHandlerContext ctx, ChargingEndFrame end) {
+        log.info("收到充电结束: {}", end);
+        
+        // 通知业务层
+        chargingDataService.onChargingEnd(end);
+    }
+    
+    /**
+     * 处理工作参数设置应答 (0x51)
+     */
+    private void handleWorkParamSetResp(ChannelHandlerContext ctx, WorkParamSetRespFrame resp) {
+        log.info("收到工作参数设置应答: {}", resp);
+        
+        // 通知业务层
+        chargingDataService.onWorkParamSetResp(resp);
+    }
+    
+    /**
+     * 验证充电桩
+     * 实际应用中应查询数据库验证桩信息
+     */
+    private boolean validatePile(String pileCode) {
+        // 简单验证:桩编号不为空且长度合法
+        return pileCode != null && !pileCode.isEmpty() && pileCode.length() <= 14;
+    }
+    
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("充电桩连接建立: {}", ctx.channel().remoteAddress());
+        super.channelActive(ctx);
+    }
+    
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.info("充电桩连接断开: {}", ctx.channel().remoteAddress());
+        
+        // 注销会话
+        sessionManager.unregisterSession(ctx.channel());
+        
+        super.channelInactive(ctx);
+    }
+    
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("消息处理异常: {} - {}", ctx.channel().remoteAddress(), cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 172 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/MockPileClientStarter.java

@@ -0,0 +1,172 @@
+/*
+ * 文 件 名:  MockPileClientStarter
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩客户端独立启动器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.mock;
+
+import com.ruoyi.ems.charging.mock.config.MockPileConfig;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClientManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Scanner;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Mock充电桩客户端独立启动器
+ * 可以独立于Spring运行,用于测试
+ *
+ * Usage:
+ *   java -jar mock-pile.jar [serverHost] [serverPort] [configType]
+ *
+ * Examples:
+ *   java -jar mock-pile.jar                          # 默认: 127.0.0.1:8234, simple配置
+ *   java -jar mock-pile.jar 192.168.1.100 8234       # 指定服务器
+ *   java -jar mock-pile.jar 127.0.0.1 8234 highway   # 高速服务区完整配置
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockPileClientStarter {
+
+    private static final Logger log = LoggerFactory.getLogger(MockPileClientStarter.class);
+
+    public static void main(String[] args) {
+        // 解析命令行参数
+        String serverHost = args.length > 0 ? args[0] : "127.0.0.1";
+        int serverPort = args.length > 1 ? Integer.parseInt(args[1]) : 8234;
+        String configType = args.length > 2 ? args[2] : "simple";
+
+        log.info("========================================");
+        log.info("   Mock充电桩客户端启动器");
+        log.info("========================================");
+        log.info("服务器地址: {}:{}", serverHost, serverPort);
+        log.info("配置类型: {}", configType);
+        log.info("----------------------------------------");
+
+        // 创建配置
+        MockPileConfig config;
+        if ("highway".equalsIgnoreCase(configType)) {
+            config = MockPileConfig.createHighwayServiceAreaConfig();
+            log.info("使用高速服务区配置:");
+            log.info("  - 4台充电主机");
+            log.info("  - 共30把充电枪");
+            log.info("  - 4台600kW水冷枪 + 26台250kW风冷枪");
+        } else {
+            config = MockPileConfig.createSimpleTestConfig();
+            log.info("使用简单测试配置:");
+            log.info("  - 1台充电主机");
+            log.info("  - 1把充电枪(120kW)");
+        }
+
+        config.setServerHost(serverHost);
+        config.setServerPort(serverPort);
+
+        // 设置模拟参数
+        config.setHeartbeatIntervalSeconds(10);
+        config.setChargingReportIntervalSeconds(5);
+        config.setChargingCycleSeconds(120);      // 2分钟一个充电周期
+        config.setChargingDurationSeconds(120);   // 充电时长2分钟
+        config.setAutoChargingEnabled(true);
+
+        log.info("模拟参数:");
+        log.info("  - 心跳间隔: {}秒", config.getHeartbeatIntervalSeconds());
+        log.info("  - 数据上报间隔: {}秒", config.getChargingReportIntervalSeconds());
+        log.info("  - 充电周期: {}秒", config.getChargingCycleSeconds());
+        log.info("  - 充电时长: {}秒", config.getChargingDurationSeconds());
+        log.info("  - 自动充电: {}", config.isAutoChargingEnabled() ? "启用" : "禁用");
+        log.info("----------------------------------------");
+
+        // 创建并启动管理器
+        MockPileClientManager manager = new MockPileClientManager(config);
+        manager.startAll();
+
+        // 添加关闭钩子
+        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+            log.info("收到关闭信号,正在停止...");
+            manager.stopAll();
+        }));
+
+        // 定时打印状态
+        ScheduledExecutorService statusPrinter = Executors.newSingleThreadScheduledExecutor();
+        statusPrinter.scheduleAtFixedRate(manager::printStatus, 30, 30, TimeUnit.SECONDS);
+
+        // 交互式命令行
+        log.info("========================================");
+        log.info("可用命令:");
+        log.info("  status  - 显示当前状态");
+        log.info("  start <桩号> <枪号> - 手动开始充电");
+        log.info("  stop <桩号> <枪号> - 手动停止充电");
+        log.info("  list    - 列出所有桩和枪");
+        log.info("  quit    - 退出程序");
+        log.info("========================================");
+
+        Scanner scanner = new Scanner(System.in);
+        while (true) {
+            System.out.print("> ");
+            String line = scanner.nextLine().trim();
+
+            if (line.isEmpty()) {
+                continue;
+            }
+
+            String[] parts = line.split("\\s+");
+            String cmd = parts[0].toLowerCase();
+
+            try {
+                switch (cmd) {
+                    case "status":
+                        manager.printStatus();
+                        break;
+
+                    case "start":
+                        if (parts.length >= 3) {
+                            manager.startCharging(parts[1], parts[2]);
+                        } else {
+                            System.out.println("用法: start <桩号> <枪号>");
+                        }
+                        break;
+
+                    case "stop":
+                        if (parts.length >= 3) {
+                            manager.stopCharging(parts[1], parts[2]);
+                        } else {
+                            System.out.println("用法: stop <桩号> <枪号>");
+                        }
+                        break;
+
+                    case "list":
+                        System.out.println("可用的桩和枪:");
+                        manager.getAllClients().forEach(client -> {
+                            System.out.println("  桩: " + client.getPileCode());
+                            client.getAllGunStates().forEach(gun -> {
+                                System.out.println("    枪" + gun.getGunNo() + ": " +
+                                        (gun.isCharging() ? "充电中" : "空闲") +
+                                        ", " + gun.getRatedPower() + "kW");
+                            });
+                        });
+                        break;
+
+                    case "quit":
+                    case "exit":
+                        System.out.println("正在退出...");
+                        statusPrinter.shutdownNow();
+                        manager.stopAll();
+                        System.exit(0);
+                        break;
+
+                    default:
+                        System.out.println("未知命令: " + cmd);
+                        break;
+                }
+            } catch (Exception e) {
+                System.out.println("命令执行出错: " + e.getMessage());
+            }
+        }
+    }
+}

+ 102 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/config/MockPileAutoConfiguration.java

@@ -0,0 +1,102 @@
+/*
+ * 文 件 名:  MockPileAutoConfiguration
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩自动配置
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ * 修改内容:  添加缺失的 @Bean 注解
+ */
+package com.ruoyi.ems.charging.mock.config;
+
+import com.ruoyi.ems.charging.mock.simulator.MockPileClientManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * Mock充电桩自动配置
+ * 通过配置文件控制是否启用Mock客户端
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@Configuration
+@ConditionalOnProperty(name = "adapter.charging-pile.mock.enabled", havingValue = "true")
+public class MockPileAutoConfiguration {
+
+    private static final Logger log = LoggerFactory.getLogger(MockPileAutoConfiguration.class);
+
+    @Value("${adapter.charging-pile.mock.server-host:127.0.0.1}")
+    private String serverHost;
+
+    @Value("${adapter.charging-pile.mock.server-port:8234}")
+    private int serverPort;
+
+    @Value("${adapter.charging-pile.mock.heartbeat-interval:10}")
+    private int heartbeatInterval;
+
+    @Value("${adapter.charging-pile.mock.charging-report-interval:5}")
+    private int chargingReportInterval;
+
+    @Value("${adapter.charging-pile.mock.idle-report-interval:300}")
+    private int idleReportInterval;
+
+    @Value("${adapter.charging-pile.mock.charging-cycle:120}")
+    private int chargingCycle;
+
+    @Value("${adapter.charging-pile.mock.charging-duration:120}")
+    private int chargingDuration;
+
+    @Value("${adapter.charging-pile.mock.auto-charging:true}")
+    private boolean autoCharging;
+
+    @Value("${adapter.charging-pile.mock.config-type:simple}")
+    private String configType;
+
+    private MockPileClientManager clientManager;
+
+    /**
+     * 修复: 添加 @Bean 注解,使Spring能够正确管理这个Bean
+     */
+    @Bean
+    public MockPileClientManager mockPileClientManager() {
+        log.info("初始化Mock充电桩客户端管理器...");
+
+        // 根据配置类型创建配置
+        MockPileConfig config;
+        if ("highway".equalsIgnoreCase(configType)) {
+            config = MockPileConfig.createHighwayServiceAreaConfig();
+            log.info("使用高速服务区配置: 4台主机, 30把枪");
+        } else {
+            config = MockPileConfig.createSimpleTestConfig();
+            log.info("使用简单测试配置: 1台主机, 1把枪");
+        }
+
+        // 应用配置参数
+        config.setServerHost(serverHost);
+        config.setServerPort(serverPort);
+        config.setHeartbeatIntervalSeconds(heartbeatInterval);
+        config.setChargingReportIntervalSeconds(chargingReportInterval);
+        config.setIdleReportIntervalSeconds(idleReportInterval);
+        config.setChargingCycleSeconds(chargingCycle);
+        config.setChargingDurationSeconds(chargingDuration);
+        config.setAutoChargingEnabled(autoCharging);
+
+        clientManager = new MockPileClientManager(config);
+
+        return clientManager;
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (clientManager != null) {
+            log.info("停止Mock充电桩客户端管理器...");
+            clientManager.stopAll();
+        }
+    }
+}

+ 403 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/config/MockPileConfig.java

@@ -0,0 +1,403 @@
+/*
+ * 文 件 名:  MockPileConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩配置
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.mock.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Mock充电桩配置
+ * 定义模拟的充电桩主机和充电枪配置
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockPileConfig {
+
+    /**
+     * 服务器地址
+     */
+    private String serverHost = "127.0.0.1";
+
+    /**
+     * 服务器端口
+     */
+    private int serverPort = 8234;
+
+    /**
+     * 充电桩主机配置列表
+     */
+    private List<PileHostConfig> pileHosts = new ArrayList<>();
+
+    /**
+     * 心跳间隔(秒)
+     */
+    private int heartbeatIntervalSeconds = 10;
+
+    /**
+     * 实时数据上报间隔 - 空闲状态(秒)
+     */
+    private int idleReportIntervalSeconds = 300;
+
+    /**
+     * 实时数据上报间隔 - 充电状态(秒)
+     */
+    private int chargingReportIntervalSeconds = 5;
+
+    /**
+     * 模拟充电周期(秒) - 多久触发一次充电开始
+     */
+    private int chargingCycleSeconds = 120;
+
+    /**
+     * 模拟充电时长(秒)
+     */
+    private int chargingDurationSeconds = 120;
+
+    /**
+     * 是否启用自动充电模拟
+     */
+    private boolean autoChargingEnabled = true;
+
+    /**
+     * 重连间隔(秒)
+     */
+    private int reconnectIntervalSeconds = 5;
+
+    /**
+     * 创建默认的高速服务区配置
+     * 南北两侧,每侧2台充电主机
+     * 主机1: 1台600kW水冷枪 + 6台250kW风冷枪 = 7枪
+     * 主机2: 1台600kW水冷枪 + 7台250kW风冷枪 = 8枪
+     */
+    public static MockPileConfig createHighwayServiceAreaConfig() {
+        MockPileConfig config = new MockPileConfig();
+
+        // 北侧-主机1: 7枪 (1*600kW + 6*250kW)
+        PileHostConfig northHost1 = new PileHostConfig();
+        northHost1.setPileCode("32010600000001");
+        northHost1.setGunCount(7);
+        northHost1.setDescription("北侧-主机1");
+        northHost1.addGun(new GunConfig("1", 600, GunConfig.GunType.WATER_COOLED));
+        for (int i = 2; i <= 7; i++) {
+            northHost1.addGun(new GunConfig(String.valueOf(i), 250, GunConfig.GunType.AIR_COOLED));
+        }
+        config.getPileHosts().add(northHost1);
+
+        // 北侧-主机2: 8枪 (1*600kW + 7*250kW)
+        PileHostConfig northHost2 = new PileHostConfig();
+        northHost2.setPileCode("32010600000002");
+        northHost2.setGunCount(8);
+        northHost2.setDescription("北侧-主机2");
+        northHost2.addGun(new GunConfig("1", 600, GunConfig.GunType.WATER_COOLED));
+        for (int i = 2; i <= 8; i++) {
+            northHost2.addGun(new GunConfig(String.valueOf(i), 250, GunConfig.GunType.AIR_COOLED));
+        }
+        config.getPileHosts().add(northHost2);
+
+        // 南侧-主机1: 7枪 (1*600kW + 6*250kW)
+        PileHostConfig southHost1 = new PileHostConfig();
+        southHost1.setPileCode("32010600000003");
+        southHost1.setGunCount(7);
+        southHost1.setDescription("南侧-主机1");
+        southHost1.addGun(new GunConfig("1", 600, GunConfig.GunType.WATER_COOLED));
+        for (int i = 2; i <= 7; i++) {
+            southHost1.addGun(new GunConfig(String.valueOf(i), 250, GunConfig.GunType.AIR_COOLED));
+        }
+        config.getPileHosts().add(southHost1);
+
+        // 南侧-主机2: 8枪 (1*600kW + 7*250kW)
+        PileHostConfig southHost2 = new PileHostConfig();
+        southHost2.setPileCode("32010600000004");
+        southHost2.setGunCount(8);
+        southHost2.setDescription("南侧-主机2");
+        southHost2.addGun(new GunConfig("1", 600, GunConfig.GunType.WATER_COOLED));
+        for (int i = 2; i <= 8; i++) {
+            southHost2.addGun(new GunConfig(String.valueOf(i), 250, GunConfig.GunType.AIR_COOLED));
+        }
+        config.getPileHosts().add(southHost2);
+
+        return config;
+    }
+
+    /**
+     * 创建简单的测试配置(单主机单枪)
+     */
+    public static MockPileConfig createSimpleTestConfig() {
+        MockPileConfig config = new MockPileConfig();
+
+        PileHostConfig host = new PileHostConfig();
+        host.setPileCode("32010600000001");
+        host.setGunCount(1);
+        host.setDescription("测试主机");
+        host.addGun(new GunConfig("1", 120, GunConfig.GunType.AIR_COOLED));
+        config.getPileHosts().add(host);
+
+        return config;
+    }
+
+    // Getters and Setters
+    public String getServerHost() {
+        return serverHost;
+    }
+
+    public void setServerHost(String serverHost) {
+        this.serverHost = serverHost;
+    }
+
+    public int getServerPort() {
+        return serverPort;
+    }
+
+    public void setServerPort(int serverPort) {
+        this.serverPort = serverPort;
+    }
+
+    public List<PileHostConfig> getPileHosts() {
+        return pileHosts;
+    }
+
+    public void setPileHosts(List<PileHostConfig> pileHosts) {
+        this.pileHosts = pileHosts;
+    }
+
+    public int getHeartbeatIntervalSeconds() {
+        return heartbeatIntervalSeconds;
+    }
+
+    public void setHeartbeatIntervalSeconds(int heartbeatIntervalSeconds) {
+        this.heartbeatIntervalSeconds = heartbeatIntervalSeconds;
+    }
+
+    public int getIdleReportIntervalSeconds() {
+        return idleReportIntervalSeconds;
+    }
+
+    public void setIdleReportIntervalSeconds(int idleReportIntervalSeconds) {
+        this.idleReportIntervalSeconds = idleReportIntervalSeconds;
+    }
+
+    public int getChargingReportIntervalSeconds() {
+        return chargingReportIntervalSeconds;
+    }
+
+    public void setChargingReportIntervalSeconds(int chargingReportIntervalSeconds) {
+        this.chargingReportIntervalSeconds = chargingReportIntervalSeconds;
+    }
+
+    public int getChargingCycleSeconds() {
+        return chargingCycleSeconds;
+    }
+
+    public void setChargingCycleSeconds(int chargingCycleSeconds) {
+        this.chargingCycleSeconds = chargingCycleSeconds;
+    }
+
+    public int getChargingDurationSeconds() {
+        return chargingDurationSeconds;
+    }
+
+    public void setChargingDurationSeconds(int chargingDurationSeconds) {
+        this.chargingDurationSeconds = chargingDurationSeconds;
+    }
+
+    public boolean isAutoChargingEnabled() {
+        return autoChargingEnabled;
+    }
+
+    public void setAutoChargingEnabled(boolean autoChargingEnabled) {
+        this.autoChargingEnabled = autoChargingEnabled;
+    }
+
+    public int getReconnectIntervalSeconds() {
+        return reconnectIntervalSeconds;
+    }
+
+    public void setReconnectIntervalSeconds(int reconnectIntervalSeconds) {
+        this.reconnectIntervalSeconds = reconnectIntervalSeconds;
+    }
+
+    /**
+     * 充电桩主机配置
+     */
+    public static class PileHostConfig {
+        /**
+         * 桩编号 (BCD码 7字节, 14位数字)
+         */
+        private String pileCode;
+
+        /**
+         * 枪数量
+         */
+        private int gunCount;
+
+        /**
+         * 描述
+         */
+        private String description;
+
+        /**
+         * 桩类型: 0-直流, 1-交流
+         */
+        private byte pileType = 0;
+
+        /**
+         * 软件版本
+         */
+        private String softwareVersion = "V1.0.0";
+
+        /**
+         * 协议版本 (乘10)
+         */
+        private int protocolVersion = 20;
+
+        /**
+         * 网络类型: 0-SIM, 1-LAN, 2-WAN
+         */
+        private byte networkType = 1;
+
+        /**
+         * 枪配置列表
+         */
+        private List<GunConfig> guns = new ArrayList<>();
+
+        public void addGun(GunConfig gun) {
+            guns.add(gun);
+        }
+
+        // Getters and Setters
+        public String getPileCode() {
+            return pileCode;
+        }
+
+        public void setPileCode(String pileCode) {
+            this.pileCode = pileCode;
+        }
+
+        public int getGunCount() {
+            return gunCount;
+        }
+
+        public void setGunCount(int gunCount) {
+            this.gunCount = gunCount;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public byte getPileType() {
+            return pileType;
+        }
+
+        public void setPileType(byte pileType) {
+            this.pileType = pileType;
+        }
+
+        public String getSoftwareVersion() {
+            return softwareVersion;
+        }
+
+        public void setSoftwareVersion(String softwareVersion) {
+            this.softwareVersion = softwareVersion;
+        }
+
+        public int getProtocolVersion() {
+            return protocolVersion;
+        }
+
+        public void setProtocolVersion(int protocolVersion) {
+            this.protocolVersion = protocolVersion;
+        }
+
+        public byte getNetworkType() {
+            return networkType;
+        }
+
+        public void setNetworkType(byte networkType) {
+            this.networkType = networkType;
+        }
+
+        public List<GunConfig> getGuns() {
+            return guns;
+        }
+
+        public void setGuns(List<GunConfig> guns) {
+            this.guns = guns;
+        }
+    }
+
+    /**
+     * 充电枪配置
+     */
+    public static class GunConfig {
+        /**
+         * 枪号 (1-N)
+         */
+        private String gunNo;
+
+        /**
+         * 额定功率 (kW)
+         */
+        private int ratedPower;
+
+        /**
+         * 枪类型
+         */
+        private GunType gunType;
+
+        public GunConfig() {
+        }
+
+        public GunConfig(String gunNo, int ratedPower, GunType gunType) {
+            this.gunNo = gunNo;
+            this.ratedPower = ratedPower;
+            this.gunType = gunType;
+        }
+
+        public enum GunType {
+            /**
+             * 水冷枪
+             */
+            WATER_COOLED,
+            /**
+             * 风冷枪
+             */
+            AIR_COOLED
+        }
+
+        // Getters and Setters
+        public String getGunNo() {
+            return gunNo;
+        }
+
+        public void setGunNo(String gunNo) {
+            this.gunNo = gunNo;
+        }
+
+        public int getRatedPower() {
+            return ratedPower;
+        }
+
+        public void setRatedPower(int ratedPower) {
+            this.ratedPower = ratedPower;
+        }
+
+        public GunType getGunType() {
+            return gunType;
+        }
+
+        public void setGunType(GunType gunType) {
+            this.gunType = gunType;
+        }
+    }
+}

+ 178 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/handler/MockClientDecoder.java

@@ -0,0 +1,178 @@
+/*
+ * 文 件 名:  MockClientDecoder
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock客户端协议解码器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.mock.handler;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.resp.HeartbeatRespFrame;
+import com.ruoyi.ems.charging.model.resp.LoginRespFrame;
+import com.ruoyi.ems.charging.model.resp.ReadRealtimeDataFrame;
+import com.ruoyi.ems.charging.model.resp.WorkParamSetFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import com.ruoyi.ems.charging.utils.CRC16Utils;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Mock客户端协议解码器
+ * 负责解析服务端下发的协议帧
+ * 服务端下发的帧类型码为偶数
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockClientDecoder extends ByteToMessageDecoder {
+
+    private static final Logger log = LoggerFactory.getLogger(MockClientDecoder.class);
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        // 最小帧长度: 起始标志(1) + 数据长度(1) + 最小数据域(4) + CRC(2) = 8
+        while (in.readableBytes() >= 8) {
+            in.markReaderIndex();
+
+            // 查找起始标志
+            byte startFlag = in.readByte();
+            if (startFlag != ProtocolConstants.START_FLAG) {
+                // 不是起始标志,继续查找
+                continue;
+            }
+
+            // 读取数据长度
+            int dataLength = in.readByte() & 0xFF;
+
+            // 检查数据完整性
+            if (in.readableBytes() < dataLength + 2) {
+                // 数据不完整,等待更多数据
+                in.resetReaderIndex();
+                return;
+            }
+
+            // 记录CRC校验起始位置
+            int crcStartIndex = in.readerIndex();
+
+            // 读取序列号、加密标志、帧类型
+            int sequenceNo = in.readShortLE() & 0xFFFF;
+            byte encryptFlag = in.readByte();
+            byte frameType = in.readByte();
+
+            // 计算消息体长度
+            int bodyLength = dataLength - 4;
+
+            // 读取消息体
+            byte[] bodyData = new byte[bodyLength];
+            if (bodyLength > 0) {
+                in.readBytes(bodyData);
+            }
+
+            // 读取CRC
+            int receivedCrc = in.readShortLE() & 0xFFFF;
+
+            // 验证CRC
+            byte[] crcData = new byte[dataLength];
+            in.readerIndex(crcStartIndex);
+            in.readBytes(crcData);
+            int calculatedCrc = CRC16Utils.calculateCRC(crcData);
+
+            // 跳过CRC字节
+            in.skipBytes(2);
+
+            if (calculatedCrc != receivedCrc) {
+                log.warn("CRC校验失败, 计算值: 0x{}, 接收值: 0x{}",
+                        String.format("%04X", calculatedCrc),
+                        String.format("%04X", receivedCrc));
+                continue;
+            }
+
+            // 根据帧类型创建帧对象
+            BaseFrame frame = createFrame(frameType);
+            if (frame == null) {
+                log.debug("未知的帧类型: 0x{}", String.format("%02X", frameType & 0xFF));
+                continue;
+            }
+
+            // 解码帧 (回退到起始位置重新解码完整帧)
+            in.readerIndex(crcStartIndex - 2);
+            byte[] fullFrame = new byte[2 + dataLength + 2];
+            in.readBytes(fullFrame);
+
+            if (frame.decode(fullFrame)) {
+                out.add(frame);
+                if (log.isDebugEnabled()) {
+                    log.debug("解码成功: {} [{}字节]", frame, fullFrame.length);
+                }
+            } else {
+                log.warn("帧解码失败: type=0x{}", String.format("%02X", frameType & 0xFF));
+            }
+        }
+    }
+
+    /**
+     * 根据帧类型创建对应的帧对象
+     * 这里只处理服务端下发的帧类型(偶数)
+     */
+    private BaseFrame createFrame(byte frameType) {
+        switch (frameType) {
+            // 服务端下发的帧(偶数)
+            case ProtocolConstants.FRAME_TYPE_LOGIN_RESP:
+                return new LoginRespFrame();
+            case ProtocolConstants.FRAME_TYPE_HEARTBEAT_RESP:
+                return new HeartbeatRespFrame();
+            case ProtocolConstants.FRAME_TYPE_READ_REALTIME_DATA:
+                return new ReadRealtimeDataFrame();
+            case ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET:
+                return new WorkParamSetFrame();
+            default:
+                // 使用通用帧处理
+                return new GenericFrame(frameType);
+        }
+    }
+
+    /**
+     * 通用帧 - 用于处理未实现的帧类型
+     */
+    private static class GenericFrame extends BaseFrame {
+        private final byte type;
+        private byte[] bodyData;
+
+        public GenericFrame(byte type) {
+            this.type = type;
+        }
+
+        @Override
+        public byte getFrameType() {
+            return type;
+        }
+
+        @Override
+        protected void encodeBody(ByteBuf buf) {
+            if (bodyData != null) {
+                buf.writeBytes(bodyData);
+            }
+        }
+
+        @Override
+        protected void decodeBody(ByteBuf buf) {
+            if (buf.readableBytes() > 0) {
+                bodyData = new byte[buf.readableBytes()];
+                buf.readBytes(bodyData);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return String.format("GenericFrame[type=0x%02X, bodyLen=%d]",
+                    type & 0xFF, bodyData != null ? bodyData.length : 0);
+        }
+    }
+}

+ 217 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/handler/MockClientHandler.java

@@ -0,0 +1,217 @@
+/*
+ * 文 件 名:  MockClientHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩客户端消息处理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.mock.handler;
+
+import com.ruoyi.ems.charging.mock.model.MockGunState;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClient;
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.req.RealtimeDataFrame;
+import com.ruoyi.ems.charging.model.req.WorkParamSetRespFrame;
+import com.ruoyi.ems.charging.model.resp.HeartbeatRespFrame;
+import com.ruoyi.ems.charging.model.resp.LoginRespFrame;
+import com.ruoyi.ems.charging.model.resp.ReadRealtimeDataFrame;
+import com.ruoyi.ems.charging.model.resp.WorkParamSetFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Mock充电桩客户端消息处理器
+ * 处理服务端下发的消息并进行响应
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockClientHandler extends SimpleChannelInboundHandler<BaseFrame> {
+
+    private static final Logger log = LoggerFactory.getLogger(MockClientHandler.class);
+
+    private final MockPileClient pileClient;
+
+    public MockClientHandler(MockPileClient pileClient) {
+        this.pileClient = pileClient;
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, BaseFrame frame) throws Exception {
+        byte frameType = frame.getFrameType();
+
+        switch (frameType) {
+            case ProtocolConstants.FRAME_TYPE_LOGIN_RESP:
+                handleLoginResponse(ctx, (LoginRespFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_HEARTBEAT_RESP:
+                handleHeartbeatResponse(ctx, (HeartbeatRespFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_READ_REALTIME_DATA:
+                handleReadRealtimeData(ctx, (ReadRealtimeDataFrame) frame);
+                break;
+            case ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET:
+                handleWorkParamSet(ctx, (WorkParamSetFrame) frame);
+                break;
+            default:
+                log.debug("[{}] 收到未处理的帧类型: 0x{}", 
+                    pileClient.getPileCode(), String.format("%02X", frameType & 0xFF));
+                break;
+        }
+    }
+
+    /**
+     * 处理登录应答 (0x02)
+     */
+    private void handleLoginResponse(ChannelHandlerContext ctx, LoginRespFrame resp) {
+        if (resp.isSuccess()) {
+            log.info("[{}] 登录成功", pileClient.getPileCode());
+            pileClient.onLoginSuccess();
+        } else {
+            log.warn("[{}] 登录失败", pileClient.getPileCode());
+            pileClient.onLoginFailed();
+        }
+    }
+
+    /**
+     * 处理心跳应答 (0x04)
+     */
+    private void handleHeartbeatResponse(ChannelHandlerContext ctx, HeartbeatRespFrame resp) {
+        log.debug("[{}] 收到心跳应答", pileClient.getPileCode());
+        pileClient.onHeartbeatResponse();
+    }
+
+    /**
+     * 处理读取实时数据请求 (0x12)
+     * 服务端主动查询,客户端需要立即响应
+     */
+    private void handleReadRealtimeData(ChannelHandlerContext ctx, ReadRealtimeDataFrame req) {
+        String gunNo = req.getGunNo();
+        log.info("[{}] 收到读取实时数据请求, 枪号: {}", pileClient.getPileCode(), gunNo);
+
+        // 查找对应枪的状态
+        MockGunState gunState = pileClient.getGunState(gunNo);
+        if (gunState == null) {
+            log.warn("[{}] 未找到枪号: {}", pileClient.getPileCode(), gunNo);
+            return;
+        }
+
+        // 构建实时数据帧并发送
+        RealtimeDataFrame dataFrame = buildRealtimeDataFrame(gunState, req.getSequenceNo());
+        ctx.writeAndFlush(dataFrame);
+
+        log.info("[{}] 响应实时数据查询: {}", pileClient.getPileCode(), dataFrame);
+    }
+
+    /**
+     * 处理工作参数设置 (0x52)
+     * 服务端下发功率限制等参数
+     */
+    private void handleWorkParamSet(ChannelHandlerContext ctx, WorkParamSetFrame req) {
+        log.info("[{}] 收到工作参数设置: allowWork={}, maxPower={}%",
+                pileClient.getPileCode(), req.isAllowWork(), req.getMaxOutputPowerPercent());
+
+        // 更新所有枪的功率限制
+        boolean success = true;
+        try {
+            int powerPercent = req.getMaxOutputPowerPercent();
+            for (MockGunState gunState : pileClient.getAllGunStates()) {
+                gunState.setMaxOutputPowerPercent(powerPercent);
+            }
+
+            // 如果不允许工作,停止所有充电
+            if (!req.isAllowWork()) {
+                for (MockGunState gunState : pileClient.getAllGunStates()) {
+                    if (gunState.isCharging()) {
+                        gunState.stopCharging();
+                        log.info("[{}] 枪[{}] 因禁用而停止充电", pileClient.getPileCode(), gunState.getGunNo());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[{}] 设置工作参数失败", pileClient.getPileCode(), e);
+            success = false;
+        }
+
+        // 发送应答
+        WorkParamSetRespFrame resp = new WorkParamSetRespFrame();
+        resp.setPileCode(req.getPileCode());
+        resp.setSetResult(success ? (byte) 0x01 : (byte) 0x00);
+        resp.setSequenceNo(req.getSequenceNo());
+
+        ctx.writeAndFlush(resp);
+
+        log.info("[{}] 工作参数设置应答: {}", pileClient.getPileCode(), success ? "成功" : "失败");
+    }
+
+    /**
+     * 构建实时数据帧
+     */
+    private RealtimeDataFrame buildRealtimeDataFrame(MockGunState gunState, int sequenceNo) {
+        RealtimeDataFrame frame = new RealtimeDataFrame();
+        frame.setSequenceNo(sequenceNo);
+
+        // 基础信息
+        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;
+    }
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("[{}] 连接已建立", pileClient.getPileCode());
+        pileClient.onConnected(ctx.channel());
+        super.channelActive(ctx);
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.warn("[{}] 连接已断开", pileClient.getPileCode());
+        pileClient.onDisconnected();
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("[{}] 连接异常: {}", pileClient.getPileCode(), cause.getMessage());
+        ctx.close();
+    }
+}

+ 545 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/model/MockGunState.java

@@ -0,0 +1,545 @@
+/*
+ * 文 件 名:  MockGunState
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电枪状态
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ * 修改内容:  修复交易流水号格式(32位而非34位)
+ */
+package com.ruoyi.ems.charging.mock.model;
+
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Mock充电枪状态
+ * 维护单个充电枪的模拟状态数据
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockGunState {
+
+    private static final Random RANDOM = new Random();
+    private static final AtomicLong TRANSACTION_COUNTER = new AtomicLong(1);
+
+    /**
+     * 修复: 日期格式改为12位 (yyMMddHHmmss)
+     * 协议要求: 桩号(14位) + 枪号(2位) + 日期时间(12位) + 序号(4位) = 32位
+     */
+    private static final DateTimeFormatter TRANS_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMddHHmmss");
+
+    /**
+     * 桩编号
+     */
+    private final String pileCode;
+
+    /**
+     * 枪号
+     */
+    private final String gunNo;
+
+    /**
+     * 额定功率(kW)
+     */
+    private final int ratedPower;
+
+    /**
+     * 当前状态
+     */
+    private byte status = ProtocolConstants.GUN_STATUS_IDLE;
+
+    /**
+     * 是否插枪
+     */
+    private boolean gunConnected = false;
+
+    /**
+     * 枪是否归位
+     */
+    private byte gunReturned = ProtocolConstants.GUN_RETURNED;
+
+    /**
+     * 当前交易流水号
+     */
+    private String transactionNo;
+
+    /**
+     * 充电开始时间
+     */
+    private LocalDateTime chargingStartTime;
+
+    /**
+     * 当前输出电压(V)
+     */
+    private double outputVoltage = 0;
+
+    /**
+     * 当前输出电流(A)
+     */
+    private double outputCurrent = 0;
+
+    /**
+     * 当前输出功率(kW)
+     */
+    private double outputPower = 0;
+
+    /**
+     * 枪线温度(℃)
+     */
+    private int gunTemperature = 25;
+
+    /**
+     * SOC(%)
+     */
+    private int soc = 0;
+
+    /**
+     * 电池组最高温度(℃)
+     */
+    private int batteryMaxTemp = 25;
+
+    /**
+     * 累计充电时间(min)
+     */
+    private int chargingTime = 0;
+
+    /**
+     * 剩余时间(min)
+     */
+    private int remainingTime = 0;
+
+    /**
+     * 充电度数(kWh)
+     */
+    private double chargingEnergy = 0;
+
+    /**
+     * 已充金额(元)
+     */
+    private double chargedAmount = 0;
+
+    /**
+     * 硬件故障码
+     */
+    private int hardwareFault = 0;
+
+    /**
+     * 最大输出功率百分比(30-100)
+     */
+    private int maxOutputPowerPercent = 100;
+
+    /**
+     * VIN码
+     */
+    private String vin;
+
+    /**
+     * 电池类型
+     */
+    private byte batteryType = 0x06; // 三元材料电池
+
+    /**
+     * 电池额定容量(Ah)
+     */
+    private double ratedCapacity = 80.0;
+
+    /**
+     * 电池额定电压(V)
+     */
+    private double ratedVoltage = 400.0;
+
+    public MockGunState(String pileCode, String gunNo, int ratedPower) {
+        this.pileCode = pileCode;
+        this.gunNo = gunNo;
+        this.ratedPower = ratedPower;
+    }
+
+    /**
+     * 开始充电
+     */
+    public void startCharging() {
+        this.status = ProtocolConstants.GUN_STATUS_CHARGING;
+        this.gunConnected = true;
+        this.gunReturned = ProtocolConstants.GUN_NOT_RETURNED;
+        this.chargingStartTime = LocalDateTime.now();
+        this.transactionNo = generateTransactionNo();
+
+        // 初始化充电参数
+        this.soc = 20 + RANDOM.nextInt(30); // 初始SOC 20-50%
+        this.chargingTime = 0;
+        this.chargingEnergy = 0;
+        this.chargedAmount = 0;
+
+        // 模拟车辆信息
+        this.vin = generateRandomVin();
+        this.batteryType = (byte) (0x03 + RANDOM.nextInt(5)); // 随机电池类型
+        this.ratedCapacity = 60 + RANDOM.nextInt(80); // 60-140 Ah
+        this.ratedVoltage = 350 + RANDOM.nextInt(100); // 350-450 V
+
+        // 计算输出参数
+        updateChargingOutput();
+    }
+
+    /**
+     * 更新充电输出(模拟充电过程中的数据变化)
+     */
+    public void updateChargingOutput() {
+        if (status != ProtocolConstants.GUN_STATUS_CHARGING) {
+            this.outputVoltage = 0;
+            this.outputCurrent = 0;
+            this.outputPower = 0;
+            return;
+        }
+
+        // 根据功率限制和SOC计算当前输出
+        double effectiveMaxPower = ratedPower * maxOutputPowerPercent / 100.0;
+
+        // SOC越高,充电功率越低(模拟真实充电曲线)
+        double socFactor = 1.0;
+        if (soc > 80) {
+            socFactor = 0.5 - (soc - 80) * 0.02;
+        } else if (soc > 60) {
+            socFactor = 0.8 - (soc - 60) * 0.015;
+        }
+        socFactor = Math.max(0.1, socFactor);
+
+        // 添加一些随机波动
+        double randomFactor = 0.95 + RANDOM.nextDouble() * 0.1;
+
+        this.outputPower = effectiveMaxPower * socFactor * randomFactor;
+        this.outputVoltage = ratedVoltage * (0.9 + soc * 0.001); // 电压随SOC略有变化
+        this.outputCurrent = outputPower * 1000 / outputVoltage;
+
+        // 限制电流不超过设备能力
+        double maxCurrent = ratedPower * 1000 / outputVoltage;
+        if (outputCurrent > maxCurrent) {
+            outputCurrent = maxCurrent;
+            outputPower = outputVoltage * outputCurrent / 1000;
+        }
+
+        // 温度变化
+        this.gunTemperature = 25 + (int) (outputPower / ratedPower * 30) + RANDOM.nextInt(5);
+        this.batteryMaxTemp = 30 + (int) (outputPower / ratedPower * 15) + RANDOM.nextInt(3);
+    }
+
+    /**
+     * 更新充电进度(每秒调用一次)
+     *
+     * @param elapsedSeconds 从上次更新经过的秒数
+     */
+    public void updateChargingProgress(int elapsedSeconds) {
+        if (status != ProtocolConstants.GUN_STATUS_CHARGING) {
+            return;
+        }
+
+        // 更新充电时间
+        if (chargingStartTime != null) {
+            this.chargingTime = (int) java.time.Duration.between(chargingStartTime, LocalDateTime.now()).toMinutes();
+        }
+
+        // 更新充电能量 (kWh = kW * h)
+        double addedEnergy = outputPower * elapsedSeconds / 3600.0;
+        this.chargingEnergy += addedEnergy;
+
+        // 更新SOC (假设电池容量换算)
+        double batteryCapacityKwh = ratedCapacity * ratedVoltage / 1000.0;
+        this.soc = Math.min(100, (int) (20 + chargingEnergy / batteryCapacityKwh * 100));
+
+        // 更新剩余时间
+        if (outputPower > 0) {
+            double remainingEnergy = batteryCapacityKwh * (100 - soc) / 100.0;
+            this.remainingTime = (int) (remainingEnergy / outputPower * 60);
+        }
+
+        // 更新金额(假设1.5元/kWh)
+        this.chargedAmount = chargingEnergy * 1.5;
+
+        // 更新输出参数
+        updateChargingOutput();
+    }
+
+    /**
+     * 停止充电
+     */
+    public void stopCharging() {
+        this.status = ProtocolConstants.GUN_STATUS_IDLE;
+        this.gunConnected = false;
+        this.gunReturned = ProtocolConstants.GUN_RETURNED;
+
+        this.outputVoltage = 0;
+        this.outputCurrent = 0;
+        this.outputPower = 0;
+        this.gunTemperature = 25;
+        this.batteryMaxTemp = 25;
+    }
+
+    /**
+     * 生成交易流水号
+     * 格式: 桩号(14位) + 枪号(2位) + 年月日时分秒(12位) + 自增序号(4位) = 32位
+     *
+     * 修复: 使用yyMMddHHmmss(12位)而非yyyyMMddHHmmss(14位)
+     */
+    private String generateTransactionNo() {
+        // 桩号14位
+        String pileCodePart = pileCode;
+        // 枪号2位
+        String gunNoPart = String.format("%02d", Integer.parseInt(gunNo));
+        // 日期时间12位 (yyMMddHHmmss)
+        String datetime = LocalDateTime.now().format(TRANS_DATE_FORMAT);
+        // 序号4位
+        long seq = TRANSACTION_COUNTER.getAndIncrement() % 10000;
+        String seqPart = String.format("%04d", seq);
+
+        String transNo = pileCodePart + gunNoPart + datetime + seqPart;
+
+        // 验证长度
+        if (transNo.length() != 32) {
+            throw new IllegalStateException("交易流水号长度错误: " + transNo.length() + ", 期望32位");
+        }
+
+        return transNo;
+    }
+
+    /**
+     * 生成随机VIN码
+     */
+    private String generateRandomVin() {
+        StringBuilder vin = new StringBuilder("L");
+        String chars = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789";
+        for (int i = 0; i < 16; i++) {
+            vin.append(chars.charAt(RANDOM.nextInt(chars.length())));
+        }
+        return vin.toString();
+    }
+
+    /**
+     * 获取完整枪号
+     */
+    public String getFullGunNo() {
+        return pileCode + String.format("%02d", Integer.parseInt(gunNo));
+    }
+
+    /**
+     * 是否正在充电
+     */
+    public boolean isCharging() {
+        return status == ProtocolConstants.GUN_STATUS_CHARGING
+            || status == ProtocolConstants.GUN_STATUS_CHARGING_PULSE;
+    }
+
+    /**
+     * 是否空闲
+     */
+    public boolean isIdle() {
+        return status == ProtocolConstants.GUN_STATUS_IDLE
+            || status == ProtocolConstants.GUN_STATUS_IDLE_PULSE;
+    }
+
+    // Getters and Setters
+    public String getPileCode() {
+        return pileCode;
+    }
+
+    public String getGunNo() {
+        return gunNo;
+    }
+
+    public int getRatedPower() {
+        return ratedPower;
+    }
+
+    public byte getStatus() {
+        return status;
+    }
+
+    public void setStatus(byte status) {
+        this.status = status;
+    }
+
+    public boolean isGunConnected() {
+        return gunConnected;
+    }
+
+    public void setGunConnected(boolean gunConnected) {
+        this.gunConnected = gunConnected;
+    }
+
+    public byte getGunReturned() {
+        return gunReturned;
+    }
+
+    public void setGunReturned(byte gunReturned) {
+        this.gunReturned = gunReturned;
+    }
+
+    public String getTransactionNo() {
+        return transactionNo;
+    }
+
+    public void setTransactionNo(String transactionNo) {
+        this.transactionNo = transactionNo;
+    }
+
+    public LocalDateTime getChargingStartTime() {
+        return chargingStartTime;
+    }
+
+    public void setChargingStartTime(LocalDateTime chargingStartTime) {
+        this.chargingStartTime = chargingStartTime;
+    }
+
+    public double getOutputVoltage() {
+        return outputVoltage;
+    }
+
+    public void setOutputVoltage(double outputVoltage) {
+        this.outputVoltage = outputVoltage;
+    }
+
+    public double getOutputCurrent() {
+        return outputCurrent;
+    }
+
+    public void setOutputCurrent(double outputCurrent) {
+        this.outputCurrent = outputCurrent;
+    }
+
+    public double getOutputPower() {
+        return outputPower;
+    }
+
+    public void setOutputPower(double outputPower) {
+        this.outputPower = outputPower;
+    }
+
+    public int getGunTemperature() {
+        return gunTemperature;
+    }
+
+    public void setGunTemperature(int gunTemperature) {
+        this.gunTemperature = gunTemperature;
+    }
+
+    public int getSoc() {
+        return soc;
+    }
+
+    public void setSoc(int soc) {
+        this.soc = soc;
+    }
+
+    public int getBatteryMaxTemp() {
+        return batteryMaxTemp;
+    }
+
+    public void setBatteryMaxTemp(int batteryMaxTemp) {
+        this.batteryMaxTemp = batteryMaxTemp;
+    }
+
+    public int getChargingTime() {
+        return chargingTime;
+    }
+
+    public void setChargingTime(int chargingTime) {
+        this.chargingTime = chargingTime;
+    }
+
+    public int getRemainingTime() {
+        return remainingTime;
+    }
+
+    public void setRemainingTime(int remainingTime) {
+        this.remainingTime = remainingTime;
+    }
+
+    public double getChargingEnergy() {
+        return chargingEnergy;
+    }
+
+    public void setChargingEnergy(double chargingEnergy) {
+        this.chargingEnergy = chargingEnergy;
+    }
+
+    public double getChargedAmount() {
+        return chargedAmount;
+    }
+
+    public void setChargedAmount(double chargedAmount) {
+        this.chargedAmount = chargedAmount;
+    }
+
+    public int getHardwareFault() {
+        return hardwareFault;
+    }
+
+    public void setHardwareFault(int hardwareFault) {
+        this.hardwareFault = hardwareFault;
+    }
+
+    public int getMaxOutputPowerPercent() {
+        return maxOutputPowerPercent;
+    }
+
+    public void setMaxOutputPowerPercent(int maxOutputPowerPercent) {
+        this.maxOutputPowerPercent = Math.max(30, Math.min(100, maxOutputPowerPercent));
+        updateChargingOutput();
+    }
+
+    public String getVin() {
+        return vin;
+    }
+
+    public void setVin(String vin) {
+        this.vin = vin;
+    }
+
+    public byte getBatteryType() {
+        return batteryType;
+    }
+
+    public void setBatteryType(byte batteryType) {
+        this.batteryType = batteryType;
+    }
+
+    public double getRatedCapacity() {
+        return ratedCapacity;
+    }
+
+    public void setRatedCapacity(double ratedCapacity) {
+        this.ratedCapacity = ratedCapacity;
+    }
+
+    public double getRatedVoltage() {
+        return ratedVoltage;
+    }
+
+    public void setRatedVoltage(double ratedVoltage) {
+        this.ratedVoltage = ratedVoltage;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("MockGunState[pile=%s, gun=%s, status=%s, power=%.1fkW, soc=%d%%, energy=%.2fkWh]",
+            pileCode, gunNo, getStatusDesc(), outputPower, soc, chargingEnergy);
+    }
+
+    private String getStatusDesc() {
+        switch (status) {
+            case ProtocolConstants.GUN_STATUS_OFFLINE:
+                return "离线";
+            case ProtocolConstants.GUN_STATUS_FAULT:
+                return "故障";
+            case ProtocolConstants.GUN_STATUS_IDLE:
+                return "空闲";
+            case ProtocolConstants.GUN_STATUS_CHARGING:
+                return "充电中";
+            default:
+                return "未知";
+        }
+    }
+}

+ 721 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/simulator/MockPileClient.java

@@ -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();
+    }
+}

+ 301 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/mock/simulator/MockPileClientManager.java

@@ -0,0 +1,301 @@
+/*
+ * 文 件 名:  MockPileClientManager
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩客户端管理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.charging.mock.simulator;
+
+import com.ruoyi.ems.charging.mock.config.MockPileConfig;
+import com.ruoyi.ems.charging.mock.config.MockPileConfig.PileHostConfig;
+import com.ruoyi.ems.charging.mock.model.MockGunState;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Mock充电桩客户端管理器
+ * 管理多个Mock充电桩客户端实例
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+public class MockPileClientManager {
+
+    private static final Logger log = LoggerFactory.getLogger(MockPileClientManager.class);
+
+    /**
+     * 配置
+     */
+    private final MockPileConfig config;
+
+    /**
+     * 客户端映射 - 桩编号 -> 客户端
+     */
+    private final Map<String, MockPileClient> clientMap = new ConcurrentHashMap<>();
+
+    /**
+     * 共享的事件循环组
+     */
+    private EventLoopGroup sharedEventLoopGroup;
+
+    /**
+     * 是否已启动
+     */
+    private volatile boolean started = false;
+
+    /**
+     * 构造函数
+     *
+     * @param config 配置
+     */
+    public MockPileClientManager(MockPileConfig config) {
+        this.config = config;
+    }
+
+    /**
+     * 启动所有客户端
+     */
+    public synchronized void startAll() {
+        if (started) {
+            log.warn("Mock充电桩客户端管理器已启动");
+            return;
+        }
+
+        log.info("启动Mock充电桩客户端管理器...");
+        log.info("服务器地址: {}:{}", config.getServerHost(), config.getServerPort());
+        log.info("充电桩主机数量: {}", config.getPileHosts().size());
+
+        // 创建共享的事件循环组
+        int workerThreads = Math.min(config.getPileHosts().size(), 4);
+        sharedEventLoopGroup = new NioEventLoopGroup(workerThreads);
+
+        // 创建并启动所有客户端
+        for (PileHostConfig hostConfig : config.getPileHosts()) {
+            MockPileClient client = new MockPileClient(hostConfig, config, sharedEventLoopGroup);
+            clientMap.put(hostConfig.getPileCode(), client);
+            client.start();
+
+            log.info("创建客户端: pileCode={}, gunCount={}, desc={}",
+                    hostConfig.getPileCode(), hostConfig.getGunCount(), hostConfig.getDescription());
+        }
+
+        started = true;
+        log.info("Mock充电桩客户端管理器启动完成, 共{}个客户端", clientMap.size());
+    }
+
+    /**
+     * 停止所有客户端
+     */
+    public synchronized void stopAll() {
+        if (!started) {
+            return;
+        }
+
+        log.info("停止Mock充电桩客户端管理器...");
+
+        // 停止所有客户端
+        for (MockPileClient client : clientMap.values()) {
+            try {
+                client.stop();
+            } catch (Exception e) {
+                log.error("停止客户端[{}]失败", client.getPileCode(), e);
+            }
+        }
+
+        clientMap.clear();
+
+        // 关闭事件循环组
+        if (sharedEventLoopGroup != null) {
+            sharedEventLoopGroup.shutdownGracefully();
+            sharedEventLoopGroup = null;
+        }
+
+        started = false;
+        log.info("Mock充电桩客户端管理器已停止");
+    }
+
+    /**
+     * 获取指定客户端
+     *
+     * @param pileCode 桩编号
+     * @return 客户端
+     */
+    public MockPileClient getClient(String pileCode) {
+        return clientMap.get(pileCode);
+    }
+
+    /**
+     * 获取所有客户端
+     *
+     * @return 客户端集合
+     */
+    public Collection<MockPileClient> getAllClients() {
+        return clientMap.values();
+    }
+
+    /**
+     * 获取所有枪的状态
+     *
+     * @return 枪状态列表
+     */
+    public List<MockGunState> getAllGunStates() {
+        List<MockGunState> allStates = new ArrayList<>();
+        for (MockPileClient client : clientMap.values()) {
+            allStates.addAll(client.getAllGunStates());
+        }
+        return allStates;
+    }
+
+    /**
+     * 获取指定枪的状态
+     *
+     * @param pileCode 桩编号
+     * @param gunNo    枪号
+     * @return 枪状态
+     */
+    public MockGunState getGunState(String pileCode, String gunNo) {
+        MockPileClient client = clientMap.get(pileCode);
+        if (client != null) {
+            return client.getGunState(gunNo);
+        }
+        return null;
+    }
+
+    /**
+     * 手动开始充电
+     *
+     * @param pileCode 桩编号
+     * @param gunNo    枪号
+     */
+    public void startCharging(String pileCode, String gunNo) {
+        MockPileClient client = clientMap.get(pileCode);
+        if (client != null) {
+            client.startCharging(gunNo);
+        } else {
+            log.warn("未找到桩编号: {}", pileCode);
+        }
+    }
+
+    /**
+     * 手动停止充电
+     *
+     * @param pileCode 桩编号
+     * @param gunNo    枪号
+     */
+    public void stopCharging(String pileCode, String gunNo) {
+        MockPileClient client = clientMap.get(pileCode);
+        if (client != null) {
+            client.stopCharging(gunNo);
+        } else {
+            log.warn("未找到桩编号: {}", pileCode);
+        }
+    }
+
+    /**
+     * 获取状态统计
+     *
+     * @return 状态统计信息
+     */
+    public Map<String, Object> getStatistics() {
+        Map<String, Object> stats = new LinkedHashMap<>();
+
+        int totalClients = clientMap.size();
+        int connectedClients = 0;
+        int loggedInClients = 0;
+        int totalGuns = 0;
+        int chargingGuns = 0;
+        int idleGuns = 0;
+        double totalPower = 0;
+        double totalEnergy = 0;
+
+        for (MockPileClient client : clientMap.values()) {
+            if (client.isConnected()) {
+                connectedClients++;
+            }
+            if (client.isLoggedIn()) {
+                loggedInClients++;
+            }
+
+            for (MockGunState gunState : client.getAllGunStates()) {
+                totalGuns++;
+                if (gunState.isCharging()) {
+                    chargingGuns++;
+                    totalPower += gunState.getOutputPower();
+                    totalEnergy += gunState.getChargingEnergy();
+                } else if (gunState.isIdle()) {
+                    idleGuns++;
+                }
+            }
+        }
+
+        stats.put("totalClients", totalClients);
+        stats.put("connectedClients", connectedClients);
+        stats.put("loggedInClients", loggedInClients);
+        stats.put("totalGuns", totalGuns);
+        stats.put("chargingGuns", chargingGuns);
+        stats.put("idleGuns", idleGuns);
+        stats.put("totalPowerKw", String.format("%.2f", totalPower));
+        stats.put("totalEnergyKwh", String.format("%.4f", totalEnergy));
+
+        return stats;
+    }
+
+    /**
+     * 打印状态
+     */
+    public void printStatus() {
+        Map<String, Object> stats = getStatistics();
+
+        log.info("========== Mock充电桩状态 ==========");
+        log.info("客户端: 总数={}, 已连接={}, 已登录={}",
+                stats.get("totalClients"),
+                stats.get("connectedClients"),
+                stats.get("loggedInClients"));
+        log.info("充电枪: 总数={}, 充电中={}, 空闲={}",
+                stats.get("totalGuns"),
+                stats.get("chargingGuns"),
+                stats.get("idleGuns"));
+        log.info("当前功率: {}kW, 累计电量: {}kWh",
+                stats.get("totalPowerKw"),
+                stats.get("totalEnergyKwh"));
+        log.info("====================================");
+
+        // 打印每个客户端的详细状态
+        for (MockPileClient client : clientMap.values()) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format("[%s] connected=%s, loggedIn=%s | ",
+                    client.getPileCode(),
+                    client.isConnected(),
+                    client.isLoggedIn()));
+
+            for (MockGunState gunState : client.getAllGunStates()) {
+                sb.append(String.format("gun%s:%s(%.1fkW) ",
+                        gunState.getGunNo(),
+                        gunState.isCharging() ? "充电" : "空闲",
+                        gunState.getOutputPower()));
+            }
+
+            log.info(sb.toString());
+        }
+    }
+
+    /**
+     * 是否已启动
+     */
+    public boolean isStarted() {
+        return started;
+    }
+
+    /**
+     * 获取配置
+     */
+    public MockPileConfig getConfig() {
+        return config;
+    }
+}

+ 208 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/BaseFrame.java

@@ -0,0 +1,208 @@
+/*
+ * 文 件 名:  BaseFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  协议帧基类
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model;
+
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import com.ruoyi.ems.charging.utils.CRC16Utils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import lombok.Data;
+
+/**
+ * 协议帧基类
+ * 帧格式: 起始标志(1B) + 数据长度(1B) + 序列号域(2B) + 加密标志(1B) + 帧类型标志(1B) + 消息体(NB) + 帧校验域(2B)
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+public abstract class BaseFrame {
+    
+    /**
+     * 起始标志 (固定0x68)
+     */
+    protected byte startFlag = ProtocolConstants.START_FLAG;
+    
+    /**
+     * 数据长度 (序列号域+加密标志+帧类型标志+消息体的字节数)
+     */
+    protected int dataLength;
+    
+    /**
+     * 序列号域 (数据包发送顺序号)
+     */
+    protected int sequenceNo;
+    
+    /**
+     * 加密标志 (0x00:不加密, 0x01:3DES)
+     */
+    protected byte encryptFlag = ProtocolConstants.ENCRYPT_NONE;
+    
+    /**
+     * 帧类型标志
+     */
+    protected byte frameType;
+    
+    /**
+     * 帧校验域 (CRC16)
+     */
+    protected int crc;
+    
+    /**
+     * 获取帧类型码
+     *
+     * @return 帧类型码
+     */
+    public abstract byte getFrameType();
+    
+    /**
+     * 编码消息体
+     *
+     * @param buf ByteBuf
+     */
+    protected abstract void encodeBody(ByteBuf buf);
+    
+    /**
+     * 解码消息体
+     *
+     * @param buf ByteBuf
+     */
+    protected abstract void decodeBody(ByteBuf buf);
+    
+    /**
+     * 编码完整帧
+     *
+     * @return 编码后的字节数组
+     */
+    public byte[] encode() {
+        // 先编码消息体
+        ByteBuf bodyBuf = Unpooled.buffer();
+        encodeBody(bodyBuf);
+        byte[] bodyBytes = new byte[bodyBuf.readableBytes()];
+        bodyBuf.readBytes(bodyBytes);
+        bodyBuf.release();
+        
+        // 计算数据长度 = 序列号域(2) + 加密标志(1) + 帧类型标志(1) + 消息体长度
+        this.dataLength = 2 + 1 + 1 + bodyBytes.length;
+        
+        // 组装完整帧
+        ByteBuf frameBuf = Unpooled.buffer();
+        frameBuf.writeByte(startFlag);                          // 起始标志
+        frameBuf.writeByte(dataLength);                         // 数据长度
+        frameBuf.writeShortLE(sequenceNo);                      // 序列号域(低位在前)
+        frameBuf.writeByte(encryptFlag);                        // 加密标志
+        frameBuf.writeByte(getFrameType());                     // 帧类型标志
+        frameBuf.writeBytes(bodyBytes);                         // 消息体
+        
+        // 计算CRC (从序列号域到数据域)
+        byte[] crcData = new byte[dataLength];
+        frameBuf.getBytes(2, crcData); // 从索引2开始读取(跳过起始标志和数据长度)
+        this.crc = CRC16Utils.calculateCRC(crcData);
+        
+        frameBuf.writeShortLE(crc);                             // 帧校验域(低位在前)
+        
+        byte[] result = new byte[frameBuf.readableBytes()];
+        frameBuf.readBytes(result);
+        frameBuf.release();
+        
+        return result;
+    }
+    
+    /**
+     * 编码到ByteBuf
+     *
+     * @param buf ByteBuf
+     */
+    public void encode(ByteBuf buf) {
+        byte[] encoded = encode();
+        buf.writeBytes(encoded);
+    }
+    
+    /**
+     * 解码完整帧
+     *
+     * @param data 字节数组
+     * @return 是否解码成功
+     */
+    public boolean decode(byte[] data) {
+        ByteBuf buf = Unpooled.wrappedBuffer(data);
+        try {
+            return decode(buf);
+        } finally {
+            buf.release();
+        }
+    }
+    
+    /**
+     * 解码完整帧
+     *
+     * @param buf ByteBuf
+     * @return 是否解码成功
+     */
+    public boolean decode(ByteBuf buf) {
+        if (buf.readableBytes() < 6) { // 最小帧长度
+            return false;
+        }
+        
+        // 读取帧头
+        this.startFlag = buf.readByte();
+        if (this.startFlag != ProtocolConstants.START_FLAG) {
+            return false;
+        }
+        
+        this.dataLength = buf.readByte() & 0xFF;
+        
+        // 检查数据长度是否合法
+        if (buf.readableBytes() < dataLength + 2) { // 数据域 + CRC
+            return false;
+        }
+        
+        // 记录CRC校验起始位置
+        int crcStartIndex = buf.readerIndex();
+        
+        this.sequenceNo = buf.readShortLE() & 0xFFFF;
+        this.encryptFlag = buf.readByte();
+        this.frameType = buf.readByte();
+        
+        // 解码消息体
+        int bodyLength = dataLength - 4; // 减去序列号域(2)+加密标志(1)+帧类型标志(1)
+        if (bodyLength > 0) {
+            decodeBody(buf);
+        }
+        
+        // 读取CRC
+        this.crc = buf.readShortLE() & 0xFFFF;
+        
+        // 验证CRC
+        buf.readerIndex(crcStartIndex);
+        byte[] crcData = new byte[dataLength];
+        buf.readBytes(crcData);
+        int calculatedCrc = CRC16Utils.calculateCRC(crcData);
+        
+        // 跳过CRC字节
+        buf.skipBytes(2);
+        
+        return calculatedCrc == this.crc;
+    }
+    
+    /**
+     * 获取帧类型名称
+     *
+     * @return 帧类型名称
+     */
+    public String getFrameTypeName() {
+        return String.format("0x%02X", getFrameType() & 0xFF);
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("%s[seq=%d, type=%s]", 
+            getClass().getSimpleName(), sequenceNo, getFrameTypeName());
+    }
+}

+ 174 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/ChargingSession.java

@@ -0,0 +1,174 @@
+/*
+ * 文 件 名:  ChargingSession
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.charging.model;
+
+import java.time.LocalDateTime;
+
+/**
+ * 充电会话
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class ChargingSession {
+    private String transactionNo;
+
+    private String fullGunNo;
+
+    private String vin;
+
+    private String batteryType;
+
+    private double ratedCapacity;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    private LocalDateTime lastUpdateTime;
+
+    private double startEnergy;
+
+    private double currentEnergy;
+
+    private double totalEnergy;
+
+    private double currentPower;
+
+    private int currentSoc;
+
+    private int finalSoc;
+
+    private int chargingTime;
+
+    public String getTransactionNo() {
+        return transactionNo;
+    }
+
+    public void setTransactionNo(String transactionNo) {
+        this.transactionNo = transactionNo;
+    }
+
+    public String getFullGunNo() {
+        return fullGunNo;
+    }
+
+    public void setFullGunNo(String fullGunNo) {
+        this.fullGunNo = fullGunNo;
+    }
+
+    public String getVin() {
+        return vin;
+    }
+
+    public void setVin(String vin) {
+        this.vin = vin;
+    }
+
+    public String getBatteryType() {
+        return batteryType;
+    }
+
+    public void setBatteryType(String batteryType) {
+        this.batteryType = batteryType;
+    }
+
+    public double getRatedCapacity() {
+        return ratedCapacity;
+    }
+
+    public void setRatedCapacity(double ratedCapacity) {
+        this.ratedCapacity = ratedCapacity;
+    }
+
+    public LocalDateTime getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(LocalDateTime startTime) {
+        this.startTime = startTime;
+    }
+
+    public LocalDateTime getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(LocalDateTime endTime) {
+        this.endTime = endTime;
+    }
+
+    public LocalDateTime getLastUpdateTime() {
+        return lastUpdateTime;
+    }
+
+    public void setLastUpdateTime(LocalDateTime lastUpdateTime) {
+        this.lastUpdateTime = lastUpdateTime;
+    }
+
+    public double getStartEnergy() {
+        return startEnergy;
+    }
+
+    public void setStartEnergy(double startEnergy) {
+        this.startEnergy = startEnergy;
+    }
+
+    public double getCurrentEnergy() {
+        return currentEnergy;
+    }
+
+    public void setCurrentEnergy(double currentEnergy) {
+        this.currentEnergy = currentEnergy;
+    }
+
+    public double getTotalEnergy() {
+        return totalEnergy;
+    }
+
+    public void setTotalEnergy(double totalEnergy) {
+        this.totalEnergy = totalEnergy;
+    }
+
+    public double getCurrentPower() {
+        return currentPower;
+    }
+
+    public void setCurrentPower(double currentPower) {
+        this.currentPower = currentPower;
+    }
+
+    public int getCurrentSoc() {
+        return currentSoc;
+    }
+
+    public void setCurrentSoc(int currentSoc) {
+        this.currentSoc = currentSoc;
+    }
+
+    public int getFinalSoc() {
+        return finalSoc;
+    }
+
+    public void setFinalSoc(int finalSoc) {
+        this.finalSoc = finalSoc;
+    }
+
+    public int getChargingTime() {
+        return chargingTime;
+    }
+
+    public void setChargingTime(int chargingTime) {
+        this.chargingTime = chargingTime;
+    }
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/RealtimeDataCache.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  RealtimeDataCache
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.charging.model;
+
+import com.ruoyi.ems.charging.model.req.RealtimeDataFrame;
+
+import java.time.LocalDateTime;
+
+/**
+ * 实时数据缓存
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class RealtimeDataCache {
+    private RealtimeDataFrame lastData;
+
+    private LocalDateTime updateTime;
+
+    private byte lastStatus;
+
+    private double lastEnergy;
+
+    public RealtimeDataFrame getLastData() {
+        return lastData;
+    }
+
+    public void setLastData(RealtimeDataFrame lastData) {
+        this.lastData = lastData;
+    }
+
+    public LocalDateTime getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(LocalDateTime updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public byte getLastStatus() {
+        return lastStatus;
+    }
+
+    public void setLastStatus(byte lastStatus) {
+        this.lastStatus = lastStatus;
+    }
+
+    public double getLastEnergy() {
+        return lastEnergy;
+    }
+
+    public void setLastEnergy(double lastEnergy) {
+        this.lastEnergy = lastEnergy;
+    }
+}

+ 173 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/ChargingEndFrame.java

@@ -0,0 +1,173 @@
+/*
+ * 文 件 名:  ChargingEndFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电结束帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电结束帧 (0x19)
+ * GBT-27930 充电桩与BMS充电结束阶段报文
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ChargingEndFrame extends BaseFrame {
+    
+    /**
+     * 交易流水号 (BCD码 16字节)
+     */
+    private String transactionNo;
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    /**
+     * BMS中止荷电状态SOC (BIN 1字节)
+     * 1%/位,0%偏移量;数据范围:0~100%
+     */
+    private int bmsFinalSoc;
+    
+    /**
+     * BMS动力蓄电池单体最低电压 (BIN 2字节)
+     * 0.01V/位,0V偏移量;数据范围:0~24V
+     */
+    private int bmsMinCellVoltage;
+    
+    /**
+     * BMS动力蓄电池单体最高电压 (BIN 2字节)
+     * 0.01V/位,0V偏移量;数据范围:0~24V
+     */
+    private int bmsMaxCellVoltage;
+    
+    /**
+     * BMS动力蓄电池最低温度 (BIN 1字节)
+     * 1℃/位,-50℃偏移量;数据范围:-50℃~+200℃
+     */
+    private int bmsMinTemp;
+    
+    /**
+     * BMS动力蓄电池最高温度 (BIN 1字节)
+     * 1℃/位,-50℃偏移量;数据范围:-50℃~+200℃
+     */
+    private int bmsMaxTemp;
+    
+    /**
+     * 电桩累计充电时间 (BIN 2字节)
+     * 1min/位,0min偏移量;数据范围:0~600min
+     */
+    private int totalChargingTime;
+    
+    /**
+     * 电桩输出能量 (BIN 2字节)
+     * 0.1kWh/位,0kWh偏移量;数据范围:0~1000kWh
+     */
+    private int outputEnergy;
+    
+    /**
+     * 电桩充电机编号 (BIN 4字节)
+     */
+    private long chargerNo;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_CHARGING_END;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, transactionNo, 16);
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+        buf.writeByte(bmsFinalSoc);
+        buf.writeShortLE(bmsMinCellVoltage);
+        buf.writeShortLE(bmsMaxCellVoltage);
+        buf.writeByte(bmsMinTemp);
+        buf.writeByte(bmsMaxTemp);
+        buf.writeShortLE(totalChargingTime);
+        buf.writeShortLE(outputEnergy);
+        buf.writeIntLE((int) chargerNo);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.transactionNo = ByteUtils.readBcd(buf, 16);
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+        this.bmsFinalSoc = buf.readByte() & 0xFF;
+        this.bmsMinCellVoltage = buf.readShortLE() & 0xFFFF;
+        this.bmsMaxCellVoltage = buf.readShortLE() & 0xFFFF;
+        this.bmsMinTemp = buf.readByte() & 0xFF;
+        this.bmsMaxTemp = buf.readByte() & 0xFF;
+        this.totalChargingTime = buf.readShortLE() & 0xFFFF;
+        this.outputEnergy = buf.readShortLE() & 0xFFFF;
+        this.chargerNo = buf.readIntLE() & 0xFFFFFFFFL;
+    }
+    
+    /**
+     * 获取最低单体电压(V)
+     */
+    public double getMinCellVoltageValue() {
+        return bmsMinCellVoltage / 100.0;
+    }
+    
+    /**
+     * 获取最高单体电压(V)
+     */
+    public double getMaxCellVoltageValue() {
+        return bmsMaxCellVoltage / 100.0;
+    }
+    
+    /**
+     * 获取最低温度(℃)
+     */
+    public int getMinTempValue() {
+        return bmsMinTemp - 50;
+    }
+    
+    /**
+     * 获取最高温度(℃)
+     */
+    public int getMaxTempValue() {
+        return bmsMaxTemp - 50;
+    }
+    
+    /**
+     * 获取输出能量(kWh)
+     */
+    public double getOutputEnergyValue() {
+        return outputEnergy / 10.0;
+    }
+    
+    /**
+     * 获取完整枪号
+     */
+    public String getFullGunNo() {
+        return pileCode + gunNo;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format(
+            "ChargingEndFrame[transNo=%s, pile=%s, gun=%s, soc=%d%%, energy=%.1fkWh, time=%dmin]",
+            transactionNo, pileCode, gunNo, bmsFinalSoc, getOutputEnergyValue(), totalChargingTime);
+    }
+}

+ 228 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/ChargingHandshakeFrame.java

@@ -0,0 +1,228 @@
+/*
+ * 文 件 名:  ChargingHandshakeFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电握手帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电握手帧 (0x15)
+ * GBT-27930 充电桩与BMS充电握手阶段报文
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ChargingHandshakeFrame extends BaseFrame {
+    
+    /**
+     * 交易流水号 (BCD码 16字节)
+     */
+    private String transactionNo;
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    /**
+     * BMS通信协议版本号 (BIN 3字节)
+     */
+    private byte[] bmsProtocolVersion;
+    
+    /**
+     * BMS电池类型 (BIN 1字节)
+     * 01H:铅酸电池;02H:氢电池;03H:磷酸铁锂电池;04H:锰酸锂电池;
+     * 05H:钴酸锂电池;06H:三元材料电池;07H:聚合物锂离子电池;08H:钛酸锂电池;FFH:其他
+     */
+    private byte bmsBatteryType;
+    
+    /**
+     * BMS整车动力蓄电池系统额定容量 (BIN 2字节)
+     * 0.1 Ah/位,0 Ah偏移量
+     */
+    private int bmsRatedCapacity;
+    
+    /**
+     * BMS整车动力蓄电池系统额定总电压 (BIN 2字节)
+     * 0.1V/位,0V偏移量
+     */
+    private int bmsRatedVoltage;
+    
+    /**
+     * BMS电池生产厂商名称 (BIN 4字节)
+     */
+    private byte[] bmsManufacturer;
+    
+    /**
+     * BMS电池组序号 (BIN 4字节)
+     */
+    private byte[] bmsBatterySerialNo;
+    
+    /**
+     * BMS电池组生产日期年 (BIN 1字节)
+     * 1985年偏移量
+     */
+    private int bmsProductionYear;
+    
+    /**
+     * BMS电池组生产日期月 (BIN 1字节)
+     */
+    private int bmsProductionMonth;
+    
+    /**
+     * BMS电池组生产日期日 (BIN 1字节)
+     */
+    private int bmsProductionDay;
+    
+    /**
+     * BMS电池组充电次数 (BIN 3字节)
+     */
+    private int bmsChargingCount;
+    
+    /**
+     * BMS电池组产权标识 (BIN 1字节)
+     * 0:租赁;1:车自有
+     */
+    private byte bmsOwnership;
+    
+    /**
+     * 预留位 (BIN 1字节)
+     */
+    private byte reserved;
+    
+    /**
+     * BMS车辆识别码VIN (BIN 17字节)
+     */
+    private String vin;
+    
+    /**
+     * BMS软件版本号 (BIN 8字节)
+     */
+    private byte[] bmsSoftwareVersion;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_CHARGING_HANDSHAKE;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, transactionNo, 16);
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+        buf.writeBytes(bmsProtocolVersion != null ? bmsProtocolVersion : new byte[3]);
+        buf.writeByte(bmsBatteryType);
+        buf.writeShortLE(bmsRatedCapacity);
+        buf.writeShortLE(bmsRatedVoltage);
+        buf.writeBytes(bmsManufacturer != null ? bmsManufacturer : new byte[4]);
+        buf.writeBytes(bmsBatterySerialNo != null ? bmsBatterySerialNo : new byte[4]);
+        buf.writeByte(bmsProductionYear);
+        buf.writeByte(bmsProductionMonth);
+        buf.writeByte(bmsProductionDay);
+        // 充电次数3字节,低位在前
+        buf.writeByte(bmsChargingCount & 0xFF);
+        buf.writeByte((bmsChargingCount >> 8) & 0xFF);
+        buf.writeByte((bmsChargingCount >> 16) & 0xFF);
+        buf.writeByte(bmsOwnership);
+        buf.writeByte(reserved);
+        ByteUtils.writeAscii(buf, vin, 17);
+        buf.writeBytes(bmsSoftwareVersion != null ? bmsSoftwareVersion : new byte[8]);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.transactionNo = ByteUtils.readBcd(buf, 16);
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+        this.bmsProtocolVersion = new byte[3];
+        buf.readBytes(this.bmsProtocolVersion);
+        this.bmsBatteryType = buf.readByte();
+        this.bmsRatedCapacity = buf.readShortLE() & 0xFFFF;
+        this.bmsRatedVoltage = buf.readShortLE() & 0xFFFF;
+        this.bmsManufacturer = new byte[4];
+        buf.readBytes(this.bmsManufacturer);
+        this.bmsBatterySerialNo = new byte[4];
+        buf.readBytes(this.bmsBatterySerialNo);
+        this.bmsProductionYear = buf.readByte() & 0xFF;
+        this.bmsProductionMonth = buf.readByte() & 0xFF;
+        this.bmsProductionDay = buf.readByte() & 0xFF;
+        // 充电次数3字节,低位在前
+        this.bmsChargingCount = (buf.readByte() & 0xFF) 
+            | ((buf.readByte() & 0xFF) << 8)
+            | ((buf.readByte() & 0xFF) << 16);
+        this.bmsOwnership = buf.readByte();
+        this.reserved = buf.readByte();
+        this.vin = ByteUtils.readAscii(buf, 17);
+        this.bmsSoftwareVersion = new byte[8];
+        buf.readBytes(this.bmsSoftwareVersion);
+    }
+    
+    /**
+     * 获取额定容量(Ah)
+     */
+    public double getRatedCapacityValue() {
+        return bmsRatedCapacity / 10.0;
+    }
+    
+    /**
+     * 获取额定电压(V)
+     */
+    public double getRatedVoltageValue() {
+        return bmsRatedVoltage / 10.0;
+    }
+    
+    /**
+     * 获取实际生产年份
+     */
+    public int getActualProductionYear() {
+        return bmsProductionYear + 1985;
+    }
+    
+    /**
+     * 获取电池类型描述
+     */
+    public String getBatteryTypeDesc() {
+        switch (bmsBatteryType) {
+            case 0x01: return "铅酸电池";
+            case 0x02: return "氢电池";
+            case 0x03: return "磷酸铁锂电池";
+            case 0x04: return "锰酸锂电池";
+            case 0x05: return "钴酸锂电池";
+            case 0x06: return "三元材料电池";
+            case 0x07: return "聚合物锂离子电池";
+            case 0x08: return "钛酸锂电池";
+            default: return "其他";
+        }
+    }
+    
+    /**
+     * 获取完整枪号
+     */
+    public String getFullGunNo() {
+        return pileCode + gunNo;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format(
+            "ChargingHandshakeFrame[transNo=%s, pile=%s, gun=%s, vin=%s, battery=%s, capacity=%.1fAh, voltage=%.1fV]",
+            transactionNo, pileCode, gunNo, vin, getBatteryTypeDesc(),
+            getRatedCapacityValue(), getRatedVoltageValue());
+    }
+}

+ 95 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/HeartbeatReqFrame.java

@@ -0,0 +1,95 @@
+/*
+ * 文 件 名:  HeartbeatReqFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩心跳包请求帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩心跳包请求帧 (0x03)
+ * 用于链路状态判断,10秒周期上送,3次未收到心跳包视为网络异常,需要重新登录
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class HeartbeatReqFrame extends BaseFrame {
+    
+    /**
+     * 桩编码 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    /**
+     * 枪状态 (BIN码 1字节)
+     * 0x00:正常, 0x01:故障
+     */
+    private byte gunStatus;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_HEARTBEAT_REQ;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+        buf.writeByte(gunStatus);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+        this.gunStatus = buf.readByte();
+    }
+    
+    /**
+     * 是否正常状态
+     *
+     * @return 是否正常
+     */
+    public boolean isNormal() {
+        return gunStatus == 0x00;
+    }
+    
+    /**
+     * 获取枪状态描述
+     *
+     * @return 状态描述
+     */
+    public String getGunStatusDesc() {
+        return isNormal() ? "正常" : "故障";
+    }
+    
+    /**
+     * 获取完整枪号 (桩编号+枪号)
+     *
+     * @return 完整枪号
+     */
+    public String getFullGunNo() {
+        return pileCode + gunNo;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("HeartbeatReqFrame[pileCode=%s, gunNo=%s, status=%s]",
+            pileCode, gunNo, getGunStatusDesc());
+    }
+}

+ 163 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/LoginReqFrame.java

@@ -0,0 +1,163 @@
+/*
+ * 文 件 名:  LoginReqFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩登录认证请求帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩登录认证请求帧 (0x01)
+ * 充电桩每次复位或通信离线,都需重新登录,登录成功后才能进行后续交互
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LoginReqFrame extends BaseFrame {
+    
+    /**
+     * 桩编码 (BCD码 7字节)
+     * 由平台生成提供给桩使用,示例:32010600019236
+     */
+    private String pileCode;
+    
+    /**
+     * 桩类型 (BIN码 1字节)
+     * 0表示直流桩,1表示交流桩
+     */
+    private byte pileType;
+    
+    /**
+     * 充电枪数量 (BIN码 1字节)
+     */
+    private int gunCount;
+    
+    /**
+     * 通信协议版本 (BIN码 1字节)
+     * 版本号乘10,v1.0表示0x0A,v2.0表示0x14
+     */
+    private int protocolVersion;
+    
+    /**
+     * 程序版本 (ASCII码 8字节)
+     * 不足8位补零
+     */
+    private String softwareVersion;
+    
+    /**
+     * 网络链接类型 (BIN码 1字节)
+     * 0x00 SIM卡, 0x01 LAN, 0x02 WAN, 0x03 其他
+     */
+    private byte networkType;
+    
+    /**
+     * SIM卡号 (BCD码 10字节)
+     * 不足10位补零,取不到置零
+     */
+    private String simCard;
+    
+    /**
+     * 运营商 (BIN码 1字节)
+     * 0x00 移动, 0x02 电信, 0x03 联通, 0x04 其他
+     */
+    private byte carrier;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_LOGIN_REQ;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        buf.writeByte(pileType);
+        buf.writeByte(gunCount);
+        buf.writeByte(protocolVersion);
+        ByteUtils.writeAscii(buf, softwareVersion, 8);
+        buf.writeByte(networkType);
+        ByteUtils.writeBcd(buf, simCard, 10);
+        buf.writeByte(carrier);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.pileType = buf.readByte();
+        this.gunCount = buf.readByte() & 0xFF;
+        this.protocolVersion = buf.readByte() & 0xFF;
+        this.softwareVersion = ByteUtils.readAscii(buf, 8);
+        this.networkType = buf.readByte();
+        this.simCard = ByteUtils.readBcd(buf, 10);
+        this.carrier = buf.readByte();
+    }
+    
+    /**
+     * 获取桩类型描述
+     *
+     * @return 桩类型描述
+     */
+    public String getPileTypeDesc() {
+        return pileType == ProtocolConstants.PILE_TYPE_DC ? "直流桩" : "交流桩";
+    }
+    
+    /**
+     * 获取协议版本描述
+     *
+     * @return 协议版本描述
+     */
+    public String getProtocolVersionDesc() {
+        return String.format("V%.1f", protocolVersion / 10.0);
+    }
+    
+    /**
+     * 获取网络类型描述
+     *
+     * @return 网络类型描述
+     */
+    public String getNetworkTypeDesc() {
+        switch (networkType) {
+            case ProtocolConstants.NETWORK_TYPE_SIM:
+                return "SIM卡";
+            case ProtocolConstants.NETWORK_TYPE_LAN:
+                return "LAN";
+            case ProtocolConstants.NETWORK_TYPE_WAN:
+                return "WAN";
+            default:
+                return "其他";
+        }
+    }
+    
+    /**
+     * 获取运营商描述
+     *
+     * @return 运营商描述
+     */
+    public String getCarrierDesc() {
+        switch (carrier) {
+            case 0x00:
+                return "移动";
+            case 0x02:
+                return "电信";
+            case 0x03:
+                return "联通";
+            default:
+                return "其他";
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("LoginReqFrame[pileCode=%s, type=%s, gunCount=%d, version=%s, sw=%s, network=%s]",
+            pileCode, getPileTypeDesc(), gunCount, getProtocolVersionDesc(), softwareVersion, getNetworkTypeDesc());
+    }
+}

+ 333 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/RealtimeDataFrame.java

@@ -0,0 +1,333 @@
+/*
+ * 文 件 名:  RealtimeDataFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  实时监测数据帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 实时监测数据帧 (0x13)
+ * 上送充电枪实时数据,周期上送时:待机5分钟、充电5秒
+ * 这是能耗平台最核心的数据帧,包含充电功率、电量等关键信息
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RealtimeDataFrame extends BaseFrame {
+    
+    /**
+     * 交易流水号 (BCD码 16字节)
+     */
+    private String transactionNo;
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    /**
+     * 状态 (BIN码 1字节)
+     * 0x00:离线, 0x01:故障, 0x02:空闲, 0x03:充电
+     * 0x04:空闲-脉冲静置, 0x05:充电中-脉冲检测
+     */
+    private byte status;
+    
+    /**
+     * 枪是否归位 (BIN码 1字节)
+     * 0x00:否, 0x01:是, 0x02:未知
+     */
+    private byte gunReturned;
+    
+    /**
+     * 是否插枪 (BIN码 1字节)
+     * 0x00:否, 0x01:是
+     */
+    private byte gunConnected;
+    
+    /**
+     * 输出电压 (BIN码 2字节)
+     * 精确到小数点后一位,待机置零
+     */
+    private int outputVoltage;
+    
+    /**
+     * 输出电流 (BIN码 2字节)
+     * 精确到小数点后一位,待机置零
+     */
+    private int outputCurrent;
+    
+    /**
+     * 枪线温度 (BIN码 1字节)
+     * 整形,偏移量-50,待机置零
+     */
+    private int gunTemperature;
+    
+    /**
+     * 枪线编码 (BIN码 8字节)
+     * 没有置零
+     */
+    private byte[] gunLineCode;
+    
+    /**
+     * SOC (BIN码 1字节)
+     * 待机置零,交流桩置零
+     */
+    private int soc;
+    
+    /**
+     * 电池组最高温度 (BIN码 1字节)
+     * 整形,偏移量-50,待机置零,交流桩置零
+     */
+    private int batteryMaxTemp;
+    
+    /**
+     * 累计充电时间 (BIN码 2字节)
+     * 单位:min,待机置零
+     */
+    private int chargingTime;
+    
+    /**
+     * 剩余时间 (BIN码 2字节)
+     * 单位:min,待机置零,交流桩置零
+     */
+    private int remainingTime;
+    
+    /**
+     * 充电度数 (BIN码 4字节)
+     * 精确到小数点后四位,待机置零
+     */
+    private long chargingEnergy;
+    
+    /**
+     * 计损充电度数 (BIN码 4字节)
+     * 精确到小数点后四位,待机置零
+     * 未设置计损比例时等于充电度数
+     */
+    private long lossCorrectedEnergy;
+    
+    /**
+     * 已充金额 (BIN码 4字节)
+     * 精确到小数点后四位,待机置零
+     * (电费+服务费)*计损充电度数
+     */
+    private long chargedAmount;
+    
+    /**
+     * 硬件故障 (BIN码 2字节)
+     * Bit位表示(0否1是)
+     */
+    private int hardwareFault;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_REALTIME_DATA;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, transactionNo, 16);
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+        buf.writeByte(status);
+        buf.writeByte(gunReturned);
+        buf.writeByte(gunConnected);
+        buf.writeShortLE(outputVoltage);
+        buf.writeShortLE(outputCurrent);
+        buf.writeByte(gunTemperature);
+        if (gunLineCode != null && gunLineCode.length == 8) {
+            buf.writeBytes(gunLineCode);
+        } else {
+            buf.writeBytes(new byte[8]);
+        }
+        buf.writeByte(soc);
+        buf.writeByte(batteryMaxTemp);
+        buf.writeShortLE(chargingTime);
+        buf.writeShortLE(remainingTime);
+        buf.writeIntLE((int) chargingEnergy);
+        buf.writeIntLE((int) lossCorrectedEnergy);
+        buf.writeIntLE((int) chargedAmount);
+        buf.writeShortLE(hardwareFault);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.transactionNo = ByteUtils.readBcd(buf, 16);
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+        this.status = buf.readByte();
+        this.gunReturned = buf.readByte();
+        this.gunConnected = buf.readByte();
+        this.outputVoltage = buf.readShortLE() & 0xFFFF;
+        this.outputCurrent = buf.readShortLE() & 0xFFFF;
+        this.gunTemperature = buf.readByte() & 0xFF;
+        this.gunLineCode = new byte[8];
+        buf.readBytes(this.gunLineCode);
+        this.soc = buf.readByte() & 0xFF;
+        this.batteryMaxTemp = buf.readByte() & 0xFF;
+        this.chargingTime = buf.readShortLE() & 0xFFFF;
+        this.remainingTime = buf.readShortLE() & 0xFFFF;
+        this.chargingEnergy = buf.readIntLE() & 0xFFFFFFFFL;
+        this.lossCorrectedEnergy = buf.readIntLE() & 0xFFFFFFFFL;
+        this.chargedAmount = buf.readIntLE() & 0xFFFFFFFFL;
+        this.hardwareFault = buf.readShortLE() & 0xFFFF;
+    }
+    
+    // ==================== 数据转换方法 ====================
+    
+    /**
+     * 获取输出电压(V)
+     */
+    public double getOutputVoltageValue() {
+        return ByteUtils.protocolToVoltage(outputVoltage);
+    }
+    
+    /**
+     * 获取输出电流(A)
+     */
+    public double getOutputCurrentValue() {
+        return ByteUtils.protocolToCurrent(outputCurrent);
+    }
+    
+    /**
+     * 获取输出功率(kW)
+     */
+    public double getOutputPower() {
+        return getOutputVoltageValue() * getOutputCurrentValue() / 1000.0;
+    }
+    
+    /**
+     * 获取枪线温度(℃)
+     */
+    public int getGunTemperatureValue() {
+        return ByteUtils.protocolToTemperature(gunTemperature);
+    }
+    
+    /**
+     * 获取电池组最高温度(℃)
+     */
+    public int getBatteryMaxTempValue() {
+        return ByteUtils.protocolToTemperature(batteryMaxTemp);
+    }
+    
+    /**
+     * 获取充电度数(kWh)
+     */
+    public double getChargingEnergyValue() {
+        return ByteUtils.protocolToEnergy(chargingEnergy);
+    }
+    
+    /**
+     * 获取计损充电度数(kWh)
+     */
+    public double getLossCorrectedEnergyValue() {
+        return ByteUtils.protocolToEnergy(lossCorrectedEnergy);
+    }
+    
+    /**
+     * 获取已充金额(元)
+     */
+    public double getChargedAmountValue() {
+        return ByteUtils.protocolToAmount(chargedAmount);
+    }
+    
+    /**
+     * 获取完整枪号
+     */
+    public String getFullGunNo() {
+        return pileCode + gunNo;
+    }
+    
+    /**
+     * 获取状态描述
+     */
+    public String getStatusDesc() {
+        switch (status) {
+            case ProtocolConstants.GUN_STATUS_OFFLINE:
+                return "离线";
+            case ProtocolConstants.GUN_STATUS_FAULT:
+                return "故障";
+            case ProtocolConstants.GUN_STATUS_IDLE:
+                return "空闲";
+            case ProtocolConstants.GUN_STATUS_CHARGING:
+                return "充电中";
+            case ProtocolConstants.GUN_STATUS_IDLE_PULSE:
+                return "空闲-脉冲静置";
+            case ProtocolConstants.GUN_STATUS_CHARGING_PULSE:
+                return "充电中-脉冲检测";
+            default:
+                return "未知";
+        }
+    }
+    
+    /**
+     * 是否正在充电
+     */
+    public boolean isCharging() {
+        return status == ProtocolConstants.GUN_STATUS_CHARGING 
+            || status == ProtocolConstants.GUN_STATUS_CHARGING_PULSE;
+    }
+    
+    /**
+     * 是否空闲
+     */
+    public boolean isIdle() {
+        return status == ProtocolConstants.GUN_STATUS_IDLE 
+            || status == ProtocolConstants.GUN_STATUS_IDLE_PULSE;
+    }
+    
+    /**
+     * 是否插枪
+     */
+    public boolean isGunConnected() {
+        return gunConnected == ProtocolConstants.GUN_CONNECTED;
+    }
+    
+    /**
+     * 解析硬件故障位
+     */
+    public String getHardwareFaultDesc() {
+        if (hardwareFault == 0) {
+            return "无故障";
+        }
+        StringBuilder sb = new StringBuilder();
+        String[] faultNames = {
+            "急停按钮动作故障", "无可用整流模块", "出风口温度过高", "交流防雷故障",
+            "交直流模块DC20通信中断", "绝缘检测模块FC08通信中断", "电度表通信中断", "读卡器通信中断",
+            "RC10通信中断", "风扇调速板故障", "直流熔断器故障", "高压接触器故障", "门打开"
+        };
+        for (int i = 0; i < faultNames.length && i < 16; i++) {
+            if ((hardwareFault & (1 << i)) != 0) {
+                if (sb.length() > 0) {
+                    sb.append(",");
+                }
+                sb.append(faultNames[i]);
+            }
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    public String toString() {
+        return String.format(
+            "RealtimeDataFrame[transNo=%s, pile=%s, gun=%s, status=%s, V=%.1f, A=%.1f, P=%.2fkW, energy=%.4fkWh, soc=%d%%, time=%dmin]",
+            transactionNo, pileCode, gunNo, getStatusDesc(),
+            getOutputVoltageValue(), getOutputCurrentValue(), getOutputPower(),
+            getChargingEnergyValue(), soc, chargingTime);
+    }
+}

+ 68 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/req/WorkParamSetRespFrame.java

@@ -0,0 +1,68 @@
+/*
+ * 文 件 名:  WorkParamSetRespFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩工作参数设置应答帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.req;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩工作参数设置应答帧 (0x51)
+ * 充电桩接收到运营平台充电桩工作参数设置时的响应
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WorkParamSetRespFrame extends BaseFrame {
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 设置结果 (BIN码 1字节)
+     * 0x00 失败, 0x01 成功
+     */
+    private byte setResult;
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET_RESP;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        buf.writeByte(setResult);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.setResult = buf.readByte();
+    }
+    
+    /**
+     * 是否设置成功
+     */
+    public boolean isSuccess() {
+        return setResult == 0x01;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("WorkParamSetRespFrame[pileCode=%s, result=%s]",
+            pileCode, isSuccess() ? "成功" : "失败");
+    }
+}

+ 89 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/HeartbeatRespFrame.java

@@ -0,0 +1,89 @@
+/*
+ * 文 件 名:  HeartbeatRespFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩心跳包应答帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.resp;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩心跳包应答帧 (0x04)
+ * 用于链路状态判断
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class HeartbeatRespFrame extends BaseFrame {
+    
+    /**
+     * 桩编码 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    /**
+     * 心跳应答 (BIN码 1字节)
+     * 置0
+     */
+    private byte heartbeatResp = 0x00;
+    
+    public HeartbeatRespFrame() {
+    }
+    
+    public HeartbeatRespFrame(String pileCode, String gunNo) {
+        this.pileCode = pileCode;
+        this.gunNo = gunNo;
+    }
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_HEARTBEAT_RESP;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+        buf.writeByte(heartbeatResp);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+        this.heartbeatResp = buf.readByte();
+    }
+    
+    /**
+     * 创建心跳应答
+     *
+     * @param pileCode   桩编码
+     * @param gunNo      枪号
+     * @param sequenceNo 序列号
+     * @return 应答帧
+     */
+    public static HeartbeatRespFrame create(String pileCode, String gunNo, int sequenceNo) {
+        HeartbeatRespFrame frame = new HeartbeatRespFrame(pileCode, gunNo);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("HeartbeatRespFrame[pileCode=%s, gunNo=%s]", pileCode, gunNo);
+    }
+}

+ 104 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/LoginRespFrame.java

@@ -0,0 +1,104 @@
+/*
+ * 文 件 名:  LoginRespFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩登录认证应答帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.resp;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩登录认证应答帧 (0x02)
+ * 运营平台回复电桩登录结果
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LoginRespFrame extends BaseFrame {
+    
+    /**
+     * 桩编码 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 登录结果 (BIN码 1字节)
+     * 0x00:登录成功, 0x01:登录失败
+     */
+    private byte loginResult;
+    
+    public LoginRespFrame() {
+    }
+    
+    public LoginRespFrame(String pileCode, boolean success) {
+        this.pileCode = pileCode;
+        this.loginResult = success ? ProtocolConstants.LOGIN_SUCCESS : ProtocolConstants.LOGIN_FAIL;
+    }
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_LOGIN_RESP;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        buf.writeByte(loginResult);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.loginResult = buf.readByte();
+    }
+    
+    /**
+     * 是否登录成功
+     *
+     * @return 是否成功
+     */
+    public boolean isSuccess() {
+        return loginResult == ProtocolConstants.LOGIN_SUCCESS;
+    }
+    
+    /**
+     * 创建成功应答
+     *
+     * @param pileCode   桩编码
+     * @param sequenceNo 序列号
+     * @return 应答帧
+     */
+    public static LoginRespFrame success(String pileCode, int sequenceNo) {
+        LoginRespFrame frame = new LoginRespFrame(pileCode, true);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    /**
+     * 创建失败应答
+     *
+     * @param pileCode   桩编码
+     * @param sequenceNo 序列号
+     * @return 应答帧
+     */
+    public static LoginRespFrame fail(String pileCode, int sequenceNo) {
+        LoginRespFrame frame = new LoginRespFrame(pileCode, false);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("LoginRespFrame[pileCode=%s, result=%s]",
+            pileCode, isSuccess() ? "成功" : "失败");
+    }
+}

+ 81 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/ReadRealtimeDataFrame.java

@@ -0,0 +1,81 @@
+/*
+ * 文 件 名:  ReadRealtimeDataFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  读取实时监测数据请求帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.resp;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 读取实时监测数据请求帧 (0x12)
+ * 运营平台根据需要主动发起读取实时数据的请求
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ReadRealtimeDataFrame extends BaseFrame {
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 枪号 (BCD码 1字节)
+     */
+    private String gunNo;
+    
+    public ReadRealtimeDataFrame() {
+    }
+    
+    public ReadRealtimeDataFrame(String pileCode, String gunNo) {
+        this.pileCode = pileCode;
+        this.gunNo = gunNo;
+    }
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_READ_REALTIME_DATA;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        ByteUtils.writeBcd(buf, gunNo, 1);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.gunNo = ByteUtils.readBcd(buf, 1);
+    }
+    
+    /**
+     * 创建读取实时数据请求
+     *
+     * @param pileCode   桩编码
+     * @param gunNo      枪号
+     * @param sequenceNo 序列号
+     * @return 请求帧
+     */
+    public static ReadRealtimeDataFrame create(String pileCode, String gunNo, int sequenceNo) {
+        ReadRealtimeDataFrame frame = new ReadRealtimeDataFrame(pileCode, gunNo);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("ReadRealtimeDataFrame[pileCode=%s, gunNo=%s]", pileCode, gunNo);
+    }
+}

+ 127 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/model/resp/WorkParamSetFrame.java

@@ -0,0 +1,127 @@
+/*
+ * 文 件 名:  WorkParamSetFrame
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩工作参数设置帧
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.model.resp;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import io.netty.buffer.ByteBuf;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 充电桩工作参数设置帧 (0x52)
+ * 远程设置充电桩是否停用;设置充电桩允许输出功率,以实现电网功率的调节
+ * 这是能耗平台进行功率调控的关键帧
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WorkParamSetFrame extends BaseFrame {
+    
+    /**
+     * 桩编号 (BCD码 7字节)
+     */
+    private String pileCode;
+    
+    /**
+     * 是否允许工作 (BIN码 1字节)
+     * 0x00 表示允许正常工作
+     * 0x01 表示停止使用,锁定充电桩
+     */
+    private byte allowWork;
+    
+    /**
+     * 充电桩最大允许输出功率 (BIN码 1字节)
+     * 1BIN表示1%,最大100%,最小30%
+     */
+    private int maxOutputPowerPercent;
+    
+    public WorkParamSetFrame() {
+    }
+    
+    public WorkParamSetFrame(String pileCode, boolean allowWork, int maxOutputPowerPercent) {
+        this.pileCode = pileCode;
+        this.allowWork = allowWork ? ProtocolConstants.WORK_ALLOW : ProtocolConstants.WORK_DISABLE;
+        this.maxOutputPowerPercent = Math.max(30, Math.min(100, maxOutputPowerPercent));
+    }
+    
+    @Override
+    public byte getFrameType() {
+        return ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET;
+    }
+    
+    @Override
+    protected void encodeBody(ByteBuf buf) {
+        ByteUtils.writeBcd(buf, pileCode, 7);
+        buf.writeByte(allowWork);
+        buf.writeByte(maxOutputPowerPercent);
+    }
+    
+    @Override
+    protected void decodeBody(ByteBuf buf) {
+        this.pileCode = ByteUtils.readBcd(buf, 7);
+        this.allowWork = buf.readByte();
+        this.maxOutputPowerPercent = buf.readByte() & 0xFF;
+    }
+    
+    /**
+     * 是否允许工作
+     */
+    public boolean isAllowWork() {
+        return allowWork == ProtocolConstants.WORK_ALLOW;
+    }
+    
+    /**
+     * 创建功率限制设置
+     *
+     * @param pileCode             桩编码
+     * @param maxOutputPowerPercent 最大输出功率百分比(30-100)
+     * @param sequenceNo           序列号
+     * @return 设置帧
+     */
+    public static WorkParamSetFrame createPowerLimit(String pileCode, int maxOutputPowerPercent, int sequenceNo) {
+        WorkParamSetFrame frame = new WorkParamSetFrame(pileCode, true, maxOutputPowerPercent);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    /**
+     * 创建启用充电桩设置
+     *
+     * @param pileCode   桩编码
+     * @param sequenceNo 序列号
+     * @return 设置帧
+     */
+    public static WorkParamSetFrame createEnable(String pileCode, int sequenceNo) {
+        WorkParamSetFrame frame = new WorkParamSetFrame(pileCode, true, 100);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    /**
+     * 创建禁用充电桩设置
+     *
+     * @param pileCode   桩编码
+     * @param sequenceNo 序列号
+     * @return 设置帧
+     */
+    public static WorkParamSetFrame createDisable(String pileCode, int sequenceNo) {
+        WorkParamSetFrame frame = new WorkParamSetFrame(pileCode, false, 0);
+        frame.setSequenceNo(sequenceNo);
+        return frame;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("WorkParamSetFrame[pileCode=%s, allowWork=%s, maxPower=%d%%]",
+            pileCode, isAllowWork() ? "是" : "否", maxOutputPowerPercent);
+    }
+}

+ 230 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/protocol/FrameParser.java

@@ -0,0 +1,230 @@
+/*
+ * 文 件 名:  FrameParser
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  协议帧解析器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.protocol;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.req.*;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import com.ruoyi.ems.charging.utils.CRC16Utils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 协议帧解析器
+ * 负责解析充电桩上送的各类消息帧
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public class FrameParser {
+    
+    private static final Logger log = LoggerFactory.getLogger(FrameParser.class);
+    
+    /**
+     * 解析帧数据
+     *
+     * @param data 原始字节数据
+     * @return 解析后的帧对象,解析失败返回null
+     */
+    public static BaseFrame parse(byte[] data) {
+        if (data == null || data.length < 6) {
+            log.warn("数据长度不足,无法解析");
+            return null;
+        }
+        
+        ByteBuf buf = Unpooled.wrappedBuffer(data);
+        try {
+            return parse(buf);
+        } finally {
+            buf.release();
+        }
+    }
+    
+    /**
+     * 解析帧数据
+     *
+     * @param buf ByteBuf
+     * @return 解析后的帧对象,解析失败返回null
+     */
+    public static BaseFrame parse(ByteBuf buf) {
+        if (buf.readableBytes() < 6) {
+            log.warn("数据长度不足,无法解析");
+            return null;
+        }
+        
+        // 标记读取位置
+        buf.markReaderIndex();
+        
+        // 读取并验证起始标志
+        byte startFlag = buf.readByte();
+        if (startFlag != ProtocolConstants.START_FLAG) {
+            log.warn("起始标志错误: 0x{}", String.format("%02X", startFlag & 0xFF));
+            buf.resetReaderIndex();
+            return null;
+        }
+        
+        // 读取数据长度
+        int dataLength = buf.readByte() & 0xFF;
+        
+        // 检查数据完整性
+        if (buf.readableBytes() < dataLength + 2) { // 数据域 + CRC
+            log.warn("数据不完整,期望长度: {}, 实际可读: {}", dataLength + 2, buf.readableBytes());
+            buf.resetReaderIndex();
+            return null;
+        }
+        
+        // 验证CRC
+        int crcStartIndex = buf.readerIndex();
+        byte[] crcData = new byte[dataLength];
+        buf.getBytes(crcStartIndex, crcData);
+        int calculatedCrc = CRC16Utils.calculateCRC(crcData);
+        
+        // 读取接收到的CRC
+        int receivedCrc = buf.getShortLE(crcStartIndex + dataLength) & 0xFFFF;
+        
+        if (calculatedCrc != receivedCrc) {
+            log.warn("CRC校验失败,计算值: 0x{}, 接收值: 0x{}", 
+                String.format("%04X", calculatedCrc),
+                String.format("%04X", receivedCrc));
+            buf.resetReaderIndex();
+            return null;
+        }
+        
+        // 读取序列号、加密标志、帧类型
+        int sequenceNo = buf.readShortLE() & 0xFFFF;
+        byte encryptFlag = buf.readByte();
+        byte frameType = buf.readByte();
+        
+        // 根据帧类型创建对应的帧对象
+        BaseFrame frame = createFrame(frameType);
+        if (frame == null) {
+            log.warn("未知的帧类型: 0x{}", String.format("%02X", frameType & 0xFF));
+            buf.resetReaderIndex();
+            return null;
+        }
+        
+        // 重置读取位置到起始标志
+        buf.resetReaderIndex();
+        
+        // 解码完整帧
+        if (!frame.decode(buf)) {
+            log.warn("帧解码失败: {}", frame.getClass().getSimpleName());
+            return null;
+        }
+        
+        log.debug("解析帧成功: {}", frame);
+        return frame;
+    }
+    
+    /**
+     * 根据帧类型创建对应的帧对象
+     *
+     * @param frameType 帧类型
+     * @return 帧对象
+     */
+    private static BaseFrame createFrame(byte frameType) {
+        switch (frameType) {
+            // 充电桩发起的帧(奇数)
+            case ProtocolConstants.FRAME_TYPE_LOGIN_REQ:
+                return new LoginReqFrame();
+            case ProtocolConstants.FRAME_TYPE_HEARTBEAT_REQ:
+                return new HeartbeatReqFrame();
+            case ProtocolConstants.FRAME_TYPE_REALTIME_DATA:
+                return new RealtimeDataFrame();
+            case ProtocolConstants.FRAME_TYPE_CHARGING_HANDSHAKE:
+                return new ChargingHandshakeFrame();
+            case ProtocolConstants.FRAME_TYPE_CHARGING_END:
+                return new ChargingEndFrame();
+            case ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET_RESP:
+                return new WorkParamSetRespFrame();
+            // 可以根据需要继续添加其他帧类型
+            default:
+                // 对于未实现的帧类型,使用通用帧处理
+                return new GenericFrame(frameType);
+        }
+    }
+    
+    /**
+     * 从ByteBuf中查找帧起始位置
+     *
+     * @param buf ByteBuf
+     * @return 起始位置索引,未找到返回-1
+     */
+    public static int findFrameStart(ByteBuf buf) {
+        int readerIndex = buf.readerIndex();
+        while (buf.readableBytes() > 0) {
+            byte b = buf.readByte();
+            if (b == ProtocolConstants.START_FLAG) {
+                int startIndex = buf.readerIndex() - 1;
+                buf.readerIndex(readerIndex);
+                return startIndex - readerIndex;
+            }
+        }
+        buf.readerIndex(readerIndex);
+        return -1;
+    }
+    
+    /**
+     * 获取完整帧长度
+     *
+     * @param buf ByteBuf
+     * @return 帧长度,数据不完整返回-1
+     */
+    public static int getFrameLength(ByteBuf buf) {
+        if (buf.readableBytes() < 2) {
+            return -1;
+        }
+        buf.markReaderIndex();
+        buf.skipBytes(1); // 跳过起始标志
+        int dataLength = buf.readByte() & 0xFF;
+        buf.resetReaderIndex();
+        
+        // 完整帧长度 = 起始标志(1) + 数据长度(1) + 数据域(dataLength) + CRC(2)
+        return 1 + 1 + dataLength + 2;
+    }
+    
+    /**
+     * 通用帧 - 用于处理未实现的帧类型
+     */
+    private static class GenericFrame extends BaseFrame {
+        private byte type;
+        private byte[] bodyData;
+        
+        public GenericFrame(byte type) {
+            this.type = type;
+        }
+        
+        @Override
+        public byte getFrameType() {
+            return type;
+        }
+        
+        @Override
+        protected void encodeBody(ByteBuf buf) {
+            if (bodyData != null) {
+                buf.writeBytes(bodyData);
+            }
+        }
+        
+        @Override
+        protected void decodeBody(ByteBuf buf) {
+            if (buf.readableBytes() > 0) {
+                bodyData = new byte[buf.readableBytes()];
+                buf.readBytes(bodyData);
+            }
+        }
+        
+        @Override
+        public String toString() {
+            return String.format("GenericFrame[type=0x%02X, bodyLen=%d]", 
+                type & 0xFF, bodyData != null ? bodyData.length : 0);
+        }
+    }
+}

+ 500 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/protocol/ProtocolConstants.java

@@ -0,0 +1,500 @@
+/*
+ * 文 件 名:  ProtocolConstants
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩协议常量定义
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.protocol;
+
+/**
+ * 充电桩协议常量定义
+ * 基于《充电桩与运营平台交互协议V2.0》
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public final class ProtocolConstants {
+    
+    private ProtocolConstants() {
+        // 私有构造函数,防止实例化
+    }
+    
+    // ==================== 协议基础常量 ====================
+    
+    /**
+     * 起始标志
+     */
+    public static final byte START_FLAG = 0x68;
+    
+    /**
+     * 协议版本 V2.0 = 0x14 (版本号乘10)
+     */
+    public static final byte PROTOCOL_VERSION = 0x14;
+    
+    /**
+     * 数据域最大长度
+     */
+    public static final int MAX_DATA_LENGTH = 200;
+    
+    /**
+     * 最大帧长度 (起始标志1 + 数据长度1 + 数据域最大200 + 帧校验2)
+     */
+    public static final int MAX_FRAME_LENGTH = 204;
+    
+    // ==================== 加密标志 ====================
+    
+    /**
+     * 不加密
+     */
+    public static final byte ENCRYPT_NONE = 0x00;
+    
+    /**
+     * 3DES加密
+     */
+    public static final byte ENCRYPT_3DES = 0x01;
+    
+    // ==================== 帧类型码定义 - 充电桩发起(奇数) ====================
+    
+    /**
+     * 充电桩登录认证 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_LOGIN_REQ = 0x01;
+    
+    /**
+     * 充电桩心跳包 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_HEARTBEAT_REQ = 0x03;
+    
+    /**
+     * 计费模型验证请求 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BILLING_VERIFY_REQ = 0x05;
+    
+    /**
+     * 充电桩计费模型请求 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BILLING_REQ = 0x09;
+    
+    /**
+     * 实时监测数据上送 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_REALTIME_DATA = 0x13;
+    
+    /**
+     * 充电握手 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CHARGING_HANDSHAKE = 0x15;
+    
+    /**
+     * 参数配置 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_PARAM_CONFIG = 0x17;
+    
+    /**
+     * 充电结束 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CHARGING_END = 0x19;
+    
+    /**
+     * 错误报文 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_ERROR = 0x1B;
+    
+    /**
+     * 充电阶段BMS中止 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BMS_STOP = 0x1D;
+    
+    /**
+     * 充电阶段充电机中止 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CHARGER_STOP = 0x21;
+    
+    /**
+     * 充电过程BMS需求、充电机输出 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BMS_DEMAND = 0x23;
+    
+    /**
+     * 充电过程BMS信息 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BMS_INFO = 0x25;
+    
+    /**
+     * 充电桩主动申请启动充电 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_START_CHARGE_REQ = 0x31;
+    
+    /**
+     * 远程启机命令回复 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_REMOTE_START_RESP = 0x33;
+    
+    /**
+     * 远程停机命令回复 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_REMOTE_STOP_RESP = 0x35;
+    
+    /**
+     * 交易记录 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_TRANSACTION = 0x3B;
+    
+    /**
+     * 余额更新应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BALANCE_UPDATE_RESP = 0x41;
+    
+    /**
+     * 卡数据同步应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CARD_SYNC_RESP = 0x43;
+    
+    /**
+     * 离线卡数据清除应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CARD_CLEAR_RESP = 0x45;
+    
+    /**
+     * 离线卡数据查询应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_CARD_QUERY_RESP = 0x47;
+    
+    /**
+     * 充电桩工作参数设置应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_WORK_PARAM_SET_RESP = 0x51;
+    
+    /**
+     * 对时设置应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_TIME_SYNC_RESP = 0x55;
+    
+    /**
+     * 计费模型应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_BILLING_SET_RESP = 0x57;
+    
+    /**
+     * 二维码设置应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_QRCODE_SET_RESP = 0x59;
+    
+    /**
+     * 地锁数据上送 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_LOCK_DATA = 0x61;
+    
+    /**
+     * 地锁控制返回 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_LOCK_CONTROL_RESP = 0x63;
+    
+    /**
+     * 脉冲检测设置响应 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_PULSE_SET_RESP = (byte) 0x81;
+    
+    /**
+     * 脉冲检测数据 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_PULSE_DATA = (byte) 0x83;
+    
+    /**
+     * 远程重启应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_REMOTE_RESTART_RESP = (byte) 0x91;
+    
+    /**
+     * 远程更新应答 (充电桩->运营平台)
+     */
+    public static final byte FRAME_TYPE_REMOTE_UPDATE_RESP = (byte) 0x93;
+    
+    // ==================== 帧类型码定义 - 运营平台发起(偶数) ====================
+    
+    /**
+     * 登录认证应答 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_LOGIN_RESP = 0x02;
+    
+    /**
+     * 心跳包应答 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_HEARTBEAT_RESP = 0x04;
+    
+    /**
+     * 计费模型验证请求应答 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_BILLING_VERIFY_RESP = 0x06;
+    
+    /**
+     * 计费模型请求应答 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_BILLING_RESP = 0x0A;
+    
+    /**
+     * 读取实时监测数据 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_READ_REALTIME_DATA = 0x12;
+    
+    /**
+     * 运营平台确认启动充电 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_START_CHARGE_RESP = 0x32;
+    
+    /**
+     * 运营平台远程控制启机 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_REMOTE_START = 0x34;
+    
+    /**
+     * 运营平台远程停机 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_REMOTE_STOP = 0x36;
+    
+    /**
+     * 交易记录确认 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_TRANSACTION_CONFIRM = 0x40;
+    
+    /**
+     * 远程账户余额更新 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_BALANCE_UPDATE = 0x42;
+    
+    /**
+     * 离线卡数据同步 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_CARD_SYNC = 0x44;
+    
+    /**
+     * 离线卡数据清除 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_CARD_CLEAR = 0x46;
+    
+    /**
+     * 离线卡数据查询 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_CARD_QUERY = 0x48;
+    
+    /**
+     * 充电桩工作参数设置 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_WORK_PARAM_SET = 0x52;
+    
+    /**
+     * 对时设置 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_TIME_SYNC = 0x56;
+    
+    /**
+     * 计费模型设置 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_BILLING_SET = 0x58;
+    
+    /**
+     * 二维码设置 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_QRCODE_SET = 0x5A;
+    
+    /**
+     * 遥控地锁升锁与降锁命令 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_LOCK_CONTROL = 0x62;
+    
+    /**
+     * 脉冲检测设置 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_PULSE_SET = (byte) 0x82;
+    
+    /**
+     * 远程重启 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_REMOTE_RESTART = (byte) 0x92;
+    
+    /**
+     * 远程更新 (运营平台->充电桩)
+     */
+    public static final byte FRAME_TYPE_REMOTE_UPDATE = (byte) 0x94;
+    
+    // ==================== 充电桩状态定义 ====================
+    
+    /**
+     * 离线
+     */
+    public static final byte GUN_STATUS_OFFLINE = 0x00;
+    
+    /**
+     * 故障
+     */
+    public static final byte GUN_STATUS_FAULT = 0x01;
+    
+    /**
+     * 空闲
+     */
+    public static final byte GUN_STATUS_IDLE = 0x02;
+    
+    /**
+     * 充电中
+     */
+    public static final byte GUN_STATUS_CHARGING = 0x03;
+    
+    /**
+     * 空闲-脉冲静置
+     */
+    public static final byte GUN_STATUS_IDLE_PULSE = 0x04;
+    
+    /**
+     * 充电中-脉冲检测
+     */
+    public static final byte GUN_STATUS_CHARGING_PULSE = 0x05;
+    
+    // ==================== 充电桩类型定义 ====================
+    
+    /**
+     * 直流桩
+     */
+    public static final byte PILE_TYPE_DC = 0x00;
+    
+    /**
+     * 交流桩
+     */
+    public static final byte PILE_TYPE_AC = 0x01;
+    
+    // ==================== 网络链接类型 ====================
+    
+    /**
+     * SIM卡
+     */
+    public static final byte NETWORK_TYPE_SIM = 0x00;
+    
+    /**
+     * LAN
+     */
+    public static final byte NETWORK_TYPE_LAN = 0x01;
+    
+    /**
+     * WAN
+     */
+    public static final byte NETWORK_TYPE_WAN = 0x02;
+    
+    /**
+     * 其他
+     */
+    public static final byte NETWORK_TYPE_OTHER = 0x03;
+    
+    // ==================== 登录结果 ====================
+    
+    /**
+     * 登录成功
+     */
+    public static final byte LOGIN_SUCCESS = 0x00;
+    
+    /**
+     * 登录失败
+     */
+    public static final byte LOGIN_FAIL = 0x01;
+    
+    // ==================== 计费模型验证结果 ====================
+    
+    /**
+     * 计费模型一致
+     */
+    public static final byte BILLING_VERIFY_MATCH = 0x00;
+    
+    /**
+     * 计费模型不一致
+     */
+    public static final byte BILLING_VERIFY_MISMATCH = 0x01;
+    
+    // ==================== 工作参数设置 ====================
+    
+    /**
+     * 允许正常工作
+     */
+    public static final byte WORK_ALLOW = 0x00;
+    
+    /**
+     * 停止使用,锁定充电桩
+     */
+    public static final byte WORK_DISABLE = 0x01;
+    
+    // ==================== 枪归位状态 ====================
+    
+    /**
+     * 未归位
+     */
+    public static final byte GUN_NOT_RETURNED = 0x00;
+    
+    /**
+     * 已归位
+     */
+    public static final byte GUN_RETURNED = 0x01;
+    
+    /**
+     * 未知
+     */
+    public static final byte GUN_UNKNOWN = 0x02;
+    
+    // ==================== 是否插枪 ====================
+    
+    /**
+     * 未插枪
+     */
+    public static final byte GUN_NOT_CONNECTED = 0x00;
+    
+    /**
+     * 已插枪
+     */
+    public static final byte GUN_CONNECTED = 0x01;
+    
+    // ==================== 心跳超时配置 ====================
+    
+    /**
+     * 心跳间隔(秒)
+     */
+    public static final int HEARTBEAT_INTERVAL_SECONDS = 10;
+    
+    /**
+     * 心跳超时次数
+     */
+    public static final int HEARTBEAT_TIMEOUT_COUNT = 3;
+    
+    // ==================== 长度常量 ====================
+    
+    /**
+     * 桩编号长度 (BCD码 7字节)
+     */
+    public static final int PILE_CODE_LENGTH = 7;
+    
+    /**
+     * 枪号长度 (BCD码 1字节)
+     */
+    public static final int GUN_NO_LENGTH = 1;
+    
+    /**
+     * 交易流水号长度 (BCD码 16字节)
+     */
+    public static final int TRANSACTION_NO_LENGTH = 16;
+    
+    /**
+     * CP56Time2a时间格式长度
+     */
+    public static final int CP56TIME2A_LENGTH = 7;
+    
+    /**
+     * 物理卡号长度
+     */
+    public static final int PHYSICAL_CARD_NO_LENGTH = 8;
+    
+    /**
+     * 逻辑卡号长度
+     */
+    public static final int LOGICAL_CARD_NO_LENGTH = 8;
+    
+    /**
+     * VIN码长度
+     */
+    public static final int VIN_LENGTH = 17;
+}

+ 292 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/service/ChargingDataService.java

@@ -0,0 +1,292 @@
+/*
+ * 文 件 名:  ChargingDataService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电数据服务
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ * 修改内容:  修复日志格式化问题和内存泄漏
+ */
+package com.ruoyi.ems.charging.service;
+
+import com.ruoyi.ems.charging.model.ChargingSession;
+import com.ruoyi.ems.charging.model.RealtimeDataCache;
+import com.ruoyi.ems.charging.model.req.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 充电数据服务
+ * 处理充电桩上报的各类数据,转化为能耗数据存储
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@Service
+public class ChargingDataService {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingDataService.class);
+
+    /**
+     * 缓存过期时间(小时) - 超过此时间未更新的缓存将被清理
+     */
+    private static final int CACHE_EXPIRE_HOURS = 24;
+
+    /**
+     * 实时数据缓存 - 枪号 -> 最新实时数据
+     */
+    private final Map<String, RealtimeDataCache> realtimeDataCache = new ConcurrentHashMap<>();
+
+    /**
+     * 充电会话缓存 - 交易流水号 -> 充电会话
+     */
+    private final Map<String, ChargingSession> chargingSessionCache = new ConcurrentHashMap<>();
+
+    /**
+     * 处理充电桩登录
+     */
+    @Async("msgExecExecutor")
+    public void onPileLogin(LoginReqFrame req, boolean valid) {
+        if (valid) {
+            log.info("充电桩登录成功 - 桩号: {}, 类型: {}, 枪数: {}, 协议版本: {}, 软件版本: {}",
+                req.getPileCode(), req.getPileTypeDesc(), req.getGunCount(),
+                req.getProtocolVersionDesc(), req.getSoftwareVersion());
+        }
+    }
+
+    /**
+     * 处理枪故障
+     */
+    @Async("msgExecExecutor")
+    public void onGunFault(String pileCode, String gunNo) {
+        String fullGunNo = pileCode + gunNo;
+        log.warn("充电枪故障告警 - 枪号: {}", fullGunNo);
+    }
+
+    /**
+     * 处理实时监测数据 - 能耗数据核心处理
+     */
+    @Async("msgExecExecutor")
+    public void processRealtimeData(RealtimeDataFrame data) {
+        String fullGunNo = data.getFullGunNo();
+        LocalDateTime now = LocalDateTime.now();
+
+        RealtimeDataCache cache = realtimeDataCache.computeIfAbsent(fullGunNo, k -> new RealtimeDataCache());
+
+        cache.setLastData(data);
+        cache.setUpdateTime(now);
+
+        if (data.isCharging()) {
+            processChargingData(data, cache, now);
+        }
+
+        if (cache.getLastStatus() != data.getStatus()) {
+            log.info("充电枪状态变化 - 枪号: {}, 状态: {} -> {}",
+                fullGunNo, getStatusDesc(cache.getLastStatus()), data.getStatusDesc());
+            cache.setLastStatus(data.getStatus());
+        }
+
+        saveRealtimeData(data);
+    }
+
+    private void processChargingData(RealtimeDataFrame data, RealtimeDataCache cache, LocalDateTime now) {
+        String transactionNo = data.getTransactionNo();
+
+        ChargingSession session = chargingSessionCache.computeIfAbsent(transactionNo, k -> {
+            ChargingSession newSession = new ChargingSession();
+            newSession.setTransactionNo(transactionNo);
+            newSession.setFullGunNo(data.getFullGunNo());
+            newSession.setStartTime(now);
+            newSession.setStartEnergy(data.getChargingEnergyValue());
+            log.info("新充电会话开始 - 流水号: {}, 枪号: {}", transactionNo, data.getFullGunNo());
+            return newSession;
+        });
+
+        session.setLastUpdateTime(now);
+        session.setCurrentEnergy(data.getChargingEnergyValue());
+        session.setCurrentPower(data.getOutputPower());
+        session.setCurrentSoc(data.getSoc());
+        session.setChargingTime(data.getChargingTime());
+
+        double energyDelta = data.getChargingEnergyValue() - cache.getLastEnergy();
+        if (energyDelta > 0) {
+            session.setTotalEnergy(session.getTotalEnergy() + energyDelta);
+        }
+
+        cache.setLastEnergy(data.getChargingEnergyValue());
+    }
+
+    private void saveRealtimeData(RealtimeDataFrame data) {
+        // 修复: SLF4J使用{}占位符,需要预先格式化浮点数
+        if (log.isDebugEnabled()) {
+            log.debug("保存实时数据 - 枪号: {}, 状态: {}, 功率: {}kW, 电量: {}kWh",
+                data.getFullGunNo(), data.getStatusDesc(),
+                String.format("%.2f", data.getOutputPower()),
+                String.format("%.4f", data.getChargingEnergyValue()));
+        }
+
+        // TODO: 实际保存到数据库的逻辑
+    }
+
+    /**
+     * 处理充电握手
+     */
+    @Async("msgExecExecutor")
+    public void onChargingHandshake(ChargingHandshakeFrame handshake) {
+        // 修复: 使用String.format格式化浮点数
+        log.info("充电握手 - 流水号: {}, 枪号: {}, VIN: {}, 电池类型: {}, 容量: {}Ah",
+            handshake.getTransactionNo(), handshake.getFullGunNo(),
+            handshake.getVin(), handshake.getBatteryTypeDesc(),
+            String.format("%.1f", handshake.getRatedCapacityValue()));
+
+        ChargingSession session = chargingSessionCache.get(handshake.getTransactionNo());
+        if (session != null) {
+            session.setVin(handshake.getVin());
+            session.setBatteryType(handshake.getBatteryTypeDesc());
+            session.setRatedCapacity(handshake.getRatedCapacityValue());
+        }
+    }
+
+    /**
+     * 处理充电结束
+     */
+    @Async("msgExecExecutor")
+    public void onChargingEnd(ChargingEndFrame end) {
+        String transactionNo = end.getTransactionNo();
+
+        // 修复: 使用String.format格式化浮点数
+        log.info("充电结束 - 流水号: {}, 枪号: {}, SOC: {}%, 能量: {}kWh, 时长: {}min",
+            transactionNo, end.getFullGunNo(), end.getBmsFinalSoc(),
+            String.format("%.1f", end.getOutputEnergyValue()),
+            end.getTotalChargingTime());
+
+        ChargingSession session = chargingSessionCache.remove(transactionNo);
+        if (session != null) {
+            session.setEndTime(LocalDateTime.now());
+            session.setFinalSoc(end.getBmsFinalSoc());
+            session.setTotalEnergy(end.getOutputEnergyValue());
+            session.setChargingTime(end.getTotalChargingTime());
+
+            // TODO: 保存充电会话记录到数据库
+        }
+
+        RealtimeDataCache cache = realtimeDataCache.get(end.getFullGunNo());
+        if (cache != null) {
+            cache.setLastEnergy(0);
+        }
+    }
+
+    /**
+     * 处理工作参数设置应答
+     */
+    @Async("msgExecExecutor")
+    public void onWorkParamSetResp(WorkParamSetRespFrame resp) {
+        log.info("工作参数设置应答 - 桩号: {}, 结果: {}", resp.getPileCode(), resp.isSuccess() ? "成功" : "失败");
+    }
+
+    /**
+     * 定期清理过期缓存 - 每小时执行一次
+     * 防止内存泄漏
+     */
+    @Scheduled(fixedRate = 3600000) // 1小时
+    public void cleanExpiredCache() {
+        LocalDateTime expireTime = LocalDateTime.now().minus(CACHE_EXPIRE_HOURS, ChronoUnit.HOURS);
+        int cleanedRealtimeCount = 0;
+        int cleanedSessionCount = 0;
+
+        // 清理过期的实时数据缓存
+        Iterator<Map.Entry<String, RealtimeDataCache>> realtimeIterator = realtimeDataCache.entrySet().iterator();
+        while (realtimeIterator.hasNext()) {
+            Map.Entry<String, RealtimeDataCache> entry = realtimeIterator.next();
+            RealtimeDataCache cache = entry.getValue();
+            if (cache.getUpdateTime() != null && cache.getUpdateTime().isBefore(expireTime)) {
+                realtimeIterator.remove();
+                cleanedRealtimeCount++;
+            }
+        }
+
+        // 清理过期的充电会话缓存(超时未结束的会话)
+        Iterator<Map.Entry<String, ChargingSession>> sessionIterator = chargingSessionCache.entrySet().iterator();
+        while (sessionIterator.hasNext()) {
+            Map.Entry<String, ChargingSession> entry = sessionIterator.next();
+            ChargingSession session = entry.getValue();
+            if (session.getLastUpdateTime() != null && session.getLastUpdateTime().isBefore(expireTime)) {
+                sessionIterator.remove();
+                cleanedSessionCount++;
+                log.warn("清理超时充电会话 - 流水号: {}, 枪号: {}", session.getTransactionNo(), session.getFullGunNo());
+            }
+        }
+
+        if (cleanedRealtimeCount > 0 || cleanedSessionCount > 0) {
+            log.info("缓存清理完成 - 清理实时数据缓存: {}条, 充电会话缓存: {}条, 剩余实时缓存: {}条, 剩余会话缓存: {}条",
+                cleanedRealtimeCount, cleanedSessionCount,
+                realtimeDataCache.size(), chargingSessionCache.size());
+        }
+    }
+
+    /**
+     * 清理指定桩的缓存(桩下线时调用)
+     */
+    public void onPileOffline(String pileCode) {
+        int cleanedCount = 0;
+
+        // 清理该桩所有枪的实时数据缓存
+        Iterator<Map.Entry<String, RealtimeDataCache>> iterator = realtimeDataCache.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<String, RealtimeDataCache> entry = iterator.next();
+            if (entry.getKey().startsWith(pileCode)) {
+                iterator.remove();
+                cleanedCount++;
+            }
+        }
+
+        if (cleanedCount > 0) {
+            log.info("桩下线清理缓存 - 桩号: {}, 清理枪缓存: {}条", pileCode, cleanedCount);
+        }
+    }
+
+    private String getStatusDesc(byte status) {
+        switch (status) {
+            case 0x00:
+                return "离线";
+            case 0x01:
+                return "故障";
+            case 0x02:
+                return "空闲";
+            case 0x03:
+                return "充电中";
+            case 0x04:
+                return "空闲-脉冲静置";
+            case 0x05:
+                return "充电中-脉冲检测";
+            default:
+                return "未知";
+        }
+    }
+
+    public RealtimeDataCache getRealtimeDataCache(String fullGunNo) {
+        return realtimeDataCache.get(fullGunNo);
+    }
+
+    public ChargingSession getChargingSession(String transactionNo) {
+        return chargingSessionCache.get(transactionNo);
+    }
+
+    /**
+     * 获取缓存统计信息(用于监控)
+     */
+    public Map<String, Integer> getCacheStats() {
+        Map<String, Integer> stats = new ConcurrentHashMap<>();
+        stats.put("realtimeDataCacheSize", realtimeDataCache.size());
+        stats.put("chargingSessionCacheSize", chargingSessionCache.size());
+        return stats;
+    }
+}

+ 214 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/service/PowerControlService.java

@@ -0,0 +1,214 @@
+/*
+ * 文 件 名:  PowerControlService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  功率控制服务
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.service;
+
+import com.ruoyi.ems.charging.core.ChargingPileSessionManager;
+import com.ruoyi.ems.charging.model.resp.ReadRealtimeDataFrame;
+import com.ruoyi.ems.charging.model.resp.WorkParamSetFrame;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 功率控制服务
+ * 用于能耗平台对充电桩进行功率调控
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+@Service
+public class PowerControlService {
+    
+    private static final Logger log = LoggerFactory.getLogger(PowerControlService.class);
+    
+    @Autowired
+    private ChargingPileSessionManager sessionManager;
+    
+    /**
+     * 待处理的设置请求回调 - 序列号 -> Future
+     */
+    private final ConcurrentHashMap<Integer, CompletableFuture<Boolean>> pendingRequests = new ConcurrentHashMap<>();
+    
+    /**
+     * 设置充电桩最大输出功率百分比
+     *
+     * @param pileCode             桩编号
+     * @param maxOutputPowerPercent 最大输出功率百分比(30-100)
+     * @return 是否发送成功
+     */
+    public boolean setMaxOutputPower(String pileCode, int maxOutputPowerPercent) {
+        if (!sessionManager.isOnline(pileCode)) {
+            log.warn("充电桩[{}]不在线,无法设置功率", pileCode);
+            return false;
+        }
+        
+        Channel channel = sessionManager.getChannel(pileCode);
+        if (channel == null || !channel.isActive()) {
+            log.warn("充电桩[{}]通道无效", pileCode);
+            return false;
+        }
+        
+        ChargingPileSessionManager.PileSession session = sessionManager.getSession(pileCode);
+        int sequenceNo = session.nextSequenceNo();
+        
+        WorkParamSetFrame frame = WorkParamSetFrame.createPowerLimit(pileCode, maxOutputPowerPercent, sequenceNo);
+        
+        ChannelFuture future = channel.writeAndFlush(frame);
+        
+        try {
+            future.await(5, TimeUnit.SECONDS);
+            if (future.isSuccess()) {
+                log.info("功率限制指令发送成功 - 桩号: {}, 功率限制: {}%", pileCode, maxOutputPowerPercent);
+                return true;
+            } else {
+                log.error("功率限制指令发送失败 - 桩号: {}", pileCode, future.cause());
+                return false;
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("功率限制指令发送超时 - 桩号: {}", pileCode);
+            return false;
+        }
+    }
+    
+    /**
+     * 启用充电桩
+     *
+     * @param pileCode 桩编号
+     * @return 是否发送成功
+     */
+    public boolean enablePile(String pileCode) {
+        if (!sessionManager.isOnline(pileCode)) {
+            log.warn("充电桩[{}]不在线,无法启用", pileCode);
+            return false;
+        }
+        
+        Channel channel = sessionManager.getChannel(pileCode);
+        if (channel == null || !channel.isActive()) {
+            return false;
+        }
+        
+        ChargingPileSessionManager.PileSession session = sessionManager.getSession(pileCode);
+        int sequenceNo = session.nextSequenceNo();
+        
+        WorkParamSetFrame frame = WorkParamSetFrame.createEnable(pileCode, sequenceNo);
+        
+        ChannelFuture future = channel.writeAndFlush(frame);
+        
+        try {
+            future.await(5, TimeUnit.SECONDS);
+            if (future.isSuccess()) {
+                log.info("充电桩启用指令发送成功 - 桩号: {}", pileCode);
+                return true;
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        return false;
+    }
+    
+    /**
+     * 禁用充电桩
+     *
+     * @param pileCode 桩编号
+     * @return 是否发送成功
+     */
+    public boolean disablePile(String pileCode) {
+        if (!sessionManager.isOnline(pileCode)) {
+            log.warn("充电桩[{}]不在线,无法禁用", pileCode);
+            return false;
+        }
+        
+        Channel channel = sessionManager.getChannel(pileCode);
+        if (channel == null || !channel.isActive()) {
+            return false;
+        }
+        
+        ChargingPileSessionManager.PileSession session = sessionManager.getSession(pileCode);
+        int sequenceNo = session.nextSequenceNo();
+        
+        WorkParamSetFrame frame = WorkParamSetFrame.createDisable(pileCode, sequenceNo);
+        
+        ChannelFuture future = channel.writeAndFlush(frame);
+        
+        try {
+            future.await(5, TimeUnit.SECONDS);
+            if (future.isSuccess()) {
+                log.info("充电桩禁用指令发送成功 - 桩号: {}", pileCode);
+                return true;
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        return false;
+    }
+    
+    /**
+     * 主动读取实时数据
+     *
+     * @param pileCode 桩编号
+     * @param gunNo    枪号
+     * @return 是否发送成功
+     */
+    public boolean readRealtimeData(String pileCode, String gunNo) {
+        if (!sessionManager.isOnline(pileCode)) {
+            log.warn("充电桩[{}]不在线,无法读取数据", pileCode);
+            return false;
+        }
+        
+        Channel channel = sessionManager.getChannel(pileCode);
+        if (channel == null || !channel.isActive()) {
+            return false;
+        }
+        
+        ChargingPileSessionManager.PileSession session = sessionManager.getSession(pileCode);
+        int sequenceNo = session.nextSequenceNo();
+        
+        ReadRealtimeDataFrame frame = ReadRealtimeDataFrame.create(pileCode, gunNo, sequenceNo);
+        
+        ChannelFuture future = channel.writeAndFlush(frame);
+        
+        try {
+            future.await(5, TimeUnit.SECONDS);
+            if (future.isSuccess()) {
+                log.info("读取实时数据指令发送成功 - 桩号: {}, 枪号: {}", pileCode, gunNo);
+                return true;
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        return false;
+    }
+    
+    /**
+     * 批量设置功率限制
+     * 用于电网功率调控场景
+     *
+     * @param powerPercent 功率百分比
+     */
+    public void setAllPilesPowerLimit(int powerPercent) {
+        log.info("批量设置所有充电桩功率限制: {}%", powerPercent);
+        
+        for (ChargingPileSessionManager.PileSession session : sessionManager.getAllOnlineSessions()) {
+            if (session.isOnline() && session.getChannel() != null && session.getChannel().isActive()) {
+                try {
+                    setMaxOutputPower(session.getPileCode(), powerPercent);
+                } catch (Exception e) {
+                    log.error("设置充电桩[{}]功率限制失败", session.getPileCode(), e);
+                }
+            }
+        }
+    }
+}

+ 456 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/utils/ByteUtils.java

@@ -0,0 +1,456 @@
+/*
+ * 文 件 名:  ByteUtils
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  字节处理工具类
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.utils;
+
+import io.netty.buffer.ByteBuf;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+
+/**
+ * 字节处理工具类
+ * 处理BCD码、BIN码(低位在前高位在后)、ASCII码转换
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public final class ByteUtils {
+
+    private ByteUtils() {
+        // 私有构造函数,防止实例化
+    }
+
+    // ==================== BCD码处理 ====================
+
+    /**
+     * 字符串转BCD码(右对齐,左补0)
+     * 例如: "32010600019236" -> [0x32, 0x01, 0x06, 0x00, 0x01, 0x92, 0x36]
+     *
+     * @param str    字符串
+     * @param length 目标字节长度
+     * @return BCD码字节数组
+     */
+    public static byte[] strToBcd(String str, int length) {
+        if (str == null) {
+            str = "";
+        }
+        // 确保字符串长度为偶数
+        int targetLen = length * 2;
+        String padded = String.format("%" + targetLen + "s", str).replace(' ', '0');
+        if (padded.length() > targetLen) {
+            padded = padded.substring(padded.length() - targetLen);
+        }
+
+        byte[] result = new byte[length];
+        for (int i = 0; i < length; i++) {
+            int high = Character.digit(padded.charAt(i * 2), 16);
+            int low = Character.digit(padded.charAt(i * 2 + 1), 16);
+            result[i] = (byte) ((high << 4) | low);
+        }
+        return result;
+    }
+
+    /**
+     * BCD码转字符串
+     * 例如: [0x32, 0x01, 0x06] -> "320106"
+     *
+     * @param bcd BCD码字节数组
+     * @return 字符串
+     */
+    public static String bcdToStr(byte[] bcd) {
+        if (bcd == null || bcd.length == 0) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bcd) {
+            sb.append(String.format("%02X", b & 0xFF));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 从ByteBuf读取BCD码并转为字符串
+     *
+     * @param buf    ByteBuf
+     * @param length 读取字节长度
+     * @return 字符串
+     */
+    public static String readBcd(ByteBuf buf, int length) {
+        byte[] bcd = new byte[length];
+        buf.readBytes(bcd);
+        return bcdToStr(bcd);
+    }
+
+    /**
+     * 将BCD字符串写入ByteBuf
+     *
+     * @param buf    ByteBuf
+     * @param str    字符串
+     * @param length 写入字节长度
+     */
+    public static void writeBcd(ByteBuf buf, String str, int length) {
+        byte[] bcd = strToBcd(str, length);
+        buf.writeBytes(bcd);
+    }
+
+    // ==================== BIN码处理(低位在前,高位在后) ====================
+
+    /**
+     * 从ByteBuf读取无符号字节
+     *
+     * @param buf ByteBuf
+     * @return 无符号值
+     */
+    public static int readUnsignedByte(ByteBuf buf) {
+        return buf.readByte() & 0xFF;
+    }
+
+    /**
+     * 从ByteBuf读取2字节无符号整数(低位在前)
+     *
+     * @param buf ByteBuf
+     * @return 无符号值
+     */
+    public static int readUnsignedShortLE(ByteBuf buf) {
+        return buf.readShortLE() & 0xFFFF;
+    }
+
+    /**
+     * 从ByteBuf读取4字节无符号整数(低位在前)
+     *
+     * @param buf ByteBuf
+     * @return 无符号值
+     */
+    public static long readUnsignedIntLE(ByteBuf buf) {
+        return buf.readIntLE() & 0xFFFFFFFFL;
+    }
+
+    /**
+     * 从ByteBuf读取5字节无符号整数(低位在前)
+     *
+     * @param buf ByteBuf
+     * @return 无符号值
+     */
+    public static long readUnsignedInt5LE(ByteBuf buf) {
+        long low = buf.readIntLE() & 0xFFFFFFFFL;
+        long high = buf.readByte() & 0xFF;
+        return (high << 32) | low;
+    }
+
+    /**
+     * 写入1字节无符号整数
+     *
+     * @param buf   ByteBuf
+     * @param value 值
+     */
+    public static void writeUnsignedByte(ByteBuf buf, int value) {
+        buf.writeByte(value & 0xFF);
+    }
+
+    /**
+     * 写入2字节无符号整数(低位在前)
+     *
+     * @param buf   ByteBuf
+     * @param value 值
+     */
+    public static void writeUnsignedShortLE(ByteBuf buf, int value) {
+        buf.writeShortLE(value & 0xFFFF);
+    }
+
+    /**
+     * 写入4字节无符号整数(低位在前)
+     *
+     * @param buf   ByteBuf
+     * @param value 值
+     */
+    public static void writeUnsignedIntLE(ByteBuf buf, long value) {
+        buf.writeIntLE((int) (value & 0xFFFFFFFFL));
+    }
+
+    /**
+     * 写入5字节无符号整数(低位在前)
+     *
+     * @param buf   ByteBuf
+     * @param value 值
+     */
+    public static void writeUnsignedInt5LE(ByteBuf buf, long value) {
+        buf.writeIntLE((int) (value & 0xFFFFFFFFL));
+        buf.writeByte((int) ((value >> 32) & 0xFF));
+    }
+
+    // ==================== ASCII码处理 ====================
+
+    /**
+     * 从ByteBuf读取ASCII字符串
+     *
+     * @param buf    ByteBuf
+     * @param length 读取字节长度
+     * @return 字符串(去除末尾的0)
+     */
+    public static String readAscii(ByteBuf buf, int length) {
+        byte[] bytes = new byte[length];
+        buf.readBytes(bytes);
+        // 找到第一个0的位置
+        int end = 0;
+        while (end < bytes.length && bytes[end] != 0) {
+            end++;
+        }
+        return new String(bytes, 0, end, StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * 将ASCII字符串写入ByteBuf(不足补0)
+     *
+     * @param buf    ByteBuf
+     * @param str    字符串
+     * @param length 写入字节长度
+     */
+    public static void writeAscii(ByteBuf buf, String str, int length) {
+        byte[] bytes = new byte[length];
+        if (str != null) {
+            byte[] strBytes = str.getBytes(StandardCharsets.US_ASCII);
+            System.arraycopy(strBytes, 0, bytes, 0, Math.min(strBytes.length, length));
+        }
+        buf.writeBytes(bytes);
+    }
+
+    // ==================== CP56Time2a时间格式处理 ====================
+
+    /**
+     * 从ByteBuf读取CP56Time2a格式时间
+     * 格式:
+     * Byte1-2: 毫秒 (0-59999)
+     * Byte3: 分 (0-59) + IV标志(bit7)
+     * Byte4: 时 (0-23) + SU标志(bit7)
+     * Byte5: 星期(高3位) + 日(低5位)
+     * Byte6: 月 (1-12)
+     * Byte7: 年 (0-99, 从2000年开始)
+     *
+     * @param buf ByteBuf
+     * @return LocalDateTime
+     */
+    public static LocalDateTime readCp56Time2a(ByteBuf buf) {
+        int milliseconds = buf.readShortLE() & 0xFFFF;
+        int minutes = buf.readByte() & 0x3F;
+        int hours = buf.readByte() & 0x1F;
+        int dayAndWeek = buf.readByte() & 0xFF;
+        int day = dayAndWeek & 0x1F;
+        int month = buf.readByte() & 0x0F;
+        int year = (buf.readByte() & 0x7F) + 2000;
+
+        int seconds = milliseconds / 1000;
+        int millis = milliseconds % 1000;
+
+        try {
+            return LocalDateTime.of(year, month, day, hours, minutes, seconds, millis * 1000000);
+        }
+        catch (Exception e) {
+            return LocalDateTime.now();
+        }
+    }
+
+    /**
+     * 将LocalDateTime写入ByteBuf(CP56Time2a格式)
+     *
+     * @param buf      ByteBuf
+     * @param dateTime 时间
+     */
+    public static void writeCp56Time2a(ByteBuf buf, LocalDateTime dateTime) {
+        if (dateTime == null) {
+            dateTime = LocalDateTime.now();
+        }
+
+        int milliseconds = dateTime.getSecond() * 1000 + dateTime.getNano() / 1000000;
+        buf.writeShortLE(milliseconds);
+        buf.writeByte(dateTime.getMinute());
+        buf.writeByte(dateTime.getHour());
+        int dayOfWeek = dateTime.getDayOfWeek().getValue() % 7; // 0=周日, 1-6=周一到周六
+        buf.writeByte((dayOfWeek << 5) | dateTime.getDayOfMonth());
+        buf.writeByte(dateTime.getMonthValue());
+        buf.writeByte(dateTime.getYear() - 2000);
+    }
+
+    // ==================== 数据转换辅助方法 ====================
+
+    /**
+     * 将电压值转换为协议格式(保留一位小数)
+     * 例如: 225.1V -> 2251
+     *
+     * @param voltage 电压值
+     * @return 协议格式值
+     */
+    public static int voltageToProtocol(double voltage) {
+        return (int) Math.round(voltage * 10);
+    }
+
+    /**
+     * 将协议格式转换为电压值
+     *
+     * @param protocolValue 协议格式值
+     * @return 电压值
+     */
+    public static double protocolToVoltage(int protocolValue) {
+        return protocolValue / 10.0;
+    }
+
+    /**
+     * 将电流值转换为协议格式(保留一位小数)
+     *
+     * @param current 电流值
+     * @return 协议格式值
+     */
+    public static int currentToProtocol(double current) {
+        return (int) Math.round(current * 10);
+    }
+
+    /**
+     * 将协议格式转换为电流值
+     *
+     * @param protocolValue 协议格式值
+     * @return 电流值
+     */
+    public static double protocolToCurrent(int protocolValue) {
+        return protocolValue / 10.0;
+    }
+
+    /**
+     * 将电量值转换为协议格式(保留四位小数)
+     *
+     * @param energy 电量值(kWh)
+     * @return 协议格式值
+     */
+    public static long energyToProtocol(double energy) {
+        return Math.round(energy * 10000);
+    }
+
+    /**
+     * 将协议格式转换为电量值
+     *
+     * @param protocolValue 协议格式值
+     * @return 电量值(kWh)
+     */
+    public static double protocolToEnergy(long protocolValue) {
+        return protocolValue / 10000.0;
+    }
+
+    /**
+     * 将金额值转换为协议格式(保留四位小数)
+     *
+     * @param amount 金额值(元)
+     * @return 协议格式值
+     */
+    public static long amountToProtocol(double amount) {
+        return Math.round(amount * 10000);
+    }
+
+    /**
+     * 将协议格式转换为金额值
+     *
+     * @param protocolValue 协议格式值
+     * @return 金额值(元)
+     */
+    public static double protocolToAmount(long protocolValue) {
+        return protocolValue / 10000.0;
+    }
+
+    /**
+     * 将费率值转换为协议格式(保留五位小数)
+     *
+     * @param rate 费率值(元/度)
+     * @return 协议格式值
+     */
+    public static long rateToProtocol(double rate) {
+        return Math.round(rate * 100000);
+    }
+
+    /**
+     * 将协议格式转换为费率值
+     *
+     * @param protocolValue 协议格式值
+     * @return 费率值(元 / 度)
+     */
+    public static double protocolToRate(long protocolValue) {
+        return protocolValue / 100000.0;
+    }
+
+    /**
+     * 温度值转换(协议中温度有-50偏移量)
+     *
+     * @param protocolValue 协议温度值
+     * @return 实际温度值
+     */
+    public static int protocolToTemperature(int protocolValue) {
+        return protocolValue - 50;
+    }
+
+    /**
+     * 实际温度转协议格式
+     *
+     * @param temperature 实际温度值
+     * @return 协议温度值
+     */
+    public static int temperatureToProtocol(int temperature) {
+        return temperature + 50;
+    }
+
+    // ==================== 调试辅助方法 ====================
+
+    /**
+     * 字节数组转16进制字符串
+     *
+     * @param bytes 字节数组
+     * @return 16进制字符串
+     */
+    public static String bytesToHex(byte[] bytes) {
+        if (bytes == null) {
+            return "null";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X ", b & 0xFF));
+        }
+        return sb.toString().trim();
+    }
+
+    /**
+     * ByteBuf转16进制字符串(不改变读索引)
+     *
+     * @param buf ByteBuf
+     * @return 16进制字符串
+     */
+    public static String byteBufToHex(ByteBuf buf) {
+        if (buf == null) {
+            return "null";
+        }
+        int readerIndex = buf.readerIndex();
+        byte[] bytes = new byte[buf.readableBytes()];
+        buf.readBytes(bytes);
+        buf.readerIndex(readerIndex);
+        return bytesToHex(bytes);
+    }
+
+    /**
+     * 16进制字符串转字节数组
+     *
+     * @param hex 16进制字符串(可以包含空格)
+     * @return 字节数组
+     */
+    public static byte[] hexToBytes(String hex) {
+        if (hex == null || hex.isEmpty()) {
+            return new byte[0];
+        }
+        hex = hex.replaceAll("\\s+", "");
+        int len = hex.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
+        }
+        return data;
+    }
+}

+ 135 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/utils/CRC16Utils.java

@@ -0,0 +1,135 @@
+/*
+ * 文 件 名:  CRC16Utils
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  CRC16校验工具类
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.ruoyi.ems.charging.utils;
+
+/**
+ * CRC16校验工具类
+ * 根据协议附录13.2实现Modbus CRC16校验
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public final class CRC16Utils {
+    
+    private CRC16Utils() {
+        // 私有构造函数,防止实例化
+    }
+    
+    /**
+     * CRC码表高字节
+     */
+    private static final int[] CRC_HI_TABLE = {
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
+        0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40
+    };
+    
+    /**
+     * CRC码表低字节
+     */
+    private static final int[] CRC_LO_TABLE = {
+        0x00, 0xc0, 0xc1, 0x01, 0xc3, 0x03, 0x02, 0xc2, 0xc6, 0x06, 0x07, 0xc7, 0x05, 0xc5, 0xc4, 0x04,
+        0xcc, 0x0c, 0x0d, 0xcd, 0x0f, 0xcf, 0xce, 0x0e, 0x0a, 0xca, 0xcb, 0x0b, 0xc9, 0x09, 0x08, 0xc8,
+        0xd8, 0x18, 0x19, 0xd9, 0x1b, 0xdb, 0xda, 0x1a, 0x1e, 0xde, 0xdf, 0x1f, 0xdd, 0x1d, 0x1c, 0xdc,
+        0x14, 0xd4, 0xd5, 0x15, 0xd7, 0x17, 0x16, 0xd6, 0xd2, 0x12, 0x13, 0xd3, 0x11, 0xd1, 0xd0, 0x10,
+        0xf0, 0x30, 0x31, 0xf1, 0x33, 0xf3, 0xf2, 0x32, 0x36, 0xf6, 0xf7, 0x37, 0xf5, 0x35, 0x34, 0xf4,
+        0x3c, 0xfc, 0xfd, 0x3d, 0xff, 0x3f, 0x3e, 0xfe, 0xfa, 0x3a, 0x3b, 0xfb, 0x39, 0xf9, 0xf8, 0x38,
+        0x28, 0xe8, 0xe9, 0x29, 0xeb, 0x2b, 0x2a, 0xea, 0xee, 0x2e, 0x2f, 0xef, 0x2d, 0xed, 0xec, 0x2c,
+        0xe4, 0x24, 0x25, 0xe5, 0x27, 0xe7, 0xe6, 0x26, 0x22, 0xe2, 0xe3, 0x23, 0xe1, 0x21, 0x20, 0xe0,
+        0xa0, 0x60, 0x61, 0xa1, 0x63, 0xa3, 0xa2, 0x62, 0x66, 0xa6, 0xa7, 0x67, 0xa5, 0x65, 0x64, 0xa4,
+        0x6c, 0xac, 0xad, 0x6d, 0xaf, 0x6f, 0x6e, 0xae, 0xaa, 0x6a, 0x6b, 0xab, 0x69, 0xa9, 0xa8, 0x68,
+        0x78, 0xb8, 0xb9, 0x79, 0xbb, 0x7b, 0x7a, 0xba, 0xbe, 0x7e, 0x7f, 0xbf, 0x7d, 0xbd, 0xbc, 0x7c,
+        0xb4, 0x74, 0x75, 0xb5, 0x77, 0xb7, 0xb6, 0x76, 0x72, 0xb2, 0xb3, 0x73, 0xb1, 0x71, 0x70, 0xb0,
+        0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
+        0x9c, 0x5c, 0x5d, 0x9d, 0x5f, 0x9f, 0x9e, 0x5e, 0x5a, 0x9a, 0x9b, 0x5b, 0x99, 0x59, 0x58, 0x98,
+        0x88, 0x48, 0x49, 0x89, 0x4b, 0x8b, 0x8a, 0x4a, 0x4e, 0x8e, 0x8f, 0x4f, 0x8d, 0x4d, 0x4c, 0x8c,
+        0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40
+    };
+    
+    /**
+     * 计算CRC16校验值
+     *
+     * @param data   数据
+     * @param offset 起始位置
+     * @param length 长度
+     * @return CRC16值(低字节在前,高字节在后)
+     */
+    public static int calculateCRC(byte[] data, int offset, int length) {
+        int crcHi = 0xFF;
+        int crcLo = 0xFF;
+        
+        for (int i = offset; i < offset + length; i++) {
+            int idx = (crcHi ^ (data[i] & 0xFF)) & 0xFF;
+            crcHi = crcLo ^ CRC_HI_TABLE[idx];
+            crcLo = CRC_LO_TABLE[idx];
+        }
+        
+        // 返回低字节在前,高字节在后的值
+        return (crcLo & 0xFF) | ((crcHi & 0xFF) << 8);
+    }
+    
+    /**
+     * 计算CRC16校验值
+     *
+     * @param data 数据
+     * @return CRC16值
+     */
+    public static int calculateCRC(byte[] data) {
+        return calculateCRC(data, 0, data.length);
+    }
+    
+    /**
+     * 验证CRC16校验值
+     *
+     * @param data        数据(包含CRC校验位)
+     * @param offset      数据起始位置
+     * @param dataLength  数据长度(不含CRC)
+     * @param receivedCrc 接收到的CRC值
+     * @return 是否验证通过
+     */
+    public static boolean verifyCRC(byte[] data, int offset, int dataLength, int receivedCrc) {
+        int calculatedCrc = calculateCRC(data, offset, dataLength);
+        return calculatedCrc == receivedCrc;
+    }
+    
+    /**
+     * 从字节数组中提取CRC值(低字节在前,高字节在后)
+     *
+     * @param data   数据
+     * @param offset CRC起始位置
+     * @return CRC值
+     */
+    public static int extractCRC(byte[] data, int offset) {
+        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
+    }
+    
+    /**
+     * 将CRC值写入字节数组(低字节在前,高字节在后)
+     *
+     * @param data   数据
+     * @param offset 写入位置
+     * @param crc    CRC值
+     */
+    public static void writeCRC(byte[] data, int offset, int crc) {
+        data[offset] = (byte) (crc & 0xFF);          // 低字节
+        data[offset + 1] = (byte) ((crc >> 8) & 0xFF); // 高字节
+    }
+}

+ 217 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/controller/MockPileController.java

@@ -0,0 +1,217 @@
+/*
+ * 文 件 名:  MockPileController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩控制接口
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.controller;
+
+import com.ruoyi.ems.charging.mock.config.MockPileAutoConfiguration;
+import com.ruoyi.ems.charging.mock.model.MockGunState;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClient;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClientManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mock充电桩控制接口
+ * 提供REST API用于手动控制Mock客户端
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@RestController
+@RequestMapping("/api/mock/charging")
+@ConditionalOnProperty(name = "adapter.charging-pile.mock.enabled", havingValue = "true")
+public class MockPileController {
+
+    @Autowired
+    private MockPileClientManager clientManager;
+
+    /**
+     * 启动mock
+     */
+    @GetMapping("/startClient")
+    public String startClient() {
+        clientManager.startAll();
+        return "success";
+    }
+
+    /**
+     * 停止mock
+     */
+    @GetMapping("/stopClient")
+    public String stopClient() {
+        clientManager.stopAll();
+        return "success";
+    }
+
+    /**
+     * 获取状态统计
+     */
+    @GetMapping("/status")
+    public Map<String, Object> getStatus() {
+        return clientManager.getStatistics();
+    }
+
+    /**
+     * 获取所有客户端状态
+     */
+    @GetMapping("/clients")
+    public List<Map<String, Object>> getClients() {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (MockPileClient client : clientManager.getAllClients()) {
+            Map<String, Object> clientInfo = new LinkedHashMap<>();
+            clientInfo.put("pileCode", client.getPileCode());
+            clientInfo.put("connected", client.isConnected());
+            clientInfo.put("loggedIn", client.isLoggedIn());
+
+            List<Map<String, Object>> guns = new ArrayList<>();
+            for (MockGunState gunState : client.getAllGunStates()) {
+                Map<String, Object> gunInfo = new LinkedHashMap<>();
+                gunInfo.put("gunNo", gunState.getGunNo());
+                gunInfo.put("status", gunState.isCharging() ? "charging" : "idle");
+                gunInfo.put("ratedPower", gunState.getRatedPower());
+                gunInfo.put("outputPower", String.format("%.2f", gunState.getOutputPower()));
+                gunInfo.put("outputVoltage", String.format("%.1f", gunState.getOutputVoltage()));
+                gunInfo.put("outputCurrent", String.format("%.1f", gunState.getOutputCurrent()));
+                gunInfo.put("soc", gunState.getSoc());
+                gunInfo.put("chargingEnergy", String.format("%.4f", gunState.getChargingEnergy()));
+                gunInfo.put("chargingTime", gunState.getChargingTime());
+                gunInfo.put("transactionNo", gunState.getTransactionNo());
+                guns.add(gunInfo);
+            }
+            clientInfo.put("guns", guns);
+
+            result.add(clientInfo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取指定枪的状态
+     */
+    @GetMapping("/gun/{pileCode}/{gunNo}")
+    public Map<String, Object> getGunStatus(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        MockGunState gunState = clientManager.getGunState(pileCode, gunNo);
+        if (gunState == null) {
+            Map<String, Object> error = new HashMap<>();
+            error.put("error", "未找到指定的枪");
+            error.put("pileCode", pileCode);
+            error.put("gunNo", gunNo);
+            return error;
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("pileCode", gunState.getPileCode());
+        result.put("gunNo", gunState.getGunNo());
+        result.put("fullGunNo", gunState.getFullGunNo());
+        result.put("status", gunState.isCharging() ? "charging" : "idle");
+        result.put("gunConnected", gunState.isGunConnected());
+        result.put("ratedPower", gunState.getRatedPower());
+        result.put("maxOutputPowerPercent", gunState.getMaxOutputPowerPercent());
+        result.put("outputPower", String.format("%.2f", gunState.getOutputPower()));
+        result.put("outputVoltage", String.format("%.1f", gunState.getOutputVoltage()));
+        result.put("outputCurrent", String.format("%.1f", gunState.getOutputCurrent()));
+        result.put("gunTemperature", gunState.getGunTemperature());
+        result.put("soc", gunState.getSoc());
+        result.put("batteryMaxTemp", gunState.getBatteryMaxTemp());
+        result.put("chargingEnergy", String.format("%.4f", gunState.getChargingEnergy()));
+        result.put("chargedAmount", String.format("%.4f", gunState.getChargedAmount()));
+        result.put("chargingTime", gunState.getChargingTime());
+        result.put("remainingTime", gunState.getRemainingTime());
+        result.put("transactionNo", gunState.getTransactionNo());
+        result.put("vin", gunState.getVin());
+        result.put("hardwareFault", gunState.getHardwareFault());
+
+        return result;
+    }
+
+    /**
+     * 手动开始充电
+     */
+    @PostMapping("/start/{pileCode}/{gunNo}")
+    public Map<String, Object> startCharging(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("pileCode", pileCode);
+        result.put("gunNo", gunNo);
+
+        try {
+            clientManager.startCharging(pileCode, gunNo);
+            result.put("success", true);
+            result.put("message", "充电已启动");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 手动停止充电
+     */
+    @PostMapping("/stop/{pileCode}/{gunNo}")
+    public Map<String, Object> stopCharging(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("pileCode", pileCode);
+        result.put("gunNo", gunNo);
+
+        try {
+            clientManager.stopCharging(pileCode, gunNo);
+            result.put("success", true);
+            result.put("message", "充电已停止");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取所有枪的状态列表
+     */
+    @GetMapping("/guns")
+    public List<Map<String, Object>> getAllGuns() {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (MockGunState gunState : clientManager.getAllGunStates()) {
+            Map<String, Object> gunInfo = new LinkedHashMap<>();
+            gunInfo.put("fullGunNo", gunState.getFullGunNo());
+            gunInfo.put("pileCode", gunState.getPileCode());
+            gunInfo.put("gunNo", gunState.getGunNo());
+            gunInfo.put("status", gunState.isCharging() ? "充电中" : "空闲");
+            gunInfo.put("ratedPower", gunState.getRatedPower() + "kW");
+            gunInfo.put("outputPower", String.format("%.1fkW", gunState.getOutputPower()));
+            gunInfo.put("soc", gunState.getSoc() + "%");
+            gunInfo.put("energy", String.format("%.2fkWh", gunState.getChargingEnergy()));
+            result.add(gunInfo);
+        }
+
+        return result;
+    }
+}

+ 37 - 15
ems/ems-cloud/ems-dev-adapter/src/main/resources/application-local.yml

@@ -46,7 +46,7 @@ spring:
 mqtt:
   server:
     host: tcp://xt.wenhq.top:8581
-    client_id: ems-dev-adapter-local
+    client_id: ems-dev-adapter-ttttt
   executor:
     msgHandle:
       corePoolSize: 20
@@ -63,26 +63,39 @@ adapter:
     notifyEnabled: true
   # 充电桩Socket服务
   charging-pile:
+    # TCP服务器配置
     server:
-      host: 127.0.0.1
-      port: 9310
-    ## 线程池配置
+      # 监听端口 - 充电桩连接此端口
+      port: 8234
+      # Boss线程数(处理连接请求)
+      boss-threads: 1
+      # Worker线程数(处理I/O)
+      worker-threads: 4
+    mock:
+      enabled: true
+      server-host: 127.0.0.1
+      server-port: 8234
+      config-type: highway
+    # 线程池配置
     executor:
+      # Boss组线程池
       bossGroup:
-        corePoolSize: 5
-        maxPoolSize: 5
+        corePoolSize: 1
+        maxPoolSize: 2
         queueCapacity: 100
-        namePrefix: 'bossGroup-'
+        namePrefix: pile-boss-
+      # Worker组线程池
       workerGroup:
-        corePoolSize: 5
-        maxPoolSize: 5
-        queueCapacity: 100
-        namePrefix: 'workerGroup-'
-      msgExec:
-        corePoolSize: 5
-        maxPoolSize: 5
+        corePoolSize: 4
+        maxPoolSize: 8
         queueCapacity: 1000
-        namePrefix: 'msgExec-'
+        namePrefix: pile-worker-
+      # 消息处理线程池
+      msgExec:
+        corePoolSize: 4
+        maxPoolSize: 16
+        queueCapacity: 2000
+        namePrefix: pile-msg-
   # 安科瑞
   acrel:
     url: http://127.0.0.1:8090
@@ -452,6 +465,15 @@ adapter:
       'Z020-N-LIGHT-11':
         setCtl-OnOff: 'C_7001_BV_0028'
         Switch: 'C_7001_DO_0005'
+      'Z020-N-LIGHT-12':
+        setCtl-OnOff: 'C_7001_BV_0030'
+        Switch: 'C_7002_BO_0005'
+      'Z020-N-LIGHT-13':
+        setCtl-OnOff: 'C_7003_BV_0028'
+        Switch: 'C_7003_DO_0001'
+      'Z020-N-LIGHT-14':
+        setCtl-OnOff: 'C_7003_BV_0030'
+        Switch: 'C_7003_DO_0002'
 # mybatis配置
 mybatis:
   # 搜索指定包别名

+ 43 - 4
ems/ems-cloud/ems-dev-adapter/src/main/resources/application-prod-ct.yml

@@ -45,7 +45,7 @@ spring:
 
 mqtt:
   server:
-    host: tcp://10.0.8.28:1883
+    host: tcp://172.17.60.27:1883
     client_id: ems-dev-adapter
   executor:
     msgHandle:
@@ -56,15 +56,44 @@ mqtt:
 
 adapter:
   ems:
-    url: http://127.0.0.1:9202
+    url: http://172.17.60.27:9202
     connectTimeout: 300
     readTimeout: 500
     writeTimeout: 500
     notifyEnabled: true
   # 充电桩Socket服务
   charging-pile:
-    host: 127.0.0.1
-    port: 9310
+    # TCP服务器配置
+    server:
+      # 监听端口 - 充电桩连接此端口
+      port: 8234
+      # Boss线程数(处理连接请求)
+      boss-threads: 1
+      # Worker线程数(处理I/O)
+      worker-threads: 4
+    mock:
+      enabled: false
+      config-type: highway
+    # 线程池配置
+    executor:
+      # Boss组线程池
+      bossGroup:
+        corePoolSize: 1
+        maxPoolSize: 2
+        queueCapacity: 100
+        namePrefix: pile-boss-
+      # Worker组线程池
+      workerGroup:
+        corePoolSize: 4
+        maxPoolSize: 8
+        queueCapacity: 1000
+        namePrefix: pile-worker-
+      # 消息处理线程池
+      msgExec:
+        corePoolSize: 4
+        maxPoolSize: 16
+        queueCapacity: 2000
+        namePrefix: pile-msg-
   # 安科瑞
   acrel:
     url: http://172.61.55.66:8090
@@ -434,6 +463,16 @@ adapter:
       'Z020-N-LIGHT-11':
         setCtl-OnOff: 'C_7001_BV_0028'
         Switch: 'C_7001_DO_0005'
+      'Z020-N-LIGHT-12':
+        setCtl-OnOff: 'C_7001_BV_0030'
+        Switch: 'C_7002_BO_0005'
+      'Z020-N-LIGHT-13':
+        setCtl-OnOff: 'C_7003_BV_0028'
+        Switch: 'C_7003_DO_0001'
+      'Z020-N-LIGHT-14':
+        setCtl-OnOff: 'C_7003_BV_0030'
+        Switch: 'C_7003_DO_0002'
+
 # mybatis配置
 mybatis:
   # 搜索指定包别名

+ 218 - 0
ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/ProtocolParserTest.java

@@ -0,0 +1,218 @@
+/*
+ * 文 件 名:  ProtocolParserTest
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  协议解析测试
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/22
+ */
+package com.huashe.test;
+
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.req.*;
+import com.ruoyi.ems.charging.model.resp.*;
+import com.ruoyi.ems.charging.protocol.FrameParser;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.utils.ByteUtils;
+import com.ruoyi.ems.charging.utils.CRC16Utils;
+
+/**
+ * 协议解析测试
+ * 验证编解码的正确性
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/22]
+ */
+public class ProtocolParserTest {
+    
+    public static void main(String[] args) {
+        System.out.println("========== 充电桩协议解析测试 ==========\n");
+        
+        // 测试CRC计算
+        testCrc();
+        
+        // 测试登录请求解析
+        testLoginRequest();
+        
+        // 测试登录应答编码
+        testLoginResponse();
+        
+        // 测试心跳请求解析
+        testHeartbeatRequest();
+        
+        // 测试心跳应答编码
+        testHeartbeatResponse();
+        
+        // 测试实时数据解析
+        testRealtimeData();
+        
+        // 测试工作参数设置编码
+        testWorkParamSet();
+        
+        System.out.println("\n========== 测试完成 ==========");
+    }
+    
+    /**
+     * 测试CRC计算
+     */
+    private static void testCrc() {
+        System.out.println("--- 测试CRC计算 ---");
+        
+        // 协议示例: 心跳请求
+        // 68 0D 0001 00 03 32010200000001 01 00 6890
+        byte[] data = ByteUtils.hexToBytes("0001 00 03 32010200000001 01 00");
+        int crc = CRC16Utils.calculateCRC(data);
+        System.out.printf("计算CRC: 0x%04X, 期望: 0x9068 (低位在前应为6890)%n", crc);
+        
+        // 验证:低字节在前,高字节在后
+        System.out.printf("CRC低字节: 0x%02X, CRC高字节: 0x%02X%n", crc & 0xFF, (crc >> 8) & 0xFF);
+        System.out.println();
+    }
+    
+    /**
+     * 测试登录请求解析
+     */
+    private static void testLoginRequest() {
+        System.out.println("--- 测试登录请求(0x01)解析 ---");
+        
+        // 协议示例登录请求
+        // 68 22 0000 00 01 55031412782305 00 02 0A 56342E312E353000 01 01010101010101010101 04 675A
+        String hexData = "68 22 00 00 00 01 55 03 14 12 78 23 05 00 02 0A 56 34 2E 31 2E 35 30 00 01 01 01 01 01 01 01 01 01 01 04 67 5A";
+        byte[] data = ByteUtils.hexToBytes(hexData);
+        
+        System.out.println("原始数据: " + ByteUtils.bytesToHex(data));
+        
+        BaseFrame frame = FrameParser.parse(data);
+        if (frame instanceof LoginReqFrame) {
+            LoginReqFrame login = (LoginReqFrame) frame;
+            System.out.println("解析成功: " + login);
+            System.out.println("  桩编码: " + login.getPileCode());
+            System.out.println("  桩类型: " + login.getPileTypeDesc());
+            System.out.println("  枪数量: " + login.getGunCount());
+            System.out.println("  协议版本: " + login.getProtocolVersionDesc());
+            System.out.println("  软件版本: " + login.getSoftwareVersion());
+            System.out.println("  网络类型: " + login.getNetworkTypeDesc());
+        } else {
+            System.out.println("解析失败或类型不匹配");
+        }
+        System.out.println();
+    }
+    
+    /**
+     * 测试登录应答编码
+     */
+    private static void testLoginResponse() {
+        System.out.println("--- 测试登录应答(0x02)编码 ---");
+        
+        LoginRespFrame resp = LoginRespFrame.success("55031412782305", 0);
+        byte[] encoded = resp.encode();
+        
+        System.out.println("编码结果: " + ByteUtils.bytesToHex(encoded));
+        System.out.println("帧长度: " + encoded.length);
+        
+        // 验证可以重新解析
+        BaseFrame parsed = FrameParser.parse(encoded);
+        System.out.println("重新解析: " + parsed);
+        System.out.println();
+    }
+    
+    /**
+     * 测试心跳请求解析
+     */
+    private static void testHeartbeatRequest() {
+        System.out.println("--- 测试心跳请求(0x03)解析 ---");
+        
+        // 协议示例
+        // 68 0D 0001 00 03 32010200000001 01 00 6890
+        String hexData = "68 0D 00 01 00 03 32 01 02 00 00 00 01 01 00 68 90";
+        byte[] data = ByteUtils.hexToBytes(hexData);
+        
+        System.out.println("原始数据: " + ByteUtils.bytesToHex(data));
+        
+        BaseFrame frame = FrameParser.parse(data);
+        if (frame instanceof HeartbeatReqFrame) {
+            HeartbeatReqFrame hb = (HeartbeatReqFrame) frame;
+            System.out.println("解析成功: " + hb);
+            System.out.println("  桩编码: " + hb.getPileCode());
+            System.out.println("  枪号: " + hb.getGunNo());
+            System.out.println("  枪状态: " + hb.getGunStatusDesc());
+        }
+        System.out.println();
+    }
+    
+    /**
+     * 测试心跳应答编码
+     */
+    private static void testHeartbeatResponse() {
+        System.out.println("--- 测试心跳应答(0x04)编码 ---");
+        
+        HeartbeatRespFrame resp = HeartbeatRespFrame.create("32010200000001", "01", 0x36);
+        byte[] encoded = resp.encode();
+        
+        System.out.println("编码结果: " + ByteUtils.bytesToHex(encoded));
+        System.out.println();
+    }
+    
+    /**
+     * 测试实时数据解析
+     */
+    private static void testRealtimeData() {
+        System.out.println("--- 测试实时数据(0x13)解析 ---");
+        
+        // 创建模拟的实时数据帧
+        RealtimeDataFrame data = new RealtimeDataFrame();
+        data.setSequenceNo(0x031A);
+        data.setTransactionNo("00000000000000000000000000000000");
+        data.setPileCode("55031412782305");
+        data.setGunNo("02");
+        data.setStatus(ProtocolConstants.GUN_STATUS_CHARGING);
+        data.setGunReturned(ProtocolConstants.GUN_RETURNED);
+        data.setGunConnected(ProtocolConstants.GUN_CONNECTED);
+        data.setOutputVoltage(ByteUtils.voltageToProtocol(380.5)); // 380.5V
+        data.setOutputCurrent(ByteUtils.currentToProtocol(125.3)); // 125.3A
+        data.setGunTemperature(ByteUtils.temperatureToProtocol(35)); // 35℃
+        data.setGunLineCode(new byte[8]);
+        data.setSoc(65);
+        data.setBatteryMaxTemp(ByteUtils.temperatureToProtocol(42));
+        data.setChargingTime(45); // 45分钟
+        data.setRemainingTime(30); // 30分钟
+        data.setChargingEnergy(ByteUtils.energyToProtocol(35.6789)); // 35.6789 kWh
+        data.setLossCorrectedEnergy(ByteUtils.energyToProtocol(35.6789));
+        data.setChargedAmount(ByteUtils.amountToProtocol(42.8147)); // 42.8147元
+        data.setHardwareFault(0);
+        
+        // 编码
+        byte[] encoded = data.encode();
+        System.out.println("编码结果: " + ByteUtils.bytesToHex(encoded));
+        System.out.println("帧长度: " + encoded.length);
+        
+        // 重新解析
+        BaseFrame parsed = FrameParser.parse(encoded);
+        if (parsed instanceof RealtimeDataFrame) {
+            RealtimeDataFrame rd = (RealtimeDataFrame) parsed;
+            System.out.println("解析成功: " + rd);
+            System.out.println("  电压: " + rd.getOutputVoltageValue() + "V");
+            System.out.println("  电流: " + rd.getOutputCurrentValue() + "A");
+            System.out.println("  功率: " + String.format("%.2f", rd.getOutputPower()) + "kW");
+            System.out.println("  电量: " + String.format("%.4f", rd.getChargingEnergyValue()) + "kWh");
+            System.out.println("  金额: " + String.format("%.4f", rd.getChargedAmountValue()) + "元");
+            System.out.println("  SOC: " + rd.getSoc() + "%");
+            System.out.println("  状态: " + rd.getStatusDesc());
+        }
+        System.out.println();
+    }
+    
+    /**
+     * 测试工作参数设置编码
+     */
+    private static void testWorkParamSet() {
+        System.out.println("--- 测试工作参数设置(0x52)编码 ---");
+        
+        // 设置功率限制为80%
+        WorkParamSetFrame frame = WorkParamSetFrame.createPowerLimit("32010200000001", 80, 0x08);
+        byte[] encoded = frame.encode();
+        
+        System.out.println("编码结果: " + ByteUtils.bytesToHex(encoded));
+        System.out.println("设置内容: " + frame);
+        System.out.println();
+    }
+}

+ 15 - 1
ems/sql/ems_init_data_ctfwq.sql

@@ -316,6 +316,11 @@ INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `dev
 INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `device_spec`, `device_status`, `location`, `location_ref`, `area_code`, `device_model`, `ref_facs`, `ps_code`, `subsystem_code`) VALUES ( 'Z020-N-LIGHT-10', '(照明)女卫灯带', '-', '-', '1', '综合楼', 'N-10101', '321283124S3002', 'M_Z020_DEV_BA_LIGHT', 'Z-ZM-02', NULL, 'SYS_BA');
 INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `device_spec`, `device_status`, `location`, `location_ref`, `area_code`, `device_model`, `ref_facs`, `ps_code`, `subsystem_code`) VALUES ( 'Z020-N-LIGHT-11', '(照明)转换区', '-', '-', '1', '综合楼', 'N-10101', '321283124S3002', 'M_Z020_DEV_BA_LIGHT', 'Z-ZM-02', NULL, 'SYS_BA');
 
+INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `device_spec`, `device_status`, `location`, `location_ref`, `area_code`, `device_model`, `ref_facs`, `ps_code`, `subsystem_code`) VALUES ( 'Z020-N-LIGHT-12', '(照明)转换区灯带', '-', '-', '1', '综合楼', 'N-10101', '321283124S3002', 'M_Z020_DEV_BA_LIGHT', 'Z-ZM-02', NULL, 'SYS_BA');
+INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `device_spec`, `device_status`, `location`, `location_ref`, `area_code`, `device_model`, `ref_facs`, `ps_code`, `subsystem_code`) VALUES ( 'Z020-N-LIGHT-13', '(照明)卫生间', '-', '-', '1', '综合楼', 'N-10101', '321283124S3002', 'M_Z020_DEV_BA_LIGHT', 'Z-ZM-02', NULL, 'SYS_BA');
+INSERT INTO `adm_ems_device` (`device_code`, `device_name`, `device_brand`, `device_spec`, `device_status`, `location`, `location_ref`, `area_code`, `device_model`, `ref_facs`, `ps_code`, `subsystem_code`) VALUES ( 'Z020-N-LIGHT-14', '(照明)走廊', '-', '-', '1', '综合楼', 'N-10101', '321283124S3002', 'M_Z020_DEV_BA_LIGHT', 'Z-ZM-02', NULL, 'SYS_BA');
+
+
 
 -- 策略初始数据
 
@@ -553,6 +558,10 @@ INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_na
 INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_W4_DEV_ELEC_MONITOR_DB', 'Measure', 'EPI', '正向有功总电能', 'kW·h', 'Value');
 
 -- 光伏监控属性
+INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_E5_SYS_PHOTOVOLTAIC', 'Base', 'interfaceType', '协议类型', NULL, 'String');
+INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_E5_SYS_PHOTOVOLTAIC', 'Base', 'url', '服务地址', NULL, 'String');
+INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_E5_SYS_PHOTOVOLTAIC', 'Base', 'token', '接口令牌', NULL, 'String');
+INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_E5_SYS_PHOTOVOLTAIC', 'Base', 'plantList', '站点信息', NULL, 'String');
 INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_W2_DEV_PHOTOVOLTAIC_COL', 'Base', 'sn', '设备SN', NULL, 'String');
 INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_W2_DEV_PHOTOVOLTAIC_COL', 'Base', 'manufacturer', '厂家', NULL, 'String');
 INSERT INTO `adm_ems_obj_attr` (`model_code`, `attr_group`, `attr_key`, `attr_name`, `attr_unit`, `attr_value_type`) VALUES ('M_W2_DEV_PHOTOVOLTAIC_COL', 'Base', 'model', '类型', NULL, 'String');
@@ -1329,6 +1338,12 @@ INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `att
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('Z020-N-WP-2', 'M_Z020_DEV_BA_WT', 'ddcTag', 'DDC1地下室', NULL);
 
 -- 光伏设备静态属性
+INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('SYS_GF', 'M_E5_SYS_PHOTOVOLTAIC', 'interfaceType', 'http', NULL);
+INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('SYS_GF', 'M_E5_SYS_PHOTOVOLTAIC', 'url', 'https://openapi-cn.growatt.com', NULL);
+INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('SYS_GF', 'M_E5_SYS_PHOTOVOLTAIC', 'token', '3g7gdk3264xfw94jn1f7oox76fln8o3q', NULL);
+INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('SYS_GF', 'M_E5_SYS_PHOTOVOLTAIC', 'plantList', '北岸(926492)', NULL);
+
+
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-COL-GFZL-1', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'ZBK0E8U01A', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-COL-GFZL-2', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'ZBK0E8U08H', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-COL-GFPD-1', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'LGK2E631S2', NULL);
@@ -1348,7 +1363,6 @@ INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `att
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-ZX-COL-GF-4', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'ZBK0E8519V', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-ZX-COL-GF-5', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'ZBK0E9L0QF', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-ZX-COL-GF-6', 'M_W2_DEV_PHOTOVOLTAIC_COL', 'sn', 'ZBK0E9L0MV', NULL);
-
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-INV-GFZL-1', 'M_E5_DEV_PHOTOVOLTAIC_INVERTER', 'sn', 'GCN4E9503G', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-INV-GFZL-1', 'M_E5_DEV_PHOTOVOLTAIC_INVERTER', 'dataloggerSn', 'ZBK0E8U01A', NULL);
 INSERT INTO `adm_ems_obj_attr_value` (`obj_code`, `model_code`, `attr_key`, `attr_value`, `update_time`) VALUES ('E5-B-INV-GFZL-2', 'M_E5_DEV_PHOTOVOLTAIC_INVERTER', 'sn', 'GCN4E9503H', NULL);

+ 4 - 4
ems/sql/ems_sys_data.sql

@@ -88,12 +88,12 @@ insert into sys_menu values ('1550',  '楼控能耗',       '155',   '1',  'adap
 insert into sys_menu values ('1551',  '智慧照明',       '155',   '2',  'adapter-zm',         'adapter/zm/index',      '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'system',        'admin', sysdate(), '', null, '智慧照明');
 insert into sys_menu values ('1552',  '电力监控',       '155',   '3',  'adapter-dljk',       'adapter/dljk/index',    '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'system',        'admin', sysdate(), '', null, '智慧照明');
 insert into sys_menu values ('1553',  '光伏',          '155',   '4',  'adapter-pv',         'adapter/pv/index',      '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'photovoltaic',   'admin', sysdate(), '', null, '光伏');
-insert into sys_menu values ('1554',  '光储直柔',       '155',   '5',  'adapter-gczr',       'adapter/gczr/index',    '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'deviceaccess',  'admin', sysdate(), '', null, '光储直柔');
-insert into sys_menu values ('1555',  '光储充',         '155',   '6',  'adapter-gcc',        'adapter/gcc/index',     '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'energyconsume', 'admin', sysdate(), '', null, '光储充');
-insert into sys_menu values ('1556',  '充电桩',         '155',   '7',  'adapter-cdz',        'adapter/cdz/index',     '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'powerstore',    'admin', sysdate(), '', null, '充电桩');
+insert into sys_menu values ('1554',  '充电桩',         '155',   '5',  'adapter-cdz',        'adapter/cdz/index',     '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'powerstore',    'admin', sysdate(), '', null, '充电桩');
+insert into sys_menu values ('1555',  '光储直柔',       '155',   '6',  'adapter-gczr',       'adapter/gczr/index',    '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'deviceaccess',  'admin', sysdate(), '', null, '光储直柔');
+insert into sys_menu values ('1556',  '光储充',         '155',   '7',  'adapter-gcc',        'adapter/gcc/index',     '', 1, 0, 'C', '0', '0',   'adapter:devc:list',     'energyconsume', 'admin', sysdate(), '', null, '光储充');
 insert into sys_menu values ('1557',  '智慧海绵',       '155',   '8',  'adapter-hm',         'adapter/hm/index',      '', 1, 0, 'C', '0', '0',   'adapter:devc:list',      'system',        'admin', sysdate(), '', null, '智慧海绵');
 insert into sys_menu values ('1558',  '垃圾厨余',       '155',   '9',  'adapter-ljcy',       'adapter/ljcy/index',    '', 1, 0, 'C', '0', '0',   'adapter:devc:list',      'system',        'admin', sysdate(), '', null, '垃圾厨余');
-insert into sys_menu values ('1559',  '设备管理',       '155',   '10',  'adapter-devc',      'adapter/devc/index',    '', 1, 0, 'M', '0', '0',   'adapter:devc:list',      'system',        'admin', sysdate(), '', null, '设备管理');
+
 
 insert into sys_menu values ('997',  '表单构建',       '99',   '1',  'build',              'tool/build/index',       '', 1, 0, 'C', '0', '0',   'tool:build:list',        'build',          'admin', sysdate(), '', null, '表单构建菜单');
 insert into sys_menu values ('998',  '代码生成',       '99',   '2',  'gen',                'tool/gen/index',         '', 1, 0, 'C', '0', '0',   'tool:gen:list',          'code',           'admin', sysdate(), '', null, '代码生成菜单');