基于树莓派的室内空气质量监测系统:从硬件选型到Web可视化全流程实践
1. 项目概述:从想法到可落地的空气质量监测方案
去年,我接手了一个挺有意思的私人项目:为一个小型联合办公空间搭建一套室内空气质量监测系统。起因很简单,空间管理者发现午后常有成员抱怨犯困、注意力不集中,他们怀疑是通风不足导致二氧化碳浓度升高。市面上成熟的商用监测设备要么功能单一,要么价格昂贵且数据封闭。于是,我决定基于手头闲置的树莓派,搭配几个常见的传感器,自己动手搭建一套开源、可定制且能实时查看数据的方案。
这个项目的核心目标很明确:低成本、可扩展、数据可视化。我们需要实时监测温度、湿度和二氧化碳(CO2)浓度这三个对室内环境舒适度和健康影响最直接的指标,并将数据通过一个清晰的网页界面展示出来,最好还能查看历史趋势。最终,我选用Raspberry Pi 3B+作为主控,连接DS18B20、DHT22和MH-Z19B传感器,后端用Python的Flask框架处理数据和逻辑,前端用HTML/CSS/JavaScript配合Chart.js库绘制图表,数据则存入MySQL数据库。整个系统搭建下来,硬件成本完全可以控制在500元人民币以内,软件部分则完全开源。
无论你是物联网爱好者、嵌入式开发初学者,还是仅仅想为自己家或工作室改善空气环境的技术控,这个项目都提供了一个从硬件连接到软件部署的完整路径。它不仅是一套可用的监测工具,更是一个理解传感器数据采集、嵌入式系统编程、Web前后端交互以及数据可视化的绝佳实践案例。
2. 核心硬件选型与电路设计解析
硬件是整个系统的基石,选型直接决定了数据的准确性、系统的稳定性以及最终成本。我的选型思路主要围绕“够用、稳定、易得”三个原则展开。
2.1 主控与传感器选型考量
主控平台:Raspberry Pi 3B+选择树莓派3B+而非更新型号(如4B)或更低型号(如Zero W),是基于一个平衡点考虑。3B+拥有完整的40针GPIO接口、有线以太网口、Wi-Fi和蓝牙,性能足够流畅运行一个轻量级的Linux系统、Python程序以及MySQL数据库。Zero W虽然更便宜小巧,但其处理器和内存(512MB)在同时运行Web服务器和数据处理时可能会比较吃力,尤其在需要绘制动态图表时。而4B性能固然更强,但功耗和发热也更大,对于需要7x24小时持续运行的环境监测设备来说,3B+的性价比和稳定性更合适。
传感器三剑客:温度、湿度、CO2
温度传感器:DS18B20我选择了DS18B20这款经典的“单总线”数字温度传感器。它最大的优点是采用独特的单总线协议,只需要一根数据线(加上电源和地线共三线)即可与树莓派通信,极大地简化了布线。其测量范围-55°C到+125°C,精度±0.5°C,完全满足室内监测需求。更重要的是,每个DS18B20都有全球唯一的64位ID,这意味着你可以在同一条总线上挂载多个传感器,方便未来扩展监测多个点位。
温湿度一体传感器:DHT22虽然DS18B20能测温度,但为了同时获取湿度数据并作为一个数据交叉验证的参考,我增加了DHT22。DHT22也是数字传感器,输出校准过的数字信号,湿度测量范围0-100%RH,精度±2%RH;温度测量范围-40~80°C,精度±0.5°C。它使用单线双向串行接口,接线同样简单。这里有一个注意事项:DHT22的响应速度较慢,两次测量之间需要至少2秒的间隔,在编程时需要设置合理的读取频率,避免频繁读取导致错误或程序阻塞。
二氧化碳传感器:MH-Z19B这是项目的核心传感器,用于测量空气中的CO2浓度(单位:ppm)。我选择MH-Z19B型号是因为它采用非色散红外(NDIR)原理,这是测量CO2的主流且相对准确的方法,比某些廉价的半导体式传感器稳定、抗干扰性强得多。其测量范围是0-5000ppm,自带温度补偿,并通过UART串口与树莓派通信,使用简单。关键点:MH-Z19B需要一段预热时间(通常约3分钟)才能输出稳定数据,且建议每24小时进行一次自动校准(ABC功能),或将其置于400ppm的新鲜空气中进行手动校准,以确保长期准确性。
2.2 电路连接与供电方案
所有传感器与树莓派的连接都需要通过面包板进行过渡和测试。树莓派的GPIO引脚工作电压是3.3V,务必确认所有传感器兼容此电压,否则需要电平转换模块。
接线明细与原理:
- DS18B20:VCC(红线)接3.3V, GND(黑线)接地, DATA(黄线)接GPIO4(物理引脚7)。必须在DATA线和3.3V之间连接一个4.7kΩ的上拉电阻,这是单总线协议稳定通信所必需的。
- DHT22:VCC接3.3V, GND接地, DATA接GPIO17(物理引脚11)。同样,建议在DATA线和3.3V之间连接一个5-10kΩ的上拉电阻,尤其在导线较长时能增强信号稳定性。
- MH-Z19B:VIN接树莓派的5V引脚(物理引脚2或4), GND接地。其TX引脚(发送端)接树莓派的RX(GPIO15,物理引脚10), RX引脚(接收端)接树莓派的TX(GPIO14,物理引脚8)。注意:这里是交叉连接,传感器的TX接树莓派的RX,传感器的RX接树莓派的TX。
供电设计:树莓派3B+满载时电流需求可达2.5A。我使用了一个输出为5V/3A的优质USB电源适配器为其供电。重要经验:务必避免使用劣质或功率不足的电源,这会导致树莓派运行不稳定、频繁重启,甚至损坏SD卡。对于需要长期运行的设备,供电的稳定性是第一要务。
LCD显示屏(可选):我额外连接了一块16x2的I2C接口LCD屏,用于本地实时显示数据。I2C接口仅需四根线(VCC, GND, SDA, SCL),节省GPIO资源。SDA接GPIO2(物理引脚3),SCL接GPIO3(物理引脚5)。这是一个很好的调试和本地查看工具,但在以Web访问为主的场景下非必需。
提示:在连接任何线缆前,请确保树莓派已断电。连接完成后,最好用万用表通断档检查一下关键连接,避免虚接或短路,这是保护硬件的第一步。
3. 软件环境搭建与核心代码实现
硬件连接妥当后,下一步就是让树莓派“活”起来,赋予它数据采集、处理和展示的能力。这个过程主要分为系统与驱动配置、后端服务编写、前端界面开发三大部分。
3.1 系统配置与传感器驱动启用
首先,为树莓派安装Raspberry Pi OS Lite(无桌面版)以节省资源。通过SSH远程登录进行配置是最高效的方式。
1. 系统更新与必要工具安装:
sudo apt update sudo apt upgrade -y sudo apt install python3-pip python3-venv git -y2. 启用接口与驱动:
- I2C(用于LCD屏):运行
sudo raspi-config,进入Interface Options->I2C,选择启用。 - 1-Wire(用于DS18B20):同样在
raspi-config中,启用1-Wire接口。 - 串口(用于MH-Z19B):树莓派的硬件串口默认用于蓝牙。我们需要将其释放给GPIO使用。编辑
/boot/config.txt文件,在末尾添加:
这行命令会禁用蓝牙,并将硬件串口(/dev/ttyAMA0)分配给GPIO14/15。重启后,MH-Z19B就可以连接到enable_uart=1 dtoverlay=disable-bt/dev/ttyAMA0了。
3. 验证传感器连接:
- DS18B20:重启后,输入
ls /sys/bus/w1/devices/,你应该能看到一个以“28-”开头的文件夹,里面w1_slave文件的内容就包含了温度数据。 - DHT22:需要通过Python库读取,后续测试。
- MH-Z19B:安装测试工具
pip3 install mh-z19,然后运行sudo python3 -m mh_z19,如果看到返回的CO2浓度值,说明串口通信正常。
3.2 后端服务:数据采集、存储与API提供
后端是系统的大脑,负责轮询传感器、处理数据、存入数据库并通过WebSocket或API提供给前端。我使用Python的Flask微框架,因为它轻量、灵活且易于上手。
1. 项目结构与虚拟环境:在树莓派上创建一个项目目录,并建立Python虚拟环境以隔离依赖。
mkdir air_quality_monitor && cd air_quality_monitor python3 -m venv venv source venv/bin/activate2. 安装Python依赖库:
pip install flask flask-cors flask-socketio mysql-connector-python gevent gevent-websocket pip install adafruit-circuitpython-dht # 用于DHT22 pip install mh-z19 # 用于MH-Z19B # DS18B20使用系统内置的1-wire接口,无需额外安装库3. 数据库设计:使用MariaDB(MySQL的兼容分支)创建数据库。设计遵循简单的规范化原则,创建两张表:
CREATE DATABASE air_quality; USE air_quality; CREATE TABLE sensors ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL, location VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sensor_data ( id INT AUTO_INCREMENT PRIMARY KEY, sensor_id INT, temperature DECIMAL(5,2), humidity DECIMAL(5,2), co2 INT, recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sensor_id) REFERENCES sensors(id) );sensors表记录传感器元信息,sensor_data表存储具体的测量数据。这种设计便于未来增加更多监测点。
4. 核心Python脚本解析:创建一个app.py作为后端主程序。其核心逻辑如下:
传感器读取函数:编写独立的函数来读取每个传感器,并加入异常处理。例如,读取DHT22时,如果失败就重试几次,避免因单次读取失败导致整个循环中断。
import adafruit_dht import board import mh_z19 import time def read_dht22(pin): dhtDevice = adafruit_dht.DHT22(pin, use_pulseio=False) # 在某些系统上需要禁用pulseio for _ in range(3): # 重试3次 try: temperature = dhtDevice.temperature humidity = dhtDevice.humidity if temperature is not None and humidity is not None: return temperature, humidity except RuntimeError as error: time.sleep(2) # DHT22需要间隔 continue return None, None数据采集循环:使用一个后台线程,每10秒读取一次所有传感器数据,并插入数据库。这里有个关键技巧:将传感器读取和数据库写入操作分离。即使数据库暂时不可用,采集的数据也可以先缓存起来,避免数据丢失。
import threading from datetime import datetime def collect_data(): while True: temp_ds18b20 = read_ds18b20() temp_dht22, humidity = read_dht22(board.D17) co2_value = read_mhz19b() # 可以选择使用两个温度传感器的平均值 avg_temp = (temp_ds18b20 + temp_dht22) / 2 if None not in (temp_ds18b20, temp_dht22) else temp_dht22 or temp_ds18b20 # 插入数据库 save_to_db(avg_temp, humidity, co2_value) time.sleep(10)Flask与Socket.IO:Flask处理HTTP请求(如提供历史数据的API),Socket.IO实现实时数据推送。
from flask import Flask, jsonify from flask_socketio import SocketIO, emit import mysql.connector app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*") @app.route('/api/history') def get_history(): # 连接数据库,查询最近N小时的数据,以JSON格式返回 pass # 当有新的传感器数据存入时,通过Socket.IO广播给所有连接的网页客户端 def save_to_db(temp, hum, co2): # ... 数据库插入逻辑 ... socketio.emit('new_data', {'temperature': temp, 'humidity': hum, 'co2': co2})
3.3 前端界面:数据可视化与交互
前端的目标是提供一个清晰、直观且响应式的数据看板。我使用纯HTML/CSS/JS开发,利用Chart.js库来绘制图表。
1. 页面结构:创建index.html,包含:
- 一个标题和简要说明。
- 三个卡片区域,分别用大字体显示温度、湿度、CO2的当前实时数值,并使用颜色提示(例如,CO2超过1000ppm显示为橙色,超过1500ppm显示为红色)。
- 一个标签页区域,包含“实时曲线”和“历史趋势”两个标签页。
- “实时曲线”页内包含三个Canvas元素,分别用于绘制三条随时间变化的折线图。
- “历史趋势”页提供时间范围选择器(如最近1小时、6小时、24小时),点击后从后端API获取数据并渲染图表。
2. 实时数据更新:使用Socket.IO的客户端库,监听后端发出的new_data事件,更新大数字显示,并同时向三个实时图表追加新的数据点。
const socket = io(); socket.on('new_data', function(data) { // 更新温度、湿度、CO2的数值显示 document.getElementById('current-temp').innerText = data.temperature.toFixed(1); // 更新实时图表 realTimeChart.data.labels.push(new Date().toLocaleTimeString()); realTimeChart.data.datasets[0].data.push(data.co2); realTimeChart.update('quiet'); // 静默更新,避免频繁动画消耗性能 });3. 历史数据获取:为“历史趋势”页的按钮绑定点击事件,触发对/api/history的AJAX请求,获取对应时间段的JSON数据,然后重新初始化Chart.js图表。
function loadHistoryData(hours) { fetch(`/api/history?hours=${hours}`) .then(response => response.json()) .then(data => { // 使用获取的数据重新绘制历史图表 renderHistoryChart(data); }); }4. 样式与响应式:使用CSS Flexbox或Grid布局,确保在手机、平板和电脑上都能有良好的显示效果。图表容器设置为width: 100%; height: 300px;。
4. 系统集成、部署与优化
当硬件、后端、前端都分别测试通过后,就需要将它们整合成一个稳定、可长期运行的系统。
4.1 系统服务化与开机自启
我们不能依赖在SSH终端里手动运行python app.py。需要将后端程序设置为系统服务,使其在树莓派启动时自动运行,并在崩溃时尝试重启。
创建系统服务文件:
sudo nano /etc/systemd/system/airquality.service写入以下配置:
[Unit] Description=Air Quality Monitor Flask Service After=network.target mariadb.service [Service] User=pi WorkingDirectory=/home/pi/air_quality_monitor Environment="PATH=/home/pi/air_quality_monitor/venv/bin" ExecStart=/home/pi/air_quality_monitor/venv/bin/python app.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target关键参数解释:
After=network.target mariadb.service:确保服务在网络和数据库就绪后才启动。Environment="PATH=...":指定虚拟环境的路径,确保服务使用我们安装的依赖包。Restart=always:服务意外退出时自动重启。RestartSec=10:重启前等待10秒。
启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable airquality.service sudo systemctl start airquality.service sudo systemctl status airquality.service # 检查运行状态
4.2 Web服务器配置(可选但推荐)
目前,Flask内置的开发服务器运行在http://树莓派IP:5000。开发服务器性能较弱且不安全,不适合生产环境。我们可以使用Nginx作为反向代理,处理静态文件(前端HTML/CSS/JS),并将动态请求转发给后端的Flask应用(通常运行在本地某个端口,如127.0.0.1:8000)。
安装Nginx:
sudo apt install nginx -y配置Nginx站点:编辑Nginx配置文件
/etc/nginx/sites-available/airquality:server { listen 80; server_name your_pi_ip_or_domain; # 填写树莓派的IP地址或域名 location / { # 首先尝试直接提供静态文件(前端页面) root /home/pi/air_quality_monitor/static; try_files $uri $uri/ @flaskapp; } location @flaskapp { proxy_pass http://127.0.0.1:8000; # 转发给Gunicorn proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Socket.IO支持 location /socket.io/ { proxy_pass http://127.0.0.1:8000/socket.io/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } }使用Gunicorn替代Flask开发服务器:在生产环境中,使用Gunicorn这类WSGI服务器来运行Flask应用更稳定。
pip install gunicorn # 在项目目录下,使用Gunicorn启动应用,绑定到本地8000端口 gunicorn --workers 2 --bind 127.0.0.1:8000 app:app同样,可以将Gunicorn也配置为系统服务。
4.3 机箱设计与制作
为了让项目看起来更专业,也为了保护内部电路,一个定制的机箱是必要的。我选择用激光切割亚克力板来制作。
设计:使用Fusion 360或Inkscape等软件,根据树莓派、面包板、传感器和屏幕的尺寸,设计一个六面体盒子的展开图(DXF或SVG格式)。需要预留:
- 传感器开孔:MH-Z19B的进气/出气孔、DHT22和DS18B20的感应窗口。
- 屏幕开孔:与LCD显示区域匹配。
- 线缆孔:电源线和网线(如果使用)的出口。
- 散热孔:在树莓派CPU上方位置开一些小孔。
材料:3mm厚的亚克力板是不错的选择,兼顾强度和美观。也可以使用中密度纤维板(MDF),成本更低但不如亚克力通透。
组装:激光切割完成后,使用亚克力胶水或卡扣结构进行组装。内部可以用尼龙柱或螺丝固定树莓派和面包板。
注意:机箱设计时,务必确保CO2传感器的进气口不被遮挡,且远离其他热源(如树莓派CPU),以免影响测量准确性。温湿度传感器也应避免被阳光直射或紧贴发热元件。
5. 常见问题排查与维护心得
在实际部署和长期运行中,你肯定会遇到各种各样的问题。下面是我踩过坑后总结的一些典型问题及其解决方法。
5.1 硬件与连接问题
问题1:DS18B20读取失败,/sys/bus/w1/devices/目录下没有设备。
- 排查:首先检查
raspi-config中1-Wire接口是否已启用。然后检查物理连接,特别是那个4.7kΩ的上拉电阻是否接在了数据线和3.3V之间,这是最常见的原因。用万用表测量数据线引脚电压,在未通信时应在3.3V左右。 - 解决:确认接线无误后,重启树莓派。如果仍不行,尝试更换一个GPIO口(需同时修改代码和
/boot/config.txt中的dtoverlay=w1-gpio,gpiopin=参数)。
问题2:DHT22经常读取失败或返回None。
- 排查:DHT22对时序要求严格。首先检查接线,同样可以尝试增加一个上拉电阻。其次,确保两次读取间隔大于2秒。检查代码中是否在读取失败后没有足够的延迟就立即重试。
- 解决:在读取函数中加入重试机制和足够的延迟(
time.sleep(2))。确保给DHT22供电的3.3V电源稳定。如果环境干扰大,可以尝试缩短传感器到树莓派的导线长度,或使用屏蔽线。
问题3:MH-Z19B返回的CO2值异常(如始终为0、410或5000)。
- 排查:
- 始终为0或410:可能是串口通信问题。使用
ls -l /dev/ttyAMA0检查设备权限,确保运行程序的用户(如pi)有读写权限(crw-rw----)。可以使用sudo chmod 666 /dev/ttyAMA0临时修改,但最好将用户加入dialout组:sudo usermod -a -G dialout pi。 - 始终为5000:可能是传感器处于预热期(上电后头3分钟),或发生了ABC自动校准干扰。MH-Z19B的ABC(自动基线校准)功能会假设传感器每周至少有一次暴露在400ppm新鲜空气(如室外)的环境中。如果长期在密闭高浓度环境运行,ABC功能会导致读数逐渐偏低。
- 始终为0或410:可能是串口通信问题。使用
- 解决:对于通信问题,检查
/boot/config.txt中串口配置,并确认TX/RX没有接反。对于ABC问题,如果监测环境长期密闭,建议关闭ABC功能。可以通过发送特定的串口指令实现(具体指令需查阅MH-Z19B手册),或者购买MH-Z19C型号,它支持更灵活的校准设置。
5.2 软件与数据问题
问题4:Web页面能打开,但实时数据不更新。
- 排查:打开浏览器的开发者工具(F12),查看“网络”(Network)选项卡中的“WS”(WebSocket)或“获取”(Fetch)请求。确认Socket.IO连接是否建立成功(状态码101)。查看控制台(Console)是否有JavaScript错误。
- 解决:检查后端Socket.IO服务是否正常运行。确认前端连接的Socket.IO服务器地址和端口是否正确。如果使用了Nginx反向代理,务必按照前面所述正确配置
/socket.io/路径的转发。
问题5:历史数据图表不显示或数据不对。
- 排查:在浏览器中直接访问后端历史数据API,例如
http://树莓派IP:5000/api/history?hours=24,查看返回的JSON数据是否正常。检查后端查询数据库的SQL语句和时间过滤逻辑是否正确。 - 解决:确保数据库服务(MariaDB)正在运行。检查数据库连接参数(主机、用户名、密码、数据库名)。在Python代码中增加更详细的错误捕获和日志记录,将异常信息写入文件,便于排查。
问题6:树莓派运行一段时间后卡顿或重启。
- 排查:很可能是电源问题或散热问题。触摸树莓派芯片,如果烫手,说明散热不足。使用
vcgencmd measure_temp命令查看核心温度,超过80°C就需要警惕。 - 解决:为树莓派加装散热片甚至小型风扇。确保使用足额(5V/3A)的优质电源。检查SD卡剩余空间,定期清理日志文件。对于7x24运行,建议使用高质量、高耐用度的工业级SD卡或改用USB SSD启动。
5.3 长期维护与优化建议
- 数据备份:定期备份MySQL数据库。可以使用
mysqldump工具创建备份脚本,并通过cron定时任务将备份文件发送到另一台电脑或云存储。 - 日志记录:为Python程序添加日志功能(使用
logging模块),将错误信息、传感器读取失败记录等写入文件,方便日后排查问题。 - 设置报警阈值:在后端代码中增加逻辑,当CO2浓度连续超过设定阈值(如1000ppm)一定时间后,可以通过发送电子邮件、Telegram消息或触发一个物理蜂鸣器的方式进行报警。
- 功耗考虑:如果希望部署在无固定电源的地方,可以考虑使用树莓派Zero 2 W(功耗更低)搭配移动电源,并优化软件,减少不必要的进程,延长续航。
- 扩展性:当前的数据库和代码结构已经考虑了多传感器扩展。你可以轻松地增加第二个、第三个监测节点(使用额外的树莓派或ESP32等单片机),将数据通过HTTP POST或MQTT协议发送到中心服务器(这台树莓派)进行汇总展示。
这个项目从构思到稳定运行,花费了我大约两周的业余时间。最大的收获不是做出了一个能用的工具,而是在这个过程中,将硬件、嵌入式Linux、网络通信、数据库、Web开发这些分散的知识点串联成了一个完整的解决方案。当你第一次在手机上打开网页,看到自己房间里真实的温湿度和CO2曲线时,那种感觉是无可替代的。它现在已经在那个联合办公空间默默运行了大半年,成为了管理者调整通风策略的重要依据。如果你也正被室内空气问题困扰,或者单纯想找一个有挑战又有成就感的物联网项目来练手,不妨就从这里开始。
