ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32+MQTT+阿里云:自制温湿度光照土壤监测装置全解析

ESP32+MQTT+阿里云:自制温湿度光照土壤监测装置全解析 简介面向ESP8266物联网开发与阿里云平台接入的学习者这套资料以正点原子ESP8266-01S为核心结合OLED显示屏、温湿度传感器、光敏电阻和土壤湿度传感器实现多路环境数据采集、本地屏幕显示与云端远程上报。资源压缩包共57MB包含311个文件覆盖Keil工程源码h、c、编译生成的hex/bin固件、OLED显示所需的BMP图片资源、PDF原理图文档、烧录工具exe及配置文件等中间文件也一并保留便于二次编译与排错文件类型以源码、固件和说明文档为主结构清晰。已有1885人学习下载。配套B站视频完整演示了从接线、烧录到平台配置的整个过程资源描述中详细列出了各传感器与ESP8266的引脚连接关系如温湿度接PB12、光照ADC接PA0等可对照视频快速复现项目。对于希望低成本搭建环境监测上报系统的开发者这是一份软硬件兼备的完整参考方案。1. 一块小屏实时显示、数据同步上云这套温湿度光照土壤监测装置到底在做什么你在B站刷到这类视频时会发现一个共性一块128x64的OLED小屏幕上温度、湿度、光照强度、土壤湿度轮流跳动旁边的ESP32开发板安静地连着WiFi阿里云物联网平台的后台里这些数据以固定频率刷新成曲线。外行人觉得是“炫”但做过的人清楚这背后其实是三件事的拼装传感器选型、MQTT上云、屏幕显示。买一台成品环境监测仪当然省事但协议封闭、采样周期不可控、数据出不来想接自己的告警规则或可视化看板几乎不可能。自己搭一套总成本不到一百块钱却能同时把传感器采集、嵌入式网络通信、云平台物模型这三块硬技能串起来。适合正在做物联网毕设的人也适合想给阳台植物或小温室做数据记录的折腾党。这套方案的核心套路很固定传感器读数、拼JSON、MQTT上报、屏幕轮播下面按这条线把每个环节的选型理由和坑位讲清楚。2. 传感器与物模型先想清楚温湿度、光照、土壤湿度这三路数据怎么量、怎么传动手接线之前先花十分钟把传感器定下来。很多视频里看起来“都能用”但每种传感器读出来的数值含义完全不同有的给百分比有的给模拟量有的给工程单位。如果一开始不定好量纲后边在阿里云上配物模型、做可视化时会反复改。这一章先解决“量什么、用什么量、以什么格式传”。2.1 温湿度DHT11、DHT22 怎么选读取间隔按数据手册来温湿度最常见的两个选项是DHT11和DHT22。DHT11便宜到可以忽略成本但精度只有±2℃、±5%RH采样周期标称1秒实际稳定读数往往要2秒以上。DHT22把温湿度精度提到±0.5℃、±2%RH价格贵一倍多点但读出来的数据在曲线上明显平滑。我的建议是做演示和课程设计DHT11够用想把数据当真拿去分析直接换DHT22省去后续反复校准的时间。这两个传感器都是单总线协议只需占用一个GPIO。读取时有个关键限制两次读取间隔不能低于数据手册规定的最小采样周期否则会拿到上一次的缓存值甚至NaN。代码层面要用时间戳控制轮询节奏而不是在loop()里连续调用读取函数。DHT库内部虽然有自己的节流逻辑但不同库实现差异很大靠dht.getMinimumSamplingPeriod()来约束读取频率是最稳的做法。2.2 光照BH1750 比光敏电阻更适合做 lx 级上报光照传感器有两个流派。光敏电阻加一个固定电阻做分压接到ADC引脚上通过电压换算成光照强度成本最低但非线性严重而且同一批电阻个体差异大不同板子之间读数没有可比性。相比之下BH1750是I2C接口的数字光照传感器直接输出勒克斯lx量程到65535适应室内到户外直射的大动态场景。它和OLED屏幕走同一条I2C总线地址是0x23OLED一般是0x3C两者不冲突。BH1750有个别小坑上电后要先begin()再读首次读取建议延时200ms连续高分辨率模式下每次转换约120ms读得过快会拿到旧值。对植物补光或室内光照监测来说100ms级别的延迟完全无感但如果你同时还要做OLED刷新和MQTT上报就要把这三个任务的时序错开避免单次loop()里串行等待导致整周期变长。2.3 土壤湿度电容式探头才是能长期插在土里的方案土壤湿度传感器是这套装置里最容易被低估的环节。市面上大量廉价模块是电阻式两根金属探针插入土壤利用土壤含水量变化引起电阻变化来读数。这类探头刚用几天确实灵敏但插在潮湿土壤里一到两周探针就会因电解反应逐渐腐蚀读数漂移得厉害。电容式土壤湿度传感器的工作原理完全不同它把探头做成一个电容器土壤作为介质含水量变化改变介电常数从而改变电容值。没有裸露电极的电解问题寿命长得多价格只贵几块钱。接线方面电容式模块一般输出模拟量AO也有加比较器输出数字量DO的版本。我们只需要AO接ESP32的ADC引脚。值得注意的是这类模块的ADC读数与湿度的方向不一定直观很多模块土壤越干输出电压越高越湿反而越低。代码里的百分比映射要按实际模块数据手册的方向做或者干脆用“干空气读数映射0%、泡水读数映射100%”的标定方式。2.4 数据模型一张表定下单位JSON 直接对齐阿里云物模型在写第一行代码之前先把字段定下来。这步跳过了后面在阿里云控制台配物模型时还得回来改。下面这张表是这套装置最常见的字段设计直接对应阿里云物模型的属性定义。属性标识符中文名数据类型单位数据来源temperature温度float℃DHT11/DHT22humidity湿度float%RHDHT11/DHT22light光照强度intlxBH1750soil土壤湿度int%电容式传感器映射对应的JSON上报结构在阿里云物模型里长这个样子后面代码中直接拼接这个结构体即可{ id: 123456, version: 1.0, method: thing.event.property.post, params: { temperature: 25.6, humidity: 60.2, light: 350, soil: 45 } }id每次上报不能重复可以用毫秒时间戳或自增计数器params里的键名必须和物模型里的属性标识符完全一致否则阿里云返回参数错误。浮点数精度只需要一位小数多了既浪费流量也会让后端存储的数值显得脏。这里还有一个经常被忽略的点数据类型的匹配。物模型里定义的是float上报时就别传字符串25.6类型不一致会被平台直接拒收这是初学者在连云阶段最常看到的报错。3. 硬件接线与采集代码三个传感器和OLED挂在同一条 I2C 上选型定下来之后接线就简单了。整套装置的核心是ESP32开发板原因有三个自带WiFi和蓝牙、ADC引脚多、主频足够同时跑传感器轮询、OLED刷新和MQTT发包。ESP8266也能做但ADC只有一个且精度一般土壤湿度、光照如果用模拟量就会紧张。下面的接线和代码以ESP32为例展开。3.1 引脚分配表ESP32 的 ADC、I2C 别和下载口抢资源接线前先看一张引脚分配表。ESP32的I2C默认走GPIO21SDA和GPIO22SCLBH1750和OLED都挂在这组引脚上设备地址不同不需要额外片选。DHT11/22走单总线接一个普通GPIO即可。土壤湿度模块的AO引脚必须接在ADC1通道上ESP32的ADC2在WiFi开启时不可用这是新手接完线发现土壤数据恒为0的最常见原因。传感器信号引脚ESP32引脚备注BH1750SDA / SCLGPIO21 / GPIO22与OLED共用I2COLED (SSD1306)SDA / SCLGPIO21 / GPIO22地址0x3CDHT11/22DATAGPIO4第2章可换电容式土壤湿度AOGPIO34ADC1通道所有模块VCC / GND3.3V / GND土壤模块可试点5V注意这里的3.3V供电问题。BH1750和OLED用3.3V没问题部分电容式土壤模块标称支持3.3V到5V但3.3V下ADC输出范围会整体下移。如果发现土壤读数在空气中和水中的差值过小把模块VCC单独接到5V引脚AO输出仍接3.3V参考的ADC注意共地即可。DHT在3.3V下工作正常不需要外部上拉部分模组自带上拉电阻。3.2 Arduino 采集骨架读三路数据再决定显示还是先上云代码整体分四块WiFi连接、传感器采集、OLED绘制、MQTT上报。先看采集和显示这两块的最小骨架下文再补阿里云连接部分。#include Wire.h #include DHTesp.h #include BH1750.h #include U8g2lib.h #define DHT_PIN 4 #define SOIL_PIN 34 #define SDA_PIN 21 #define SCL_PIN 22 DHTesp dht; BH1750 lightMeter; U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8G2_USE_SW_I2C); float temperature 0, humidity 0; uint16_t lightLux 0; uint8_t soilPercent 0; unsigned long lastSensorRead 0; void readSensors() { unsigned long nowMs millis(); if (nowMs - lastSensorRead dht.getMinimumSamplingPeriod()) { temperature dht.getTemperature(); humidity dht.getHumidity(); lastSensorRead nowMs; } lightLux lightMeter.readLightLevel(); int soilRaw analogRead(SOIL_PIN); // 电容式传感器方向不定需根据实测标定 soilPercent map(soilRaw, 3400, 1400, 0, 100); soilPercent constrain(soilPercent, 0, 100); } void drawScreen() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB10_tr); char line[32]; snprintf(line, sizeof(line), Temp: %.1f C, temperature); u8g2.drawStr(2, 14, line); // 湿度、光照、土壤同理绘制 u8g2.sendBuffer(); } void setup() { Serial.begin(115200); Wire.begin(SDA_PIN, SCL_PIN); dht.setup(DHT_PIN, DHTesp::DHT22); lightMeter.begin(); u8g2.begin(); } void loop() { readSensors(); drawScreen(); delay(100); }readSensors()里两个关键点。第一DHT读取用dht.getMinimumSamplingPeriod()做节流保证两次读取间隔不低于其内部状态机要求。第二map(soilRaw, 3400, 1400, 0, 100)里的两个源区间值不是随便写的它们分别对应传感器在干燥空气和泡水状态下的实测ADC输出每个模块都不一样必须用自己的板子实测后替换。代码里加了constrain()防止校准不准时百分比越界这是仪表类显示必须做的保护。3.3 三个必踩的读数坑DHT 首次 NaN、土壤上电稳定时间、BH1750 量程先说DHT的NaN问题。ESP32上电瞬间DHT还没准备好第一次读温度几乎必然是NaN表现为串口打出一行nan。解决方式除了跳过前两次读取外更稳妥的是用isnan()判断读到的值无效就不更新显示变量保持上一次有效值。这个习惯同时适用于很多I2C传感器能避免NaN一路传到云平台变成脏数据。土壤湿度的坑在稳定时间。电容式土壤模块是一个RC振荡电路上电瞬间输出电压要经过几十到几百毫秒才稳定。如果你在setup()里上电后立刻读第一次ADC拿到的往往是随机的中间值。常见做法是给土壤模块单独用一个GPIO控制电源读取前先写高电平延时100ms再读读完可以断电省电。BH1750的量程问题不属于读取失败而是场景误用。它最高量程65535lx室外正午阳光直射可能接近甚至超出这个值读出来封顶在65535。放在室内或阳台遮阴处没问题但如果设备要放户外全天候跑就得考虑在后面加一层遮光片或者改用支持更高量程的传感器。另外BH1750在连续高分辨率模式下电流约1mA断电重开会丢配置代码里要保证begin()只在setup()执行一次。4. 上传阿里云产品、设备、MQTT连接参数与上下行测试本地采集和显示跑通后剩下来最耗时间的就是阿里云连接这一关。很多人的设备在本地显示一切正常一上云就出现“连接被拒绝”“鉴权失败”“上行消息没有数据”原因大都集中在三元组抄错、MQTT签名串顺序不对、Topic拼错这三个地方。下面按从平台配置到设备端程序的顺序走一遍。4.1 控制台准备从创建产品到拿三元组公共实例和企业版注意点阿里云物联网平台现在区分公共实例和企业版实例两者在设备接入流程上基本一致但接入地址和部分控制台入口不同。个人学习、毕设、B站视频教程里绝大多数用的是公共实例免费额度够用产品数、设备数、消息量都有上限对这套传感器数据上报的场景完全够。控制台操作分三步。第一步创建产品产品名称随意所属品类选“温湿度传感器”或“自定义”认证方式选“设备密钥”。第二步定义物模型在产品详情页的“物模型”标签下添加上文表里的四个属性注意属性标识符一旦定义修改会影响已注册设备。第三步注册设备得到DeviceName设备名、ProductKey产品密钥、DeviceSecret设备密钥三元组。其中DeviceSecret只在创建时完整显示一次一定要复制保存丢失后只能删除设备重建。新用户容易在这几步翻车地域选错公共实例接入地址跟随地域变化华东2上海、华南1深圳等地域的Broker地址各不相同产品品类选错导致物模型模板里带了不应有的属性再改就很麻烦。建议在创建产品前先明确“我只需要温湿度、光照、土壤湿度这四个属性”然后选自定义品类从头建模。4.2 手写 MQTT 签名ClientID/密码的 HmacSHA1 算法在 ESP32 上实现获取三元组后设备端要构造MQTT连接的三个关键字段。以公共实例为例Broker地址一般为${productKey}.iot-as-mqtt.${regionId}.aliyuncs.com其中regionId是地域代码上海是cn-shanghai。端口1883。这三个参数在控制台设备详情页的“连接信息”里都有现成展示可以直接复制。期望避免因人而异出错的方法是先拿“设备详情”里给出的Mqtt连接参数原样填进MQTTX桌面客户端测试通了再写进ESP32代码。ClientID、用户名、密码的生成规则为参数取值ClientID${deviceName}|securemode3,signmethodhmacsha1,timestamp${timestamp}|用户名${deviceName}密码对特定内容串做HmacSHA1摘要密钥为DeviceSecret其中签名内容串的拼接顺序固定为clientId${deviceName}deviceName${deviceName}productKey${productKey}timestamp${timestamp}。timestamp是当前毫秒时间戳每次连接都要重新生成。注意ClientID中的设备名、内容串里的clientId字段等都不能缺少否则平台返回“鉴权失败”。在ESP32上用mbedtls实现一次HmacSHA1完整函数如下#include mbedtls/md.h String hmacSha1(String key, String content) { byte hmacResult[20]; mbedtls_md_context_t ctx; mbedtls_md_init(ctx); mbedtls_md_setup(ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), 1); mbedtls_md_hmac_starts(ctx, (const unsigned char *)key.c_str(), key.length()); mbedtls_md_hmac_update(ctx, (const unsigned char *)content.c_str(), content.length()); mbedtls_md_hmac_finish(ctx, hmacResult); mbedtls_md_free(ctx); String hex ; for (int i 0; i 20; i) { if (hmacResult[i] 16) hex 0; hex String(hmacResult[i], HEX); } return hex; }mbedtls_md_hmac_finish拿到的原始摘要必须编码成十六进制小写字符串这一步漏掉或转大写都会导致鉴权失败。ESP32的Arduino核心自带mbedtls不需要装额外库。相比使用现成的阿里云SDK手写这套签名的好处是出错时能定位到具体字段也更利于理解每个库内部做了一层什么封装。设备端连接时使用PubSubClient库connect入参中依次填入ClientID、用户名、密码WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); String clientId String(deviceName) |securemode3,signmethodhmacsha1,timestamp timestamp |; String username deviceName; String password hmacSha1(deviceSecret, clientId String(deviceName) deviceName String(deviceName) productKey String(productKey) timestamp timestamp); mqttClient.setServer(mqttHost, 1883); mqttClient.connect(clientId.c_str(), username.c_str(), password.c_str());setServer里填的域名不要带http://也不要加端口以外的路径。connect返回false时用mqttClient.state()打印返回码4表示用户名或密码错误5表示未授权查看距离问题最快。4.3 属性上报 Topic 与响应回调confirmed 不上行别慌看日志连接成功后设备属性上报的Topic是固定格式/sys/{productKey}/{deviceName}/thing/event/property/post上报的Payload就是第2章那段JSON直接publish即可。如果只是单向上报不关心平台是否收到QoS可以用0但为了排查方便建议订阅平台的响应Topic/sys/{productKey}/{deviceName}/thing/event/property/post_reply然后通过setCallback打印返回消息。平台返回code:200说明上报成功如果返回类似code:400或code:460说明JSON格式或物模型字段不对这时去控制台的“监控运维 - 日志服务”里查详细原因比在串口上盲猜快得多。一个很常见的现象是publish返回true但平台日志里没有上行消息。先确认设备是否真的处于在线状态控制台显示在线不代表当前连接就一定可用有些情况下TCP连接断了但平台还没来得及标记离线。此时在设备端setKeepAlive(60)主动缩短心跳周期同时观察串口是否周期性收到平台断连回调。4.4 连不上阿里云的排查顺序表把常见的连接失败和排查方向按顺序列出来照表查节省时间。现象优先检查项处理方式connect返回4密码签名串或DeviceSecret重新对照完整的签名内容串拼写connect返回5设备名或ClientID不匹配三元组与ClientID里不能有换行和空格connect返回-2网络不通或域名拼错先ping域名或换电脑NTP确认时间正确连上但publish后无响应Topic拼写或Payload格式对照4.3 Topic复制平台日志里的错误码控制台显示在线但收不到数据上报频率超过实例阈值降低上报频率公共实例有TPS限制设备反复掉线重连时间戳没更新或会话残留重连时必须重新生成timestamp时间戳这个坑值得单独强调。因为每次刷新页面生成的新连接阿里云后台都会校验时间戳的有效窗口。复用了上一次的时间戳去重连后台判定为重放直接拒绝。代码里把生成时间戳和重连封装在同一个函数里别把它放在setup()里只执行一次。5. OLED 屏幕显示再抠细一点中文字库、轮播、时间与重连实话说上云之后屏幕显示这部分最容易被人忽略但这也是整个装置天天摆在桌上给人看的部分。一堆英文缩写飘在屏幕上不如一屏中文“温度、湿度、光照、土壤”来得直观。很多B站教程在这块讲得比较浅都是简单调库drawStr两行英文。下面几个细节是能直接提升观感和稳定性的做法。5.1 128×64 画面的分屏布局和三秒轮播128×64分辨率放下四路数据加标签会很挤。如果全部显示在一屏上字体会缩到难以阅读。常见做法是分成两屏第一屏显示温度和湿度第二屏显示光照和土壤湿度每三秒切换一次。放一下其中一屏的绘制逻辑另一屏对称处理。void drawScreenPage1() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB10_tr); u8g2.drawStr(0, 14, Temp: 25.6 C); u8g2.drawStr(0, 32, Humi: 60.2%); u8g2.drawStr(0, 50, T: 2025-01-01 12:00); u8g2.sendBuffer(); }用millis()做节流切换每3秒切一屏。这样每屏只有两三行内容字可以做很大隔两米也能看清数值。注意drawStr的y坐标是文字基线而不是顶部用12、32这种间距配合字高14px的字体比较稳妥。另外屏幕刷新和数据读取是两个独立节奏别在delay()里高频重绘否则DHT的采样窗口会被挤占。5.2 UTF-8 中文字体与 u8g2 的编码坑想在OLED上显示中文u8g2库里要启用中文字体最常用的是u8g2_font_unifont_t_chinese字高16像素128像素宽一行能放下7到8个汉字。启用方式u8g2.setFont(u8g2_font_unifont_t_chinese); u8g2.drawUTF8(0, 16, 温度); u8g2.drawUTF8(0, 36, 湿度);这里有三处容易翻车。第一字符串函数必须用drawUTF8而不是drawStr后者只会按字节逐个画中文会变成乱码。第二Arduino IDE的编码要保证源文件保存为UTF-8Windows平台经常默认GBK字符在编辑界面显示正常编译后全乱。第三u8g2_font_unifont_t_chinese是一个大字体会明显多吃Flash和RAMESP32内存充裕无所谓ESP8266就得考虑用十六点阵压缩字库或只显示英文字符。5.3 NTP对时与掉线自动重连的配合屏幕上的时间直接从设备本地时间取容易越走越偏。ESP32每次重启后时间都会重置上云的同时顺手做一次NTP对时。阿里云时间服务器ntp1.aliyun.com在国内连通性和稳定性都很好使用configTime(8 * 3600, 0, ntp1.aliyun.com); struct tm timeinfo; if (getLocalTime(timeinfo)) { strftime(timeStr, sizeof(timeStr), %Y-%m-%d %H:%M:%S, timeinfo); }注意configTime的第一个参数是UTC8的秒数偏移填8×3600。如果用的是ESP8266configTime会调用底层的LwIP的SNTP重启后需要等待几秒到几十秒才能拿到有效时间。在此基础上把getLocalTime失败时的降级策略设置为“暂时显示--:--”而不是切断工作主循环。这行时间同时可以拼进MQTT上报的JSON里作为事件时间比依赖平台服务器时间更灵活。掉线重连的逻辑要跟时间戳绑定在一起。每次重连前重新生成timestamp再重新计算密码然后才调用mqttClient.connect()。重连间隔建议10秒起步不想阻塞主循环就用if (!mqttClient.connected() millis() - lastReconnectMs 10000) { lastReconnectMs millis(); reconnectMqtt(); }这套装置的最后一个验证动作是去阿里云控制台看实时数据曲线能否连续更新。屏幕显示是给你自己看的云端的数据才是这个系统真正沉淀下来的资产。等这两条路都通了之后再回头看B站那期视频里的演示流程你会发现自己已经具备了从传感器到云端的完整链路理解剩下的无非是在这个骨架上加告警、加历史曲线、加更多传感器。本文还有配套的精品资源点击获取
返回列表