ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ESP32与Python智能物联网种植系统:自动灌溉实战

ESP32与Python智能物联网种植系统:自动灌溉实战 简介《物联网Python项目开发实战——智能物联网种植系统》配套PDF面向农场、大棚等种植场景适合有Python基础、希望动手完成物联网综合项目的开发者与院校学生。内容按终端设备、网关、后台服务器三层架构展开逐模块拆解环境监测温湿度、光照、雨滴、水位、土壤湿度等传感器采集与图表呈现、滴灌系统手动、定时、自动三种模式及Web配置、安防报警人体红外检测、2G拨号与短信告警、灯光控制手动、定时、自动与光照联动、设备管理心跳上报、电量预警、数据库同步与故障设备无缝替换等实现细节。压缩包内为1个PDF文档约3.93MB按章节与项目模块组织便于对照阅读。已有1079人学习可据此理解STM32终端、LoRa通信、Web后台与Python服务端的协作方式获取完整项目源码思路与调试排错参考。1. 智能物联网种植系统这套方案到底为谁而写阳台上那盆番茄早上浇透下午两点土面发白晚上七点叶子打蔫——浇水的时机从来不是「一天一次」能解决的。智能物联网种植系统要做的事很朴素让土壤湿度、空气温湿度、光照这几路数据持续落到同一条时间线上再由一段 Python 逻辑判断「现在该不该开水泵、开多久」把凭感觉浇水换成可复现的阈值、区间和状态机。标题里写的「项目开发实战」重点不在从零焊一块板子而在把采集、传输、决策、执行、存储这五段串成一个能长期跑的最小闭环并且用 Python 把中间那层逻辑写清楚。它适合三类人做物联网毕业设计、需要能把架构讲明白的学生家里有阳台或小菜地、想装一套自动灌溉的折腾党还有想找个真实场景把 Python 的 MQTT、并发、数据库一并练手的开发者。后面四章按硬件选型、设备端上报、网关决策、排错与进阶推进代码都能直接抄下来改引脚和阈值就跑。2. 智能物联网种植系统的硬件选型与 Python 端技术栈2.1 采集节点用 ESP32 还是树莓派跑 Python这是最容易一开始就选错的一步。很多人听说「Python 物联网」就默认整条链路都用树莓派结果一块板子既要采模拟量又要跑 Web 服务夏天户外机箱一闷就死机。常见的可靠做法是把职责拆开采集节点用 ESP32 跑 MicroPython只干「读传感器 上报 收指令开继电器」决策、存储、面板全部放在一台常供电的 Python 网关上。方案运行环境适合承担的职责主要代价ESP32 / ESP32-S3 MicroPython单片 MCU无操作系统定时采集、MQTT 上报、驱动继电器内存小复杂逻辑难写断网要自己缓存树莓派 CPython完整 Linux可用 pip 生态MQTT 网关、决策、SQLite、Flask 面板功耗高户外供电麻烦SD 卡怕断电Arduino pyserialC 采集串口交给 Python已有 Arduino 基础、只做短距离串口协议要自己定线缆长度受限选 ESP32-S3 的现实理由是它有更多可用的 ADC 通道和 GPIO后续想加光照、水位、电磁阀都不至于引脚不够。毕业设计里如果要求「Python 项目开发」把 Python 放在网关侧反而更容易讲清楚设备端是嵌入式服务端是 Python 编程两边职责不重叠。2.2 土壤湿度、温湿度与执行器的选型参数传感器选型直接决定这套系统能活多久。电阻式土壤湿度探头便宜但通电后电极会被电解腐蚀埋土里几周就漂移电容式不裸露电极长期埋土稳定性好得多。空气温湿度上DHT22 便宜但响应慢SHT30 走 I2C、精度和一致性更好如果这套种植系统要联动补光或通风建议直接上 SHT30。部件常见型号接口关键参数土壤湿度电容式 Soil Moisture v1.2 / v2.0模拟量输出 03.0V需 ADC 采样并两点标定空气温湿度SHT30 / DHT22I2C / 单总线SHT30 精度 ±0.3℃、±3%RH光照BH1750I2C量程 165535 lx直接读数值执行器5V 继电器模块 微型直流水泵GPIO继电器驱动电流约 1520mA需外部 5V 供电供电5V/2A 适配器—水泵启动瞬间电流可达额定值 23 倍水泵一定要单独供电别从开发板的 5V 引脚取电。启动瞬间的浪涌会把 MCU 拉复位表现出来就是「一浇水设备就重启」很多人会误判成程序 bug。2.3 Python 网关的环境与依赖装法网关侧我一般用虚拟环境隔离依赖避免和系统自带的 Python 打架。在 VSCode 里做 Python 环境配置时记得把解释器指到.venv/bin/python否则终端能跑的代码调试器里会报找不到模块。# 在网关机器上 python3 -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # paho-mqtt 负责订阅上报与下发指令flask 只做最小查询面板 pip install paho-mqtt flask # 确认解释器路径VSCode 里把它设为工作区解释器 python -c import sys; print(sys.executable)依赖只留三个就够paho-mqtt是 Python 侧最成熟的 MQTT 客户端回调模型简单flask用来把数据库里的曲线露成一个网页sqlite3属于标准库不用装。不建议一上来就引入 InfluxDB、Grafana、Docker Compose 那一整套一台小机器跑不动排查问题时反而多一层黑盒。真正需要图表时再考虑把 SQLite 换成时序库代价只是改写入层。3. 用 MicroPython 把 ESP32 接进智能物联网种植系统的数据链路3.1 引脚接线与 ADC 通道的坑先把线接对再谈代码。ESP32 系列有一个特别容易踩的坑ADC2 与 Wi-Fi 模块共用资源联网状态下读 ADC2 通道会直接失败或返回乱值。所以凡是模拟量传感器——土壤湿度、水位、光敏电阻——全部安排到 ADC1 上。ESP32-S3 的 ADC1 集中在 GPIO1GPIO10数字量的继电器随便找个普通 GPIO 就行。功能建议引脚说明土壤湿度模拟输入GPIO5ADC1_CH43.3V 供电信号线接 ADCGND 共地继电器控制GPIO7输出高电平触发模块本身带光耦隔离SHT30 / BH1750GPIO8 / GPIO9I2C 的 SDA / SCL两个设备可共总线供电5V 与 GND水泵走独立 5V/2A与开发板共地注意土壤湿度探头上电后不要一直通电泡在土里长期直流偏置会加速极板老化。常见做法是只在采样前 200ms 给探头供电读完立刻断电。3.2 MicroPython 采集与 MQTT 上报的最小代码设备端代码越短越好活。下面这段main.py上电自动运行连 Wi-Fi、连 MQTT、每 60 秒发一次数据逻辑足够撑起一个入门级项目。# main.py —— 烧到 ESP32-S3 后上电自动执行 import time, json, network from machine import ADC, Pin from umqtt.simple import MQTTClient WIFI_SSID your-ssid WIFI_PASS your-pass MQTT_HOST 192.168.1.10 # Python 网关的局域网地址 TOPIC_UP bgarden/plot1/telemetry soil ADC(Pin(5)) soil.atten(ADC.ATTN_11DB) # 量程约 0~3.3V soil.width(ADC.WIDTH_12BIT) # 读数范围 0~4095 pump Pin(7, Pin.OUT, value0) # 上电默认关泵 def read_soil_pct(): raw soil.read() # 3200 空气中干读数, 1200 泡水读数, 必须按自己探头实测替换 pct (3200 - raw) * 100 // (3200 - 1200) return max(0, min(100, pct)) def wifi_connect(): sta network.WLAN(network.STA_IF) sta.active(True) sta.connect(WIFI_SSID, WIFI_PASS) for _ in range(40): # 最多等 20 秒 if sta.isconnected(): return True time.sleep(0.5) return False def main(): while not wifi_connect(): time.sleep(2) # 连不上就重试不要直接崩 c MQTTClient(esp32-plot1, MQTT_HOST, keepalive30) c.connect() while True: c.publish(TOPIC_UP, json.dumps({soil: read_soil_pct()})) time.sleep(60) main()逻辑上分三段read_soil_pct把 12 位原始值映射成百分比wifi_connect用轮询而不是死等断网后重连不至于卡死主循环只做一件事——发数据。两个标定值必须自己测探头悬空读一次记下干值插进泡透的土里读一次记下湿值替换掉 3200 和 1200否则百分比全是假的。keepalive30意味着客户端每 30 秒内至少要和一个包发生交互否则 broker 会判定掉线。以 60 秒为上报周期时这个值偏小会触发频繁重连改成 90 更稳。真实部署里我会把采样周期设为 5 分钟一次、上报周期 1 分钟一次采样值在本地缓冲避免土温剧变时数据太稀疏。3.3 主题与 payload 的约定主题结构一开始定好后面加地块、加设备都不用改代码结构。我一般按garden/{plot}/{方向}三段来分方向只有 telemetry上行数据、cmd下行指令、ack执行确认三种。主题方向payload 示例说明garden/plot1/telemetry设备到网关{soil:42,temp:24.6,lux:8100}扁平 JSON字段名固定不变garden/plot1/cmd网关到设备{pump:1,max_s:90}1 开泵0 关泵max_s 为强制关断时长garden/plot1/ack设备到网关{pump:1,ok:true}让网关知道指令确实执行了payload 坚持扁平结构别嵌套三层。字段名一旦上线就当成协议改一次要同步改两端毕业设计答辩时这也是个能讲的点设备端没有 RTCtime.time()从 2000 年起算时间戳统一由网关在收到消息时打设备只负责报数值。3.4 上报断了先查这四件事现象通常是「面板上曲线突然断了」。按顺序排查Wi-Fi 是否掉线串口里看sta.isconnected()broker 是否在同一网段MQTT_HOST写公网域名而网关在内网、又没做端口转发一定连不上供电是否够用万用表量水泵启动瞬间的 5V 是否跌到 4.5V 以下设备是否在反复复位串口打印会重复出现启动信息八成是浪涌或电源功率不足。这四件事覆盖了九成以上的「数据不动了」。4. Python 网关侧的自动灌溉决策与数据落库4.1 订阅回调与数据校验网关是这套智能物联网种植系统的大脑核心就是一个订阅回调。回调里只做三件事校验、决策、落库。不要在回调线程里做耗时的网络请求否则 MQTT 心跳会被拖死。import json, time, sqlite3, threading import paho.mqtt.client as mqtt DB garden.db LOCK threading.Lock() def on_connect(client, userdata, flags, reason_code, propertiesNone): client.subscribe(garden//telemetry, qos1) # 通配符支持多地块 def on_message(client, userdata, msg): try: data json.loads(msg.payload) soil float(data[soil]) except (ValueError, KeyError, TypeError): return # 脏数据直接丢弃 if not 0 soil 100: return # 越界视为传感器故障 plot msg.topic.split(/)[1] with LOCK: conn sqlite3.connect(DB) conn.execute( INSERT INTO telemetry(plot, soil, ts) VALUES(?,?,?), (plot, soil, time.time())) conn.commit() conn.close() decide_and_irrigate(client, plot, soil) client mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_idgarden-gw) client.on_connect on_connect client.on_message on_message client.connect(127.0.0.1, 1883, 60) client.loop_forever()三个细节值得注意qos1保证至少送达一次代价是可能重复所以入库时按(plot, ts)做去重SQLite 不是线程安全的用LOCK串行化写入loop_forever()本身会阻塞所有耗时操作都要控制在一百毫秒级别否则 QoS 1 的重发会堆积。4.2 阈值加滞回灌溉决策的 5 个必调参数只用单一阈值是新手最常见的错。土壤湿度在阈值附近抖动时水泵会一分钟内开合十几次继电器寿命按天折算。正确做法是设一个滞回区间低于下限才开高于上限才关中间区间保持原状态。参数建议初值作用调大/调小的后果soil_low35%触发灌溉的下限调高更勤浇烂根风险上升soil_high55%停止灌溉的上限与 low 差距建议 ≥15差距太小会震荡min_interval1800秒两次灌溉最小间隔防止传感器误报导致连环浇水max_pump_s90秒单次最长开泵时间兜底保护防止水位不足时干烧sample_window3次决策前取几次采样均值抑制单点噪声采样周期 60 秒时为 3 分钟这张表是整套系统最值得花时间调的部分。soil_low和soil_high的差值是滞回带宽度我一般取 1520 个百分点sample_window用滑动窗口均值代替瞬时值能消掉探头接触不良带来的尖刺。4.3 水泵状态机与三重安全保护决策逻辑写成一个显式状态机比一堆 if 好维护也方便在答辩时画出来。下面这段包含三层保护最大运行时长、最小间隔、传感器异常时不动作。from collections import deque STATE {} # plot - {on: bool, last_off: 0.0, started: 0.0} HIST {} # plot - deque 最近 N 次土壤湿度 def avg_soil(plot, value, n3): d HIST.setdefault(plot, deque(maxlenn)) d.append(value) return sum(d) / len(d) def decide_and_irrigate(client, plot, value): st STATE.setdefault(plot, {on: False, last_off: 0.0, started: 0.0}) now time.time() soil avg_soil(plot, value) if st[on]: # 到上限、或超过单次上限、或传感器读数异常 → 关泵 if soil SOIL_HIGH or now - st[started] MAX_PUMP_S: client.publish(fgarden/{plot}/cmd, json.dumps({pump: 0})) st[on], st[last_off] False, now return # 低于下限、且距上次关泵超过最小间隔 → 开泵 if soil SOIL_LOW and now - st[last_off] MIN_INTERVAL: client.publish(fgarden/{plot}/cmd, json.dumps({pump: 1, max_s: MAX_PUMP_S})) st[on], st[started] True, nowst[on]是本地镜像有可能和真实继电器状态不一致——比如指令发出但设备掉线。所以设备端也必须实现max_s的本地强制关断双保险。这一条在答辩或代码评审里几乎必被问到远端保护不能替代本地保护。4.4 SQLite 建表与 Flask 查询接口数据落库的表结构简单但要有索引否则一周以后按时间查曲线会明显变慢。-- 采样表只存数值与时间标的元信息另表管理 CREATE TABLE IF NOT EXISTS telemetry ( plot TEXT NOT NULL, soil REAL NOT NULL, ts REAL NOT NULL ); CREATE INDEX IF NOT EXISTS idx_telemetry_plot_ts ON telemetry(plot, ts); -- 事件表每一次开泵关泵都记便于回溯「为什么今天浇了 6 次」 CREATE TABLE IF NOT EXISTS events ( plot TEXT NOT NULL, action TEXT NOT NULL, -- pump_on / pump_off reason TEXT, -- low / high / timeout ts REAL NOT NULL );查询接口用一个 Flask 路由就够返回最近 24 小时的曲线点from flask import Flask, jsonify app Flask(__name__) app.get(/api/plot/plot/recent) def recent(plot): conn sqlite3.connect(DB) rows conn.execute( SELECT ts, soil FROM telemetry WHERE plot? AND ts? ORDER BY ts, (plot, time.time() - 86400)).fetchall() conn.close() return jsonify([{ts: r[0], soil: r[1]} for r in rows])把events表和telemetry曲线对着看是排查误灌溉最快的方法曲线显示湿度一直高于 50 却在开泵说明要么标定失效要么回调里混进了别的地块数据。5. 断网补传、阈值自学习与不上土的验证方法5.1 设备端断网补传的环形缓冲Wi-Fi 断个十几分钟很常见直接丢数据会让曲线出现空洞。MicroPython 里内存紧张做法是开一个固定长度的列表做环形缓冲连上以后再补发。BUF [] # 每项是 (ts_local, soil)最多留 60 条 def push(soil): BUF.append((time.time(), soil)) if len(BUF) 60: # 只保留最近一小时按 60 秒周期算 BUF.pop(0) # 上报成功后再清空缓冲失败则留在本地等下一轮 def flush(c): while BUF: t, s BUF[0] try: c.publish(TOPIC_UP, json.dumps({soil: s, t: t})) c.wait_msg() if False else None BUF.pop(0) except OSError: return # 连不上保留数据下一轮再试阈值 60 条是按「断网一小时不丢点」定的实际内存占用很小。补传的数据带的是设备端相对时间网关收到后要用「接收时间减去队列位置」来还原简单做法是在 payload 里带一个自增序号网关按序号插值补点。5.2 用历史曲线做阈值自学习固定阈值用一季没问题但春秋蒸发量差别很大。可行的做法是拿过去两周的曲线统计每天土壤湿度的下降斜率反推出日耗水速度再据此调整min_interval。这不是玄学本质是让灌溉节奏跟着环境走。AI 编程助手在这里很好用——把表结构和一段样例数据丢给它让它生成统计代码但提示词里必须写清字段名、时间单位、预期输出否则返回的代码字段名对不上改起来比自己写还慢。5.3 不上土也能验证状态机最省事的验证方式是把传感器数据录成 CSV用回放脚本跑一遍决策逻辑看开泵次数是否合理全程不用接水泵。# 录制格式ts,soil 每行一条历史采样 python replay.py sample_week.csv --low 35 --high 55 --interval 1800回放脚本把每行数据喂给decide_and_irrigate只打印决策不真发 MQTT输出形如t3h12m soil34.1 - pump_on。调整参数后重跑比较一周内的开泵次数和总时长次数突然翻倍说明滞回带太窄总时长异常高说明标定值漂了。这个习惯能让你在没有真土、没有水泵的情况下把控制逻辑先调到可信再去接硬件。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进