
简介上海交通大学电院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、SOHPCS的功率与状态电表的充放电量环境的温度和烟雾报警这些数据都从设备侧来后端要做的事就是把它们接住、存下来、用起来。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都有现成starterMyBatis-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() { ListDeviceInfo 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 QueryWrapperRealtimeData() .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 ListHistoryData pageHistory(String deviceId, String pointCode, long startTime, long endTime, int page, int size) { PageHistoryData p new Page(page, size); QueryWrapperHistoryData 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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里的serverTimezoneAsia/Shanghai必须加不加会出现时间差8小时的问题modbus.tcp.host要改成实际BMS网关或串口服务器的IPport默认502是标准Modbus TCP端口timeout-ms建议保持3000毫秒左右设备响应慢时超过3秒就跳过本轮logging.level改成debug能在排查采集问题时看到更多输出正式部署时再调回info。提示serverTimezone这一项漏掉排查时间问题会浪费半小时起步。不信你试试先跑起来再补这个参数中间的时间差足以让你怀疑是MySQL的问题还是代码的问题。对照表再列一下方便手边参考配置项默认值说明server.port8080后端接口端口spring.datasource.url127.0.0.1:3306/lab518必改换成实际MySQLmodbus.tcp.host192.168.1.100必改换成现场设备网关IPmodbus.tcp.port502标准Modbus TCP端口一般不用动modbus.tcp.timeout-ms3000单次轮询超时设备慢可适当调大4.3 初始化数据库与设备档案工程通常带一个db/init.sql里面是建库建表语句和初始设备档案数据。我一般不用它自带的建表方式而是用MyBatis-Plus的自动建表能力根据Java实体类生成建表SQL这在少量点位表上特别省事。做法是在实体类上写好TableName和字段注解启动时把ddl-auto配成updateSpring 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协议统一用协议文档里的地址减1PCS协议也按各自的文档核对然后在decoder里统一做一次地址偏移处理。我现在的习惯是拿到新设备协议文档后先用Modbus Poll这类工具手工读一遍寄存器确认地址和数值都对得上再写解码器代码。提示如果手工读正常而代码读不对基本就是地址偏移或字节序问题再看解码器里有没有做乘以比例因子的处理漏掉这步数据就会差一个数量级。5.2 告警时间差8小时时区问题现象测试告警功能时发现数据库里的告警时间比本地时间晚了8个小时大屏上的时间轴和实际对不上。原因JDBC连接串里没写serverTimezoneMySQL驱动在解析DATETIME时用了系统默认时区而服务器和本地电脑时区不一致差的就是东八区这8小时。解决连接串里显式加serverTimezoneAsia/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章顺序别反。希望帮到你。本文还有配套的精品资源点击获取