Arduino是一个开放源码的电子原型平台,它可以让你用简单的硬件和软件来创建各种互动的项目。Arduino的核心是一个微控制器板,它可以通过一系列的引脚来连接各种传感器、执行器、显示器等外部设备。Arduino的编程是基于C/C++语言的,你可以使用Arduino IDE(集成开发环境)来编写、编译和上传代码到Arduino板上。Arduino还有一个丰富的库和社区,你可以利用它们来扩展Arduino的功能和学习Arduino的知识。
Arduino的特点是:
1、开放源码:Arduino的硬件和软件都是开放源码的,你可以自由地修改、复制和分享它们。
2、易用:Arduino的硬件和软件都是为初学者和非专业人士设计的,你可以轻松地上手和使用它们。
3、便宜:Arduino的硬件和软件都是非常经济的,你可以用很低的成本来实现你的想法。
4、多样:Arduino有多种型号和版本,你可以根据你的需要和喜好来选择合适的Arduino板。
5、创新:Arduino可以让你用电子的方式来表达你的创意和想象,你可以用Arduino来制作各种有趣和有用的项目,如机器人、智能家居、艺术装置等。


随着建筑材料和保温方法的演变,室内空气质量已成为一个主要问题。总挥发性有机化合物 (TVOC) 是我们家中最常见且具有潜在危险的污染物之一。本文提出了一种基于 ESP32 微控制器的互联环境监测设备,能够实时测量各种空气质量参数,例如 TVOC、二氧化碳当量 (eCO₂)、温度和湿度。

本项目使用的东西
硬件组件
LILYGO T-Display S3 (ESP32-S3 + 1.9“ ST7789)
ENS160+AHT21 二氧化碳 CO2 eCO2 TVOC 空气质量和温度湿度传感器替代 Arduino 的 CCS811
软件应用程序和在线服务
Arduino IDE 2.x
ESP32 板级支持包 (“esp32 by Espressif Systems”)
TFT_eSPI
PNGdec 公司
ScioSense _ENS160
AHTxx
WebServer (ESP32 核心)
WiFi(ESP32 内核)
手动工具和制造机
烙铁(通用)
Dupont Wires
3D 打印机(例如 Creality Ender 3)
Les TVOC
什么是 TVOC?
总挥发性有机化合物 (TVOC) 是指给定环境中存在的一组挥发性有机化合物 (VOC),以一组形式测量,不区分单个化学物质。它们是在室温下容易蒸发的化学物质,因此特别关注室内空气质量。
TVOC 的由来
TVOC 来自广泛的自然和人为来源。它们由建筑和室内设计产品(如油漆、清漆、胶水和树脂)排放。洗涤剂、清洁剂和除臭剂等家用产品也是一个来源。此外,电子设备,尤其是打印机,会在使用中释放溶剂或塑料。人类活动,如烹饪、吸烟或使用蜡烛和家居香水,也有助于它们的存在。
特性和健康影响
TVOC 包括多种分子,例如碳氢化合物(苯、甲苯)、醇类(乙醇)、醛类(甲醛)和酮类(丙酮)。它们的高挥发性使其即使在低温下也存在于气相中,使它们能够在封闭空间内迅速扩散。
长期或急性暴露于 TVOC 会导致眼睛、鼻子和喉咙刺激、头痛、恶心和疲劳。从长远来看,它们会损害呼吸功能,增加过敏或哮喘的风险,有些可能致癌,如苯或甲醛。
TVOC 测量的重要性
在封闭空间中,TVOC 的浓度可比室外空气高十倍。测量 TVOC 是能够识别室内污染源并采取适当解决方案的可能性,例如通风、过滤或消除有问题的材料(因此,我倾向于在室内使用古董家具)。这确保了健康的环境,尤其是在人们花费大量时间的空间,例如办公室、学校和家庭。不幸的是,这些问题有很大的改进空间。
环境 MEMS 传感器可实现对 TVOC 的实时监测。它们提供准确的数据来调整通风水平,优化舒适度和健康状况,并快速识别污染峰值。

现代建筑的问题
现代建筑倾向于增加气密性以提高能源效率。虽然这减少了热量损失,但也限制了空气的自然更新。然后,包括 TVOC 在内的室内污染物会积累到令人担忧的水平。
有人会认为一个简单的解决方案是只使用不释放 TVOC 的材料。然而,在实践中,这很困难。即使是已知是惰性的材料也可以发射低但不可忽略的水平的 TVOC。此外,TVOC 的来源不仅限于建筑材料:家具和家居用品也做出了重大贡献。因此,不幸的是,在现代家庭环境中,消除所有可能的 TVOC 来源几乎是不可能的。
为了克服这个问题,现代建筑中使用了受控机械通风 (CMV) 系统。它们确保室内空气的不断更新,同时从排出的空气中回收热量以预热进入的空气(用于双流)。然而,这些系统通常以恒定的流速运行,而不考虑实时空气质量变化。
这就是 CMV 的伺服控制根据 TVOC 水平发挥作用的地方。配备传感器的 CMV 系统可以根据伺服控制检测到的污染物浓度来调节气流。
这样的系统已经存在,并且越来越多地被整合到现代建筑和翻新中。此外,TVOC 传感器越来越多地用于这些应用,这有助于降低其成本:这解释了为什么现在它们可以在 AliExpress 等平台上以几欧元的价格集成到分线板中。

低成本环境传感器:AHT20 和 ENS160
我想要一台袖珍型显示器,它可以立即显示这些峰值,而无需将数据传输到任何云。结果是一个 25 欧元的 ESP32-S3 节点,带有板载彩色屏幕和自己的 Wi-Fi 热点以及自动刷新的 HTTP 页面——您需要实时“看到”周围的空气。
我在设备中使用了一个分线板,上面有两个环境传感器可用:AHT20 和 ENS160。正如我上面所说,它们的成本很低,主要是因为它们越来越多地用于通风系统和其他空气质量监测应用,从而降低了它们的单价。
AHT20 是一款高精度温度和湿度传感器。它具有出色的稳定性和低功耗。另一方面,ENS160 基于 MOS(金属氧化物半导体)技术。它配备了四个加热板,每个加热板都涂有不同的敏感材料。这些热板允许修改热条件,以便能够检测各种挥发性有机化合物。当这些化合物与板接触时,化学反应会改变材料的电导率,然后通过集成到传感器中的电子元件将其转换为数字数据(这就是为什么传感器需要少量加热时间才能使用测量的原因)。该原理通过检测广泛的 TVOC 来实时监测室内空气质量。

为什么这个小盒子值得信赖
核心传感器是 ScioSense 的 ENS160,这是一种四元件 MOX 芯片,可输出 TVOC、等效 CO₂ 和符合德国 UBA 量表的五级 AQI。 ScioSense :它的典型响应 (t₉₀ ) 在预热后不到一分钟,数据表建议偶尔进行软件重置以限制基线漂移——我的固件每五分钟执行一次。 ScioSense 温度和湿度来自 AHT20,精确到 ±0.3 °C 和 ±2 % RH;这些值直接反馈给 ENS160 进行片上补偿。 adafruit.com LILYGO T-Display S3 在一块 PCB 上提供 ESP32-S3、8 MB PSRAM 和 1.9 英寸 ST7789 TFT,保持真正紧凑的构建。

固件的工作原理
启动时,微控制器解码闪存中的 PNG 启动画面,并使用 Larry Bank 的 PNGdec 库逐行流式传输,无需 SD 卡。然后,它启动一个名为 ESP8266_Mesure 的软 AP,修复 192.168.4.22 的 IP,并启动 Arduino 样式的 .循环每秒读取 AHT20,将该数据推送到 ENS160 中,并刷新 TFT 仪表板和浏览器页面。渐变和文本是使用 TFT_eSPI 绘制的, 是一个针对 ESP32 硬件进行深度优化的图形库。 WebServer
实际测试显示的内容
在例行炒菜中,监测器在不到 30 秒的时间内从平静的 30 ppb 跃升至超过 400 ppb 的 TVOC,将 AQI 条从绿色变为纯橙色。打开一扇窗户后,指数在大约 12 分钟内回到 100 以下,与同行评审通风研究中报告的厨房 VOC 衰减曲线一致。ScienceDirectScienceDirect 构建视频中出现了一个更戏剧性的演示:我拿着一杯用纸板密封的丙酮,距离传感器几厘米。揭开盖子的那一刻,ENS160 立即标记出 AQI 5——“严重污染”。当您注意到丙酮的蒸气压在 25 °C 时约为 30 kPa 时,这并不奇怪,这意味着它充满房间的速度比您后退的速度要快。TFT 上的条形会猛烈地变为红色,而任何连接到热点的手机都会看到相同的峰值。
DIY ESP32 空气质量监测器:使用 ENS160 传感器进行实时 AQI 检测
夜间读数讲述了另一个故事:在门关闭且没有活动的情况下,该装置的 eCO₂ 稳定在 450 ppm 左右,这个值通常被认为是占用空间基线通风充足的标志。
外壳
为了确保空气质量监测仪的耐用性和便携性,设计了一个定制的外壳。该设计可容纳 ESP32-S3 开发板、ENS160 气体传感器和 AHT20 温湿度传感器,为每个组件提供精确的切口。外壳是使用 PLA 材料 3D 打印的。这种保护外壳不仅可以保护内部电子设备,还可以增强设备的美感,使其适用于桌面和便携式应用。

下一步
由于所有内容都已经位于 ESP32-S3 上,因此可以直接添加 MQTT 并馈送 Home-Assistant,或者在两次扫描之间将开发板置于调制解调器睡眠状态以延长电池寿命——乐鑫的电源管理文档显示,当 Wi-Fi 在两次突发之间打盹时,电流下降了一个数量级。锂离子电池和 TP4056 充电器将使节点真正实现无线连接,而 ENS160 的原始电阻模式为设备上的机器学习打开了大门,该机器学习可以仅从气体特征中识别“烹饪”、“清洁”或“喷雾罐”。
了解您呼吸的空气不需要实验室工作台或云订阅。凭借少量全球速卖通零件、20 欧元和一晚上的焊接,这个小热点可以让您看到室内污染绽放——更重要的是,在您打开窗户的那一刻就消失了。

设备和演示概述
开发的设备集成了用于数据处理和 Wi-Fi 连接的 ESP32、用于环境测量的 AHT20 和 ENS160 传感器、用于实时显示的 SPI ST7789 显示器,以及托管在 ESP32 上的 Wi-Fi 服务器,用于通过 Web 浏览器访问数据。
整个设计非常紧凑且节能。
#include <AHTxx.h>
#include <Arduino.h>
// #include <BluetoothSerial.h> // Inclure la bibliothque BluetoothSerial
#include <PNGdec.h>
#include <TFT_eSPI.h> // Bibliothque graphique
#include <WebServer.h>
#include <WiFi.h>
#include <Wire.h>
#include <image.h>
#include "SPI.h"
#include "ScioSense_ENS160.h" // ENS160 library
const char *ssid = "ESP8266_Mesure";
const char *password = "12345678";
// Configuration de l'IP fixe
IPAddress local_IP(192, 168, 4, 22); // Adresse IP fixe
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
WebServer server(80);
// Dfinition des constantes
#define MAX_IMAGE_WIDTH 240 // Ajustez pour vos images
// Dfinition des variables
int16_t xpos = 0;
int16_t ypos = 0;
float temperature, humidity, eCO2, TVOC;
int AQI = 0;
int AQI_precedent = 0;
uint16_t compteur = 0;
// Dfinition des variables pour les bibliothques
TFT_eSPI tft = TFT_eSPI(); // Dclaration de l'instance de l'cran
ScioSense_ENS160 ens160(ENS160_I2CADDR_1);
AHTxx aht20(AHTXX_ADDRESS_X38, AHT2x_SENSOR); // Adresse du capteur, type du capteur
PNG png; // Instance du dcodeur PNG
// BluetoothSerial SerialBT; // Instance de la bibliothque BluetoothSerial
/// FONCTIONS
// Page principale
void handleRoot() {
server.sendHeader("Cache-Control", "no-store");
String html =
"<!DOCTYPE html>\
<html>\
<head>\
<meta charset='UTF-8'>\
<title>ESP32 Mesures</title>\
<style>\
body { font-family: Arial, sans-serif; text-align: center; background-color: #f4f4f9; color: #333; margin: 0; padding: 0; }\
h1 { font-size: 28px; margin-top: 20px; }\
ul { list-style: none; padding: 0; font-size: 20px; }\
li { margin: 15px 0; }\
.aqi-bar { width: 80%; height: 25px; margin: 20px auto; background: linear-gradient(to right, green, yellow, orange, red); position: relative; border-radius: 5px; overflow: hidden; }\
.aqi-fill { height: 100%; background: white; width: " +
String((1 - (AQI / 5.0)) * 100) +
"%; position: absolute; right: 0; top: 0; }\
footer { margin-top: 20px; font-size: 14px; color: #666; }\
</style>\
<script>\
setTimeout(function() { location.reload(true); }, 1000); // Force le rechargement toutes les secondes\
</script>\
</head>\
<body>\
<h1>ESP32 - Mesures de Qualit de l'Air</h1>\
<ul>\
<li><strong>Temprature :</strong> " +
String(temperature, 2) +
" C</li>\
<li><strong>Humidit :</strong> " +
String(humidity, 2) +
" %</li>\
<li><strong>eCO2 :</strong> " +
String(eCO2, 0) +
" ppm</li>\
<li><strong>TVOC :</strong> " +
String(TVOC, 0) +
" ppb</li>\
<li><strong>AQI :</strong> " +
String(AQI) +
"</li>\
</ul>\
<div class='aqi-bar'>\
<div class='aqi-fill'></div>\
</div>\
<footer>Page mise jour automatiquement toutes les secondes</footer>\
</body>\
</html>";
server.send(200, "text/html", html);
}
// Fonction de rappel pour dessiner des pixels sur l'cran
void pngDraw(PNGDRAW *pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer);
}
void fond_ecran() {
tft.fillScreen(TFT_BLACK); // Efface l'cran
tft.setCursor(20, 10);
tft.setTextColor(TFT_GREEN);
tft.printf("Temp: ", temperature);
tft.setCursor(20, 30);
tft.printf("Hum.: ", humidity);
tft.setCursor(20, 55);
tft.printf("eCO2: ", eCO2);
tft.setCursor(20, 75);
tft.printf("TVOC: ", TVOC);
}
void miseajour_ecran() {
tft.fillRect(105, 0, 120, 94, TFT_BLACK);
tft.setTextSize(2);
tft.setTextColor(TFT_WHITE);
tft.setCursor(105, 10);
tft.printf("%.2f C\n", temperature);
tft.setCursor(105, 30);
tft.printf("%.2f %%\n", humidity);
tft.setCursor(105, 55);
tft.printf("%.0f ppm \n", eCO2);
tft.setCursor(105, 75);
tft.printf("%.0f ppb \n", TVOC);
if (AQI_precedent != AQI) {
tft.fillRect(17, 100, 3, 25, TFT_BLACK);
tft.fillRect(220, 100, 3, 25, TFT_BLACK);
tft.fillRectHGradient(20, 100, 200, 25, TFT_GREEN, TFT_RED);
tft.setTextSize(3);
tft.setTextColor(TFT_BLACK);
tft.setCursor(95, 101);
tft.printf("AQI");
float position = 18 + 200 * (25 * (AQI - 1) / 100.00);
tft.fillRect((int)position, 100, 4, 25, TFT_YELLOW);
}
}
/*--------------------------------------------------------------------------
SETUP function
initiate sensor
--------------------------------------------------------------------------*/
void setup() {
// Initialisation cran
tft.init();
tft.setRotation(1); // Rotation de l'cran, ajustez selon vos besoins
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextSize(2);
tft.setCursor(0, 0);
int16_t rc = png.openFLASH((uint8_t *)fond_reduit, sizeof(fond_reduit), pngDraw);
if (rc == PNG_SUCCESS) {
Serial.println("Fichier PNG ouvert avec succs");
tft.startWrite();
uint32_t dt = millis();
rc = png.decode(NULL, 0);
Serial.print(millis() - dt);
Serial.println("ms");
tft.endWrite();
}
// Initialiser le bus I2C avec les pins spcifies
Wire.begin(21, 22);
Serial.begin(9600);
// // Initialisation Bluetooth
// SerialBT.begin("ESP32_AirQuality"); // Nom du priphrique Bluetooth
// Serial.println("Le priphrique est prt jumeler!");
// Configuration de l'IP fixe
WiFi.config(local_IP, gateway, subnet);
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("Point d'accs IP address: ");
Serial.println(IP);
server.on("/", handleRoot);
server.begin();
Serial.println("Serveur web dmarr");
while (!Serial) {
}
delay(500);
Serial.print("ENS160...");
ens160.begin();
Serial.println(ens160.available() ? "done." : "failed!");
if (ens160.available()) {
// Print ENS160 versions
Serial.print("\tRev: ");
Serial.print(ens160.getMajorRev());
Serial.print(".");
Serial.print(ens160.getMinorRev());
Serial.print(".");
Serial.println(ens160.getBuild());
ens160.setMode(ENS160_OPMODE_RESET);
delay(100);
ens160.setMode(ENS160_OPMODE_STD);
// Initialisation AHT20
while (aht20.begin() != true) {
Serial.println(F("AHT2x non connect ou chec du chargement des coefficients de calibration"));
delay(5000);
}
Serial.println(F("AHT20 OK"));
}
fond_ecran();
}
/*--------------------------------------------------------------------------
MAIN LOOP FUNCTION
Cycle every 1000ms and perform measurement
--------------------------------------------------------------------------*/
void loop() {
if (ens160.available()) {
temperature = aht20.readTemperature(); // read 6-bytes via I2C, takes 80 milliseconds
humidity = aht20.readHumidity();
ens160.set_envdata(temperature, humidity);
ens160.measure(true);
ens160.measureRaw(true);
AQI_precedent = AQI;
AQI = ens160.getAQI();
TVOC = ens160.getTVOC();
eCO2 = ens160.geteCO2();
// Envoyer les donnes par Bluetooth
// SerialBT.printf("Temp: %.2f C, Hum: %.2f %%, eCO2: %.0f ppm, TVOC: %.0f ppb, AQI: %d\n", temperature, humidity, eCO2, TVOC, AQI);
}
miseajour_ecran();
server.handleClient();
delay(1000);
compteur++;
if (compteur > 300) {
compteur = 0;
ens160.setMode(ENS160_OPMODE_RESET);
delay(250);
ens160.setMode(ENS160_OPMODE_STD);
}
}
结论
监测室内空气质量对于确保健康的环境至关重要,尤其是在气密性增加会促进污染物积累的现代家庭中。虽然最好只使用不排放 TVOC 的材料,但这在实践中是不可行的,因为石化副产品自 50 年代以来就侵入了我们的生活。
在这种情况下,像这样的物联网解决方案是相关的,它可以测量室内空气的 TVOC 含量。
这就是 CMV 的伺服控制根据 TVOC 水平发挥作用的地方。配备传感器的 CMV 系统可以根据伺服控制检测到的污染物浓度来调节气流。
这样的系统已经存在,并且越来越多地被整合到现代建筑和翻新中。此外,TVOC 传感器越来越多地用于这些应用,这有助于降低其成本:这解释了为什么现在它们可以在 AliExpress 等平台上以几欧元的价格集成到分线板中。

附录
项目链接:https://www.hackster.io/bertrand_selva/compact-low-cost-esp32-iot-air-quality-monitor-ap-mode-d25159#toc-enclosure-3
项目作者:贝特兰 · 塞尔瓦(Bertrand Selva)
项目视频:https://www.youtube.com/watch?v=8MPRWKDx4Xw
项目代码:https://www.hackster.io/code_files/668659/download
评论