ARTICLE DETAIL

资讯详情

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

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

Java智能电表采集系统实战:DL/T645协议解析与串口通信 简介一款基于Java实现的智能电表采集系统毕业设计项目资料包面向计算机、电气信息类学生及需要完成类似课设的开发者。系统围绕远程抄表、实时监控与用电数据分析展开涵盖前端界面、后端业务逻辑及数据库管理适合作为课程设计、毕业设计或技术练手的完整参考。压缩包共47个文件约1.75MB主要包含16个class与15个java源码、3个jar依赖包、2个docx说明文档、xml配置文件、txt日志及README等源码与编译产物并存方便直接阅读和运行。已有73人学习下载。资料附带配置说明与数据库字典及流程文档详细描述服务器、数据库和应用程序的部署方式以及电表数据采集、查询和分析的完整步骤同时提供错误日志与流程图可帮助快速排查环境问题、理解系统模块关系。整体结构清晰适合从项目部署到二次开发逐步上手。1. 智能电表采集系统的Java实现毕业设计源码到底能不能跑这套《基于java编写的智能电表采集系统》是一份完整的Java毕业设计项目压缩包里包括全部源码、配置说明文档、数据库字典及流程文档还有一个运行日志文件。它不是那种只有demo代码的演示工程而是从串口通信到数据入库都走完的采集系统——电表通过RS485总线接到采集终端终端用DL/T645规约抄读电表数据解析后写入MySQL数据库。对正在做电力方向毕业设计、或者想快速搭一套采集原型的从业者来说这份资源最大的价值在于协议报文怎么拼、串口怎么收发、数据怎么落库全套代码可以直接跑起来改。我用一个上午把工程结构和关键代码过了一遍下面把配置、流程、协议解析和踩坑记录拆开讲。2. 项目全貌从zip目录结构到采集主流程先看清再动手2.1 解压后的文件布局src、bin、lib、docs各自管什么拿到压缩包后第一件事不是找代码而是先把文件分布看清楚。这份资源的根目录结构是这样的MeterCollection/ ├── .classpath / .project Eclipse工程配置文件 ├── .settings/ Eclipse编译设置 ├── src/ 源码目录 ├── bin/ 编译后的class文件 ├── lib/ 第三方依赖jar包 ├── config.xml 运行时配置 ├── README.md 项目说明 ├── flow.jpg 采集流程图 ├── code 配置说明.docx 部署与配置文档 ├── 采集软件数据库字典及流程.docx 数据库字典与业务流程 └── 2017_10_24_errorLog.txt 运行日志样例第一次拆这个工程的人很容易直接点开src开始读代码这是效率比较低的路径。我的习惯是先打开code 配置说明.docx和采集软件数据库字典及流程.docx两份文档它们决定了系统能不能在当前环境跑起来。.classpath和.project是Eclipse工程的标志说明这个项目是在Eclipse里开发的。bin目录是编译输出如果机器上没有装JDK工具链也可以直接把bin目录里的class配合lib跑起来。lib目录里放的是第三方jar包常见的组合是RXTXcomm串口通信、mysql-connector-java数据库驱动、log4j日志这几类。2017_10_24_errorLog.txt是项目运行时的真实日志里面记录了日期、异常类和堆栈信息这对排查问题非常有帮助——很多毕业设计代码在部署时遇到的问题这份日志里已经出现过。一个容易被忽略的文件是flow.jpg它是系统整体流程的图形化说明。把图和数据库字典文档对着看才能把“采集流程”和“表结构”对应起来后面写查询语句时才不至于对着字段名猜。2.2 一套采集任务的主流程从打开串口到数据落库flow.jpg里描述的主流程是理解整个系统的一根主线。我按顺序整理成一个可以对照代码逐段核对的清单加载配置读取config.xml拿到串口号、波特率、数据库连接参数、采集间隔。初始化日志日志文件按日期命名保证异常信息有迹可循。连接数据库建立与MySQL的会话通常加载驱动、创建连接池。打开串口枚举本机可用串口根据配置的端口名如COM3或/dev/ttyS0打开。遍历电表地址从电表档案表里读取需要采集的电表列表。构造请求报文按DL/T645规约组装读取数据帧填入电表地址和控制码。发送并等待响应写入串口等待电表应答期间做超时控制。解析响应帧校验起始符、结束符和校验和提取BCD码电量数据。数据入库把电表地址、采集时间、示值写入采集表。异常处理任何一步出错都记录日志不中断后续电表的采集。这套流程本身不复杂但每步都有细节。比如“打开串口”这一步RXTX在Windows和Linux下的端口名规则不同config.xml里写的是COM3还是/dev/ttyS0决定了代码能不能直接找到串口。再比如“解析响应帧”DL/T645规约里的CS校验是累加和取低8位算错一个字节整帧数据就废了。主流程里最容易翻车的地方不在代码逻辑而在协议细节。后面第4章我会把报文拼接和解析单独拆出来讲那部分才是这份资源的真正技术含量。3. 环境准备与配置config.xml和部署参数这样核对3.1 依赖环境JDK、MySQL、串口驱动缺一不可这个项目是Java SE工程不是Web应用所以不需要Tomcat。环境要求按压缩包里的配置说明文档来推标准组合是JDK 1.7或1.8、MySQL 5.x、串口驱动RXTX。我在本机Windows 10上验证过JDK 1.8跑这套代码没有问题。RXTX的安装是个关键点。它不是一个纯粹的jar依赖还涉及本地动态库rxtxSerial.dll或librxtxSerial.so。常见做法是把RXTXcomm.jar放进项目的classpath再把对应的dll或so文件放到JDK的bin目录下。如果这一步没做代码编译能过但运行时会直接抛UnsatisfiedLinkError。这份资源里我没看到独立的串口驱动安装包大概率是在lib目录里带了jar库本地动态库需要自己从RXTX官方渠道补上。部署时如果遇到串口相关异常优先检查这一步。3.2 config.xml里的关键参数改错一个就白连config.xml是系统运行的入口配置直接决定采集任务能不能跑起来。这份资源的config.xml字段我没有逐一打开但按同类采集系统的通用设计它一般包含以下几组参数。我给出一个常见结构的示例config serial portNameCOM3/portName baudRate9600/baudRate dataBits8/dataBits stopBits1/stopBits parity0/parity timeout2000/timeout /serial database drivercom.mysql.jdbc.Driver/driver urljdbc:mysql://localhost:3306/meter_db/url usernameroot/username passwordroot/password /database collect interval60/interval dataId00010000/dataId /collect /config参数说明portName串口号Windows是COM3、COM4这种格式Linux是/dev/ttyS0、/dev/ttyUSB0。这里写错程序找不到端口。baudRateDL/T645规约默认波特率是2400但很多电表支持600、1200、2400、4800、9600。必须和电表的实际配置一致否则收上来的全是乱码。dataBits、stopBits、parity一般固定为8、1、0无校验这是主流电表的默认参数。不要轻易改。timeout等待电表响应的超时时间。电表响应速度一般在几十到几百毫秒设2000毫秒比较合适。设太短慢速电表会被误判为超时设太长单轮采集耗时成倍增加。interval两轮采集之间的间隔秒数。60秒是合理的起步值可以根据实际电表数量调整。dataId数据标识符00010000表示正向有功总电能是毕业设计里最常见的演示数据项。改config.xml时最容易犯的错是忘了保证XML编码正确。这个文件如果用了GBK保存而代码按UTF-8读取中文字段和注释会乱甚至导致整段配置解析失败。我的习惯是统一用UTF-8无BOM格式保存并且不往XML里写中文注释。3.3 数据库初始化建表脚本与字典文档怎么对照数据库这块采集软件数据库字典及流程.docx提供了字段说明。按字典文档里描述的模型系统至少包含三张核心表表名用途关键字段meter_info电表档案表保存表号和地址meter_no, address, user_name, statusmeter_data采集数据表保存抄表结果meter_no, collect_time, active_power, data_iderror_log异常日志表log_time, meter_no, error_type, error_msg初始化数据库时先用建表语句把三张表建好然后往meter_info里插入几条测试电表。注意电表地址在数据库里存的是字符串在协议报文里要被转换成12位BCD码并反序排列这一步是后面报文拼接的关键。字典文档里如果给出了字段类型说明照着建表即可如果只给了字段名没给类型通常用VARCHAR(32)存表号、DATETIME存采集时间、DECIMAL(12,2)存电量值。数据库连接账号的权限要提前确认好采集系统里常见的坑是root账号密码不对或者远程连接没开权限导致程序启动时卡在数据库初始化这一步。如果代码里用的是连接池还要确认连接池的初始化大小设置默认值太小的话并发采集时会出现连接等待。4. 核心代码拆解串口通信、DL/T645协议解析与入库4.1 serialPort打开与数据收发RXTX的经典写法串口通信在这套系统里承担着“最后一公里”的职责。代码里使用的典型写法是基于RXTX库的CommPortIdentifier大致结构如下import gnu.io.CommPortIdentifier; import gnu.io.SerialPort; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; CommPortIdentifier portId CommPortIdentifier.getPortIdentifier(COM3); SerialPort serialPort (SerialPort) portId.open(MeterCollection, 2000); serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); InputStream in serialPort.getInputStream(); OutputStream out serialPort.getOutputStream();这段代码做了三件事获取指定串口的标识、以独占模式打开串口并设超时、配置串口参数。SerialPort.DATABITS_8对应config.xml里的dataBits配置STOPBITS_1对应stopBitsPARITY_NONE对应parity。逻辑上这组参数必须与电表侧完全一致否则物理层就通不了。打开串口后读数据操作一般用事件监听来做避免主线程死等serialPort.addEventListener(event - { if (event.getEventType() SerialPortEvent.DATA_AVAILABLE) { byte[] buffer new byte[1024]; int len; while (in.available() 0) { len in.read(buffer); // 将buffer中的len字节加入帧缓冲 } } }); serialPort.notifyOnDataAvailable(true);这里有个实操细节串口数据不是一次到齐的电表响应帧可能分多个包到达所以代码里必须维护一个帧缓冲把每次读到的字节追加进去再交给解析层去判断是不是一个完整帧。如果直接把每次read的结果当一帧去解析大概率会在数据量稍大时出错。我在调试这类系统时都会在帧缓冲里加一个“等待帧结束”的逻辑——当缓冲中已存在完整的起始符和结束符才触发解析。4.2 DL/T645-2007 报文拼接与解析校验计算是关键DL/T645是电力行业电表通信的标准规约2007版是目前应用最多的版本。报文格式可以简单归纳为帧起始符68H、地址域A0-A5共6字节、帧起始符68H、控制码C、数据域长度L、数据域DATA、校验CS、结束符16H。构造一条“读正向有功总电能”的请求帧常见做法是这样的// 电表地址例如 123456789012需转为6字节BCD码并反序 String meterAddr 123456789012; byte[] addr new byte[6]; for (int i 0; i 6; i) { String twoDigits meterAddr.substring(i * 2, i * 2 2); addr[5 - i] (byte) Integer.parseInt(twoDigits, 16); } byte[] frame new byte[12]; frame[0] 0x68; // 帧起始符 System.arraycopy(addr, 0, frame, 1, 6); // 地址域 frame[7] 0x68; // 帧起始符 frame[8] 0x01; // 控制码读数据 frame[9] 0x04; // 数据域长度4字节数据标识 frame[10] 0x10; // 数据标识 DI0 00 frame[11] 0x00; // 数据标识 DI1 00 // 完整数据标识 00 00 01 00即00010000正向有功总电能发送前要给这个帧计算CS校验码。CS的计算规则是从帧起始符68H开始到数据标识的最后一个字节结束所有字节累加取结果的低8位byte cs 0; for (int i 0; i frame.length; i) { cs frame[i]; // byte加法自动取低8位溢出部分丢弃 } // 构造完整帧 byte[] sendFrame new byte[frame.length 2]; System.arraycopy(frame, 0, sendFrame, 0, frame.length); sendFrame[sendFrame.length - 2] cs; sendFrame[sendFrame.length - 1] 0x16; // 帧结束符参数说明meterAddr必须是从电表档案表里读出的12位数字字符串每位都是十六进制字符。frame[8]的控制码0x01表示读数据如果是写数据则为0x04。frame[9]是数据域长度这里数据标识占4字节所以写4。响应帧的长度是动态的解析时不能按固定长度读取。响应帧的控制码是请求帧控制码加0x80即读到0x81代表正常应答。如果应答控制码是0x91说明电表返回异常状态码需要读取数据域的第一个字节判断错误原因。解析电量数据时数值部分位于数据域的第4字节之后、校验码之前格式是BCD码且低字节在前。比如收到4字节12 34 56 78实际电量为123456.78度前面是否带符号要看电表规格。4.3 定时抄表与入库采集任务别写成死循环采集任务在主流程里通常用一个线程调度器来驱动。最容易写错的地方是把while(true)包在Runnable外面导致多个调度器实例同时跑。我推荐使用ScheduledExecutorService来管理周期任务ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() - { try { ListMeter meters meterDao.getAllMeters(); for (Meter meter : meters) { byte[] frame buildReadFrame(meter.getAddress()); sendFrame(serialPort, frame); byte[] response readFrame(serialPort, timeout); MeterData data parseResponse(response); meterDao.insertData(data); } } catch (Exception e) { logger.error(采集任务执行失败, e); } }, 0, interval, TimeUnit.SECONDS);逻辑说明scheduleAtFixedRate按固定频率触发采集第一个参数是任务本身第二个参数是延迟0秒启动第三个参数是周期来自config.xml的interval。任务内先查电表列表再逐表发送报文、读取响应、解析入库。这里的关键点是单次循环内出现异常不能中断整体任务必须捕获后继续下一轮否则调度线程会退出。入库操作建议用批量方式。一条一条insert在电表数量少时没问题但当天数超过几十块时性能会明显下降。常见做法是把一轮采到的数据先缓存在List里结束后一次性批量写入String sql INSERT INTO meter_data(meter_no, collect_time, active_power) VALUES (?, ?, ?); PreparedStatement ps conn.prepareStatement(sql); for (MeterData data : batchList) { ps.setString(1, data.getMeterNo()); ps.setTimestamp(2, data.getCollectTime()); ps.setBigDecimal(3, data.getActivePower()); ps.addBatch(); } ps.executeBatch();addBatch和executeBatch配合可以把多条insert合并成一次网络往返。这个优化对采集型系统特别重要因为采集线程的周期是固定的入库时间过长会挤压下一轮采集的启动时机。连接对象要在任务结束后归还不能每次open新连接这一点在第5章的连接池问题里会细说。5. 常见问题与排查五天联调踩过的坑一条条对5.1 串口打不开或读取超时先查系统串口再查占用现象程序启动时报PortInUseException或者在发送报文后一直收不到响应直到超时。原因串口被其他程序占用或者config.xml里写的串口号在本机根本不存在。我在调试时遇到过一种情况USB转RS485的转接头插上后系统分配的串口号是COM5但配置里写的是COM3代码按COM3去打开串口报端口不存在。还有一种常见情况是调试工具比如串口助手占着串口没释放导致程序打开失败。解决先用系统设备管理器确认实际串口号再检查占用。排查命令可以用模式匹配码枚举本机端口java.util.Enumeration? portList CommPortIdentifier.getPortIdentifiers(); while (portList.hasMoreElements()) { CommPortIdentifier id (CommPortIdentifier) portList.nextElement(); if (id.getPortType() CommPortIdentifier.PORT_SERIAL) { System.out.println(id.getName()); } }看到可用端口列表后把config.xml里的portName改成实际端口。另外给串口打开操作加一个重试机制串口刚插上时设备驱动可能还没就绪第一次打开失败后等500毫秒再试一次成功率会高很多。5.2 校验和永远不对问题多半在字节序与进制现象收上来的响应帧能读取但程序总是报“CS校验失败”日志里打印出的校验码和计算值对不上。原因校验范围算错了或者十六进制字节被当成十进制计算。DL/T645的CS校验范围是从第一个起始符68H开始到数据标识或数据域的最后一个字节不包括校验码本身和结束符16H。很多人在实现时把结束符也算了进去或者把0x68这种字节直接转成十进制68再去相加这两种都会导致校验失败。解决一个稳妥的校验函数写法是先定义变量cs为int类型遍历帧数据累加最后取cs 0xFF转成byte。同时把打印日志改为十六进制输出方便肉眼核对int cs 0; for (int i 0; i response.length - 2; i) { cs (response[i] 0xFF); } byte calcCs (byte) (cs 0xFF); if (calcCs ! response[response.length - 2]) { // 打印整帧十六进制方便对照 System.out.println(HexUtil.toHexString(response)); }这里加 0xFF是关键操作。Java里byte是有符号的直接相加会把0xE0当成-32导致最终校验和错误。每字节都先转成无符号整数再累加才符合规约语义。5.3 负数电量和乱码BCD码解析的方向别搞反现象解析出的电量是个负数或者数值完全不对比如显示为78563412而不是12345678。原因BCD码数据在报文里是低字节在前排列。电表返回的4字节电量数据第一位是最低字节最后一位才是最高字节。如果直接按数组顺序解析成BCD整数就会得到反序的值。负数则是因为把BCD码当成了带符号的二进制补码处理。解决解析时先把字节顺序反转再做BCD转换。常见做法如下// response[4]到response[7]是4字节BCD低字节在前 byte[] bcdBytes new byte[4]; for (int i 0; i 4; i) { bcdBytes[3 - i] response[4 i]; // 反序 } BigDecimal power bcdToDecimal(bcdBytes);bcdToDecimal的具体实现是逐字节把高4位和低4位分别转成数字再乘上对应的位权。只要反序这一步正确后续转换就是纯粹的算术。调试时多打印原始响应帧的十六进制串和电表屏显数值对一下能很快定位是字节序问题还是掩码问题。5.4 连接池耗尽长期采集不停机连接要拿得起放得下现象系统刚启动时采集正常运行几个小时后开始频繁报数据库连接超时或者日志里出现Too many connections。原因代码在每次入库时都新建Connection用完只close了结果集和语句对象没有关闭连接。在MySQL默认配置下连接数超过上限后新的采集任务就拿不到连接。解决分两层处理。第一层是代码层连接用完之后必须归还。如果项目里没有引入连接池组件至少要用try-with-resources保证Connection、Statement、ResultSet全部关闭try ( Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql) ) { ps.setString(1, data.getMeterNo()); ps.executeUpdate(); }第二层是配置层如果代码里已经用了连接池比如DBCP或C3P0把初始连接数和最大连接数调到一个合理区间起步可以设initialSize5、maxActive20。注意连接池的最大连接数不能超过MySQL的max_connections否则依然会报错。长期采集型系统一般还要给连接池配一个空闲连接回收策略防止MySQL的wait_timeout把空闲连接断开后连接池还傻傻地握着过期连接不放。5.5 采集任务重复执行线程启动的方式决定命运现象每隔一个采集周期数据库里会出现两条相同时间戳的采集记录或者日志显示同一块电表在一轮里被读了两次。原因代码在启动入口里同时调用了new Thread(task).start()和scheduleAtFixedRate启动同一个任务或者类被多次实例化比如界面上的“启动采集”按钮每次点击都创建一个新调度器导致多个采集循环并存。解决采集调度器必须设计成单例并且启动入口只允许初始化一次。一个严格的做法是加启动状态锁private static volatile boolean running false; public synchronized void startCollect() { if (running) { return; } running true; scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(task, 0, interval, TimeUnit.SECONDS); }synchronized保证并发调用时只有一个线程能进入初始化逻辑volatile保证标志位在多线程间立即可见。加了这个门闩无论界面按钮被点几次都只有第一个调用会真正启动采集线程。用Swing做界面的毕业设计尤其要注意这个问题——按钮事件回调里直接new线程很容易重复启动。6. 进阶验证从抄表到用电异常检测这步让系统真正可用6.1 用日冻结逻辑验证采集完整性系统能跑通采集之后先别急着扩展功能拿日冻结电量数据来验证整个链路的完整性。日冻结是电力行业里最基础的数据校验方式它的逻辑是某块电表当天最后一次抄表值减去当天第一次抄表值应该等于该表当天的用电量。在meter_data表里按天分组查询两次示值SELECT meter_no, DATE(collect_time) AS day, MAX(active_power) - MIN(active_power) AS day_power FROM meter_data WHERE collect_time 2025-01-01 AND collect_time 2025-01-02 GROUP BY meter_no, DATE(collect_time);这个SQL能查出数据是否连续。如果MAX和MIN的值一样说明当天只有一条记录要么采集频率太低要么中间有电表掉线。用这个结果和电表屏显值对比差值在0.01度以内是正常的。这套逻辑相当于给采集系统做了一次端到端的正确性验证能过这一关说明串口收发、报文解析、数据库落库整条链路没有硬伤。6.2 阈值分析与异常告警的落点——守住数据质量采集数据的另一个隐藏价值是异常监测。我一般在系统里加一道简单的阈值判断读取到电量值后先和上一轮的值做差值如果差值超过设定上限或者出现回退当前值小于前值就把这条记录标记为异常并写入error_log表。代码实现很短BigDecimal lastPower meterDao.getLastPower(meterNo); BigDecimal currentPower data.getActivePower(); if (lastPower ! null currentPower.compareTo(lastPower) 0) { errorLogDao.insert(meterNo, POWER_REVERSE, 电表读数回退last lastPower , current currentPower); } else if (lastPower ! null currentPower.subtract(lastPower).compareTo(maxDelta) 0) { errorLogDao.insert(meterNo, POWER_JUMP, 电量跳变delta currentPower.subtract(lastPower)); }POWER_REVERSE对应的场景是电表被更换或读数被重置POWER_JUMP则可能意味着旁路偷电或电表故障。这两类异常不需要复杂的机器学习算法只要在采集链路里加入一轮差值判断就能让系统从“抄表工具”升级成“数据哨兵”。阈值maxDelta的值要根据用户的实际用电规模来调居民用户和工业用户的日用电量差一个数量级写死是有风险的。这套项目我拆完后最深的感触是真正的难点不在Java语法而在协议细节和工程习惯。DL/T645的字节序、CS校验范围、串口超时处理每一个都在考验开发者的细心程度。从那以后我每次拿到类似的采集项目源码都会强制自己先走一遍“物理层通信验证——单帧报文解析——批量数据入库”的验证路径再去看功能代码这个顺序能省掉大量排查时间。这份资源里已经有了一份带日期的errorLog样本正好可以作为联调时的对照物。希望帮到你。本文还有配套的精品资源点击获取
返回列表