☰
基于Java的储能平台后端源码解析:Spring Boot与Modbus采集链路落地实践
2026/10/26 1:06:10 网站建设 项目流程

简介:上海交通大学电院Lab518储能平台后端设计源码,基于Java语言实现,面向电气工程领域储能系统的后端开发与学习场景。项目以Maven进行构建管理,代码组织在src目录下,包含完整的Java业务逻辑、配置资源与运行日志,适合有一定Java基础的开发者参考后端工程结构、模块划分及储能平台的数据处理流程。

压缩包共69个文件,大小271KB,以56个Java源文件为主,辅以XML/YAML配置、Maven构建脚本、JAR依赖、日志及Git忽略文件等,涵盖典型Java后端项目的必要组成。其中Gzip压缩的日志文件便于查看历史运行记录与排错,readme文档则有助于快速了解项目说明与使用方式。

目前已有283人学习下载,适合用于学习高校实验室级储能平台的后端设计思路、Maven构建配置及日常开发规范,也可作为Java Web后端项目的入门参考。

1. 基于Java的Lab518储能平台后端:这套源码能不能直接落地

拿到这份基于Java语言的上海交大电院Lab518储能平台后端设计源码时,我第一个想法是:它到底能直接跑,还是又一个只能启动不能用的架子?拆过太多储能类后端工程,大部分问题不是代码写不出来,而是采集链路根本对不上现场设备。Lab518这套不一样,它是给实验室储能柜做真实数据采集与监控的,不是课程设计——BMS、PCS、电表、温湿度传感器,从Modbus总线一路采上来,落库、告警、推送,整条链路是完整的。适合两类人:一类是实验室或小规模储能项目的后端负责人,想要一个现成的采集与告警骨架;另一类是Java后端开发者,想找个真实业务场景,把Spring Boot加MyBatis再加Redis这套组合从理论拉到实战里。

2. 储能平台后端到底在做什么:功能边界与技术选型

2.1 储能平台后端的功能边界

先讲清楚它负责什么。一个储能平台,最核心的不是登录、不是权限,是数据。电池簇的电压、电流、SOC、SOH,PCS的功率与状态,电表的充放电量,环境的温度和烟雾报警,这些数据都从设备侧来,后端要做的事就是把它们接住、存下来、用起来。

Lab518这个场景里,设备侧大多是Modbus RTU或Modbus TCP接口,BMS和PCS各有一个从站地址,电表和温湿度传感器挂在同一根总线上。后端负责轮询这些寄存器、解析数据帧、清洗异常值、写入MySQL,同时把最新状态同步到Redis,供大屏、小程序或Web端实时读取。告警是另一条主线:电池过压、欠压、温度越限、SOC跳变,都要在秒级内触发告警记录并推送。

所以这份后端源码的功能边界可以归纳成六块:设备接入与数据采集、数据解析与清洗、设备状态管理、告警与事件记录、历史数据查询、对外接口服务。设计上只围绕这六块做,不掺多余的业务模块,这也是它能作为模板复用的原因。

对比一下常见的“伪后端工程”:接口文档一大摞,业务代码全是增删改查模板,但接入层一个设备驱动都没有,这叫管理系统,不叫储能平台后端。Lab518这份工程的差异点就在于它把采集链路完整实现了,这正好是大部分Java后端最陌生的部分,也是最有复用价值的部分。

2.2 技术栈选型:为什么是Spring Boot加MyBatis加Redis

从工程里的pom依赖看,技术栈是标准的企业级组合:Spring Boot 2.x系列做容器和接口框架,MyBatis-Plus做数据访问,MySQL 5.7做历史数据存储,Redis做实时状态缓存,Modbus4j做设备通信,WebSocket做实时推送,JWT做接口鉴权。

我整理了一张选型表,每个组件都是按储能场景的实际约束挑的:

组件使用方式选型理由
Spring Boot2.x系列生态成熟,嵌入式Tomcat部署简单,定时任务、WebSocket都有现成starter
MyBatis-Plus3.x系列单表CRUD不用写SQL,分页插件好用,适合设备点位表这类写多读多的场景
MySQL5.7及以上历史数据落库标准选择,储能点位数据量不算大,单机足够
Redis5.x及以上设备最新状态与告警标志位放Redis,读延迟低,大屏轮询不压数据库
Modbus4j最新稳定版开源Modbus协议库,支持RTU和TCP,读写保持寄存器够用
WebSocketSpring自带大屏告警实时推送,比前端轮询省资源

这套选型里最关键的一条判断是:数据量不大,但实时性和稳定性要求高。储能单站的点位通常几百个,5秒一轮询,一天下来增量也不大,MySQL单机完全扛得住,没必要为了“大数据”而上ClickHouse或时序数据库。Redis在这里的角色不是缓存热点数据,而是承担“设备最新状态”这个语义——大屏上看到的每一个实时值都来自Redis,历史曲线才查MySQL。这两个库一分工,后端的压力就分开了。

为什么不用Netty手写协议解析?储能设备侧的Modbus协议是公开且固定的标准,用Modbus4j足够了,自己写字节解析容易翻车,而且Modbus4j的坑网上都有人踩过,出了问题好查。为什么不用Quartz而用Spring的@Scheduled?单站点轮询任务只有几个,不需要分布式调度,Scheduled的cron表达式足够,少引一个依赖就少一份维护成本。权限这块也不用Spring Security,设备量少、用户也少,一个拦截器加JWT注解就能控制接口权限,这也是选型里做减法的体现。

2.3 采集层整体链路:从设备寄存器到前端大屏

整个后端的核心链路可以描述成一条数据通路:设备寄存器、Modbus轮询任务、协议解码、实体对象、MySQL历史表、Redis状态表、WebSocket推送。我一般会把它拆成三层:接入层负责和物理设备对话,服务层负责业务逻辑与计算,存储与推送层负责把数据送到该去的地方。

接入层是这份工程最有特色的部分。它没有把Modbus逻辑散落到service里,而是单独放了一个decoder包,每种设备类型对应一个解码器:BMSDecoder、PCSDecoder、MeterDecoder。这样一来,新增一种设备时只需要新写一个解码器并注册到设备类型枚举里,不动原有逻辑。服务层的定时任务只管触发和解码器分发,具体怎么解析寄存器、怎么处理字节序,都在解码器内部完成。

这个分层解决了我常常见到的一个问题:很多Java后端拿到Modbus协议就开始在Controller里拼接字节,写了几天发现代码根本没法维护。协议解析和数据入库必须分开,协议解析放在独立包,数据入库走统一的Service入口,中间用实体对象传递,这条原则在这份工程的包结构里看得很清楚。

3. 源码工程拆解:启动类、采集任务与数据落库的完整链路

3.1 工程目录结构与分层

工程展开后的目录结构大致如下:

lab518-backend/ ├── pom.xml ├── src/main/java/com/lab518/ │ ├── Application.java │ ├── common/ │ │ ├── Result.java │ │ ├── PageResult.java │ │ └── exception/ │ ├── config/ │ │ ├── RedisConfig.java │ │ ├── WebSocketConfig.java │ │ └── CorsConfig.java │ ├── controller/ │ │ ├── AuthController.java │ │ ├── DeviceController.java │ │ ├── RealtimeController.java │ │ └── HistoryController.java │ ├── entity/ │ │ ├── DeviceInfo.java │ │ ├── DevicePoint.java │ │ ├── RealtimeData.java │ │ ├── HistoryData.java │ │ └── AlarmRecord.java │ ├── mapper/ │ │ ├── DeviceInfoMapper.java │ │ ├── DevicePointMapper.java │ │ ├── HistoryDataMapper.java │ │ └── AlarmRecordMapper.java │ ├── scheduler/ │ │ └── EnergyCollectTask.java │ ├── decoder/ │ │ ├── BmsDecoder.java │ │ ├── PcsDecoder.java │ │ └── MeterDecoder.java │ ├── service/ │ │ ├── CollectService.java │ │ ├── AlarmService.java │ │ └── QueryService.java │ └── ws/ │ └── RealTimePushServer.java └── src/main/resources/ ├── application.yml ├── db/ │ └── init.sql └── mapper/

这个结构有几个值得注意的设计。entity里把设备档案、点位定义、实时数据、历史数据、告警记录明确分开,这个划分避免了“一个表装天下”的坏味道。scheduler包只放定时任务,decoder包只放协议解析,两个包之间通过CollectService衔接,职责清晰。common里的Result统一了所有接口的返回结构,前端对接时只需要处理一种格式。

3.2 启动类的设计

启动类写法是典型的Spring Boot入口,加了@EnableScheduling启用定时任务、@EnableWebSocket启用WebSocket服务:

package com.lab518; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.socket.config.annotation.EnableWebSocket; @EnableScheduling @EnableWebSocket @MapperScan("com.lab518.mapper") @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

这里三行注解是关键。@EnableScheduling让Spring解析所有@Scheduled注解,采集任务才能按cron跑起来;@MapperScan把mapper包下的接口扫成MyBatis的代理Bean,不用每个Mapper都写@Mapper注解;@EnableWebSocket把WebSocket相关Bean注册进容器,实时推送才生效。@SpringBootApplication就是常规的组合注解,默认扫描启动类所在包及子包,所以controller、service、scheduler这些子包代码不需要额外配置。

3.3 轮询采集任务:Modbus TCP读取与数据解析

核心的采集任务写在scheduler包里的EnergyCollectTask,它按固定周期轮询所有在线设备,每个设备分发给对应的解码器。示意代码如下:

package com.lab518.scheduler; import com.lab518.decoder.BmsDecoder; import com.lab518.decoder.PcsDecoder; import com.lab518.entity.DeviceInfo; import com.lab518.entity.RealtimeData; import com.lab518.service.CollectService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; @Component public class EnergyCollectTask { private static final Logger log = LoggerFactory.getLogger(EnergyCollectTask.class); @Resource private CollectService collectService; @Scheduled(cron = "0/5 * * * * ?") public void collectAll() { List<DeviceInfo> devices = collectService.listOnlineDevices(); for (DeviceInfo device : devices) { try { RealtimeData data = dispatch(device); collectService.saveRealtimeData(data); } catch (Exception e) { // 单设备采集失败不影响其他设备 log.error("采集失败, deviceId={}, error={}", device.getId(), e.getMessage()); } } } private RealtimeData dispatch(DeviceInfo device) { switch (device.getDeviceType()) { case "BMS": return BmsDecoder.decode(device); case "PCS": return PcsDecoder.decode(device); default: throw new IllegalArgumentException("未支持的设备类型: " + device.getDeviceType()); } } }

这段代码有三个值得注意的参数设置。cron表达式“0/5 * * * * ?”表示每5秒触发一次,这是储能平台常用的采集周期,太快会打爆Modbus总线,太慢又看不到实时性;listOnlineDevices只查设备状态为在线的设备,避免对已停机的设备做无效轮询;单设备异常只记日志不中断整个批次,这是采集程序最重要的容错习惯,一个设备的通信故障拖垮全部采集是新手最常见的翻车场景。

解码器内部做的是Modbus寄存器操作。以BmsDecoder为例,它会根据设备档案里配置的从站号、起始地址、寄存器数量,使用Modbus4j读取保持寄存器,再按BMS的协议映射关系换算成物理量:电压、电流、SOC、SOH、单体最高最低温度。这里有个细节:很多储能设备的寄存器值不是直接可读的数值,而是带有比例因子的原始值,比如0.1V一个LSB,解码器里要乘回去,这个换算逻辑就放在decoder包里。

3.4 数据落库与历史查询:MyBatis-Plus的使用方式

采集到的数据要同时做两件事:写历史表、更新Redis状态。历史数据用MyBatis-Plus插入,代码在CollectService里。示意如下:

package com.lab518.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lab518.entity.HistoryData; import com.lab518.entity.RealtimeData; import com.lab518.mapper.HistoryDataMapper; import com.lab518.mapper.RealtimeDataMapper; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; @Service public class CollectService { @Resource private HistoryDataMapper historyDataMapper; @Resource private RealtimeDataMapper realtimeDataMapper; @Resource private StringRedisTemplate redisTemplate; public void saveRealtimeData(RealtimeData data) { // 最近一条实时数据只保留半小时,减少无效查询 realtimeDataMapper.delete(new QueryWrapper<RealtimeData>() .lt("create_time", System.currentTimeMillis() - 30 * 60 * 1000)); realtimeDataMapper.insert(data); HistoryData history = new HistoryData(); history.setDeviceId(data.getDeviceId()); history.setPointCode(data.getPointCode()); history.setPointValue(data.getPointValue()); history.setCreateTime(data.getCreateTime()); historyDataMapper.insert(history); // 最新值写入Redis,大屏实时读这里 redisTemplate.opsForValue().set( "device:realtime:" + data.getDeviceId() + ":" + data.getPointCode(), data.getPointValue().toString()); } public List<HistoryData> pageHistory(String deviceId, String pointCode, long startTime, long endTime, int page, int size) { Page<HistoryData> p = new Page<>(page, size); QueryWrapper<HistoryData> qw = new QueryWrapper<>(); qw.eq("device_id", deviceId) .eq("point_code", pointCode) .between("create_time", startTime, endTime) .orderByDesc("create_time"); return historyDataMapper.selectPage(p, qw).getRecords(); } }

这里我特意把实时表和历史表分开写。实时表只保留最近半小时的数据,每次写入前先清理过期数据,这个做法的原因是:大屏的“实时值”应该读Redis,实时表只是给“最近状态”兜底,不需要长期保留。历史表才是按时间累积的,查询时用分页插件,前端做曲线图时一次拉一两千点足够。Redis的键设计成“device:realtime:{设备ID}:{点位编码}”,语义清晰,也方便用keys或scan批量获取某设备的所有实时值。

4. 跑通这个后端:环境准备、配置参数与接口验证

4.1 环境准备:JDK、Maven、MySQL、Redis

跑这个工程需要的环境很常规:JDK 1.8以上,Maven 3.6以上,MySQL 5.7以上,Redis 5.x。JDK配置网上教程很多,配置JAVA_HOME时有一个细节容易坑人:JAVA_HOME要指到JDK的安装根目录,不是bin目录,PATH里加的是%JAVA_HOME%\bin,配完在命令行敲java -version验证一下,千万不要配到jre目录,否则Spring Boot启动会报无法加载主类。

MySQL要建一个lab518的库,Redis不用做额外配置,默认端口启动就行。Maven建议配阿里云镜像,不然第一次拉Spring Boot依赖可能要等很久,这也是国内开发环境的一个常识性准备。我一般建议用Idear或Eclipse直接打开工程,等待依赖下载完成后先编译一遍,确认没有红叉再继续配置。

4.2 application.yml里的四个关键参数

工程的核心配置都在src/main/resources/application.yml里。我拆几个必须改的参数:

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/lab518?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 modbus: tcp: enabled: true host: 192.168.1.100 port: 502 timeout-ms: 3000 retry-times: 2 logging: level: com.lab518: debug

四个关键参数:datasource.url里的serverTimezone=Asia/Shanghai必须加,不加会出现时间差8小时的问题;modbus.tcp.host要改成实际BMS网关或串口服务器的IP,port默认502是标准Modbus TCP端口;timeout-ms建议保持3000毫秒左右,设备响应慢时超过3秒就跳过本轮;logging.level改成debug能在排查采集问题时看到更多输出,正式部署时再调回info。

提示:serverTimezone这一项漏掉,排查时间问题会浪费半小时起步。不信你试试,先跑起来再补这个参数,中间的时间差足以让你怀疑是MySQL的问题还是代码的问题。

对照表再列一下,方便手边参考:

配置项默认值说明
server.port8080后端接口端口
spring.datasource.url127.0.0.1:3306/lab518必改,换成实际MySQL
modbus.tcp.host192.168.1.100必改,换成现场设备网关IP
modbus.tcp.port502标准Modbus TCP端口,一般不用动
modbus.tcp.timeout-ms3000单次轮询超时,设备慢可适当调大

4.3 初始化数据库与设备档案

工程通常带一个db/init.sql,里面是建库建表语句和初始设备档案数据。我一般不用它自带的建表方式,而是用MyBatis-Plus的自动建表能力,根据Java实体类生成建表SQL,这在少量点位表上特别省事。做法是在实体类上写好@TableName和字段注解,启动时把ddl-auto配成update,Spring Boot会自动补表。不过生产环境我建议还是用init.sql,因为自动建表不会给你加索引,历史表这种查询量大的表没有索引会越来越慢。

init.sql里要重点看的是device_point表,这张表定义了每个设备的点位编码、寄存器起始地址、寄存器数量、比例因子和数据类型。设备接入的准确性全在这张表上,现场改一个BMS从站地址,不需要动代码,改这张表的连接参数就能生效。

这里我要给个场景化说明:这份资源解决的是“拿到就能跑”的问题,但如果现场设备和Lab518实验室的设备不一样,要改的是device_info表和device_point表的数据,而不是Java代码。理解这一点,才算真正会用这份工程。

4.4 启动与接口验证

启动很简单,IDE里直接运行Application主类,或者Maven打包后命令行启动:

mvn clean package -DskipTests java -jar target/lab518-backend.jar

启动成功后看控制台日志,出现“Started Application”就说明容器起来了。然后用curl验证几个核心接口:

# 登录获取token curl -X POST http://127.0.0.1:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}' # 用token查实时数据 curl http://127.0.0.1:8080/api/realtime/latest \ -H "Authorization: Bearer <token>" # 查设备列表 curl http://127.0.0.1:8080/api/device/list \ -H "Authorization: Bearer <token>"

登录接口返回的JSON里有一个token字段,复制出来放到后面接口的Authorization头里。如果返回401,先检查token有没有复制全;如果返回500,去日志里看具体的异常堆栈,最常见的两个:一是数据库没连上,二是设备没在线导致NPE。都没有问题的话,能看到realtime接口返回每个设备的最新点位数据,这个数据就是从Redis里读出来的。

4.5 前后端联调:跨域与token

如果是前后端分离项目,前端工程连后端时经常会遇到跨域报错。工程里的CorsConfig已经配置了跨域,允许的来源、方法、请求头都在代码里,需要改动时直接调整allowedOrigins数组即可。这里有个易错点:跨域配置必须在Spring的拦截器生效前加载,所以CorsConfig我用的是addCorsMappings方式注册,而不是简单的@CrossOrigin注解到处贴,后者在遇到自定义拦截器时经常会失效。

package com.lab518.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000", "http://127.0.0.1:8081") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOrigins要写前端实际部署地址,不要写*,因为allowCredentials(true)和*不能同时用,这是新手很容易踩的坑;allowedMethods里必须包含OPTIONS,浏览器跨域预检请求会先发OPTIONS,漏了会导致前端报关跨域错误。token这边也有个细节:前端把token放在Authorization头里发送时,如果用的是axios拦截器,要确保token过期后能统一跳回登录页,而不是让每个接口各自报401。

5. 避坑与常见问题:Lab518这类储能后端的5条血泪记录

5.1 Modbus寄存器地址偏移:读出来全是0或错位

现象:设备在线,采集任务也执行了,但入库的数值全是0,或者A设备和B设备的数据互相串。

原因:Modbus寄存器地址有两种编号方式,0-based和1-based。Modbus4j内部默认从0开始,而很多厂商的设备寄存器表按照1-based标注,起始地址差了1,读出来的就是相邻寄存器。这个偏差在单个点位时不容易发现,因为电压、电流的数值范围看起来都“合理”,等到做告警联调时才对不上。

解决:在设备点位表里增加一个字段记录协议类型,BMS协议统一用协议文档里的地址减1,PCS协议也按各自的文档核对,然后在decoder里统一做一次地址偏移处理。我现在的习惯是,拿到新设备协议文档后,先用Modbus Poll这类工具手工读一遍寄存器,确认地址和数值都对得上,再写解码器代码。

提示:如果手工读正常而代码读不对,基本就是地址偏移或字节序问题;再看解码器里有没有做乘以比例因子的处理,漏掉这步,数据就会差一个数量级。

5.2 告警时间差8小时:时区问题

现象:测试告警功能时,发现数据库里的告警时间比本地时间晚了8个小时,大屏上的时间轴和实际对不上。

原因:JDBC连接串里没写serverTimezone,MySQL驱动在解析DATETIME时用了系统默认时区,而服务器和本地电脑时区不一致,差的就是东八区这8小时。

解决:连接串里显式加serverTimezone=Asia/Shanghai,同时把MySQL服务端的time_zone也改成+08:00。只改连接串不彻底,服务器重启后MySQL的time_zone可能被重置,时间照样漂。可以用show variables like '%time_zone%'确认,如果是CST,执行set global time_zone = '+08:00'并写入MySQL配置文件的mysqld段。

验证方法:改完连接串后重启服务,插入一条带当前时间的记录,select出来对比,时间一致就说明时区问题解决了。

5.3 多实例部署后采集数据翻倍:定时任务重复执行

现象:用两台服务器做高可用部署,发现同一个BMS的数据出现了两份,历史曲线出现重影。

原因:两台实例都在执行定时采集任务,同一套cron同时触发,数据重复写入。单机部署时这个问题不会暴露,但只要一上多实例,必现,属于部署架构里最容易漏的一环。

解决:最简单的方式是用ShedLock给定时任务加分布式锁,核心逻辑是锁记录放数据库,任务执行时抢锁,抢到的才跑。接入方法是引入shedlock-spring依赖,建一张shedlock表,在定时任务上加@SchedulerLock注解。锁的默认时长要大于一次采集的耗时,否则任务没执行完锁就过期,造成并发重入。我踩过把锁时长设成1分钟,而采集一次要2分钟的坑,数据照样翻倍。

5.4 Redis存实时值变乱码:序列化问题

现象:用RedisTemplate存设备状态,存进去是对象,读出来的是\xAC\xED开头的乱码,前端拿到直接解析失败。

原因:默认的RedisTemplate用的是JDK序列化,存到Redis里的不是可读字符串,而是Java序列化二进制。

解决:换成StringRedisTemplate存字符串,或者自定义RedisTemplate的序列化器为Jackson2JsonRedisSerializer。我自己的做法是直接用StringRedisTemplate存值的toString,简单直观,前端拿到就是标准JSON。另外注意:如果用Jackson序列化,存进去的对象可能带类名信息,前端拿到的字符串会带Java类路径,同样不好解析。最省心的方案还是直接存JSON字符串,入库前用Fastjson或Jackson把对象转成字符串,出库时再反序列化。

5.5 Tomcat线程池被采集任务占满:Modbus阻塞调用

现象:采集任务跑了一段时间后,普通接口请求变得非常慢,甚至超时,重启后恢复正常,过一会儿又卡了。

原因:默认的Tomcat线程池最大200,而采集逻辑里用了同步阻塞调用,如果某次Modbus请求没有设置超时,线程会一直挂着等。积累到临界点,线程池耗尽,接口请求就全部排队。

解决:给Modbus调用统一收口到一个独立的线程池,连接超时和读超时分开设置,连接3秒、读5秒,同时把Tomcat的max-threads调到500。排查时用jstack看线程栈,如果看到大量线程卡在Modbus socket read,基本就是这个原因。采集线程池的拒绝策略用DiscardOldestPolicy,宁可丢弃一次采样也不能让接口线程被拖死。

6. 进阶改造:把轮询改成MQTT主动上报并验证链路

很多储能设备其实支持主动上报,用MQTT代替Modbus轮询可以大幅降低后端负担,实时性也更好。这份工程当前是轮询模型,如果现场设备侧支持MQTT,改造成被动接收的核心步骤有四步:引入paho依赖;写一个MqttConsumerService,启动时连Broker并订阅topic;在回调函数里解析JSON,走同一个CollectService.saveRealtimeData落库;通过配置开关决定启用轮询还是MQTT,两个采集入口并存。

@Bean public MqttClient mqttClient() throws MqttException { String broker = "tcp://127.0.0.1:1883"; String clientId = "lab518-backend-" + System.currentTimeMillis(); MqttClient client = new MqttClient(broker, clientId); MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(true); options.setConnectionTimeout(10); options.setKeepAliveInterval(30); client.connect(options); client.subscribe("lab518/device/+/data", 1); return client; }

clientId建议加时间戳,避免两台实例用同一个clientId互相踢下线;qos用1,保证消息至少送达一次,储能数据丢一条可能正好丢掉一次告警;keepAliveInterval设30秒,超过这个时间没发心跳,Broker会判定设备掉线。

验证用一个小方法:本地起一个EMQ X或者用Mosquitto,再用MQTTX客户端模拟设备端,每2秒往topic发一条JSON。同时打开MySQL的general_log或者直接查history_data表count,跑10分钟对比入库总条数,如果和发送条数一致,说明链路没有问题。

接入MQTT之后要注意的一个点:设备离线判断不能依赖Modbus轮询的重试次数了,要看MQTT的last will遗嘱消息。后续如果上生产,建议把设备在线状态改成在MQTT遗嘱里标记,比轮询感知快得多。

我自己接储能项目时,第一件事永远是问设备侧支持Modbus还是MQTT,先定采集协议再定架构。曾经一个项目在轮询模型跑了一个月后客户要求改成MQTT,重构成本比一开始做双模采集高了三倍。从那以后,每次拿到设备协议文档,我都先做一张通信方式确认表再动手,这份工程下载下来,也建议先跑通第4章再改第6章,顺序别反。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询