IoT-For-Beginners 农场项目实战:标定“泵运行秒数—土壤湿度”曲线,构建更高效的自动浇花循环
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本篇基于 IoT-For-Beginners 仓库中第 2 项目(Digital Agriculture / 智能农业)第 3 课的课后任务 Build a more efficient watering cycle 展开:在已实现“继电器—水泵”联动浇水的基础上,通过实测标定“泵每运行 1 秒对应土壤湿度读数下降多少”的线性关系,并据此改造服务器端代码,让水泵只运行到刚好补足土壤湿度所需的时长。读完后你将掌握一套完整的 IoT 执行器标定方法(定水量多次测量、均值估算、闭环反馈),并能把固定时长浇水策略升级为按需计算的时长策略。
任务背景:继电器控制水泵为什么需要“标定”
本仓库第 2 项目第 3 课(自动浇花课 README)先讲了三个前置知识点,它们是理解本任务的必要背景:
- 继电器(relay)是低压设备控制高功率负载的标准方案。IoT 开发板只能输出 3.3V/5V、小于 1A 的电流,无法直接驱动水泵;继电器控制回路只需低压信号即可吸合电磁铁,输出回路可承载 250V/10A 级别的负载,从而让单片机“像手指按开关一样”接通水泵电源。课程提供了一个继电器接线样例,水泵串联在继电器的输出回路中,继电器吸合即通电抽水。
土壤湿度传感器的读数方向是“反的”。课程使用的是 Grove 电容式土壤湿度传感器,读数越高代表土壤越干、读数越低代表土壤越湿(见 virtual-device-relay.md 中的说明)。这也是为什么课程中所有控制逻辑都写成“读数大于 450 就开继电器”。
传感器与执行器之间存在时间差。水从水泵流出、渗入土壤、到达传感器探针需要时间(课程中观察到的稳定时间约为 20 秒),所以不能“读到干燥就一直开泵”,而应采用“开泵一小段时间 → 关闭 → 等待稳定 → 重新测量”的循环策略。
课程给出的参考实现 code-timing/server/app.py 采用了固定时长策略:water_time = 5(开泵 5 秒)+wait_time = 20(等待 20 秒)。这能工作,但有两个明显问题:如果土壤只比阈值干一点点,5 秒可能浇过头;如果土壤非常干,5 秒又远远不够。本任务要解决的正是:用实测数据算出“目标湿度提升需要泵运行多少秒”,把固定 5 秒替换成按当前读数计算的时长。
标定实验:测量“泵运行秒数 → 土壤湿度读数”的映射关系
任务文档(assignment.md)给出的核心原理是:对于同一份固定体积的土壤,泵运行固定时长,对土壤湿度的影响应当是可重复的。因此可以离线测量出“每 1 秒泵运行对应读数下降多少”,再用于在线控制。
操作步骤
从干燥的土壤开始,测量并记录初始土壤湿度读数。
加入固定量的水:让泵运行 1 秒,或手动倒入固定的水量。
关键前提:泵必须始终以恒定速率运行,即每运行 1 秒输送的水量必须相同,否则标定数据不可用。
等待土壤湿度稳定后再读数。土壤湿度传感器不是瞬时响应的——水需要时间渗入并扩散到探针位置,课程 README 中提到若浇水太靠近传感器,可能先看到读数快速下降又回升(近处水被整体土壤摊薄的缘故),所以要等数值稳定。
重复多次,把结果整理成一张标定表。文档给出的示例表如下(读数为 ADC 0-1023 量程,数值越大越干):
| 累计泵运行时间 | 土壤湿度读数 | 相对上次下降量 | | --- | --: | -: | | 干(初始) | 643 | 0 | | 1s | 621 | 22 | | 2s | 601 | 20 | | 3s | 579 | 22 | | 4s | 560 | 19 | | 5s | 539 | 21 | | 6s | 521 | 18 |
计算平均下降量:示例中 6 秒内读数共下降 643 − 521 = 122,平均每秒下降 122 / 6 ≈20.3。
用这个系数改造服务器代码,让泵运行到土壤湿度达到目标值所需的时长(见下文)。
使用虚拟硬件时的模拟方法
任务文档特别提示:如果你没有实体土壤和泵,可以使用虚拟 IoT 硬件(CounterFit 仿真平台)完成整个流程,方法是在继电器开启期间,手动把土壤湿度读数按固定步长每秒递减,以此模拟泵加水对土壤的影响。仓库提供的虚拟设备代码 code-mqtt/virtual-device/soil-moisture-sensor/app.py 就是为此准备的:它通过CounterFitConnection.init('127.0.0.1', 5000)连接本地 CounterFit 服务,用counterfit_shims_grove里的ADC和GroveRelay(5)模拟 Grove 土壤湿度传感器和继电器(继电器挂在 Pin 5),每 10 秒把{'soil_moisture': ...}发布到<ID>/telemetry主题,同时订阅<ID>/commands主题,收到relay_on: true/false就调用relay.on()/relay.off()。你只需要在 CounterFit 的 Web 界面里手动调整传感器 Value 滑块来“加水”,即可完成标定实验并观察虚拟继电器的通断。
改造服务器代码:从固定 5 秒到按需计算时长
先看课程现有的服务器端时序控制逻辑。code-timing/server/app.py 的核心结构是:
id = '<ID>' client_telemetry_topic = id + '/telemetry' server_command_topic = id + '/commands' client_name = id + 'soilmoisturesensor_server' mqtt_client = mqtt.Client(client_name) mqtt_client.connect('test.mosquitto.org') mqtt_client.loop_start() water_time = 5 # 继电器(泵)开启时长,秒 wait_time = 20 # 等待土壤湿度稳定,秒 def send_relay_command(client, state): command = { 'relay_on' : state } print("Sending message:", command) client.publish(server_command_topic, json.dumps(command)) def control_relay(client): print("Unsubscribing from telemetry") mqtt_client.unsubscribe(client_telemetry_topic) # 浇水期间取消订阅遥测 send_relay_command(client, True) time.sleep(water_time) send_relay_command(client, False) time.sleep(wait_time) print("Subscribing to telemetry") mqtt_client.subscribe(client_telemetry_topic) # 稳定后重新订阅 def handle_telemetry(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) if payload['soil_moisture'] > 450: threading.Thread(target=control_relay, args=(client,)).start()其中有两个设计点在改造时都要保留:
- 浇水期间取消订阅遥测(
unsubscribe),防止 10 秒一条的遥测消息在 25 秒的浇水周期内再次触发新的浇水循环;课程 README 明确说明:之所以不把设备端改成“每分钟才发一次遥测”,是因为农场场景下可能需要完整保留浇水过程中的湿度数据供其他服务分析,取消订阅只影响本服务器,遥测数据仍在 broker 上供其他订阅者使用。 - 在独立线程中执行
control_relay(threading.Thread(...).start()),避免阻塞 MQTT 主事件循环。
在此基础上,结合标定的平均下降量,可以把固定water_time替换为按读数计算的时长。以示例数据为例:目标湿度阈值 450、平均每秒下降 20.3,则当前读数 643 时所需泵运行时长为 (643 − 450) / 20.3 ≈9.5 秒,而不是固定 5 秒。改造后的关键代码如下(完整基础结构沿用上述code-timing版本,仅替换时长计算与control_relay的签名):
moisture_change_per_second = 20.3 # 标定结果:泵每运行 1 秒,读数平均下降量 target_soil_moisture = 450 # 目标土壤湿度读数阈值 wait_time = 20 # 等待土壤湿度稳定的时间,秒 def control_relay(client, water_seconds): print("Unsubscribing from telemetry") mqtt_client.unsubscribe(client_telemetry_topic) send_relay_command(client, True) time.sleep(water_seconds) # 按标定计算的时长开泵 send_relay_command(client, False) time.sleep(wait_time) print("Subscribing to telemetry") mqtt_client.subscribe(client_telemetry_topic) def handle_telemetry(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) if payload['soil_moisture'] > target_soil_moisture: # 需要的泵运行秒数 = 读数超出目标的幅度 / 每秒下降量 water_seconds = (payload['soil_moisture'] - target_soil_moisture) \ / moisture_change_per_second threading.Thread(target=control_relay, args=(client, water_seconds)).start()需要注意的适用前提与限制:
- 线性假设只在标定范围内成立。土壤从很干到很湿,单位水量对应的读数变化并不严格恒定(示例表里 18~22 之间也在波动)。计算出的时长本质上是一个初估,课程 README 强调的正确做法是“先试、看传感器数据、再用恒定反馈循环调整”——即保留“湿度仍高于阈值就再浇一轮”的循环,用多轮短浇水逼近目标,宁可少浇不可浇多(水无法从土壤里抽走)。
moisture_change_per_second与具体土壤强绑定。换一盆土、换一种土壤介质,标定表需要重做;这也是课程把它列为独立课后任务的原因。- 参考实现使用公共 broker
test.mosquitto.org,客户端 ID 约定为<ID>soilmoisturesensor_server(服务器端)与<ID>soilmoisturesensor_client(设备端),运行前需将代码中的<ID>替换为你自己的前缀,避免与他人冲突。
作为对照,改造前(无时序控制)的服务器版本见 code-mqtt/server/app.py,它在handle_telemetry中每收到一条遥测就无条件发布relay_on指令;而设备端直接控制继电器的版本见 code-relay/pi/soil-moisture-sensor/app.py(Raspberry Pi + Grove ADC,soil_moisture > 450时relay.on())。三者对照可以清楚看到控制逻辑从“设备端单点判断”演进到“服务器端集中决策 + 时序控制 + 标定时长”的完整路径。
评分标准(Rubric)
任务文档给出的评分表如下,可作为自查清单:
| 评分项 | 优秀(Exemplary) | 合格(Adequate) | 待改进(Needs Improvement) |
|---|---|---|---|
| 采集土壤湿度数据 | 能在每次加入固定水量后采集多次读数 | 能用固定水量采集部分读数 | 只能采集一两次读数,或无法使用固定水量 |
| 标定服务器代码 | 能计算平均下降量,并更新服务器代码使用该值 | 能计算平均下降量,但无法更新服务器代码;或平均计算有误但能正确用该值更新代码 | 无法计算平均下降量,也无法更新服务器代码 |
小结
这篇任务把 IoT 中一个非常通用的工程问题——执行器作用到被测量之间存在延迟且幅度未知——落到了一套可复现的操作流程上:固定输入(每秒泵量)、等待稳定(避开传感器/介质的响应时间)、多次测量取均值(对抗单次读数噪声)、把均值写回控制回路(water_seconds = (当前读数 − 目标读数) / 每秒下降量)。配合仓库中 code-timing 的 MQTT 时序骨架,你可以在实体设备或 CounterFit 虚拟环境中完整验证这条从标定时域数据到闭环控制代码的路径。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考