简介:基于Java的智慧农业物联网平台设计源码,聚焦气象站、水肥一体机、墒情监控和视频监控四大模块,面向智慧农业项目开发者、系统集成人员及农业信息化课程设计者。该源码将物联网技术融入传统种植养殖场景,帮助解决生产环境感知、水肥精准调控、土壤墒情预警和远程可视化管理等实际问题。压缩包仅99KB,共21个文件,其中12个Java源文件承担核心业务逻辑与设备数据接入,另有Markdown文档、Maven配置、Git忽略文件等辅助内容,便于梳理工程结构和启动方式。已有495人学习,表明这套设计具备一定参考价值。通过研读源码,可以理解气象数据采集、水肥联动策略、墒情阈值判断及视频接入服务等关键实现,并在此基础上扩展适配不同作物或地域的定制功能,是学习Java农业物联网平台设计的实用入门资料。
1. 智慧农业物联网平台源码:不是大屏展示,是设备联动决策
第一次接触这套基于 Java 的智慧农业物联网平台源码时,我的第一反应是“又一个大屏可视化项目”吧?但把代码拉下来跑了一遍才发现,核心根本不在一块炫酷的展示页面,而是把气象站、墒情传感器、水肥一体机三类硬件真正联动起来的决策逻辑。平台把设备接入、数据入库、阈值告警、灌溉控制串成一条完整链路,视频监控反而是辅助位。对正在做农业物联网外包项目的 Java 工程师、想拿完整案例做课程设计的在校生、以及要快速搭产品原型的团队来说,这套源码的价值在于:给你一条能跑通业务闭环的参考实现,而不是一堆没法落地的页面截图。
2. 平台技术栈与代码结构:Netty 网关、Spring Boot 服务和四个业务模块
2.1 设备接入层:为什么选 MQTT 而不是 HTTP 直连
农业现场的硬件设备分布在大棚、田间、山地,网络环境普遍不稳定,很多 DTU 用的还是 4G 信号。如果让每个传感器通过 HTTP 轮询上报数据,服务端压力大不说,设备在弱网条件下的重传逻辑也很难写。这套源码在设备接入层走了典型的物联网方案:MQTT 协议做消息通道,Netty 做私有协议的接入网关。
看源码里的netty包能看到一条清晰的职责链:TcpServer负责启动监听端口,ProtocolDecoder处理粘包拆包,MessageHandler把二进制报文转成统一的内部消息对象。这里我一般会把 Netty 和 MQTT Broker 的分工讲清楚:Netty 解决的是“设备用了自定义二进制协议怎么接入”的问题,MQTT Broker 解决的是“设备端用标准 MQTT 协议怎么接入”的问题。这套源码两者都接,所以既兼容标准 Modbus/DTU 透传场景,也兼容直接上 MQTT SDK 的智能硬件。
设备接入后,统一走DeviceRegistry做在线状态管理。设备上线、下线、心跳超时都会更新 Redis 里的状态位,这样 Web 端查询设备在线离线时不用频繁扫数据库。
写一个最核心的设备报文解码器片段:
public class WeatherDataDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 等待完整帧:帧头0xAA + 数据长度 + 数据体 + CRC16 if (in.readableBytes() < 6) { return; } in.markReaderIndex(); if (in.readUnsignedByte() != 0xAA) { // 帧头不对,丢弃一个字节重新找帧边界 in.resetReaderIndex(); in.readByte(); return; } int payloadLen = in.readUnsignedShort(); if (in.readableBytes() < payloadLen + 2) { in.resetReaderIndex(); return; } byte[] payload = new byte[payloadLen]; in.readBytes(payload); short crc = in.readShort(); // 这里省略 CRC 校验,校验通过则构造统一设备消息 DeviceMessage msg = new DeviceMessage(); msg.setDeviceId(parseDeviceId(payload)); msg.setPayload(payload); out.add(msg); } }这段代码是典型的 Netty 粘包半包处理:markReaderIndex和resetReaderIndex配合使用,数据不够时回退指针等待下一次读取。新手容易犯的错是不做帧头校验就直接拆字节,网关连上几个不同厂商的设备就会乱码。帧格式里的 CRC 校验一定要做,农业设备的 RS485 总线在长线传输时数据错位很常见。
2.2 服务端架构与数据流:一条气象站数据从网口到数据库的路径
平台的服务端是标准的 Spring Boot 工程,数据持久化用 MyBatis-Plus,缓存用 Redis,实时推送用 WebSocket。我翻了 pom 文件里的依赖和配置,这套组合在 Java 农业物联网项目里确实是主流选择:Spring Boot 负责业务接口和定时任务,MyBatis-Plus 减少单表 CRUD 的样板代码,Redis 存设备状态和告警开关,WebSocket 把实时数据推给前端大屏。
一条气象站数据的完整路径大致是这样的:
- 气象站传感器通过 RS485 总线把数据传给 DTU,DTU 打包成 MQTT 报文上报
- MQTT Broker 收到主题
agri/weather/{deviceId}的消息 - Spring Boot 里的
MqttSubscriber消费者解析 JSON 报文体 - 解析后的数据写入 MySQL 的
weather_data表,同时写入 Redis 的最近一条缓存 - WebSocket 服务把最新数据广播给前端页面
- 如果数据超过阈值,
AlertService生成告警记录并推送
这里请注意,MQTT 的 QoS 级别决定了消息会不会丢。农业生产数据不像金融交易那么严苛,气象站和墒情数据用 QoS 0 或 QoS 1 足够,但水肥一体机的控制指令建议用 QoS 1,至少保证消息送达一次,避免“指令丢了、设备没开阀”这种问题。
2.3 代码包结构与四个模块的边界
源码的业务代码按功能拆成四个模块,包结构划分得很容易辨认。我把核心模块整理成一张表:
| 模块 | 核心控制类 | 数据表 | 主要职责 |
|---|---|---|---|
| 气象站 | WeatherController | weather_data | 温度、湿度、风速、风向、雨量采集与展示 |
| 墒情监控 | MoistureController | soil_moisture_data | 土壤湿度分层数据采集与趋势分析 |
| 水肥一体机 | FertigationController | fertigation_task | 灌溉任务下发、阀门控制、配方管理 |
| 视频监控 | VideoController | video_channel | 摄像头通道管理、RTSP 流转播放 |
这种模块划分比较常规,但有一个值得借鉴的设计:四个模块不是各干各的,而是通过DecisionService聚合。墒情数据低了,DecisionService会去查气象站最近 24 小时有没有降雨,再决定要不要给水肥一体机下发灌溉指令。这就是我开头说的“联动决策”,源码里的核心价值在这个服务类上,而不在增删改查。
3. 气象站接入实战:报文解析、批量入库与阈值告警
3.1 气象站硬件选型与接入参数配置
做农业气象站接入,先要清楚硬件侧到底能给你什么数据。常见的配置是:温湿度传感器、风速风向传感器、雨量筒、大气压传感器通过 RS485 总线接到采集器,采集器再通过 DTU 走 4G 网络把数据送上来。DTU 一般支持 MQTT 协议,配置界面里填上 Broker 地址、端口、主题前缀、上报间隔就能把数据推到平台。
在源码的application.yml里可以看到 MQTT 连接配置的思路:
mqtt: broker-url: tcp://localhost:1883 client-id: agri-platform-server username: agri_user password: agri_pass topics: - agri/weather/+/data - agri/moisture/+/data - agri/fertigation/+/status注意主题里的+通配符,它匹配任意设备 ID。用主题分级的好处是后续加设备不需要改代码,新设备只要按agri/weather/{新设备ID}/data这个规则上报,服务端订阅就能自动收到。这里我一般会强调:设备 ID 必须全局唯一,最好在设备注册时由平台分配,不要用硬件厂商的 MAC 地址直接做设备 ID,因为一旦换了 DTU,MAC 变了,历史数据就关联不上了。
3.2 数据实体与批量入库
气象站的数据上报频率一般是 5 到 10 分钟一条,但大棚数量一多,比如管理 50 个大棚,每条数据 10 个字段,高峰期并发写入的压力也不小。源码里用了 MyBatis-Plus 的批量插入配合定时批量刷新的策略。实体类定义是这样的:
@Data @TableName("weather_data") public class WeatherData { @TableId(type = IdType.AUTO) private Long id; private String deviceId; private BigDecimal temperature; private BigDecimal humidity; private BigDecimal windSpeed; private BigDecimal windDirection; private BigDecimal rainfall; private BigDecimal pressure; private Date collectTime; private Date createTime; }对应批量插入的 XML 写法:
<insert id="batchInsert" parameterType="list"> insert into weather_data (device_id, temperature, humidity, wind_speed, wind_direction, rainfall, pressure, collect_time, create_time) values <foreach collection="list" item="item" separator=","> (#{item.deviceId}, #{item.temperature}, #{item.humidity}, #{item.windSpeed}, #{item.windDirection}, #{item.rainfall}, #{item.pressure}, #{item.collectTime}, now()) </foreach> </insert>批量插入一定要控制 batch 大小,我一般每条事务控制在 200 到 500 条之间。数据量太小浪费数据库连接,太大又容易造成事务过长、锁表时间变久。另外collectTime必须用设备上报的时间,不能直接用now(),否则设备端网络延迟时数据时间会错乱,后面做气象趋势分析时会得出错误的结论。
3.3 阈值告警的触发逻辑
气象告警是农户真正关心的功能:大风预警要提前关棚膜,高温要启动遮阳网,低温要准备保温措施。源码里的WeatherAlertService实现了规则判断:
public void evaluateAlert(WeatherData data) { String deviceId = data.getDeviceId(); // 从 Redis 读取该设备绑定的告警阈值配置 WeatherThreshold threshold = thresholdCache.get(deviceId); if (threshold == null) { return; } if (data.getWindSpeed().compareTo(threshold.getMaxWindSpeed()) > 0) { alertService.push(deviceId, AlertType.WIND, data.getWindSpeed()); } if (data.getTemperature().compareTo(threshold.getMaxTemp()) > 0 || data.getTemperature().compareTo(threshold.getMinTemp()) < 0) { alertService.push(deviceId, AlertType.TEMP, data.getTemperature()); } }告警阈值放 Redis 而不是放数据库,是因为判断逻辑每次上报都要查一次,数据库查询在高频场景下撑不住。Redis 的读取成本低很多。阈值配置由用户在 Web 端修改后写库再刷新缓存,修改生效时间控制在秒级。这里要注意阈值是设备级的,不是全局一个配置,因为不同大棚种的作物不同,耐热耐寒能力差很多,一刀切配置只会造成误报。
4. 墒情监控与水肥一体机联动:自动灌溉决策的最小实现
4.1 墒情数据采集与数据修正
土壤墒情传感器一般埋在地下 10cm、20cm、40cm 三个深度,测的是土壤体积含水量。很多初学者直接把采集值拿来用,结果发现土壤明明很干,数据却显示湿度 50% 以上。这是因为不同厂家的传感器输出格式不一样:有的是百分数,有的是 0 到 100 的整数映射,有的还得乘一个校准系数。
源码里的SoilMoistureDataMapper对应的采集表有calibration_factor字段,数据上报后在 service 层做了修正:
public SoilMoistureData calibrate(SoilMoistureData raw) { BigDecimal factor = raw.getCalibrationFactor(); if (factor != null && factor.compareTo(BigDecimal.ZERO) > 0) { raw.setMoisture(raw.getMoisture().multiply(factor)); } return raw; }这个设计虽然简单,但在实际项目里能避免大坑。土壤墒情数据不能只看单次值,要看趋势。一次测量偏差可能是传感器探头没插好,连续几次值都在下降才是真的缺水。所以做墒情监控不能上来就搞自动灌溉,至少要攒几天的数据基线,再设定阈值。
4.2 水肥一体机控制指令下发
水肥一体机的控制比数据采集复杂一个等级,因为它是“写操作”,涉及设备安全。下发指令前要先确认设备在线,设备在执行完动作后要回传状态确认。源码里的FertigationCommandService实现了这个流程:
public boolean sendIrrigationCommand(String deviceId, BigDecimal duration) { // 1. 检查设备在线状态 if (!deviceRegistry.isOnline(deviceId)) { log.warn("device {} is offline, command rejected", deviceId); return false; } // 2. 构造控制指令报文 IrrigationCommand cmd = new IrrigationCommand(); cmd.setDeviceId(deviceId); cmd.setAction("START"); cmd.setDuration(duration); // 3. 通过 MQTT 下发,QoS=1 boolean published = mqttGateway.publish( "agri/fertigation/" + deviceId + "/command", JSON.toJSONString(cmd), 1 ); // 4. 登记任务记录,等待设备回执 if (published) { fertigationTaskService.createPendingTask(cmd); } return published; }注意 MQTT publish 只是把消息交给了 Broker,不代表设备真的收到并执行了。所以源码里有一个pendingTask机制:指令下发后任务状态是“待确认”,设备执行完会回一条状态消息到agri/fertigation/{deviceId}/status,那时任务才变成“已完成”。如果超过 30 秒没收到回执,任务状态自动变成“失败”并触发重试。我一般会加一个最大重试次数,比如 3 次,超过就转人工处理,避免死循环把设备打挂。
4.3 联动策略:墒情数据、气象数据和灌溉决策如何结合
这一节是源码的精华。DecisionService把墒情数据、气象数据、水肥一体机串起来:
public void evaluateIrrigation(String deviceId) { // 取最近一次墒情数据 SoilMoistureData moisture = moistureService.getLatest(deviceId); // 取最近一次气象数据 WeatherData weather = weatherService.getLatest(deviceId); // 关键判断:土壤湿度低于阈值 且 最近24小时无降雨 if (moisture.getMoisture().compareTo(moistureThreshold) < 0) { boolean hasRain = weatherService.hasRainfall(deviceId, 24); boolean isRaining = weather.getRainfall().compareTo(BigDecimal.ZERO) > 0; if (!hasRain && !isRaining) { // 计算缺水量,按面积和作物系数估算灌溉时长 BigDecimal duration = calculateDuration(moisture, cropConfig); fertigationService.sendIrrigationCommand(deviceId, duration); } } }这个逻辑看起来简单,但解决了农业灌溉里的一个大问题:如果只看墒情数值,刚下完雨土壤湿度回升,系统不会误触发灌溉,而如果只看气象预报,又忽略了土壤本身的保水能力。两者结合才是合理做法。calculateDuration里考虑了作物类型、生长期、土壤类型等参数,源码里给了一套经验公式参数,实际部署时要根据当地土质调。
5. Java 物联网平台避坑指南:五条高频踩坑记录
5.1 设备一会在线一会离线:心跳机制没对齐
现象:Web 端设备列表里,气象站设备每隔几分钟就显示离线,然后又自动恢复在线,一天能跳十几次。
原因:设备端的心跳上报间隔是 60 秒,但服务端 Netty 的IdleStateHandler设置的读超时是 30 秒。30 秒没收到心跳就判定设备离线,而设备的实际心跳周期是 60 秒,于是每次心跳还没到就被踢掉了。
解决:把服务端的读超时时间改成设备心跳间隔的 2 到 3 倍。设备 60 秒心跳一次,服务端设成 180 秒比较稳妥。同时把 MQTT Broker 的心跳参数也调一致,EMQ X 的heartbeat配置要和设备端约定好,避免双重判断打架。从那以后我每次对接新设备第一件事就是确认心跳周期,先在纸上算清楚再写配置。
5.2 前端显示的时间比服务器时间早 8 小时
现象:前端页面显示的气象站数据时间比实际时间晚(或早)8 个小时。
原因:设备上报时间戳用的是设备本地时区,但代码里new Date(timestamp)默认按服务器时区解析。设备是中国时区(UTC+8),服务器部署在 UTC 时区,两个时区不一致导致展示时间错乱。
解决:统一约定时间戳格式。设备端上报时必须带时区偏移,服务端统一转成 UTC 存储,前端展示时再转成本地时区。我一般在数据库里直接存datetime类型,服务端解析时明确指定TimeZone.getTimeZone("Asia/Shanghai"),不依赖服务器默认时区。这条经验后来救了我好几次,尤其是部署到云服务器时,云厂商给的默认时区五花八门。
5.3 大棚数量过百后 MySQL 写入变慢
现象:设备从 30 台扩展到 120 台后,数据入库越来越慢,接口响应时间从 50ms 涨到 500ms,数据库 CPU 飙升。
原因:每台设备上报数据都走一次单条 insert,120 台设备 5 分钟上报一次,高峰期每秒要执行几十次插入,MySQL 的提交事务开销被放大。
解决:改成批量入库。代码里用BlockingQueue累积数据,定时任务每 5 秒刷一次批量写入,批量大小控制在 300 条左右。同时在weather_data表上建了(device_id, collect_time)联合索引,查询历史趋势时避免全表扫描。改造后数据库 CPU 降了 60%,写入延迟可以忽略。
5.4 视频监控画面卡顿,延迟超过 5 秒
现象:大棚摄像头接入平台后,Web 端播放视频延迟严重,点开画面要等好几秒,画面还时不时花屏。
原因:源码默认用 HLS 协议播放,HLS 切片本身就有 3 到 5 秒的分片延迟。加上摄像头 RTSP 流经过服务器转码转封装,多了一层处理损耗。
解决:局域网内直接走 WebRTC 或 HTTP-FLV,延迟能压到 1 秒以内;公网环境用 HLS 配合 CDN 也能接受。源码里的VideoStreamService做了协议适配,可以配置stream.protocol=hls或stream.protocol=flv。农业园区的摄像头一般和大棚在同一局域网,建议优先用 HTTP-FLV,低延迟对现场操作更友好。
5.5 传感器上报的数据解析失败,丢包率突然上升
现象:接入的墒情传感器一部分数据解不出来,后台日志频繁出现 “JSON parse error” 或 “can't decode payload”。
原因:设备厂商的固件升级后,JSON 报文里新增了字段,还把原来的数字字段temp改成了字符串格式"temp": "23.5"。服务端的解析代码写的是getBigDecimal("temp"),遇到字符串直接抛异常。
解决:解析逻辑做容错。统一封装一个ParseUtil,字符串和数字类型互相兼容转换,未知字段忽略不报错。同时给设备固件版本建一个字段存在 SQLite/Redis 配置表里,记录每个固件版本的报文差异。从那以后我每次对接传感器都先要一份报文样例,把可能出现的变化格式统一测一遍再上线。
6. 本地跑通四端联调的验证清单:从数据库初始化到模拟设备上报
6.1 推荐启动顺序
把源码下载下来后,不要急着改代码。先按下面的顺序跑一遍,确认环境通再去改逻辑。
- 初始化数据库:执行
sql/init.sql创建库和表结构,里面有weather_data、soil_moisture_data、fertigation_task、video_channel等核心表。 - 启动 Redis:默认连接
localhost:6379,如果本机 Redis 端口不同,改application.yml里的配置。 - 启动 MQTT Broker:源码默认连本地 1883 端口,没有安装 EMQ X 的可以先装一个社区版,或者用开发模式自带的 Netty 网关模拟。
- 启动 Spring Boot 服务:运行
AgriPlatformApplication,看到日志输出 “MQTT connected successfully” 说明订阅成功。 - 启动模拟设备:源码的
tools目录下有个DeviceSimulator.java,可以模拟气象站和墒情传感器上报数据,不用真实硬件就能验证整条链路。
启动模拟设备的代码片段:
public class DeviceSimulator { public static void main(String[] args) throws Exception { MqttClient client = new MqttClient("tcp://localhost:1883", "sim-weather-001"); client.connect(); Random rand = new Random(); while (true) { JSONObject payload = new JSONObject(); payload.put("temperature", 20 + rand.nextDouble() * 15); payload.put("humidity", 40 + rand.nextDouble() * 40); payload.put("windSpeed", rand.nextDouble() * 10); payload.put("rainfall", rand.nextDouble() * 2); client.publish("agri/weather/sim-weather-001/data", payload.toJSONString().getBytes(), 0); Thread.sleep(5000); } } }模拟器每 5 秒发一条数据,跑起来后去前端页面看实时数据有没有刷新,去数据库查weather_data表有没有新记录。这是验证全链路最快的方式。
6.2 验证清单
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 设备上线 | 启动模拟器 | 前端设备列表状态变为“在线” |
| 数据入库 | 等 1 分钟 | weather_data表持续新增记录 |
| 实时刷新 | 打开大屏页面 | 温度、湿度每 5 秒更新一次 |
| 告警触发 | 手动把模拟器温度改成 45 度 | 页面出现高温告警记录 |
| 灌溉联动 | 把墒情值改成低于阈值 | 日志输出灌溉指令下发,设备端收到指令 |
| 视频播放 | 接入一个 RTSP 摄像头 | 页面能正常播放画面,延迟可接受 |
6.3 二次开发技巧:用适配器模式接新传感器
源码接入了三类传感器,但实际项目里肯定会遇到新设备,比如土壤 pH 传感器、光照传感器、二氧化碳传感器。我习惯的做法是新增一个DeviceAdapter接口,每种设备一个实现类,接入新设备不需要改核心逻辑。
public interface DeviceAdapter { String getDeviceType(); DeviceMessage decode(byte[] rawData); DeviceData toEntity(DeviceMessage message); }新增传感器时写一个PhAdapter implements DeviceAdapter,在AdapterRegistry里注册,其他逻辑直接复用。这套源码的模块边界清晰,在service包下面扩展新模块不会动到已有的代码结构。跑一次全链路验证把所有端点都走通后,再去改业务逻辑,你会少踩很多隐形的雷。希望帮到你。
本文还有配套的精品资源,点击获取