资讯详情

树莓派+Flask+DHT11:构建稳定Web温湿度监控页面

📅 2026/10/12 7:49:04 | 华诺云谱 👁 阅读
树莓派+Flask+DHT11:构建稳定Web温湿度监控页面
1. 从一个尴尬的现场说起为什么我要把温湿度搬上网页去年帮一个做园艺的朋友改造他的小型温室他提的需求特别朴素想在手机上随时看到棚里的温湿度不用专门跑过去看墙上的指针表。我第一反应是拿现成的智能传感器套装但算下来一套要小几百而且数据在别人的云平台上导出和分析都不方便。手头正好有一块吃灰的树莓派和几个DHT11温湿度模块干脆自己搭一套本地化的Web监控页面。这个方案的核心链路其实就三件事树莓派通过GPIO读取DHT11的温湿度数据用Flask把数据包装成HTTP接口前端页面定时拉取并渲染成图表。听起来简单但真动手的时候坑一个接一个——DHT11的时序对不上、Flask多线程和GPIO抢资源、页面刷新导致数据抖动、长时间运行后传感器读数卡死。这些问题在零散的教程里很少有人讲透我前后折腾了差不多两个周末才把整套东西跑稳。这篇文章就是把这套树莓派 Flask DHT11 Web显示的完整实现过程拆开讲清楚。适合三类人看手上有树莓派想练手的新手、需要做本地化环境监测的开发者、以及被DHT11时序问题折磨过的同行。我会从硬件接线讲到后端接口设计再到前端刷新策略和长期运行的稳定性处理每一步都说明为什么这么做而不是只丢一段代码让你抄。读完你应该能独立复现一套能连续跑几周不崩的温湿度监控页面。2. 硬件层DHT11到底难在哪接线和供电的细节2.1 DHT11不是插上就能读的传感器很多人对DHT11的印象是便宜、简单、单总线但真正用过就知道它的单总线协议对时序极其敏感。DHT11每次通信要经历主机发送起始信号、传感器响应、连续40位数据传输这几个阶段每一位数据靠高电平的持续时间来区分0和1——26到28微秒是高电平表示070微秒左右表示1。这个时间窗口非常窄如果树莓派的Linux系统在读取过程中被其他进程抢占CPU时序就会错乱读出来的就是一堆乱码或者校验失败。这也是为什么早期很多教程用Adafruit_DHT库跑着跑着就报Checksum error。根本原因不是传感器坏了而是Linux不是实时操作系统Python层面的时序控制精度不够。我实测下来纯Python轮询的成功率大概在70%到85%之间波动系统负载一高就掉得更厉害。解决思路有两条一是换用带独立MCU的传感器模块比如DHT22配合专用驱动芯片二是接受DHT11的不稳定性在软件层做重试和容错。我选的是后者因为成本摆在那里而且温湿度这种慢变量偶尔丢一两个点完全不影响趋势判断。2.2 接线上拉电阻不是可选项DHT11模块通常有三根线VCC、DATA、GND。市面上卖的模块大多已经板载了一个4.7kΩ到10kΩ的上拉电阻如果你买的是裸传感器四脚的那种必须自己在DATA和VCC之间接一个4.7kΩ到10kΩ的电阻。这个电阻的作用是把数据线在空闲时拉到高电平因为DHT11的数据线是开漏输出没有上拉的话信号会浮空读出来全是噪声。我一开始图省事用了裸传感器没加上拉结果读数时好时坏换了三个传感器都以为是坏的最后加上电阻立刻稳定。这个坑很典型值得单独拎出来说。供电方面DHT11的工作电压是3.3V到5.5V树莓派的3.3V引脚可以直接带。但要注意如果你同时挂了多个传感器或者有其他外设3.3V引脚的电流能力有限树莓派4B的3.3V引脚大概能提供几百毫安最好单独供电或者用5V引脚配合电平转换。我实测单个DHT11用3.3V完全没问题数据线直接接GPIO不需要电平转换。接线对应关系我整理成表格方便对照DHT11引脚树莓派物理引脚BCM编号说明VCC1号3.3V-供电DATA7号GPIO4数据线需上拉GND6号-接地选GPIO4是因为它默认没有被其他功能占用而且位置好接。你也可以换成其他GPIO代码里改一下引脚号就行。2.3 为什么不用现成的Adafruit库前面提到Adafruit_DHT库时序不稳其实还有一个原因这个库底层是用C写的通过/dev/gpiomem直接操作寄存器理论上比纯Python快。但它在高版本树莓派系统上经常编译失败而且已经很久没维护了。我试过在较新的系统上装要么报错要么跑起来还是丢数据。后来我换成了pigpio库。pigpio的核心优势是它有一个常驻的守护进程pigpiodGPIO的时序操作在守护进程里完成精度能达到微秒级不受Python主进程调度的影响。用pigpio读DHT11成功率能稳定在95%以上。代价是需要额外启动一个服务多了一点部署步骤但为了稳定性完全值得。安装pigpio很简单sudo apt-get update sudo apt-get install pigpio python3-pigpio sudo systemctl enable pigpiod sudo systemctl start pigpiod启动后pigpiod会监听本地端口Python通过socket和它通信。这里有个细节pigpiod默认只允许本地连接如果你后续想把读取逻辑放到别的机器上需要改配置但本地用完全不用管。3. 后端Flask接口设计里的并发陷阱3.1 为什么读取逻辑不能放在请求处理函数里最直觉的写法是在Flask的路由函数里直接调DHT11读取前端一请求就读一次。这个写法在单人调试时没问题但一旦页面开了自动刷新或者多个人同时打开页面问题就来了。DHT11的采样频率有限制数据手册建议两次读取间隔不少于1秒实际上我实测间隔低于2秒就容易读到上一次的缓存值或者直接失败。如果每个HTTP请求都触发一次读取页面每3秒刷新一次加上多标签页读取频率轻松超过传感器的承受能力结果就是大量校验失败。更严重的是并发问题。Flask默认的开发服务器是单线程的或者多线程但共享状态多个请求同时进来会竞争GPIO资源。pigpio虽然有自己的锁机制但多个线程同时发起读取时序还是会互相干扰。我的解决方案是引入一个后台采集线程独立于Web请求运行。这个线程每隔固定间隔我设的是5秒读一次传感器把结果存到一个全局变量或者简单的内存队列里。Flask的接口只负责返回最近一次的有效读数不直接碰硬件。这样读取频率完全可控Web层的并发压力也和硬件解耦了。3.2 采集线程的实现细节采集线程用Python的threading模块就行核心逻辑是一个while True循环每次读取成功后把数据和时间戳存起来然后sleep一段时间。这里有几个细节要注意。第一读取失败的处理。DHT11偶尔失败是正常的不能因为一次失败就退出线程。我的做法是连续失败超过5次才记录警告单次失败直接跳过保留上一次的有效数据。这样前端看到的数据不会因为偶发失败而断档。第二数据存储用collections.deque设置一个最大长度比如720个点按5秒间隔算正好是一小时的数据。deque是线程安全的追加和读取不需要额外加锁而且自动淘汰旧数据不会无限增长占内存。第三时间戳一定要带。前端画图表需要横轴而且判断数据是否新鲜也要靠时间戳。我用的是time.time()返回的Unix时间戳前端再转成本地时间显示。采集线程的骨架大概是这样import threading import time from collections import deque import pigpio SENSOR_PIN 4 READ_INTERVAL 5 MAX_POINTS 720 data_buffer deque(maxlenMAX_POINTS) pi pigpio.pi() def read_dht11(): # 具体的读取逻辑返回(temperature, humidity)或None ... def collector(): fail_count 0 while True: result read_dht11() if result: temp, humi result data_buffer.append({ timestamp: time.time(), temperature: temp, humidity: humi }) fail_count 0 else: fail_count 1 if fail_count 5: print(连续读取失败检查传感器连接) time.sleep(READ_INTERVAL) threading.Thread(targetcollector, daemonTrue).start()daemonTrue保证主进程退出时采集线程也跟着结束不会卡住。3.3 Flask接口的两种设计全量返回还是增量拉取接口设计上我纠结过一阵。最简单的方案是提供一个/api/data接口每次返回缓冲区里的全部数据。前端拿到后整体重绘图表。这个方案实现简单但有个问题随着缓冲区增长每次返回的数据量越来越大虽然720个点也就几十KB但每次刷新都传全量有点浪费。另一种方案是增量拉取前端记住上次拿到的最新时间戳请求时带上这个时间戳后端只返回比它新的数据。这个方案传输量小但前端要维护数据拼接逻辑复杂度上去了。我最后选了折中方案提供一个/api/latest接口只返回最新一条数据用于页面上的大数字显示再提供一个/api/history接口返回全量数据只在页面初次加载时调用一次。后续的图表更新靠前端把/api/latest的新点追加到已有数据里。这样既避免了频繁传全量又不用实现复杂的增量协议。接口返回格式统一用JSONfrom flask import Flask, jsonify app Flask(__name__) app.route(/api/latest) def latest(): if data_buffer: return jsonify(data_buffer[-1]) return jsonify({error: no data}), 503 app.route(/api/history) def history(): return jsonify(list(data_buffer))注意/api/latest在缓冲区为空时返回503前端要处理这个情况显示等待数据而不是报错。3.4 跨域和静态文件服务的处理开发阶段前端页面可能单独跑在一个端口上会有跨域问题。生产环境其实不需要因为Flask可以直接把前端页面作为静态文件服务出去同源就没有跨域。我的做法是把HTML、CSS、JS都放在Flask的static和templates目录下用render_template返回主页面接口和页面同源省去跨域配置。如果你确实需要跨域比如前端部署在别的地方装个flask-cors扩展一行代码搞定from flask_cors import CORS CORS(app)但本地监控这种场景同源部署是最省心的不建议给自己找麻烦。4. 前端让数据活起来但不闪屏4.1 图表库的选择为什么我放弃了Chart.js前端画实时曲线最常见的方案是Chart.js或者ECharts。我一开始用Chart.js功能没问题但发现一个体验问题每次追加新数据点后调用update()整个图表会有一个轻微的重绘闪烁数据点多了之后尤其明显。而且Chart.js默认的动画效果在实时场景下反而成了负担每次更新都要跑一遍动画。后来我换成了uPlot。这个库非常轻量压缩后几十KB专为实时数据设计更新数据时只重绘变化的部分没有闪烁。API也比Chart.js简洁适合这种一条温度线一条湿度线的简单场景。如果你不想引入任何图表库其实用原生Canvas画两条折线也不难但坐标轴、网格、tooltip这些都要自己写工作量不小。uPlot在轻量和功能之间平衡得比较好我推荐它。4.2 轮询间隔和数据新鲜度的视觉反馈前端刷新用setInterval定时请求/api/latest就行间隔设成和采集间隔一致5秒比较合理。设太短没意义因为后端数据本来就没更新设太长又显得迟钝。这里有个体验细节值得做显示数据新鲜度。如果最新数据的时间戳距离当前时间超过15秒也就是三个采集周期说明采集线程可能出问题了页面上应该给个视觉提示比如数字变灰或者加个数据延迟的标签。这个功能在调试阶段特别有用能一眼看出是传感器挂了还是网络断了。实现上就是在每次更新数据后检查时间差const age Date.now() / 1000 - data.timestamp; if (age 15) { valueElement.classList.add(stale); } else { valueElement.classList.remove(stale); }配合CSS把.stale类的文字颜色调淡用户一眼就能感知。4.3 页面布局大数字 趋势图 极值统计页面布局我改了好几版最后定下来的是三段式顶部两个大数字显示当前温度和湿度中间是趋势曲线图底部是最近一小时的最高最低值统计。大数字用等宽字体字号要大方便远处扫一眼就能看清。温度和湿度用不同颜色区分温度偏暖色湿度偏冷色符合直觉。趋势图横轴是时间纵轴是数值。温度和湿度量纲不同摄氏度 vs 百分比如果画在同一个坐标系里湿度会压扁温度曲线。我的做法是画两个独立的图上下排列共享同一个时间轴。uPlot支持这种多图联动配置稍微麻烦一点但效果很好。极值统计直接从缓冲区数据里算每次更新时重新计算一遍。720个点的遍历开销可以忽略不计。4.4 移动端适配的几个小调整朋友主要用手机看所以移动端适配是刚需。几个关键点viewport的meta标签必须加否则手机上字小得看不清大数字的字号用vw单位而不是px这样能随屏幕宽度缩放图表容器的高度用vh控制保证在竖屏手机上也能完整显示。还有一个容易忽略的点手机浏览器在后台时会暂停setInterval切回前台时可能积压了一堆待执行的定时器。我的处理是在visibilitychange事件里重新校准定时器页面重新可见时先立即请求一次数据再重启定时器。document.addEventListener(visibilitychange, () { if (!document.hidden) { fetchLatest(); restartPolling(); } });这个细节不做的话切回页面会看到数据卡在旧值上好几秒。5. 稳定性让这套东西连续跑几周不崩5.1 传感器读数卡死的现象和根因这套系统跑起来之后我遇到的最诡异的问题是连续运行两三天后温湿度读数会卡在某个值不动但接口还在正常返回前端也没报错。重启采集线程就恢复但过几天又卡。排查了很久才定位到原因pigpio的pi.read()在某些异常情况下会阻塞导致采集线程卡在读取调用上后面的sleep和循环都执行不到。这种情况通常发生在传感器供电不稳或者数据线接触不良的时候DHT11没有正常响应pigpio等待超时的时间比预期长。解决方案是给读取操作加超时保护。pigpio本身没有提供读取超时参数我的做法是在采集线程里记录每次循环的开始时间如果发现距离上次成功读取超过一定阈值比如30秒就主动重启pigpio连接last_success time.time() def collector(): global last_success while True: if time.time() - last_success 30: pi.stop() time.sleep(1) pi pigpio.pi() result read_dht11() ...这个看门狗机制虽然粗暴但实测有效跑了两周没再出现卡死。5.2 用systemd把服务管起来开发阶段用python app.py直接跑没问题但要长期运行必须用systemd托管。好处是开机自启、崩溃自动重启、日志统一管理。写一个service文件放到/etc/systemd/system/下[Unit] DescriptionTemperature Monitor Afternetwork.target pigpiod.service [Service] Typesimple Userpi WorkingDirectory/home/pi/temp-monitor ExecStart/usr/bin/python3 app.py Restartalways RestartSec5 [Install] WantedBymulti-user.target注意After里要加上pigpiod.service保证GPIO守护进程先启动。Restartalways让服务崩溃后自动拉起RestartSec5避免频繁重启。启用服务sudo systemctl daemon-reload sudo systemctl enable temp-monitor sudo systemctl start temp-monitor之后用journalctl -u temp-monitor -f就能实时看日志比在终端里盯着方便多了。5.3 数据持久化内存缓冲之外的备份内存缓冲有个天然缺陷服务重启数据就没了。对于趋势展示来说重启后图表从空白开始体验不好。我的做法是加一个轻量的持久化层每隔一段时间把缓冲区数据写到一个SQLite文件里服务启动时先加载历史数据。SQLite的好处是零配置、单文件、Python标准库直接支持。建一张表存时间戳、温度、湿度采集线程每次成功读取后插入一条同时定期清理超过一定时间的老数据比如保留最近7天。import sqlite3 conn sqlite3.connect(sensor.db, check_same_threadFalse) conn.execute(CREATE TABLE IF NOT EXISTS readings (ts REAL, temp REAL, humi REAL)) def save_reading(ts, temp, humi): conn.execute(INSERT INTO readings VALUES (?, ?, ?), (ts, temp, humi)) conn.commit()check_same_threadFalse是因为采集线程和Flask请求线程可能同时访问数据库。SQLite的写入是串行的并发量不大的话没问题。如果担心锁竞争可以再加一个写队列但对我们这个场景有点过度设计。5.4 日志和告警出问题时怎么快速定位长期运行的系统日志是排查问题的命脉。我用Python标准库的logging模块把采集线程的每次失败、看门狗触发、数据库写入异常都记下来输出到文件并按天切割。import logging from logging.handlers import TimedRotatingFileHandler handler TimedRotatingFileHandler(monitor.log, whenmidnight, backupCount7) handler.setFormatter(logging.Formatter(%(asctime)s %(levelname)s %(message)s)) logger logging.getLogger(monitor) logger.addHandler(handler) logger.setLevel(logging.INFO)backupCount7保留最近7天的日志不会把SD卡写满。树莓派的SD卡寿命有限日志量要控制别把调试级别的日志一直开着。如果想让告警更主动一点可以在连续失败超过阈值时发一封邮件或者写一条到某个消息队列。但考虑到这套系统是本地化的我倾向于保持简单页面上有数据延迟提示就够了没必要引入外部依赖。6. 几个我踩过的坑和对应的解法6.1 树莓派系统升级后pigpio失效有一次手贱跑了apt upgrade升级完pigpiod起不来了报错说GPIO权限问题。查了半天发现是新版系统对/dev/gpiomem的权限做了调整pigpiod需要以root运行或者把用户加到gpio组。解法很简单sudo usermod -a -G gpio pi然后重新登录生效。这个坑提醒我生产环境不要随便升级系统要升级先在另一张卡上测。6.2 前端时间显示时区错乱前端拿到Unix时间戳后用new Date(timestamp * 1000)转换显示的是浏览器所在时区的时间。这本来没问题但我在树莓派上跑的时候系统时区设的是UTC导致日志里的时间和页面显示的时间对不上排查问题时很迷惑。统一方案树莓派系统时区设成本地时区前端也用本地时区显示两边一致。改时区用sudo raspi-config里的Localisation选项或者直接sudo timedatectl set-timezone Asia/Shanghai。6.3 多个标签页同时打开导致请求翻倍调试的时候我习惯开好几个标签页对比结果发现采集线程的日志里读取频率没变但Flask的访问日志里请求量翻了好几倍。虽然接口只是读内存压力不大但这是个隐患。如果在意这个可以在前端加个简单的去重用localStorage记录当前是否有活跃页面新页面打开时检测到已有活跃页面就降低自己的刷新频率。不过说实话本地监控场景下这个优化优先级很低知道有这回事就行。6.4 DHT11的精度问题要有心理预期DHT11的标称精度是温度±2°C、湿度±5%RH实际用下来偏差可能更大。如果你需要更准的数据直接上DHT22或者SHT30别在DHT11上纠结。我做这个项目的目的就是看趋势比如温室里温度是不是在升高、湿度是不是在下降绝对值的几度偏差不影响判断。想清楚用途就不会对精度有不切实际的期待。7. 后续可以怎么扩展这套东西跑稳之后能扩展的方向不少。比如加一个简单的阈值告警温度超过某个值就在页面上闪红或者把数据推到一个本地的时间序列数据库做更长时间的历史分析再或者加个继电器控制温度高了自动开风扇。这些都是在现有架构上叠加采集层和Web层不用动。我个人觉得最有价值的扩展是加一个数据导出按钮把SQLite里的历史数据导成CSV方便用表格软件做进一步分析。实现起来就是一个Flask路由查询数据库然后返回文件流十几行代码的事。另外如果你手上有多个树莓派分布在不同的位置可以把采集层和展示层拆开每个树莓派只负责采集和上报中心节点统一展示。这时候接口设计就要考虑认证和跨机器通信了复杂度会上一个台阶但架构思路是一样的。最后分享一个我在实际部署中的小习惯每次改完代码先在本机用python app.py跑一遍确认接口正常再systemctl restart部署到服务上。直接改服务文件然后重启出错了看日志来回折腾效率反而低。这个习惯帮我省了不少时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑