简介:这份PDF面向物联网与Python开发的学习者与开发者,围绕智能物联网种植系统这一实战项目,讲解从整体架构到各模块的完整开发细节,适合具备Python基础、希望掌握物联网项目落地方法的中高级读者。资源共1个PDF文件,压缩包约3.93MB,内容以图文并茂的教程形式呈现,便于按章节系统阅读。项目由终端设备、网关和后台服务器三部分组成,涵盖环境监测、滴灌系统、安防报警、灯光控制与设备管理五大模块,涉及温湿度、光照、雨滴、水位、土壤湿度等传感器数据采集,以及手动、定时、自动三种滴灌与灯光控制模式,并通过Web页面完成配置与操作。读者可从中获得完整的项目架构思路、模块化开发流程与设备心跳、电量预警、数据库同步备份等工程实现方法,理解如何用Python打通终端、网关与服务器,提升解决复杂物联网问题的能力。目前已有1080人学习下载。
1. 从一份 PDF 说起:智能物联网种植系统到底能跑通什么
如果你手头正压着一个“智慧农业”的课程设计或者实训项目,大概率绕不开三个死结:传感器数据读上来是乱码、网关和终端各说各话、Web 端只能看不能控。这份《物联网 Python 项目开发实战——智能物联网种植系统》的 PDF 教程,恰好是冲着这三个死结去的。它不是一个只讲概念的 PPT 合集,而是一套从终端设备、网关到后台服务器的完整链路拆解,编程语言几乎全部落在 Python 上,终端侧用的是 MicroPython 跑在 TPYBoard 核心板上,通信走 LoRa,后台用 Web 页面做控制和展示。面向的场景很具体:农场、大棚这类需要同时管环境监测、滴灌、安防、灯光和设备在线状态的地方。适合谁看?如果你已经写过 Python 基础语法,但对“传感器时序怎么读”“设备心跳怎么设计”“故障设备怎么无缝替换”这些工程问题没底,这份资源能让你少走一大截弯路。它把每个外设的驱动都拆成了独立文件,主程序按模块加载,这种组织方式本身就值得抄作业。
2. 终端设备驱动层:DHT11 单总线时序与数据解析
终端设备是整个系统的第一层,直接跟真实世界打交道。核心板是 TPYBoard,外设包括 DHT11 温湿度、GY-30 光照、FC-37 雨滴、水位传感器、土壤湿度传感器,再加上继电器控制的水泵和灯光、人体红外、液晶屏。这一章重点拆 DHT11,因为单总线时序是新手翻车最集中的地方,把这一块吃透,后面 I2C 和 ADC 的驱动基本是顺水推舟。
2.1 单总线时序为什么不能“差不多就行”
DHT11 只有一根数据线,主机和传感器之间是严格的主从关系。主机先拉低总线至少 18 毫秒作为起始信号,然后释放;传感器检测到起始信号后,拉低 80 微秒再拉高 80 微秒作为响应;接着传感器一次性吐出 40 位数据,高位先出。这 40 位里,前 16 位是湿度(整数+小数),中间 16 位是温度(整数+小数),最后 8 位是校验位,校验位等于前四个字节之和的低 8 位。
问题在于,MicroPython 的time.sleep精度在微秒级并不靠谱,如果你用time.sleep_us去卡 80 微秒,读出来的数据大概率校验失败。教程里的做法是用忙等待循环去数电平持续的时间,通过统计高电平期间循环计数k的大小来判断这一位是 0 还是 1。k < 3判为 0,否则判为 1。这个阈值不是拍脑袋来的,它跟核心板的主频直接相关,换一块板子就得重新标定。
提示:如果你用的是其他 MicroPython 开发板,先把
k的阈值打印出来看看实际分布,别直接抄 3。
2.2 DHT11 驱动代码逐段拆解
下面这段代码来自教程中的DHT11.py,我按自己的理解加了注释,把关键参数标出来。
import pyb from pyb import Pin import time class DHT11: def __init__(self, pin_): self.PinName = pin_ time.sleep(1) # 上电后等 1 秒,让传感器稳定 self.gpio_pin = Pin(pin_, Pin.OUT_PP) def read_temp_hum(self): data = [] j = 0 gpio_pin = self.gpio_pin gpio_pin = Pin(self.PinName, Pin.OUT_PP) # 切换为推挽输出 gpio_pin.low() time.sleep(0.018) # 起始信号:拉低 18ms gpio_pin.high() # 等待传感器响应 gpio_pin = Pin(self.PinName, Pin.IN) # 切换为输入,释放总线 while gpio_pin.value() == 1: continue while gpio_pin.value() == 0: continue while gpio_pin.value() == 1: continue # 开始接收 40 位数据 while j < 40: k = 0 while gpio_pin.value() == 0: continue while gpio_pin.value() == 1: k += 1 if k > 100: break # 超时保护,防止死循环 if k < 3: data.append(0) else: data.append(1) j = j + 1 # 按字节切分 humidity_bit = data[0:8] humidity_point_bit = data[8:16] temperature_bit = data[16:24] temperature_point_bit = data[24:32] check_bit = data[32:40] # 逐位累加转十进制 humidity = 0 humidity_point = 0 temperature = 0 temperature_point = 0 check = 0 temp_negative = 0 # 温度小数位最高位为 1 表示负温度 if data[24] == 1: data[24] = 0 temp_negative = 1 for i in range(8): humidity += humidity_bit[i] * 2 ** (7 - i) humidity_point += humidity_point_bit[i] * 2 ** (7 - i) temperature += temperature_bit[i] * 2 ** (7 - i) temperature_point += temperature_point_bit[i] * 2 ** (7 - i) check += check_bit[i] * 2 ** (7 - i) tmp = humidity + humidity_point + temperature + temperature_point if check == tmp: if temp_negative == 1: return -(temperature + temperature_point / 10), humidity + humidity_point / 10 else: return temperature + temperature_point / 10, humidity + humidity_point / 10 else: print('checksum ERROR') return 0, 0逻辑说明:__init__里先延时 1 秒,是因为 DHT11 上电后需要稳定时间,跳过这步会读到全 0。起始信号拉低 18 毫秒用的是time.sleep(0.018),这个精度在毫秒级是够的。接收 40 位数据时,k > 100是超时保护,防止传感器没接好导致死循环。校验失败返回(0, 0)而不是抛异常,是为了让主程序能继续轮询,不至于整个终端卡死。
参数说明:pin_传入的是 TPYBoard 的引脚编号,教程里用的是'X8'。k < 3这个阈值跟核心板主频有关,换板子要重新测。温度小数位的data[24]是负温度标志位,置 1 后要手动清零再参与累加,否则校验和会错。
2.3 主程序验证与硬件连接
驱动写完后,用main.py做验证。硬件连接上,DHT11 的 VCC 接 3.3V,GND 接地,DATA 接 X8。
import pyb import micropython import time from DHT11 import DHT11 if __name__ == '__main__': S = DHT11('X8') while True: print('----------------------------------------') temp = 0 hum = 0 temp, hum = S.read_temp_hum() print('Temperature: %s' % temp) print('Humidity: %s' % hum) time.sleep(3)把DHT11.py和main.py一起放进 TPYBoard 的映射盘,启动后串口每隔 3 秒打印一次温湿度。如果读到checksum ERROR,先检查接线是否松动,再检查k的阈值是否偏小。我一般会先把data数组打印出来,看看 40 位是不是完整,如果位数不够,多半是忙等待循环被中断了。
3. 光照、雨量、水位与土壤湿度:I2C 和 ADC 的驱动套路
DHT11 搞定之后,剩下的传感器驱动基本是同一套思路:初始化总线、发命令、读数据、做转换。这一章把 GY-30 光照、FC-37 雨滴、水位和土壤湿度放在一起讲,因为它们分别代表了 I2C 和 ADC 两种典型接口,把这两种吃透,终端侧的外设驱动就没有盲区了。
3.1 GY-30 光照模块的 I2C 读写与量程选择
GY-30 用的是 BH1750FVI 芯片,标准 I2C 通信,地址由 ADO 引脚决定:接 GND 时地址是 0x23,接 VCC 时是 0x5C。教程里接的是 GND,所以DEVICE = 0x23。光照度范围 0 到 65535 Lux,内置 16 位 ADC,直接数字输出。
驱动里定义了几个关键命令字:POWER_ON = 0x01上电,RESET = 0x07复位数据寄存器,CONTINUOUS_HIGH_RES_MODE_1 = 0x10连续高分辨率模式,分辨率 1 Lux,测量时间约 120 毫秒。还有一个CONTINUOUS_LOW_RES_MODE = 0x13,分辨率 4 Lux,测量时间约 16 毫秒。选哪个模式取决于你的采样频率要求,如果只是做环境趋势监测,低分辨率模式完全够用,还能省电。
from pyb import I2C import time DEVICE = 0x23 POWER_ON = 0x01 RESET = 0x07 CONTINUOUS_HIGH_RES_MODE_1 = 0x10 i2c = I2C(1, I2C.MASTER) def convertToNumber(data): # BH1750 返回两个字节,高字节在前 return int((data[1] + (256 * data[0])) / 1.2) def readLight(addr=DEVICE): i2c.send(CONTINUOUS_HIGH_RES_MODE_1, DEVICE) time.sleep(0.2) # 等待转换完成,高分辨率模式需要 120ms 以上 data = i2c.mem_read(8, DEVICE, 2) # 从地址 2 开始读 2 个字节 return convertToNumber(data)逻辑说明:i2c.send把命令字发给从机,time.sleep(0.2)是等传感器完成一次转换,高分辨率模式典型值 120 毫秒,留 200 毫秒余量比较稳。i2c.mem_read(8, DEVICE, 2)里的8是读取位数,2是寄存器地址。convertToNumber里的除以 1.2 是芯片手册给的换算系数,不同量程模式下这个系数不一样,低分辨率模式要改成除以 4.8。
参数说明:如果你把 ADO 接到了 VCC,DEVICE要改成0x5C,否则读出来全是 0。time.sleep的时间不能省,省了会读到上一次的旧数据。
3.2 雨滴、水位与土壤湿度的 ADC 采集
FC-37 雨滴传感器有数字和模拟两种输出。数字口在没雨时输出高电平,有雨时输出低电平;模拟口输出雨量大小,直接接单片机的 ADC 引脚。教程里数字口接 X12,模拟口接 X11。
import pyb from pyb import Pin p_in = Pin('X12', Pin.IN, Pin.PULL_UP) adc = pyb.ADC(Pin('X11')) def getRainAo(): return adc.read() def getRainDo(): return p_in.value()水位传感器和土壤湿度传感器都是纯模拟输出,驱动更简单,只读 ADC 值。
import pyb from pyb import Pin adc = pyb.ADC(Pin('A7')) def getWaterLevel(): return adc.read()逻辑说明:adc.read()返回的是 0 到 4095 的 12 位 ADC 值,对应 0 到 3.3V 的电压。水位越深,输出电压越高,ADC 值越大。土壤湿度传感器同理,湿度越高,ADC 值越大。实际使用时,你需要根据具体传感器的特性曲线做一次线性映射,把 ADC 值转成物理量。教程里没有给映射公式,因为不同厂家的传感器特性不一样,我一般会先做一次标定:把传感器放在干燥空气里读一个值,再完全浸入水里读一个值,用这两点做线性插值。
注意:土壤湿度传感器长期埋在土里会电解腐蚀,探头寿命有限。如果项目要长期跑,建议选电容式而不是电阻式。
3.3 多传感器轮询的调度策略
终端设备上挂了这么多传感器,如果每个都单独while True轮询,主程序会乱成一锅粥。教程的做法是每个外设独立成文件,主程序按需调用。我一般会再加一层简单的调度:用一个ticks_ms做时间片,温湿度 3 秒读一次,光照 2 秒读一次,雨滴和水位 1 秒读一次,土壤湿度 5 秒读一次。这样既不会漏掉突变,也不会让总线太忙。
import time last_dht = time.ticks_ms() last_light = time.ticks_ms() last_rain = time.ticks_ms() while True: now = time.ticks_ms() if time.ticks_diff(now, last_dht) > 3000: temp, hum = dht.read_temp_hum() last_dht = now if time.ticks_diff(now, last_light) > 2000: light = light_sensor.readLight() last_light = now if time.ticks_diff(now, last_rain) > 1000: rain_ao = rainfall.getRainAo() rain_do = rainfall.getRainDo() last_rain = now time.sleep_ms(50)逻辑说明:time.ticks_diff是 MicroPython 里处理毫秒级时间差的正确方式,直接用减法在计数器溢出时会出错。time.sleep_ms(50)让出 CPU,避免忙等待把功耗拉高。
4. 网关与后台:LoRa 通信、心跳机制与设备无缝替换
终端设备把数据读上来之后,要通过 LoRa 发给网关,网关再同步到后台服务器。这一层最容易出问题的地方不是通信本身,而是设备管理:设备掉线了怎么发现、电池快没电了怎么预警、设备坏了怎么换。教程里给了一套很实用的方案,我把它拆成三个部分来讲。
4.1 LoRa 串口通信的帧格式设计
终端和网关之间走 LoRa,核心板通过串口控制 LoRa 模块。教程里没有展开讲帧格式,但这是实际项目里必须自己定的。我一般会定义一个简单的帧结构:帧头 2 字节、设备 ID 4 字节、数据类型 1 字节、数据长度 1 字节、数据区 N 字节、校验 1 字节。设备 ID 用 4 字节是为了支持足够多的节点,数据类型用来区分是温湿度、光照还是心跳。
import struct FRAME_HEADER = b'\xAA\x55' def build_frame(device_id, data_type, payload): length = len(payload) frame = FRAME_HEADER + struct.pack('>I', device_id) + bytes([data_type, length]) + payload checksum = sum(frame) & 0xFF return frame + bytes([checksum]) def parse_frame(frame): if frame[:2] != FRAME_HEADER: return None device_id = struct.unpack('>I', frame[2:6])[0] data_type = frame[6] length = frame[7] payload = frame[8:8+length] checksum = frame[8+length] if sum(frame[:8+length]) & 0xFF != checksum: return None return device_id, data_type, payload逻辑说明:struct.pack('>I', device_id)把设备 ID 打包成 4 字节大端序,>I里的>表示大端。校验和用简单的累加取低 8 位,够用且计算量小。parse_frame里先验帧头再验校验和,任何一步失败都返回None,让调用方决定是丢弃还是重试。
参数说明:FRAME_HEADER选0xAA55是因为这个组合在数据区里出现的概率低,减少误同步。如果你的数据区里可能包含0xAA55,需要加转义或者换更长的帧头。
4.2 心跳包与电量上报的实现
设备管理模块的核心是心跳。每个终端设备定期向网关发心跳包,心跳包里带上设备 ID、剩余电量、运行状态。网关收到后更新数据库里的在线状态和电量字段。后台服务器定期从网关同步数据库,如果某个设备超过一定时间没发心跳,就标记为离线。
import time HEARTBEAT_INTERVAL = 30000 # 30 秒 def send_heartbeat(device_id, battery_level): payload = struct.pack('>H', battery_level) # 电量用 2 字节表示,单位 mV frame = build_frame(device_id, 0x01, payload) # 0x01 表示心跳类型 lora_uart.write(frame) last_heartbeat = time.ticks_ms() while True: now = time.ticks_ms() if time.ticks_diff(now, last_heartbeat) > HEARTBEAT_INTERVAL: battery = read_battery() send_heartbeat(DEVICE_ID, battery) last_heartbeat = now time.sleep_ms(100)逻辑说明:HEARTBEAT_INTERVAL设 30 秒是一个折中,太短费电,太长掉线发现不及时。电量用 2 字节表示,单位毫伏,范围 0 到 65535 mV,够用。read_battery通过 ADC 读电池分压后的电压,再换算回实际电压。
参数说明:如果你的设备是电池供电且对功耗敏感,心跳间隔可以拉到 60 秒甚至 120 秒,但后台的离线判定阈值要相应放宽。离线判定阈值一般是心跳间隔的 2 到 3 倍。
4.3 设备故障后的无缝替换逻辑
教程里有一个很聪明的设计:设备的所有运行状态和设备信息都存在网关数据库里,网关定期同步到服务器。当某个设备发生故障时,只需要把新设备的 ID 写成跟故障设备一致,就能从服务器备份的数据库里获取故障前的所有运行状态和设备信息,做到完整修复和无缝替换。
这个逻辑的关键在于:设备 ID 是业务标识,不是硬件标识。新设备烧录固件时,把DEVICE_ID配成跟旧设备一样,上电后第一次心跳就会被网关识别为“已知设备重新上线”,后台自动把历史数据关联过去。我一般会在后台加一个“设备替换”操作,把旧设备的记录标记为已替换,新设备继承旧设备的配置和阈值,这样滴灌的定时策略、灯光的自动控制参数都不用重新配。
提示:设备 ID 冲突是这套方案唯一的风险点。如果旧设备没坏只是离线,新设备用同一个 ID 上线,后台会看到两个设备交替发心跳。解决办法是在替换前先把旧设备断电,或者后台加一个“强制替换”确认。
5. 避坑与排查:终端侧最容易翻车的五个地方
这一章不讲新功能,专门讲我在复现这套系统时踩过的坑。每个坑都按“现象 → 原因 → 解决”写,你照着排查能省不少时间。
5.1 DHT11 读出来全是 0 或者校验失败
现象:串口打印Temperature: 0 Humidity: 0,或者频繁checksum ERROR。
原因:最常见的是起始信号拉低时间不够。time.sleep(0.018)在有些 MicroPython 固件上实际只有 15 毫秒左右,DHT11 没识别到起始信号。其次是k < 3的阈值偏小,把 1 判成了 0。
解决:把time.sleep(0.018)改成time.sleep_ms(20),多等 2 毫秒不影响。阈值先用print(k)把 40 位的k值打出来,看看 0 和 1 的实际分布,再定阈值。我遇到过一块板子 0 的k在 1 到 2 之间,1 的k在 5 到 8 之间,阈值设 3 刚好。
5.2 GY-30 读出来一直是 65535 或者 0
现象:光照值要么顶格要么归零,用手电筒照也没变化。
原因:I2C 地址错了。ADO 接 GND 是 0x23,接 VCC 是 0x5C,接错就读不到。另一个原因是time.sleep(0.2)被省掉了,读到的是上一次的旧数据。
解决:先用 I2C 扫描工具确认设备地址。TPYBoard 上可以跑一段扫描代码,把总线上所有应答的地址打出来。确认地址后,检查readLight里的延时是否保留。
5.3 雨滴传感器数字口一直输出低电平
现象:getRainDo()一直返回 0,不管有没有水。
原因:FC-37 模块上有一个灵敏度调节电位器,出厂时可能调得太灵敏,导致数字口一直触发。另外,数字口的输出逻辑是“有雨低电平、无雨高电平”,如果你期望的是反过来,就会觉得不对。
解决:用小螺丝刀调电位器,调到没水时数字口输出高电平为止。如果调了没用,检查Pin.PULL_UP是否加上,有些模块的数字口是开漏输出,不加内部上拉读不到高电平。
5.4 LoRa 通信距离短或者丢包严重
现象:终端和网关离得稍微远一点就收不到数据,或者数据断断续续。
原因:LoRa 模块的发射功率和扩频因子没配好。默认配置可能是最低功率和最小扩频因子,距离自然短。另外,天线没接好或者用了不匹配的天线也会导致距离骤降。
解决:通过串口命令把发射功率调到最大(一般是 20 dBm),扩频因子调到 7 到 12 之间,空中速率降到 1.2 kbps 左右。天线必须接匹配的 433 MHz 或者 868 MHz 天线,不能空载发射,否则可能烧模块。
5.5 设备心跳正常但后台显示离线
现象:终端串口能看到心跳包发出去了,网关也收到了,但后台 Web 页面显示设备离线。
原因:网关到服务器的同步链路断了。网关定期把数据库同步到服务器,如果同步脚本挂了或者网络不通,后台就看不到最新状态。另一个原因是后台的离线判定阈值设得太短,心跳间隔 30 秒,阈值设了 35 秒,网络稍微抖一下就误判。
解决:先检查网关的同步日志,确认最后一次同步时间。然后把离线判定阈值改成心跳间隔的 3 倍,30 秒心跳对应 90 秒阈值。如果同步脚本经常挂,加一个守护进程或者用系统定时任务拉起。
6. 进阶技巧:用 Web 端做滴灌和灯光的自动控制
前面几章把终端和网关跑通了,这一章落到后台的自动控制逻辑上。滴灌和灯光的自动模式是这套系统里最能体现“智能”两个字的地方,也是实际部署时用户最关心的功能。我以滴灌的土壤湿度自动控制为例,把 Web 端的配置、后台的判定逻辑和终端执行串起来讲。
6.1 自动滴灌的阈值配置与迟滞设计
自动滴灌的逻辑很简单:土壤湿度低于设定值就开水泵,高于设定值就关。但实际跑起来,如果只用一个阈值,水泵会在阈值附近频繁启停,继电器咔咔响,寿命很快就没了。我一般会加一个迟滞区间:低于下限才开,高于上限才关,中间区间保持当前状态。
| 参数 | 含义 | 典型值 |
|---|---|---|
| lower_threshold | 开泵湿度下限 | 30% |
| upper_threshold | 关泵湿度上限 | 50% |
| check_interval | 检测间隔 | 60 秒 |
| max_duration | 单次最长开泵时间 | 300 秒 |
后台的判定逻辑用 Python 写大概是这个结构:
def auto_irrigation(soil_moisture, current_state, config): lower = config['lower_threshold'] upper = config['upper_threshold'] if current_state == 'off' and soil_moisture < lower: return 'on' if current_state == 'on' and soil_moisture > upper: return 'off' return current_state逻辑说明:current_state是水泵当前状态,从数据库读。soil_moisture是终端上报的最新土壤湿度。只有低于下限才开,高于上限才关,中间区间维持原状。max_duration是保护逻辑,如果水泵连续开了 300 秒还没到上限,强制关闭并报警,防止传感器故障导致水泵一直抽水。
参数说明:lower_threshold和upper_threshold的差值建议不小于 15%,太小了迟滞效果不明显。check_interval跟土壤湿度的采样频率有关,土壤湿度变化慢,60 秒一次足够。
6.2 灯光自动控制的照度阈值与时间段叠加
灯光自动控制比滴灌多一个维度:时间。白天光照强的时候不需要开灯,晚上才需要根据照度补光。所以判定逻辑要同时看照度值和当前时间。
import datetime def auto_light(light_intensity, current_state, config): now = datetime.datetime.now().time() night_start = config['night_start'] # 比如 18:00 night_end = config['night_end'] # 比如 06:00 threshold = config['light_threshold'] # 比如 50 Lux is_night = now >= night_start or now <= night_end if not is_night: return 'off' if current_state == 'off' and light_intensity < threshold: return 'on' if current_state == 'on' and light_intensity > threshold * 1.5: return 'off' return current_state逻辑说明:is_night判断当前是否在夜间时段,不在夜间直接关灯,不管照度多少。夜间时段内,照度低于阈值开灯,高于阈值 1.5 倍关灯,同样有迟滞。threshold * 1.5是关灯阈值,比开灯阈值高 50%,避免灯光自身的光照导致频繁开关。
参数说明:night_start和night_end根据当地日出日落时间调整,夏天和冬天要改。light_threshold取决于作物类型,叶菜类一般 50 到 100 Lux 补光就够,果菜类可能需要 200 Lux 以上。
6.3 从 Web 配置到终端执行的完整链路验证
配置改完之后,怎么确认终端真的执行了?我一般会走一遍完整链路:在 Web 页面把土壤湿度下限从 30% 改成 40%,然后手动把土壤传感器从土里拿出来(模拟湿度下降),观察三个地方:后台日志有没有打印“触发自动滴灌”,网关数据库里水泵状态有没有变成on,终端串口有没有收到 LoRa 下发的控制帧。三个地方都对上了,链路才算通。
如果中间断了,按这个顺序排查:Web 配置有没有写进数据库、后台判定脚本有没有在跑、网关有没有把控制帧发出去、终端有没有收到并执行。我遇到过 Web 配置写进去了但后台脚本读的是缓存,重启脚本就好了。也遇到过网关发了控制帧但终端没收到,查下来是 LoRa 模块的地址配错了。
从那以后我每次改自动控制参数,都强制走一遍“改配置 → 模拟触发 → 看日志 → 看终端”的完整流程,不再只看 Web 页面显示成功就完事。希望帮到你。
本文还有配套的精品资源,点击获取