ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java智能电表采集系统实战:DL/T645规约解析与串口通信

Java智能电表采集系统实战:DL/T645规约解析与串口通信 简介《基于Java编写的智能电表采集系统》毕业设计资源包面向计算机、电气及自动化专业本科生针对智能电表远程抄表、实时监控和用电数据分析等典型需求完成了从Web前端、Java后端到数据库设计的整套工程实现并采用模块化结构方便二次开发与功能扩展。资源包共47个文件压缩后大小约1.75MB核心内容为15个Java源文件与16个class文件3个jar包提供运行依赖库另含配置说明.docx和采集软件数据库字典及流程.docx两份文档分别说明部署配置与数据流程config.xml、README.md及errorLog.txt辅助理解运行参数、项目说明和排错信息并附一张流程示意图便于快速把握整体架构。目前已有73人学习下载适合在校生用于毕业设计参考或课程项目实践。获取后可获得完整可运行的Java工程源码、配置部署指南、数据库字典与流程说明以及日志排错和项目结构参考可直接用于系统调试、文档撰写或功能增强。1. 智能电表采集系统从毕设到真实用采之间只差一张报文如果毕业设计还停留在“XX管理系统”的CRUD里那你大概率没体会过给一套系统配上真实硬件是什么感觉。智能电表采集系统不一样它要做的不是维护一堆假数据而是通过通信链路把电表里的电量、电压、电流读回来存进数据库并展示给上层用。用Java写这套系统意味着要处理串口通信、DL/T645规约、帧解析、定时任务和掉线重试任何一个环节松了采集回来的就是一堆乱码或者直接没有响应。这篇笔记的目标很明确从协议讲到代码从配置讲到排查把一个能跑的采集系统落地让新手照着能复现熟手能少踩几个坑。2. 智能电表采集系统的技术底座DL/T645规约与两种接法2.1 为什么采集系统绕不开DL/T645国内的电表通信协议最常碰到的就是DL/T645电力行业的计量规约分1997版和2007版基本统治了低压电能表的数据交换。你买回来的智能电表只要支持远传功能默认提供的通信接口多半就是按这个规约来应答的。所以做智能电表采集系统第一步不是写界面而是搞清楚这个规约的数据帧长什么样。DL/T645的帧结构很固定起始符68H开头接着是6字节的地址域再到控制码、数据域长度、数据域、校验和最后以16H结束。地址域这6个字节是电表的表号控制码用来区分是读数据、写数据还是读后续数据数据域里放的是数据标识和具体内容。最容易被忽略的是校验和它是从起始符到数据域所有字节累加后模256得到的不是简单的CRC校验很多人在这上面翻车。2.2 串口直连和TCP透传两条最常见链路硬件链路方面最常见的做法有两种。第一种是串口直连电脑或工控机上插一个RS-485转USB模块用串口线和电表的485端口对接Java里通过串口库直接发包收包。这种方式适合实验室环境和单相表测试配置简单调试直观但受距离和串口数量限制工业现场看不到一两百台表。第二种是TCP透传用电采终端或者集中器先把485总线上的电表数据汇聚起来再通过网络口把报文转给服务端。这种方案下Java程序只需要监听一个TCP端口终端把电表的原始报文透传上来解析逻辑和串口模式完全一致。毕业设计阶段如果手头只有一块表用串口直连最稳如果要做批量采集演示就走TCP仿真终端。我一般在代码里把链路层抽象成一个接口串口和TCP各自实现测试的时候用串口调通部署的时候切TCP业务代码不用动。2.3 Java侧的分层与边界把采集系统做成一个工程时我会严格划分三层通信层、协议层、业务层。通信层只负责把字节流发出去并收回来不关心内容是什么协议层负责组帧、拆帧、算校验和、解析BCD码业务层负责把解析出来的数据落到数据库、做告警和展示。这样划分的好处是协议层写完之后可以用一段静态字节数组做单元测试不用一直依赖真实电表。有一点需要说清楚智能电表采集系统不等于抄表系统。抄表只管读电量采集系统要管理的还有对象档案、采集任务、数据状态、补采重试。所以在架构上我会把电表档案表和采集数据表分开设计定时任务按档案表里的表号批量下发指令每块表上次采集是否成功要有记录。这个思路做完你会发现这套系统不仅仅是毕设拿到真实项目里也能顶上去。3. 用Java写核心采集与解析代码从读表命令到入库3.1 串口初始化参数错一个就是乱码串口通信的参数对不上报文发出去就是废的。电表485通信默认参数一般是波特率2400、8数据位、1停止位、偶校验这跟很多人的直觉不一样——大多数调试工具默认是9600无校验所以第一反应往往不是找线的问题而是被参数坑了。用Java做串口我常用的是jSerialComm轻量、跨平台比老的RXTX包省心很多。import com.fazecast.jSerialComm.SerialPort; public SerialPort openPort(String portName) { SerialPort port SerialPort.getCommPort(portName); port.setBaudRate(2400); // 电表默认波特率 port.setNumDataBits(8); // 数据位 port.setNumStopBits(SerialPort.ONE_STOP_BIT); // 1个停止位 port.setParity(SerialPort.EVEN_PARITY); // 偶校验必须匹配 port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED); // 关闭流控 port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 2000, 2000); // 读超时2秒 if (!port.openPort()) { throw new IllegalStateException(串口打开失败请检查占用情况); } return port; }这段代码里最需要解释的是时机的设置。TIMEOUT_READ_BLOCKING表示没有数据到达时最多等2秒这直接决定了一次采集的超时时间如果电表响应慢或者总线上有多个设备建议把超时拉长到3秒。流控必须关很多调试线默认开了硬件流控导致电表回的数据被吞掉。3.2 构造DL/T645读数据帧校验和是第一个坎帧结构看起来简单但构造的时候有一个细节容易错数据域长度是后面数据域的字节长度不包含控制码和长度本身。读电量这种常见操作数据域里要放4字节数据标识、4字节密码和4字节操作者代码密码和操作者代码默认填零一共12字节长度就是0x0C。public byte[] buildReadFrame(byte[] meterAddr, byte[] dataId) { // meterAddr: 6字节电表地址低字节在前 // dataId: 4字节数据标识例如当前正向有功电能 ByteArrayOutputStream frame new ByteArrayOutputStream(); frame.write(0x68); // 起始符 frame.write(meterAddr, 0, 6); // 地址域 byte[] dataField new byte[12]; System.arraycopy(dataId, 0, dataField, 0, 4); // dataField[4..7] 为密码dataField[8..11] 为操作者代码默认0 frame.write(0x11); // 控制码读数据 frame.write(dataField.length); // 数据域长度 L frame.write(dataField, 0, dataField.length); byte[] pre frame.toByteArray(); int cs 0; for (byte b : pre) { cs (b 0xFF); // 累加注意转无符号 } frame.write(cs 0xFF); // 模256校验和 frame.write(0x16); // 结束符 return frame.toByteArray(); }这里有个非常典型的坑Java的byte是有符号的累加时如果不做 0xFF遇到0xFA这种字节会按负数累加导致校验和算错。这种错非常隐蔽因为报文肉眼看着差不多但电表就是不回或者回一个错误帧。采集系统调不通一半以上的原因出在这个符号转换上。3.3 解析应答帧BCD码、数据标识和补位电表返回正常应答时控制码是0x91也就是读数据命令0x11加上最高位。解析的时候需要先校验帧头帧尾和校验和再读取数据域长度最后按数据标识把数据切出来。电量数据用BCD码存储比如读到0x12 0x34 0x56 0x78表示的就是123456.78这个数值小数位数取决于具体数据标识对应的格式。public BigDecimal parseEnergyResponse(byte[] frame) throws IllegalStateException { // frame[0] 0x68, frame[1..6] 地址域, frame[7] 控制码 // frame[8] 数据域长度, 数据域从 frame[9] 开始 if (frame.length 9 || (frame[0] 0xFF) ! 0x68 || (frame[frame.length - 1] 0xFF) ! 0x16) { throw new IllegalStateException(帧边界不正确); } int cs 0; for (int i 0; i frame.length - 2; i) { // 不含校验和和结束符 cs (frame[i] 0xFF); } if ((cs 0xFF) ! (frame[frame.length - 2] 0xFF)) { throw new IllegalStateException(校验和不匹配); } int length frame[8] 0xFF; byte[] data new byte[length]; System.arraycopy(frame, 9, data, 0, length); // 数据域前4字节是数据标识后面才是电能量数据 long raw 0; for (int i 4; i length; i) { int bcd (data[i] 4) 0x0F; int low data[i] 0x0F; raw raw * 100 bcd * 10 low; } return new BigDecimal(raw).movePointLeft(2); }这段代码解决了两个常见问题。第一是校验和的判断范围要累加到数据域最后一个字节为止校验和字节和结束符不参与累加。第二是BCD解析一个字节的高四位是十位低四位是个位所以一个字节能表示0到99。注意这里按照电能数据的惯例把小数位定为2位但不同数据项的小数位不同比如电压数据没有小数位配置表里最好给每个数据标识定义好小数位不要写死。3.4 定时采集任务间隔、超时和重试采集不能只跑一次要按周期执行。Spring的Scheduled够用但有两个边界必须处理一是上一次采集还没结束下一次调度又触发了二是某块表采集失败后要不要立刻重试、间隔多久。常见的做法是用ReentrantLock加非阻塞尝试配合一个简单的失败计数表。Component public class MeterCollector { private final ReentrantLock lock new ReentrantLock(); Scheduled(fixedDelayString ${collect.interval:60000}) public void collectAll() { if (!lock.tryLock()) { log.warn(上一次采集任务未结束跳过本次调度); return; } try { ListMeter meters meterMapper.selectFailureFirst(); for (Meter m : meters) { try { MeterData data collectOne(m); meterDataMapper.insert(data); meterMapper.markSuccess(m.getId()); } catch (Exception e) { meterMapper.markFailure(m.getId(), e.getMessage()); } } } finally { lock.unlock(); } } }fixedDelayString和fixedRate的区别是很多人没注意到的。fixedRate从任务开始时间算下一次触发如果任务跑得比间隔久会出现任务堆积fixedDelay是上一次任务跑完后再等固定时长对采集这种IO密集型操作更安全。失败重试不要用固定间隔硬刷常见策略是失败次数越多下一次重试间隔越长比如1分钟、5分钟、30分钟逐级拉长。4. 配置说明与流程说明让系统从“能编译”走到“能跑”4.1 application.yml 的核心配置项一个采集系统的配置项不算多但每一样都决定成败。串口名、波特率、采集间隔、连接超时、数据库连接池这些散落在不同地方的参数我会统一收敛到application.yml里方便部署时按环境改。尤其是串口名这种和操作系统强相关的参数Windows上叫COM3Linux上可能叫ttyUSB0不改配置就得改代码。collect: port-name: COM3 baud-rate: 2400 read-timeout: 3000 interval: 60000 retry-times: 3 spring: datasource: url: jdbc:mysql://localhost:3306/meter?useSSLfalse username: root password: root jackson: time-zone: GMT8read-timeout建议和interval联动看如果60秒一轮采集单表超时3秒那网络抖动时一轮可能跑不完所有表需要留足缓冲。useSSLfalse在本地调试没问题部署到生产环境建议按实际要求调整。GMT8时区设置容易被忘但电量数据一旦跨日比对时区错8小时就会闹出乌龙。4.2 数据库初始化电表档案表和采集数据表采集系统的表结构核心是两张表电表档案表和采集数据表。档案表维护表号、通信地址、倍率、采集状态数据表存每次读回来的值和采集时间。这两张表的关系是本文最关键的。用MyBatis-Plus的实体类可以直接生成建表SQL但我觉得手写SQL更可控索引设计也更明确。CREATE TABLE meter_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_no VARCHAR(12) NOT NULL COMMENT 电表表号BCD地址, meter_name VARCHAR(64) COMMENT 电表名称, multiplier DECIMAL(10, 2) DEFAULT 1.00 COMMENT 倍率, status TINYINT DEFAULT 0 COMMENT 0正常 1离线, fail_count INT DEFAULT 0 COMMENT 连续失败次数, last_success_time DATETIME COMMENT 最后成功采集时间, UNIQUE KEY uk_meter_no (meter_no) ); CREATE TABLE meter_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_no VARCHAR(12) NOT NULL, collect_time DATETIME NOT NULL, active_energy DECIMAL(18, 2) COMMENT 正向有功电能 kWh, voltage_a DECIMAL(10, 2) COMMENT A相电压, current_a DECIMAL(10, 4) COMMENT A相电流, KEY idx_meter_time (meter_no, collect_time) );表号字段一定用VARCHAR(12)存12位电表地址不要用数值类型因为表号经常以0开头数值类型会丢失前导零后面做地址域解析时又要补回来。last_success_time这个字段特别有用做离线判断和补采排序时直接依赖它不用再去查询数据表。4.3 启动与运行流程先人工拨测再交给调度整套系统的启动和运行流程我建议按下面这个顺序走而不是写完代码直接就挂定时任务。第一步启动前检查硬件链路用串口调试助手发一条读数据命令看电表是否正常应答这一步通常在写代码之前就该做了。第二步启动服务先用一个简单的CommandLineRunner对第一块表做一次人工拨测确认从组帧到解析再到入库全链路通了。第三步才是开启定时采集观察一个调度周期内的数据是否有空洞。人工拨测为什么不能省因为定时任务一旦开跑报错会被大量日志淹没很难分辨是通信问题、解析问题还是数据库问题。先跑单表把链路验证明白再开放批量任务排查范围会小得多。拨测的命令也很简单就是3.2节里那个buildReadFrame接口启动时手动调一次并打印原始报文Hex。5. 常见问题排查五个最容易翻车的点5.1 报文能发不能收串口参数、线序和流控现象程序发包正常但读串口永远超时或者收到的全是0xFF。原因要分几层看。第一种是波特率和校验位对不上电表端是2400偶校验程序里配成9600无校验电表根本识别不了。第二种是RS-485的A、B线接反了这种情况在调试阶段最容易出现直接表现就是能发不能收。第三种是按了自动流控数据被硬件层面的握手信号卡住。解决先把串口参数调成2400/8/E/1再检查线序最后在代码里显式关闭流控。调试时不要用USB转TTL直接接电表要用RS-485转USB模块因为电表485接口是差分信号TTL电平是单端信号接上就是烧芯片的隐患。5.2 返回乱码与控制码不对从0x91读回什么现象电表回了数据但解析出来控制码不是0x91而是0x81或者0xD1数据域内容明显不对。这通常不是协议写错了而是总线上有多块表地址域没有正确匹配。DL/T645的地址域是6字节如果程序把表号解析错了或者把广播地址当成单点地址用电表会返回异常应答。解决先打印原始帧的十六进制确认地址域部分是否对应目标表。这里真的有个反直觉的细节电表地址在帧里是低字节在前也就是说表号202408130001在地址域里的排布不是按人眼顺序直接放进去的。我一般会写一个地址转换函数把字符串表号转成低字节在前的字节数组并且在单元测试里用固定的表号验证转换结果。5.3 数据全是0或者差一位BCD解析与数据标识现象采集成功帧校验也通过但读出来的电量永远是0或者比实际值差10倍。一个是数据标识发错了比如要读正向有功电能却发了反向有功电能的数据标识返回的数据自然不对另一个是BCD解析时位序写反了一个字节0x12被解析成21数据就完全偏了。解决先核对规约附录里的数据标识表不同厂商的表计支持的标识会有差异调试时先用厂商提供的读表指令确认数值。BCD解析写完后用已知的字节序列做一次单元测试比如0x12 0x34 0x56 0x78应该被解析为123456.78不要直接用真表对数。5.4 表号对不上地址域低字节在前现象档案表里的表号明明没错但采集的时候电表一直不应答。这属于最容易出问题又是一个玄学级别的坑。DL/T645地址域的6个字节按帧内顺序表示的是表号的低字节在前高字节在后。比如表号123456789012帧内应该是12 34 56 78 90 12但部分表计是反的。解决不要凭记忆写转换逻辑而是做一个表号转地址域的工具方法用两个已知的表号做正反转换测试。这块的逻辑一旦错了换多少块表都是同样的问题所以接线的时候就要确认电表手册里对地址域的说明。5.5 定时任务重叠调度同步与失败补偿现象日志里出现大量重复采集的SQL或者某块表被两个线程同时下发指令导致电表响应混乱。原因是Scheduled默认是单线程的但如果配置了多个任务方法或者部署了多实例就会出现并发问题。还有一个常见场景是把fixedRate和耗时操作一起用任务还没执行完下个周期又开始了。解决单实例下用ReentrantLock做非阻塞锁多实例下要给采集任务加分布式锁。采集失败后不要在同一轮里疯狂重试用一个失败计数表和定时补偿任务来处理失败次数越高重试间隔越长。6. 从“能跑”到“能交付”三个值得做的进阶点采集系统做到能读表、能入库、能展示只是完成了基本盘。如果想让这套系统在答辩或项目评审时更扎实可以往三个方向做深一点。第一个是通讯成功率统计。在meter_info表里维护成功次数和失败次数每次都更新这两个字段界面上展示本轮采集成功率。这个指标看着简单但它是评估采集系统健康度的第一量化指标面试官和评审老师最认这种实实在在的东西。实现时注意不要每采集一块表就UPDATE一次数据库批量提交更合理。第二个是补采机制。采集失败的表不是下一轮再试一次就完了而是要做成分级补采失败后1分钟补采一次再失败5分钟补采一次超过三次就标记离线并告警。这个机制要做好核心在于把失败状态和时间戳管理起来不然补采和周期采集会打架。第三个是串口接入的多线程处理。一块串口在物理上是半双工的一收一发必须串行不能像TCP那样并发收发。所以哪怕有多个采集任务也要让它们通过同一个串口发送队列来下发回包再按请求ID匹配响应。这块做好的工程价值极高因为真实项目里一个采集点下面经常挂着几十块表。我自己做这类系统时有一条习惯每次调试前把电表返回的原始报文用十六进制打印出来存档调不通的时候回头看原始报文比在解析代码里反复打断点管用得多。如果你是在做毕设建议把这段调试日志也保留下来写在说明文档里老师看到你会从原始报文层面讲问题会有眼前一亮的感觉。刚开始对着DL/T645文档看帧结构确实有点枯燥但等第一块表的数据在你的程序里正确入库那种成就感是写十个CRUD都比不上的。希望帮到你。本文还有配套的精品资源点击获取
返回列表