# dl645-meter-reader **Repository Path**: iocs/dl645-meter-reader ## Basic Information - **Project Name**: dl645-meter-reader - **Description**: DL645/2007与Q/GDW 1376.1-2013通信协议解析 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-12-24 - **Last Updated**: 2025-12-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DL/T 645 集中器通信解析服务 ## 一、服务概述 本服务基于 Netty 实现,用于接收集中器上传的 DL/T 645 协议电表数据,支持 **DTZY-GF** 和 **DDZY22C** 两种电表型号的解析,并提供集中器心跳处理、指令下发能力,确保通信稳定与数据精准。 ## 二、核心功能 | 功能模块 | 具体能力 | |------------------|--------------------------------------------------------------------------| | 数据接收与解析 | 接收集中器 TCP 上传数据,自动按帧长度匹配电表型号(51字节→DTZY-GF,53字节→DDZY22C),解析总电能、尖/峰/平/谷费率数据 | | 心跳保活 | 识别20字节心跳包,自动回复应答帧并重置空闲计时器,避免连接误断开 | | 指令下发 | 缓存集中器通信通道(Channel),支持下发 DL/T 645 协议指令(如读取电表数据、设置参数) | | 异常处理 | 包含帧结构校验、索引越界兼容、数据域长度适配等逻辑,降低解析失败率 | ## 三、环境依赖 | 依赖项 | 版本要求 | |----------------|-------| | JDK | 17 | | Spring Boot | 3.5.x | | Netty | 4.1.x | | Maven/Gradle | 构建工具 | ## 四、关键配置 ### 1. Netty 服务配置 在 `NettyServerConfig.java` 中配置服务端口、空闲检测等参数,核心配置如下: ```java @Configuration public class NettyServerConfig { // 服务监听端口 private static final int PORT = 8212; // 空闲检测:30秒无数据触发读空闲(需大于集中器心跳间隔) private static final int READ_IDLE_SECONDS = 30; @Bean public ServerBootstrap serverBootstrap() { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup(), workerGroup()) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true) // 开启TCP保活 .childOption(ChannelOption.TCP_NODELAY, true) // 禁用Nagle算法 .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); // 空闲检测处理器 pipeline.addLast(new IdleStateHandler(READ_IDLE_SECONDS, 0, 0, TimeUnit.SECONDS)); // 业务处理器(集中器数据处理) pipeline.addLast(concentratorServerHandler); } }); return bootstrap; } } ``` ### 2. 电表解析策略配置 两种电表的解析逻辑通过独立策略类实现,核心字段索引如下: | 电表型号 | 总电能索引(数据域内部) | 尖费率索引 | 峰费率索引 | 平费率索引 | 谷费率索引 | |------------|--------------------------|------------|------------|------------|------------| | DTZY-GF | 17~21 | 22~26 | 27~31 | 32~36 | 37~41 | | DDZY22C | 16~20 | 21~25 | 26~30 | 31~35 | 36~40 | 策略类需注入 Spring 容器,示例: ```java @Component public class DtzyGfParseStrategy implements MeterParseStrategy { // DTZY-GF 解析逻辑(参考章节五) } @Component public class Ddzy22cParseStrategy implements MeterParseStrategy { // DDZY22C 解析逻辑(参考章节五) } ``` ## 五、核心代码说明 ### 1. 数据解析核心逻辑(以 DTZY-GF 为例) ```java public class DtzyGfParseStrategy implements MeterParseStrategy { @Override public MeterData parse(byte[] dataField) { MeterData data = new MeterData(); // 1. 解析总电能(数据域内部17~21字节,小端BCD→大端→十进制) if (dataField.length >= 22) { byte[] totalBytes = Arrays.copyOfRange(dataField, 17, 21); data.setPositiveTotal(parseBcdToEnergy(totalBytes, "总电能")); } // 2. 解析尖/峰/平/谷费率(逻辑类似,按索引提取) // ...(省略其他字段解析) return data; } // BCD码转电能值(固定2位小数) private double parseBcdToEnergy(byte[] bcdLittleBytes, String fieldName) { byte[] bcdBigBytes = reverseBytes(bcdLittleBytes); // 小端转大端 StringBuilder bcdStr = new StringBuilder(); // BCD字节转字符串(过滤非法字符) for (byte b : bcdBigBytes) { int high = (b & 0xF0) >> 4; int low = b & 0x0F; bcdStr.append(high > 9 ? 0 : high).append(low > 9 ? 0 : low); } // 去除前导0+插入小数点(保留2位) String noLeadingZero = bcdStr.toString().replaceFirst("^0+", ""); if (noLeadingZero.isEmpty()) return 0.0; String energyStr = noLeadingZero.length() <= 2 ? "0." + String.format("%02d", Integer.parseInt(noLeadingZero)) : noLeadingZero.substring(0, noLeadingZero.length()-2) + "." + noLeadingZero.substring(noLeadingZero.length()-2); return Double.parseDouble(energyStr); } } ``` ### 2. 心跳处理逻辑 在 `ConcentratorServerHandler.java` 中识别心跳包并回复: ```java @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { byte[] data = (byte[]) msg; int frameLength = data.length; String concentratorId = currentConcentratorId.get(); // 识别20字节心跳包 if (frameLength == 20) { System.out.println("ℹ️ 收到[" + concentratorId + "]心跳包"); // 1. 构建应答帧(修改控制码 C9→D9,重新计算校验码) byte[] ackFrame = buildHeartAckFrame(data); // 2. 发送应答 ctx.writeAndFlush(Unpooled.copiedBuffer(ackFrame)); // 3. 重置空闲计时器 ctx.read(); return; } // 非心跳包:按电表型号解析(省略) // ... } ``` ### 3. 指令下发逻辑 通过 `ConcentratorCommandService.java` 缓存通道并下发指令: ```java @Service public class ConcentratorCommandService { // 缓存集中器ID→Channel private static final ConcurrentHashMap channelMap = new ConcurrentHashMap<>(); // 下发指令 public boolean sendCommand(String concentratorId, byte[] commandFrame) { Channel channel = channelMap.get(concentratorId); if (channel == null || !channel.isActive()) { System.err.println("❌ 通道不存在或已断开"); return false; } // 发送DL/T 645指令帧 channel.writeAndFlush(Unpooled.copiedBuffer(commandFrame)).sync(); return true; } // 构建“读取总电能”指令帧(示例) public byte[] buildReadTotalEnergyCommand(String meterAddress) { // 帧结构:68H + 6字节地址 + 68H + 控制码(01H) + 数据域长度(04H) + 数据标识(00 64 0C B4) + 校验码 + 16H // ...(省略具体拼接逻辑,需按DL/T 645协议实现) } } ``` ## 六、常见问题排查 ### 1. 集中器频繁断开连接 - **原因1**:IdleStateHandler 未重置计时器 → 修复:心跳包处理中添加 `ctx.read()` 或 `ctx.fireChannelRead(msg)`; - **原因2**:空闲时间配置过短 → 调整 `READ_IDLE_SECONDS` 为30~60秒(大于集中器心跳间隔); - **原因3**:未回复心跳应答 → 确保 `buildHeartAckFrame` 方法正确构建应答帧(控制码、校验码需匹配集中器协议)。 ### 2. 电表数据解析数值异常 - **原因1**:字段索引错位 → 核对 `MeterParseStrategy` 中的索引(参考章节四.2的索引表); - **原因2**:BCD码转义错误 → 检查 `parseBcdToEnergy` 方法的小端转大端、小数点插入逻辑; - **原因3**:数据域提取不完整 → 确认 `extractDataFieldByModel` 方法中数据域结束索引是否覆盖目标字段。 ### 3. 指令下发失败 - **原因1**:通道未缓存 → 检查集中器 `channelActive` 时是否正确添加到 `channelMap`; - **原因2**:指令帧格式错误 → 确认指令包含完整的 DL/T 645 帧结构(帧头、校验码、帧尾不可缺); - **原因3**:集中器未响应 → 排查集中器是否支持该指令(如权限、数据标识是否匹配)。 ## 七、日志说明 | 日志标识 | 含义说明 | |----------|--------------------------------------------------------------------------| | 📥 | 收到集中器数据(包含集中器ID、数据长度、十六进制内容) | | ℹ️ | 普通信息(帧头查找、数据域提取、心跳处理等) | | ⚠️ | 警告信息(数据域长度不足、索引越界兼容等) | | ✅ | 成功信息(解析成功、指令下发成功、心跳应答发送成功等) | | ❌ | 错误信息(解析失败、通道断开、指令下发失败等) | ## 八、扩展建议 1. **新增电表型号**:实现 `MeterParseStrategy` 接口,添加新型号的字段索引与解析逻辑,在 `getMeterModelByLength` 中关联帧长度与型号; 2. **数据持久化**:解析后的 `MeterData` 可存入 MySQL/InfluxDB,用于后续统计分析; 3. **监控告警**:添加通道断开、解析失败率超阈值等告警(如集成 Prometheus + Grafana); 4. **协议兼容**:若集中器支持 DL/T 645-2007/2013 多版本,可在 `isValidDl645Frame` 中添加版本识别逻辑。 ## 九、联系方式 若遇到未覆盖的问题,可参考 DL/T 645 协议文档(DL/T 645-2007《多功能电能表通信协议》),或联系开发人员协助排查。