news 2026/10/18 14:30:53

MQTT+规则引擎:智慧农业自动化控制与告警联动系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MQTT+规则引擎:智慧农业自动化控制与告警联动系统

简介:这是一套面向农业信息化开发者的智慧农业集成管理系统源码,基于 Java Web 技术实现。系统覆盖农田环境监测、设备远程控制、灌溉施肥计划、用户认证等核心场景,把物联网数据采集与后台管理有机结合,适合需要学习 Java 项目分层设计、实体建模、数据持久化及前后端交互的开发者参考。压缩包共 224 个文件,以 142 个 Java 源文件为主,另有 30 个 HTML 页面、25 个 XML 配置、20 个 JavaScript 脚本及 SQL 数据库脚本等,整体仅 476KB,结构紧凑,便于快速导入项目运行。系统前端涵盖地块、传感器、设备、方案等管理界面,后端处理业务逻辑与数据存取,配合数据库脚本可搭建一套完整原型,可用于课程设计、项目实训或技术原型搭建。目前已有 174 人学习浏览,对希望了解智慧农业系统模块划分与 JavaWeb 工程结构的读者具有较好的参考价值。

1. 智慧农业集成管理系统:从“半夜被电话吵醒”到“大棚自己会决策”

凌晨三点,值班室连续弹出十几条“温度超限”告警,你却不敢把手机调成静音——因为告警只负责通知,不负责处理,棚里的卷帘和风机还是得自己摸黑去按。这套智慧农业集成管理系统解决的正是这个矛盾:它把环境感知、阈值判断、设备联动放在同一条链路上,让大棚在没人盯着的时候也能自己做出反应。它不是单一功能的小插件,而是一个从设备接入到管理后台的完整工程,包含环境数据采集、告警规则引擎、自动化控制、可视化看板等模块。适合正在做农业物联网平台化、想把硬件数据和业务逻辑真正打通的技术团队参考。下面按架构设计、核心代码、运行步骤和二次开发的顺序来拆。

2. 架构设计:一次环境上报如何变成一组合法操作

2.1 技术栈选型:为什么用 EMQX 而不是自研长连接

很多农业项目上来就自己封装 Socket,设备端连服务器、控制端也连服务器,最后连接管理、断线重连、消息去重全要自己写,折腾半年还不敢保证稳定。这套系统直接采用 MQTT Broker 做消息中枢,服务端不维护设备长连接,只作为消息的订阅者,设备的上下线、心跳保活都交给 Broker 处理。服务端用的是 Spring Boot 2.x + MyBatis Plus + MySQL + Redis,前端是 Vue3 + Element Plus,规则引擎和数据接入都是独立模块,便于替换和扩展。

这种选型对一个以“管理”为核心的农业系统来说比较务实:硬件采集端只需要支持 MQTT 协议,后端通过订阅主题拿到原始数据,存入 MySQL 做持久化和报表,Redis 做告警冷却和设备令牌缓存。实时推送用 WebSocket,不需要引入额外的流计算框架,单机支撑几百个设备绰绰有余。

2.2 工程目录结构:四块物理隔离,按部署单元划分

解压后整体分为四个部分:后端服务、前端管理台、数据库脚本、说明文档。看目录之前先有个心理预期——这属于典型的单体应用,但模块边界清晰,不会出现“业务都堆在 controller 里”的情况。

smart-agriculture/ ├── backend/ # Spring Boot 后端服务 │ ├── src/main/java/ │ │ ├── controller/ # 管理端 REST 接口 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Plus 数据访问层 │ │ ├── mqtt/ # MQTT 订阅与消息处理 │ │ ├── rule/ # 告警规则引擎 │ │ ├── websocket/ # 实时推送 │ │ └── config/ # 数据源、Redis、MQTT 配置 │ └── src/main/resources/ │ └── application.yml ├── frontend/ # Vue3 管理后台 │ ├── src/ │ │ ├── views/ # 大屏监控、设备管理、告警记录 │ │ ├── api/ # 接口封装 │ │ └── router/ │ └── package.json ├── sql/ │ └── init.sql # 建库建表脚本与初始数据 └── docs/ └── 部署说明.md

后端里的mqtt和rule是两个独立包,这是这套系统的精华区域。mqtt负责把协议层的脏活隔离在业务之外,rule负责把告警和联动做成可配置项。实际改造时,更换硬件协议只需要动mqtt包,调整控制策略只需要动rule包,互不干扰,这就是模块划分带来的收益。

2.3 核心数据链路:从传感器到风机的完整路径

把一次设备上报到最终动作执行串起来,整条链路是:传感器定时上报 → EMQX Broker 接收到主题smart-agriculture/{deviceId}/telemetry的消息 → 后端 MQTT 回调解析 JSON 数据 → 落库至device_data表 → 规则引擎拉取该设备绑定的告警规则逐条比对 → 命中规则后生成告警记录并推送到 WebSocket → 若规则配置了联动动作,则调用设备控制接口下发指令 → 设备端收到指令执行(如打开风机卷帘)。

设备上报的消息体长这样:

{ "deviceId": "G001", "ts": 1720000000, "type": "telemetry", "data": { "temperature": 28.6, "humidity": 75.2, "soilMoisture": 32, "light": 1800 } }

ts字段是 Unix 时间戳,代表设备端采集时间,而不是服务器接收时间。规则引擎在做阈值判断时会拿这个设备时间戳作为依据,避免网络延迟导致的数据乱序。这也是我在类似项目里吃过亏的地方——如果不区分“采集时间”和“到达时间”,告警排序和联动判断会出现错位。

3. 核心模块拆解:告警引擎与自动控制的真实代码

3.1 MQTT 接入层:从主题解析到数据分发

先看mqtt包里的消息回调处理器。它负责订阅主题、解析负载、分发数据。这里有一个关键设计:用 Topic 携带设备 ID,消息体只放业务数据,这样 MQTT 端的通配符订阅能够天然按设备维度路由,后端不需要在消息体里额外解析设备路由信息。

@Component public class MqttInboundHandler implements MqttCallback { private final DeviceDataService deviceDataService; private final RuleEngine ruleEngine; private final MqttClient mqttClient; @EventListener(ApplicationReadyEvent.class) public void connectAndSubscribe() throws MqttException { mqttClient.setCallback(this); // QoS=1 保证消息至少一次送达,避免关键采集数据丢失 mqttClient.subscribe("smart-agriculture/+/telemetry", 1); } @Override public void messageArrived(String topic, MqttMessage message) { // topic 形如 smart-agriculture/G001/telemetry String[] segments = topic.split("/"); String deviceId = segments[1]; JSONObject payload = JSON.parseObject(new String(message.getPayload())); JSONObject data = payload.getJSONObject("data"); DeviceData entity = new DeviceData(); entity.setDeviceId(deviceId); entity.setTemperature(data.getDouble("temperature")); entity.setHumidity(data.getDouble("humidity")); entity.setSoilMoisture(data.getDouble("soilMoisture")); entity.setLight(data.getDouble("light")); entity.setCollectTime(new Date(payload.getLong("ts") * 1000)); // 先落库,再进规则引擎,保证告警可追溯 deviceDataService.save(entity); ruleEngine.evaluate(entity); } }

这段代码有几个值得注意的参数细节。订阅时指定 QoS 为 1,意味着消息在 Broker 和客户端之间至少送达一次,代价是可能出现重复消息,因此规则引擎里要做幂等控制。collectTime用的是设备端时间戳,如果设备本地时钟不准,这里会出现时间偏差,部署时要用 NTP 统一设备时钟,否则后续的告警时长统计会失真。数据落库和规则判断是串行的,如果设备量大导致save变慢,常见做法是把save改为异步批量写,或者引入消息队列解耦。

3.2 告警规则引擎:阈值比对、持续时间与去重冷却

规则引擎是这套系统的核心,决定了告警链路的可靠性。它做的事并不复杂——从数据库查出该设备绑定的规则,逐条判断当前值是否触发,触发后再验证是否满足“持续时间”要求,最后通过 Redis 做冷却去重。

@Service public class RuleEngine { private final AlarmRuleMapper ruleMapper; private final AlarmRecordMapper alarmRecordMapper; private final StringRedisTemplate redisTemplate; private final DeviceControlClient controlClient; public void evaluate(DeviceData data) { List<AlarmRule> rules = ruleMapper.selectByDeviceId(data.getDeviceId()); for (AlarmRule rule : rules) { double actual = extractMetricValue(data, rule.getMetric()); if (!compare(actual, rule.getOperator(), rule.getThreshold())) { continue; } // 持续时间检查:只有当指标连续超过阈值一定秒数才告警 String key = "rule:trigger:" + rule.getId() + ":" + data.getDeviceId(); Long startTime = redisTemplate.opsForValue().increment(key); if (startTime == 1) { redisTemplate.expire(key, Duration.ofSeconds(rule.getDurationSec())); } Long elapsed = redisTemplate.getExpire(key, TimeUnit.SECONDS); if (rule.getDurationSec() - elapsed < rule.getDurationSec()) { // 触发时长未达到设定值,继续累积 continue; } // 冷却去重:同一规则同一设备在 cooldown 内只推一次告警 Boolean acquired = redisTemplate.opsForValue() .setIfAbsent("alarm:cooldown:" + rule.getId() + ":" + data.getDeviceId(), "1", Duration.ofSeconds(rule.getCooldownSec())); if (Boolean.TRUE.equals(acquired)) { AlarmRecord record = new AlarmRecord(); record.setRuleId(rule.getId()); record.setDeviceId(data.getDeviceId()); record.setActualValue(actual); record.setLevel(rule.getLevel()); alarmRecordMapper.insert(record); // 如果规则配置了联动动作,触发设备控制 if (rule.getAction() != null) { controlClient.execute(rule.getAction(), data.getDeviceId()); } } } } private double extractMetricValue(DeviceData data, String metric) { switch (metric) { case "temperature": return data.getTemperature(); case "humidity": return data.getHumidity(); case "soilMoisture": return data.getSoilMoisture(); case "light": return data.getLight(); default: throw new UnsupportedOperationException("不支持指标: " + metric); } } private boolean compare(double actual, String op, double threshold) { switch (op) { case ">": return actual > threshold; case "<": return actual < threshold; case ">=": return actual >= threshold; case "<=": return actual <= threshold; default: return false; } } }

许多初版项目死在“告警风暴”上——温度刚越过阈值就疯狂推告警,几分钟刷几百条,运营直接把通知关掉。这个引擎用 Redis 做了两层控制:第一层是“持续时间”,指标必须持续越界指定秒数才产生告警,避免单次毛刺导致误报;第二层是“冷却时间”,同一设备同一规则在冷却期内只推一条,避免重复打扰。metric和operator字段都是可配置的,新增一种传感器指标只需要在extractMetricValue里加一个分支,这也是扩展成本最低的设计方式。

3.3 设备控制链路:告警触发后指令如何安全下发

规则命中后执行联动动作,例如温度超过 32 度持续 60 秒就打开风机。指令下发不能直接裸调设备接口,中间要经过一个控制服务,统一处理指令格式、反馈确认和失败补偿。

@Service public class DeviceControlClient { private final MqttPublisher mqttPublisher; private final StringRedisTemplate redisTemplate; public void execute(JSONObject action, String deviceId) { String commandTopic = "smart-agriculture/" + deviceId + "/command"; String requestId = UUID.randomUUID().toString(); JSONObject payload = new JSONObject(); payload.put("requestId", requestId); payload.put("action", action.getString("action")); payload.put("param", action.getJSONObject("param")); // 发送前记录待确认状态,Redis key 10秒过期,用于超时重试 mqttPublisher.publish(commandTopic, payload.toJSONString()); redisTemplate.opsForValue().set( "device:ack:" + requestId, deviceId, Duration.ofSeconds(10)); } }

这里用了“请求 ID + 待确认状态”的异步确认模式。设备执行指令后会上报一条包含requestId的 ack 消息,后端确认后删除 Redis 里的待确认 key。10 秒内没有确认则判定下发失败,触发补偿逻辑重发指令或记录异常工单。payload中的action是动作名(如open_fan、roll_curtain),param是动作参数,由后台规则配置。这种设计把“下发”和“成功”解耦,不会出现设备离线时指令凭空消失、操作记录里却显示已执行的情况。

4. 本地运行与避坑记录:从建库到告警联动跑通

4.1 环境准备与数据库初始化

运行前需要准备 MySQL 5.7+、Redis 5.0+、EMQX 4.x/5.x,以及 JDK 1.8 和 Node.js 16+。把sql/init.sql导入 MySQL,脚本里包含了设备表、告警规则表、告警记录表、用户表和初始数据。建库后建议先登录 MySQL 确认alarm_rule表里是否插入了示例规则:

SELECT * FROM alarm_rule; -- 预期结果:至少包含一条 temperature > 32.00 的规则,并配置了 open_fan 动作

还没有示例规则就手动插入一条测试用规则:

INSERT INTO alarm_rule (rule_name, device_id, metric, operator, threshold, duration_sec, action, cooldown_sec) VALUES ('温度过高自动开风机', 'G001', 'temperature', '>', 32.00, 60, '{"action":"open_fan","param":{"level":1}}', 300);

duration_sec表示连续越界 60 秒才触发,cooldown_sec表示触发后 300 秒内不再重复告警。这里的参数直接影响系统灵敏度,测试阶段建议把duration_sec调小到 5 秒,便于快速验证联动效果。

4.2 后端配置与启动

后端配置集中在application.yml。需要改三处:MySQL 地址、Redis 地址、EMQX Broker 地址。其他保持默认即可。

spring: datasource: url: jdbc:mysql://localhost:3306/smart_agri?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mqtt: broker: tcp://localhost:1883 client-id: server-agri-${random.value} username: emqx_user password: emqx_pass

client-id必须全局唯一,且不能与设备端共用同一个 ID,否则 Broker 会互相踢下线。serverTimezone=Asia/Shanghai是为了保证数据库时间字段和采集时间一致,这里省略会导致时间差 8 小时的经典问题。启动后端后看日志,出现Connected to MQTT broker和Subscribed to smart-agriculture/+/telemetry说明连接和订阅都正常。

4.3 前端启动

前端启动前先安装依赖:

cd frontend npm install npm run dev

启动后访问本地开发服务器地址,默认端口在vite.config.js里配置。登录页账号密码在init.sql的sys_user表里。进入系统后先打开“设备管理”页面,确认能查看到导入的设备列表;再打开“大屏监控”,这个页面会通过 WebSocket 实时刷新数据。

4.4 避坑记录:5 个真实踩过的坑

坑 1:时间戳差了 8 个小时,告警统计对不上

现象:设备上报的采集时间在管理后台显示比当前时间晚 8 小时,按小时统计的温湿度曲线整体右移。

原因:MySQL 连接串没加serverTimezone,而程序里存的是带时区的 JavaDate,驱动默认用了服务器本地时区做转换。

解决:在 JDBC URL 中显式指定serverTimezone=Asia/Shanghai,并在设备端确认上报的时间戳是 Unix 时间戳,不要传带时区的字符串。

坑 2:偶发出现“有告警通知但没执行联动”,复查日志发现消息根本没到服务端

现象:规则引擎输出日志时有时无,Mqtt 订阅看似正常,但告警直接漏掉。

原因:MQTT 消息的 QoS 设置为 0,即“最多一次”,Broker 不保证送达。当后端短暂阻塞或网络抖动时,消息直接丢弃。

解决:订阅和发布统一使用 QoS 1,同时在后端消费逻辑里做幂等,防止 QoS 1 带来的重复送达。

坑 3:生产环境上温度告警刷屏,几分钟推了几百条

现象:单台设备温度越界后,告警记录和微信通知瞬间被刷满。

原因:规则表里没有冷却机制,每次上报数据命中规则都立即生成告警。

解决:在规则表增加cooldown_sec字段,用 RedisSETNX实现固定时间窗口内的去重,同一个设备和规则在冷却窗口内只允许产生一条告警。

坑 4:WebSocket 推送在大屏页面偶发断开,刷新页面就好

现象:监控大屏连续运行几小时后推送停止,控制台提示 WebSocket 连接关闭。

原因:有些前端代码里对onbeforeunload做了清理操作,但没有妥善管理重连;后端也没有检测心跳,连接被中间层静默回收后无人感知。

解决:前端封装一个带指数退避的自动重连逻辑,后端每 30 秒发送一次 ping 帧保活,超过三次未收到 pong 就主动关闭该连接。

坑 5:前端表格显示“undefined”,接口数据明明有

现象:设备列表里的温度、湿度字段全显示成 undefined,但网络请求返回值正常。

原因:后端返回的是驼峰命名(如collectTime),数据库表字段是下划线命名(如collect_time),MyBatis Plus 的映射配置没打开。

解决:在application.yml里配置map-underscore-to-camel-case: true,或保证实体类字段与数据库列名严格对应。

5. 二次开发:把一套固定系统改成可配置的交付物

把这个系统从“只管一套大棚”变成“能复制给多个基地用”,重点改造方向有两个:设备接入的通用化和规则配置的可视化。

先看设备接入层。现在MQTT主题里写死了smart-agriculture前缀,不同基地的租户如果共用一套服务,主题前缀就得变成tenantId/deviceId/telemetry这种形式。消息回调里解析出tenantId后,再根据租户去查规则。这样不用改核心引擎,就能支持多租户隔离。如果新接入的设备不是 MQTT 协议,比如通过 Modbus RTU 采集,可以单独写一个协议适配器,把 Modbus 数据封装成标准DeviceData对象丢给RuleEngine,复用告警和联动链路。

另一个值得改的地方是规则配置。当前alarm_rule表的action字段是 JSON 字符串,要改动作只能写 SQL。把后台“告警规则”页面扩成一个表单界面,操作符、阈值、持续时间、动作类型都做成下拉选择,动作参数动态渲染成输入框。这样运营人员能在界面上调整联动策略,不用再请开发改数据。改造时注意:规则表的变更要有版本号字段,一旦规则被修改,旧告警记录仍能通过版本号追溯当时使用的规则内容。

分享一个我的教训:有一次给某基地做演示,我把设备 ID 直接写死在 Service 里,现场换了一套设备就全盘翻车。从那以后,我每次接入新设备都强制走一遍配置文件注册流程,设备 ID、短信通知开关、阈值参数一律从数据库读取,再也不敢在代码里埋固定值。希望这套系统的拆分思路能帮到你,尤其是别让告警引擎和控制链路耦合在一起——改起来确实天差地别。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 14:30:38

波形扩散模型:面向低光照图像的信号级增强方法

简介&#xff1a;本资源是一套基于波形扩散模型的低光照图像增强算法Python实现&#xff0c;面向计算机视觉方向的学生、科研人员及工程实践者&#xff0c;聚焦解决夜间、隧道、监控等场景下图像细节丢失、噪声显著、对比度低等核心问题。项目完整复现了Wavelet Diffusion Mode…

作者头像 李华
网站建设 2026/10/18 14:27:22

冒险岛085服务端源码进游戏实战:登录链路打通与避坑指南

简介&#xff1a;这份冒险岛085服务端源码面向游戏开发学习者与私服研究者&#xff0c;提供了一套可实际进入游戏的Java服务端实现&#xff0c;适合想深入理解2D横版网游后台运作机制、进行二次开发或功能修复的开发者。压缩包内共456个文件&#xff0c;全部为java源码&#xf…

作者头像 李华
网站建设 2026/10/18 14:25:18

Qt C++植物大战僵尸源码解析:从工程结构到游戏循环与碰撞检测

简介&#xff1a;这是一份基于Qt与C的简易植物大战僵尸游戏源码包&#xff0c;定位为完整实战型Qt入门项目&#xff0c;适合有一定C基础、想学习GUI开发的读者。压缩包里共有36个文件&#xff0c;其中17个.cpp源文件和16个.h头文件分别用于实现植物、僵尸、子弹、地图等游戏类&…

作者头像 李华
网站建设 2026/10/18 14:22:42

华为流程分级L1-L6深度拆解:从业务域到任务的操作指南

简介&#xff1a;这份PDF资料聚焦华为流程管理体系的L1至L6层级划分&#xff0c;面向企业管理者、流程架构师及希望系统学习流程建设的从业者&#xff0c;帮助解决流程框架搭建、模块设计与建模方法论等核心问题。资源包内含1个PDF文件&#xff0c;大小约2.53MB&#xff0c;内容…

作者头像 李华
网站建设 2026/10/18 14:22:09

MySQL 5.7.40离线安装包部署指南:glibc2.12兼容与生产级配置

简介&#xff1a;本资源为MySQL 5.7.40官方二进制离线安装包&#xff0c;专为x86_64架构、glibc 2.12及以上版本的Linux系统设计&#xff0c;面向数据库运维工程师、Linux系统管理员及后端开发人员&#xff0c;解决无外网环境下的MySQL快速部署与初始化问题。压缩包共379个文件…

作者头像 李华
网站建设 2026/10/18 14:20:19

图像处理文献综述文档使用指南:快速定位研究方向与构建综述框架

简介&#xff1a;这份文献综述文档面向图像处理方向的本科生与研究生&#xff0c;尤其适合正在撰写毕业设计或课程论文、需要系统梳理图像滤波知识脉络的读者。内容围绕图像滤波这一核心环节展开&#xff0c;涵盖其发展历程、去噪与细节保留的平衡问题&#xff0c;以及中值滤波…

作者头像 李华