IoT-For-Beginners 智慧农业实战:用 6 堂课从土壤传感器到云端物联网农场
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
数字农业正在重塑农业生产的每一个环节——从预测作物生长、感知土壤墒情,到自动灌溉、上云迁移与设备安全。本篇文章基于开源课程项目 IoT-For-Beginners 的第二个学习项目"Zemеделие с IoT(Farming with IoT)"系列(见 translations/bg/2-farm/README.md,英文原版见 2-farm/README.md),完整梳理其 6 节课的知识脉络与可运行代码,帮助你掌握一套从零搭建"传感器 → MQTT/云 → 执行器"闭环智慧灌溉系统的完整技术方案。
为什么农业需要物联网
随着全球人口持续增长,农业承受的压力与日俱增。可供耕作的土地总量几乎不变,而气候变化却在不断加剧挑战——尤其是对于依赖种植成果养家糊口的约 20 亿"自给自足型农民"(subsistence farmers)而言。物联网恰恰能帮助农民做出更聪明的决策:种什么、何时收,从而提升产量、减少人工、及时发现并处理虫害。
在这个项目中,你将学会如何把物联网应用到农业场景中,最终实现"数字化农业(Digital Agriculture)"的核心能力:
- 温度测量——通过温度预测植物生长与成熟周期;
- 自动化灌溉——测量土壤湿度,仅在土壤缺水时启动灌溉系统,取代定时浇水(定时浇水在炎热干旱期会浇水不足,在雨季又会过度浇水);
- 虫害防治——利用自动化机器人或无人机上的摄像头巡检虫害,只在需要处喷洒农药,减少农药用量及其对水源的污染。
课程总览:6 节课的递进路线
本系列共 6 节课,从单点传感器测量一路演进到云端安全部署,形成一条完整的工程主线:
| 序号 | 主题 | 核心能力 |
|---|---|---|
| 1 | 用物联网预测植物生长 | 测量环境温度、计算生长度日(GDD)、MQTT 发布遥测、服务端落盘 |
| 2 | 检测土壤湿度 | 湿度传感器原理、传感器与 IoT 设备通信协议、传感器标定 |
| 3 | 自动化植物浇水 | 继电器控制大功率水泵、MQTT 下发命令、传感器/执行器时序控制 |
| 4 | 将植物迁移到云端 | 云与 IoT Hub 概念、Azure CLI 建资源组与 IoT Hub、设备注册与直连 |
| 5 | 将应用逻辑迁移到云端 | Serverless、Azure Functions、IoT Hub 事件触发器、Registry Manager 下发直连方法 |
| 6 | 保障植物安全 | 加密原理、对称密钥与连接字符串、X.509 证书认证 |
课程中会使用部分云资源;若未完整完成本项目的所有课时,建议参考 clean-up.md 清理云资源,避免产生不必要的费用。
第 1 课:用物联网预测植物生长
温度为什么对种植至关重要
植物生长需要水、二氧化碳、养分、光照与热量。大多数人对前几项很熟悉,却常常忽略热量——这正是植物在春季气温回升时开花、温室能促进植物生长、暖冬时雪莲花或水仙提前发芽的原因。针对日均温度,每种植物都存在三个关键温度:
- 基础温度(Base temperature):植物能够生长的最低日均温度;
- 最适温度(Optimum temperature):植物生长最快的日均温度;
- 最高温度(Maximum temperature):植物能承受的温度上限,超过后植物会停止生长以保水存活。
💁 以上均为昼夜平均温度。植物昼夜需要不同温度,白天更高效地进行光合作用、夜间节省能量。
不同植物物种这三个温度值各不相同,这就是为什么有些植物适合热带、有些适合寒带。下图展示了"生长速率—温度"关系曲线:低于基础温度无生长,速率随温度升高至最适温度达峰,之后回落,至最高温度时生长停止:
如果农民能控制温度(例如商业温室),就可以为植物优化环境——比如商业番茄温室白天约 25°C、夜间约 20°C,以获得最快生长。
生长度日(Growing Degree Days,GDD)
生长度日(又称生长度单位)是以温度衡量植物生长的指标:在水分、养分、CO₂ 充足的前提下,温度决定生长速率。GDD 按天计算,即一天的平均温度(°C)减去植物基础温度。每种植物成熟、开花、结果都需要一定数量的 GDD,每天累积的 GDD 越多,植物长得越快。
GDD 的完整公式较复杂,课程使用一个常用的简化近似公式:
- GDD:生长度日数量
- T_max:当日最高温度(°C)
- T_min:当日最低温度(°C)
- T_base:植物基础温度(°C)
玉米示例:不同品种的玉米成熟需要 800~2,700 个 GDD,基础温度 10°C。若某日测得最高温 16°C、最低温 12°C,则当日 GDD = (16 + 12) / 2 − 10 = 4。若该品种玉米需 800 GDD 成熟,则还需累积 796 个 GDD:
💁 还有处理 T_max 高于 30°C 或 T_min 低于 T_base 的变体公式,本课先忽略这些边界情况。
从传感器数据计算 GDD 的完整架构
植物不会按固定日期生长,例如你无法确定种子播下后 100 天必然结果。农民只能凭经验粗略判断成熟时间,再每日巡检——这对大型农场是巨大的人力负担,还容易漏掉意外早熟的作物。通过测量温度计算 GDD,农民只需在接近预期成熟期时检查。
物联网方案的标准架构是:IoT 设备测量温度 → 通过 MQTT 等协议将遥测数据发布到互联网 → 服务端代码订阅数据并存储(如数据库或 CSV)→ 后续分析(如夜间任务计算当日 GDD、累计各作物 GDD,并在接近成熟时告警):
服务端还可以为数据补充额外信息:IoT 设备发布自身标识,服务端据此查表得到设备位置与监测的作物类型;还可以补充当前时间——因为部分 IoT 设备缺少精确时钟硬件,或需要额外代码通过网络读取时间。
实战:发布温度遥测并落盘为 CSV
课程提供了三套硬件路径的引导文档(Wio Terminal / Raspberry Pi / 虚拟设备),温度发布与 MQTT 连接说明见 2-farm/lessons/1-predict-plant-growth。服务端完整实现见 code-server/temperature-sensor-server/app.py,其核心逻辑:
client_name = id + 'temperature_sensor_server' # 客户端标识反映项目名 temperature_file_name = 'temperature.csv' # CSV 文件名 fieldnames = ['date', 'temperature'] # 两列:日期与温度 if not path.exists(temperature_file_name): with open(temperature_file_name, mode='w') as csv_file: writer = csv.DictWriter(csv_file, fieldnames=fieldnames) writer.writeheader() # 首行写入列头 def handle_telemetry(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) with open(temperature_file_name, mode='a') as temperature_file: temperature_writer = csv.DictWriter(temperature_file, fieldnames=fieldnames) temperature_writer.writerow({'date': datetime.now().astimezone() .replace(microsecond=0).isoformat(), 'temperature': payload['temperature']})关键要点:
client_name使用id + 'temperature_sensor_server'形式,保证 MQTT 客户端标识唯一;- CSV 采用追加模式(
mode='a')写入,日期列使用ISO 8601 格式(含时区、不含微秒),温度列来自遥测消息; - 运行一段时间后的
temperature.csv形如:
date,temperature 2021-04-19T17:21:36-07:00,25 2021-04-19T17:31:36-07:00,24理想情况下应连续采集一整天数据再做 GDD 计算(若用虚拟设备,可勾选 random 并设置范围,避免每次返回相同温度值)。
实战:手工计算 GDD
例如草莓基础温度为 10°C。某日temperature.csv中最高温 25°C、最低温 12°C,则:(25 + 12) / 2 − 10 = 8.5,当日草莓获得8.5 个 GDD。草莓结果约需 250 个 GDD,因此还需继续累积:
课程的课后作业要求使用仓库提供的 gdd.ipynb Jupyter Notebook 可视化温度数据并计算 GDD(安装pandas、matplotlib、jupyter后执行jupyter notebook gdd.ipynb),评分标准是至少捕获 1~2 天的完整数据。
第 2 课:检测土壤湿度
土壤湿度的意义与测量方式
植物吸收水分用于三件事:光合作用(水+CO₂+光生成碳水化合物与氧气)、蒸腾作用(通过叶片气孔交换 CO₂,同时输送养分、类似出汗般为植物降温)、以及维持结构(植物约 90% 是水,比人类的 60% 还高,水让细胞保持刚性,缺水会萎蔫甚至死亡)。
土壤过干植物无法吸收足够水分,过湿则根部无法获得足够氧气导致根系死亡。因此理想的土壤应"不干不涝",物联网通过测量土壤湿度让农民按需浇水。常用的传感器有两种:
- 电阻式(Resistive):两根探针插入土壤,一端通电、另一端接收,通过测量土壤电阻判断湿度。水是良好导体,土壤含水越高电阻越低;
- 电容式(Capacitive):测量正负电极板间可存储电荷量(电容)。土壤电容随湿度变化并转换为电压,土壤越湿输出电压越低。
两者都是模拟传感器,通过电压表示湿度——那么电压如何变成代码里的数值?这引出了传感器与 IoT 设备的通信机制。
传感器如何与 IoT 设备通信
GPIO 引脚
GPIO 是一组可连接硬件的引脚(Raspberry Pi、Wio Terminal 等开发板常见),部分引脚提供 3.3V/5V 电压,部分为接地,其余可通过程序设为输出(发送电压)或输入(接收电压)。仅需开关量(高/低)的数字传感器与执行器可直接使用 GPIO:例如按钮连接在 5V 与输入引脚之间,按下时回路接通、读到高电平;LED 连接在输出引脚与地之间(需串联电阻防止烧毁),输出高电平即点亮。
模拟引脚与 ADC
Arduino 等设备提供带 ADC(模数转换器)的模拟引脚,将电压范围转换为数值。通常 ADC 为 10 位分辨率,即把电压映射为 0~1,023 的整数。例如 3.3V 板子上传感器返回 3.3V 则读到 1,023,返回 1.65V 则读到 511。土壤湿度传感器依赖电压,因此走模拟引脚,返回 0~1,023。回顾第一项目"小夜灯"课程:光传感器返回 0~1,023——Wio Terminal 直接接模拟引脚;Raspberry Pi 则经 Grove Base Hat 上集成 ADC 的模拟引脚通信;虚拟设备模拟模拟引脚发送 0~1,023 的值。
通信协议:I²C、UART、SPI 与无线
- I²C:多控制器多外设协议,数据以带地址的数据包传输,设备地址通常出厂硬编码。总线由 SDA(串行数据)、SCL(串行时钟)两根主线和 VCC、GND 两根电源线组成。数据发送时,某设备发出起始条件成为控制器,发送目标设备地址及读/写标志,传输完成后发停止条件。I²C 有三种速度模式:标准模式 100Kbps、快速模式(Raspberry Pi 上限 400Kbps)、高速模式 3.4Mbps;
- UART:两设备间点对点串行通信,各有一个发送(Tx)与接收(Rx)引脚,交叉相连实现双向传输。通过波特率(常用 9,600bps)确定传输速度,用起始位与停止位界定一个字节(8 位)数据;
- SPI:面向短距离通信(如微控制器与闪存),基于单控制器多外设模型,使用 COPI/CIPO/SCLK 三根主线加每外设一根 CS(片选)线。SPI 是全双工的,通过时钟同步无需起始/停止位,通常可每秒传输数 MB;
- 无线协议:蓝牙(主要是 BLE)、LoRaWAN(远距离低功耗)、WiFi 等。商用土壤湿度传感器常用 LoRaWAN 将田间数据发给集线设备;Zigbee 则用 WiFi 构建网状网络,设备互为中继直至协调器接入互联网。
传感器标定(Calibration)
传感器测量的是电阻/电容等电气量,需经标定转换为实用单位。有的传感器出厂已标定(如温度传感器直接返回 °C)。土壤湿度可用重量含水量(gravimetric,每公斤干土中水的公斤数)或体积含水量(volumetric,每立方米干土中水的立方米数)衡量。由于土壤成分会影响电气特性,理想做法是把传感器读数与实验室科学测量对照,绘出"电压—土壤湿度"曲线并拟合直线,此后 IoT 设备的读数即可经该直线换算为真实土壤湿度。注意:电阻式传感器电压随湿度增大而升高,电容式则相反(曲线下倾)。
硬件实测引导见 2-farm/lessons/2-detect-soil-moisture,该课代码目录下包含 Wio Terminal(PlatformIO C++)、Raspberry Pi 与虚拟设备的完整示例。
第 3 课:自动化植物浇水
用继电器控制大功率设备
IoT 设备电压很低(通常提供 3.3V/5V,电流小于 1A),不足以驱动水泵等大功率硬件,强行驱动会烧毁开发板。解决办法是让水泵接外部电源,用一个执行器像"开关"一样控制其通断——这与手指拨动电灯开关接通 110V/240V 市电的原理相同。
继电器(Relay)是最常用的电机械开关:控制电路为电磁铁通电,电磁铁吸合杠杆闭合输出电路的一组触点;断电后杠杆释放、触点断开。继电器是数字执行器——高电平开启、低电平关闭。输出电路可承载远高于控制电路的功率:例如 Grove 继电器输出电路最高可处理 250V/10A。接线示例中,USB 电源 +5V 经继电器输出电路一端接入,另一端接水泵,水泵再接电源地;继电器闭合即向水泵供电。
注意:继电器存在多种类型,例如上电时是接通还是断开、是否有多个输出电路;杠杆吸合时通常能听到清脆的"咔嗒"声(早期电门铃的蜂鸣器正是利用继电器快速通断发出嗡嗡声)。
通过 MQTT 控制植物
商业灌溉系统的控制逻辑是集中式的:基于多个传感器数据做决策,配置只改一处。为模拟这一点,课程用 MQTT 控制继电器:
- 在
soil-moisture-sensor项目中加入 MQTT 库,客户端 ID 为<ID>soilmoisturesensor_client; - 设备代码发送
soil_moisture遥测属性; - 新建
soil-moisture-sensor-server服务端,订阅遥测并下发控制命令,命令消息属性为relay_on,客户端 ID 为<ID>soilmoisturesensor_server; - 设备端收到命令后按
relay_on属性控制继电器;若soil_moisture大于 450 则发送relay_on: true,否则false。
完整示例见 code-mqtt。
传感器与执行器的时序问题
用实体传感器做过土壤湿度实验会发现:浇水后读数需要几秒才下降——这不是传感器迟钝,而是水渗透土壤需要时间(靠近传感器浇水可能先快速下降又回升,那是水分扩散的结果)。同理,执行器动作到传感器读数变化之间存在延迟:若像小夜灯那样"读数低于阈值立刻开泵、达到阈值立刻关泵",水仍在继续渗透,导致过浇、浪费水甚至烂根。
因此最佳浇水循环应该是:
- 开启水泵 5 秒
- 等待 20 秒(让水渗透、读数稳定)
- 重新检查土壤湿度
- 若仍高于目标值(湿度仍不够),重复以上步骤
具体时长高度依赖你的设备、被测量属性与传感器/执行器;宁可少浇(还能再开泵),不可多浇(无法从土壤中取水)。更精细的做法是"每高于目标 100 就开泵 1 秒"而非固定 5 秒;露天种植还可结合天气预报——预报有雨则推迟浇水,雨后再评估是否仍需浇。
给植物控制服务端添加时序
服务端逻辑为:收到遥测 → 检查土壤湿度 → 若湿度不足,则发命令开继电器 → 等 5 秒 → 发命令关继电器 → 等 20 秒待湿度稳定。由于遥测每 10 秒一条,而整个浇水循环约 25 秒,需要避免重叠触发新循环。相比把设备改为每分钟上报一次(不利于大农场的水流分析),更好的方案是在浇水循环期间退订遥测:代码在无法处理时忽略遥测,但其他订阅同一数据的服务仍可读取。
完整实现见 code-timing/server/app.py:
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()要点:import threading让control_relay在后台独立线程运行,避免阻塞 MQTT 消息循环;退订—浇水—等待—重新订阅的模式保证了循环期间不会处理新的遥测消息。
第 4 课:将植物迁移到云端
什么是云
云常常被戏称为"别人的计算机":你不必自建数据中心(涉及买机器、硬件维护、供电散热、网络、安全、软件安装更新等高昂成本),而是按需向云服务商租用算力——需求高峰多租、低谷少租。云数据中心遍布全球,一些大型中心面积可达数平方公里,甚至自带发电站;其规模化效应也使其比大量小型数据中心更环保。
本课使用Microsoft Azure。你需要一个云订阅,有两种免费方式:
- Azure for Students(18 岁以上学生):无需信用卡,用学校邮箱验证,注册即获 US$100 额度及包含免费 IoT 服务在内的免费服务,有效期 12 个月、可逐年续期;
- Azure 免费订阅(非学生):需信用卡验证身份但不会扣费,前 30 天有 US$200 额度及免费层服务,额度用完也不会自动扣款。
云 IoT 服务为什么优于公共测试 Broker
之前使用的公共 MQTT broker 适合学习,但在商用场景有四大缺陷:可靠性(免费无保障,随时可能关闭)、安全性(公开可窃听遥测或下发恶意命令)、性能(仅适合少量测试消息)、可发现性(无法得知哪些设备在线)。云 IoT 服务则具备高可靠性、内建安全、高性能可扩展,并知晓所有已注册设备——未注册设备连接会被拒绝。设备通过设备 SDK或直接走 MQTT/HTTP 等协议接入,应用其他组件通过服务 SDK与设备通信:
用 Azure CLI 创建 IoT Hub
- 安装 Azure CLI 后添加 IoT 扩展并登录:
az extension add --name azure-iot az login- 多订阅环境下先列出并选择订阅:
az account list --output table az account set --subscription <SubscriptionId>- 查询可用区域(本课写作时约 65 个)并创建资源组:
az account list-locations --output table az group create --name soil-moisture-sensor --location <location>- 创建免费层 IoT Hub(免费层每日 8,000 条消息,每个订阅仅限一个):
az iot hub create --resource-group soil-moisture-sensor \ --sku F1 \ --partition-count 2 \ --name <hub_name>--sku F1选择免费层;--partition-count 2定义 IoT Hub 支持的数据分区数(创建免费层 IoT Hub 的必需参数)。hub 名称需全局唯一,因为它会出现在访问 URL 中。
IoT Hub 的通信方式
IoT Hub 定义了若干设备↔云通信机制(底层可走 MQTT、HTTPS 或 AMQP):
- 设备到云(D2C)消息:设备发往 IoT Hub 的遥测,应用代码可读取(底层基于 Azure Event Hubs,读取时通常称为"事件");
- 云到设备(C2D)消息:应用代码经 IoT Hub 发给设备的消息;
- 直连方法请求(Direct method):请求设备执行动作(如控制执行器),必须响应以便应用代码确认处理成功;
- 设备孪生(Device twins):在设备与 IoT Hub 间保持同步的 JSON 文档,用于存储设备上报属性或云端期望(desired)配置。
IoT Hub 可暂存消息与直连方法请求(默认一天),设备离线重连后仍能取回离线期间的消息;设备孪生则永久保存在 IoT Hub。
注册设备并验证云端连通
az iot hub device-identity create --device-id soil-moisture-sensor --hub-name <hub_name> az iot hub device-identity connection-string show --device-id soil-moisture-sensor \ --output table --hub-name <hub_name>连接字符串是含服务标识(URL)与密钥(SharedAccessKey)的文本,由设备 SDK 使用——必须妥善保管。随后可监控设备遥测:
az iot hub monitor-events --hub-name <hub_name>输出中可见payload为{"soil_moisture": 376}等消息;加--properties anno可查看自动附加的注解(annotations),如iothub-connection-device-id(发送方设备 ID)与iothub-enqueuedtime(UNIX 时间戳)等。还可以下发直连方法控制继电器:
az iot hub invoke-device-method --device-id soil-moisture-sensor \ --method-name relay_on \ --method-payload '{}' \ --hub-name <hub_name>设备端会打印Direct method received - relay_on。注意:设备每 10 秒发一条遥测,一天约 8,640 条,已接近免费层 8,000 条/日的上限——课程挑战题正是思考如何调整上报频率以留在免费层内。
第 5 课:将应用逻辑迁移到云端
什么是 Serverless
Serverless(无服务器计算,又称 FaaS)是指在云端以小块代码响应各类事件:事件发生时你的代码被加载运行,并传入事件数据;无事发生时代码不存活。因此它天然可扩展——大量事件同时发生时云提供商会并行运行你的函数实例。代价是函数间共享信息需存到数据库等外部存储,不能依赖内存。计费方式为"按代码运行时间与内存用量付费",不运行时零费用。
对 IoT 开发者而言,Serverless 是理想模型:写一个响应"任何已连接设备发到云 IoT 服务的消息"的函数即可,代码仅在需要时运行。
创建 Azure Functions 应用
Microsoft 的无服务器服务是Azure Functions,开箱支持 Python、JavaScript、TypeScript、C#、F#、Java 与 PowerShell(本课用 Python)。Functions 应用由若干触发器(triggers)组成,共享同一套配置。
本地开发链路:安装 Azure Functions Core Tools 与 VS Code 扩展 → 安装 Node.js → 全局安装存储模拟器:
npm install -g azurite mkdir azurite azurite --location azuriteAzurite 会在本地启动 Blob(10000 端口)、Queue(10001)、Table(10002)三个服务。然后创建项目:
mkdir soil-moisture-trigger cd soil-moisture-trigger python3 -m venv .venv source ./.venv/bin/activate # Linux/macOS;Windows 用 .venv\Scripts\activate.bat 或 Activate.ps1 func init --worker-runtime python soil-moisture-trigger pip install -r requirements.txtfunc init会生成三个文件:host.json(应用级设置)、local.settings.json(本地运行设置,如 IoT Hub 连接字符串,不应提交到源码控制)、requirements.txt(Pip 依赖)。需把AzureWebJobsStorage设为UseDevelopmentStorage=true以连接 Azurite。
创建 IoT Hub 事件触发器
触发器要读取 IoT Hub 的消息流,需连接 IoT Hub 的Event Hub 兼容端点(IoT Hub 基于 Azure Event Hubs,读消息方式与 Event Hubs 相同)。获取连接字符串并写入local.settings.json的Values段:
az iot hub connection-string show --default-eventhub --output table --hub-name <hub_name>"IOT_HUB_CONNECTION_STRING": "<connection string>"创建触发器(没有专门的 IoT Hub 触发器模板,使用 Event Hub 触发器):
func new --name iot-hub-trigger --template "Azure Event Hub trigger"生成的iot-hub-trigger文件夹包含:
__init__.py:核心是main函数,每次有消息发到 IoT Hub 即被调用:
import logging import azure.functions as func def main(event: func.EventHubEvent): logging.info('Python EventHub trigger processed an event: %s', event.get_body().decode('utf-8'))function.json:含bindings(绑定)配置。关键字段:"type": "eventHubTrigger"(监听事件)、"name": "events"(对应 Python 参数名)、"direction": "in"(输入绑定)、"connection": ""(从设置读取连接字符串——不能直接写入连接字符串)。
因模板缺陷,需手工修正三处:把cardinality从many改为one(否则报'list' object has no attribute 'get_body')、把connection设为IOT_HUB_CONNECTION_STRING、把eventHubName清空(连接字符串已含EntityPath,重复指定会报错)。运行func start后,函数会处理过去一天内已发到 IoT Hub 的事件:
Functions: iot-hub-trigger: eventHubTrigger ... Python EventHub trigger processed an event: {"soil_moisture":628}从 Serverless 代码下发直连方法
监听之外还需向设备发命令,这要通过Registry Manager(注册表管理器)——它管理已注册设备、可发送 C2D 消息/直连方法/更新设备孪生。用ServiceConnect策略获取连接字符串(该策略允许连接并发送消息给设备,遵循最小权限原则):
az iot hub connection-string show --policy-name service --output table --hub-name <hub_name>在requirements.txt中加入azure-iot-hub并pip install -r requirements.txt。改造__init__.py的main函数(完整代码见 code/functions):
import json import os from azure.iot.hub import IoTHubRegistryManager from azure.iot.hub.models import CloudToDeviceMethod body = json.loads(event.get_body().decode('utf-8')) device_id = event.iothub_metadata['connection-device-id'] soil_moisture = body['soil_moisture'] if soil_moisture > 450: direct_method = CloudToDeviceMethod(method_name='relay_on', payload='{}') else: direct_method = CloudToDeviceMethod(method_name='relay_off', payload='{}') registry_manager_connection_string = os.environ['REGISTRY_MANAGER_CONNECTION_STRING'] registry_manager = IoTHubRegistryManager(registry_manager_connection_string) registry_manager.invoke_device_method(device_id, direct_method)要点:设备 ID 从事件的iothub_metadata['connection-device-id']注解中取出,因此直连方法会发给正确的单一设备——比之前 MQTT 版"广播给所有设备"更适合多套传感器/继电器并存;连接字符串在本地从local.settings.json读为环境变量,部署后则从云端 Application Settings 读取。
部署到云端
- 创建存储账户(Functions 需要少量云存储,
Standard_LRS为最低成本通用账户,无免费层但费用很低,存储最贵也不到每 GB 每月 US$0.05):
az storage account create --resource-group soil-moisture-sensor \ --sku Standard_LRS \ --name <storage_name>存储账户名需全局唯一,仅限小写字母与数字,最多 24 字符。
- 创建 Functions App(Python 仅支持 Linux 托管):
az functionapp create --resource-group soil-moisture-sensor \ --runtime python \ --functions-version 3 \ --os-type Linux \ --consumption-plan-location <location> \ --storage-account <storage_name> \ --name <functions_app_name>- 把本地设置上传为 Application Settings(部署后从环境变量读取):
az functionapp config appsettings set --resource-group soil-moisture-sensor \ --name <functions_app_name> \ --settings "IOT_HUB_CONNECTION_STRING=<connection string>" # 同样设置 REGISTRY_MANAGER_CONNECTION_STRING- 发布:
func azure functionapp publish <functions_app_name>输出Deployment successful.并列出已部署的触发器iot-hub-trigger - [eventHubTrigger]即成功。此后调整土壤湿度即可看到云端逻辑自动控制继电器开关。
💡 注意:IoT Hub 触发器无法退订,之前 MQTT 版"浇水循环期间退订"的时序策略在此不适用,需要另想他法(这正是课程的挑战题)。
第 6 课:保障植物安全
为什么要保护 IoT 设备
IoT 安全要确保:只有预期的设备能连入云 IoT 服务并发送遥测,只有你的云服务能向设备发命令。不安全的代价可能是:伪造设备持续上报高湿度导致灌溉永不启动、植物干死;黑客操控设备长时间浇水导致烂根并造成水费损失;读取个人/商业敏感数据;以设备为跳板入侵内部网络;甚至以个人数据勒索。历史上已有大量真实案例(如鱼缸恒温器漏洞被用于入侵赌场网络、Mirai 僵尸网络利用默认口令的 IoT 设备发起大规模 DDoS 攻击、联网玩具数据库泄露儿童语音消息、健身应用暴露用户住址等)。
密码学基础
设备用 ID 自证身份,但 ID 可被克隆。解决之道是加密:用只有设备与云端知晓的密钥(encryption key)把数据打乱成密文,云端用相同或配对的解密密钥还原;无法解密即视为被入侵而拒绝。
- 早期密码学:替代密码,如凯撒密码(按固定位移替换字母)、维吉尼亚密码(用单词对不同字母作不同位移);
- 现代密码学:基于复杂数学,密钥空间大到暴力破解不可行。HTTPS 即浏览器与服务器间加密通信的典型应用;
- 对称加密:加密与解密用同一密钥,发送方与接收方需共享密钥,共享过程可能泄密,速度快但安全性较低;
- 非对称加密:公钥/私钥对。公钥用于加密(可公开),私钥用于解密(仅持有者保留)。更安全但更慢;常见做法是用非对称加密安全地共享对称密钥,再用对称密钥加密数据。
用对称密钥保护设备:连接字符串与 SAS
此前连接 IoT Hub 用的连接字符串由三部分(分号分隔的键值对)组成:
HostName=soil-moisture-sensor.azure-devices.net;DeviceId=soil-moisture-sensor;SharedAccessKey=Bhry+ind7kKEIDxubK61RiEHHRTrPl7HUow8cEm/mU0=| 键 | 值 | 说明 |
|---|---|---|
| HostName | soil-moisture-sensor.azure-devices.net | IoT Hub 的 URL |
| DeviceId | soil-moisture-sensor | 设备唯一 ID |
| SharedAccessKey | Bhry+ind7kKEIDxubK61RiEHHRTrPl7HUow8cEm/mU0= | 设备与 IoT Hub 共同持有的对称密钥 |
密钥从不直接传输。设备首次连接时发送共享访问签名(SAS)令牌:含 IoT Hub URL、过期时间戳(通常为当前时间 +1 天)与签名(URL+过期时间用共享密钥加密)。IoT Hub 用共享密钥解密签名并核对 URL 与过期时间,同时验证当前时间早于过期时间,防止恶意设备截获真实设备的 SAS 令牌重放。也正因有过期时间,设备需要准确的时钟(通常从 NTP 服务器读取),时间不准会导致连接失败。
💁 密钥不应写死在代码里(代码泄露=密钥泄露,且发布时要为每台设备重新编译)。理想做法是从硬件安全模块读取;学习阶段写进代码可以,但切勿提交到公开源码库。设备有 2 个密钥与 2 条对应连接字符串,便于密钥轮换(rotate)。
X.509 证书:非对称加密的落地
非对称加密下,你需要把公钥交给发送方——但对方如何确认公钥确实属于你?解决方案是X.509 证书:把公钥放入由可信第三方(证书颁发机构,CA)签发并数字签名的数字文档中。证书含持有人信息、签发 CA、有效期与公钥本身,使用前应验证其确由原 CA 签发。CA 签发收费,测试可"自签名"(自己签发),但生产环境绝不可用自签名证书。
X.509 证书可跨设备共享:创建一份证书上传到 IoT Hub,所有设备共用;每台设备只需知道私钥来解密来自 IoT Hub 的消息。设备用于加密发往 IoT Hub 消息的证书由微软发布,与许多 Azure 服务共用,甚至内置于 SDK。
实战:创建 X.509 设备身份
Azure CLI 可一步完成"生成密钥对 + 创建自签名证书 + 注册设备":
az iot hub device-identity create --device-id soil-moisture-sensor-x509 \ --am x509_thumbprint \ --output-dir . \ --hub-name <hub_name>该命令在当前目录生成两个文件:soil-moisture-sensor-x509-key.pem(设备私钥,切勿提交到公开源码控制)与soil-moisture-sensor-x509-cert.pem(X.509 证书)。设备侧接入引导见 single-board-computer-x509.md 与 wio-terminal-x509.md。本课是项目最后一课,完成后应参照 clean-up.md 清理云端资源。
课程配套资源速查
整个项目贯穿"理论 + 三套硬件路径(Wio Terminal / Raspberry Pi / 虚拟设备)+ 服务端代码"的模式,可在各课目录中直接查阅与运行:
- 第 1 课:发布温度并落盘 CSV 的完整服务端实现 app.py;GDD 可视化作业 gdd.ipynb;
- 第 2 课:三种硬件的土壤湿度测量代码 2-farm/lessons/2-detect-soil-moisture/code;
- 第 3 课:MQTT 遥控继电器 code-mqtt、继电器直控 code-relay、带时序控制的浇灌服务端 app.py;
- 第 4 课:三种硬件连接 IoT Hub 的接入代码 2-farm/lessons/4-migrate-your-plant-to-the-cloud/code;
- 第 5 课:Azure Functions 触发器完整工程 code/functions;
- 第 6 课:X.509 认证的设备代码 2-farm/lessons/6-keep-your-plant-secure/code。
如果只完成部分课时而用到了云端资源,务必按 clean-up.md 中的az group delete --name <resource-group-name>删除整个资源组以清理全部云资源、避免潜在费用。从一棵盆栽的土壤湿度监测,到可部署于云端、按需灌溉整片农场的自动化系统——这 6 堂课恰好完整覆盖了数字农业 IoT 应用从原型到生产化的全链路。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考