升级改造做了一个有显示屏的可移动可联网可记录数据的环境监测仪。
之前基于mqtt的环境监测仪,比较勉强地实现了物联网功能,能够在手机客户端上查看环境监测数据,但是这个数据似乎难以保存。而且考虑到后面的使用场景,如果能有个显示屏实时显示会更好。所以周末有空进一步升级改造。
硬件准备
新的环境监测仪,需要在 ESP32 开发板上接入三个设备:
- DHT22 温湿度传感器
- SGP30 气体传感器
- 0.96寸 OLED显示屏
需要注意的是,SGP30 和 OLED显示屏都是 I2C 通讯的设备,刚开始我担心引脚冲突,实时上这两个可以连接相同的引脚:
- SDA(数据):21
- SCL(时钟):22
然后 ESP32 刚好能够接两个 I2C 设备,通过 find_I2C_address
代码能够确定这两个设备的地址。在代码中 display.begin(SSD1306_SWITCHCAPVCC, 0x3C)
中的 0x3C
就是显示屏的地址。
find_I2C_address
代码如下:
/*********
Rui Santos
Complete project details at https://randomnerdtutorials.com
*********/
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(115200);
Serial.println("\nI2C Scanner");
}
void loop() {
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ ) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("I2C device found at address 0x");
if (address<16) {
Serial.print("0");
}
Serial.println(address,HEX);
nDevices++;
}
else if (error==4) {
Serial.print("Unknow error at address 0x");
if (address<16) {
Serial.print("0");
}
Serial.println(address,HEX);
}
}
if (nDevices == 0) {
Serial.println("No I2C devices found\n");
}
else {
Serial.println("done\n");
}
delay(5000);
}
而 DHT22 温湿度传感器,还是老规矩接到 14 号引脚上。
网络准备
新环境监测仪,我希望是便携式,所以联网就是使用手机的热点。需要注意的是,手机开热点的时候不要同时连WiFi。
然后监测仪联网之后,会把数据直接发送到我的具有公网IP的云服务器上,云服务器上的脚本(如:broker.php
)将数据记录到一个日志文件中即可。
<?php
$message = $_GET['message'];
$time = date("Y-m-d H:i:s");
$file = "data.txt";
$current = file_get_contents($file);
$current .= "\n" . $time . " " . $message ;
file_put_contents($file, $current);
echo "Message received: " . $message;
?>
adruino代码
代码经过了多次调试优化:
- 自定义报送数据结构
- 断网重连,无网络不影响显示屏显示
- 数据精度与测量精度匹配
#include <Wire.h> // 使用 I2C 的库
#include <Adafruit_GFX.h> //Adafruit 库写入显示器
#include <Adafruit_SSD1306.h>
#include <Adafruit_SGP30.h>
#include "DHT.h"
#include <WiFi.h>
#include <HTTPClient.h>
// 手机热点,开热点以启动联网功能,采集数据保存下来
// 手机开热点时不能使用WiFi,只能使用移动流量
// 定义 Wi-Fi 名与密码
const char* ssid = "test";
const char* password = "12345678";
HTTPClient http;
unsigned long lastReconnectAttempt = 0; // 上次尝试连接的时间
#define LED 2
// 初始化 DHT 对象
#define DHTPIN 14
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// 定义SGP30实例,注意引脚和显示屏默认引脚冲突,因为都是走I2C
#define I2C_SDA 21 // 根据ESP32实际的SDA引脚定义
#define I2C_SCL 22 // 根据ESP32实际的SCL引脚定义
Adafruit_SGP30 sgp;
uint32_t getAbsoluteHumidity(float temperature, float humidity) {
// approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
return absoluteHumidityScaled;
}
// 使用的是 128×64 OLED 显示屏, SDA=21, SCL=22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64 //
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// I2C 通信协议
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);// (-1) 参数表示您的 OLED 显示器没有 RESET 引脚
void connect2wifi(){
// 尝试连接WiFi,但不在这里等待
WiFi.begin(ssid, password);
if (WiFi.status() != WL_CONNECTED) {
delay(500); // 短时间延迟,避免立即重试
Serial.print("Connecting to WiFi...");
}
if (WiFi.status() == WL_CONNECTED) {
// 使用板载 LED 反馈连接成功
pinMode(LED, OUTPUT);
digitalWrite(LED, HIGH);
delay(100);
digitalWrite(LED, LOW);
delay(100);
digitalWrite(LED, HIGH);
delay(200);
digitalWrite(LED, LOW);
delay(100);
}
}
void setup() {
Serial.begin(115200);//115200 的波特率初始化串行监视器以进行调试
// DHT11开启
dht.begin();
// 开始I2C通信到 sgp30 传感器
Wire.begin(I2C_SDA, I2C_SCL);
if (!sgp.begin(&Wire)) {
Serial.println("Failed to connect SGP30");
while (true); // 停在这里,直到连接成功
}
Serial.println("SGP30 connected successfully!");
// OLED显示器
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) // Address 0x3D for 128x64
{
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// delay(1000);
display.clearDisplay();// 清除显示
display.setTextSize(1); //
display.setTextColor(WHITE);
display.setCursor(0, 30);
display.println(F("Powered by sheldonxxd"));
display.display(); // 屏幕上实际显示文本
connect2wifi();
}
void loop() {
// 读取湿度
int humid = dht.readHumidity();
// 读取温度
float temp = dht.readTemperature();
sgp.setHumidity(getAbsoluteHumidity(temp, humid));
sgp.IAQmeasure();
// 读取和更新CO2与VOC值
int eCO2 = sgp.eCO2;
int TVOC = sgp.TVOC;
// 计算时间
unsigned long currentTimeMillis = millis();
int t = currentTimeMillis / 1000;
display.clearDisplay();
display.setTextSize(1); //
display.setTextColor(WHITE);
display.setCursor(10, 0);
display.println(F("Environment Monitor"));
display.setCursor(5, 12);
display.print(F("Temp. = "));
display.print(temp);
display.println(F(" C"));
display.setCursor(5, 22);
display.print(F("Humid. = "));
display.print(humid);
display.println(F(" %"));
display.setCursor(5, 32);
display.print(F("CO2 = "));
display.print(eCO2);
display.println(F(" ppm"));
display.setCursor(5, 42);
display.print(F("TVOC = "));
display.print(TVOC);
display.println(F(" ppb"));
display.setCursor(5, 54);
display.print(F("Running "));
display.print(t);
display.println(F(" sec."));
// 如果连接上了手机热点,就发送数据
if (WiFi.status() == WL_CONNECTED){
// 每间隔5秒发布内容
if (t % 5 == 0) {
String result = "";
result += String(temp, 1); //保留一位小数
result += "_"; //以下划线作为分隔符,避免url构造问题
result += String(humid);
result += "_";
result += String(eCO2);
result += "_";
result += String(TVOC);
result += "_env-01"; // 这个设备名称就是 env-01
String url = "https://sheldonxxd.cn/broker.php?message="+result;
Serial.println(url);
http.begin(url); // 修改为你的PHP文件路径
int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(response);
} else {
Serial.print("Error on sending GET request: ");
Serial.println(httpResponseCode);
}
http.end();
}
}
display.display();
if (WiFi.status() != WL_CONNECTED){
connect2wifi();
} else {
delay(1000);
}
}
运行测试与数据分析
连接好各个模块,并将代码下载到ESP32开发板上之后,进行了两次测试。第一次是卧室过夜。记录的数据如下:
数据读取和原始信号作图代码如下:
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns
with open("data.txt") as f:
lines = f.readlines()
box = []
for a in lines:
b = a.strip()
cs =b.split(" ")
if len(cs)==3:
date = cs[0]
time = cs[1]
info = cs[2]
ds = info.split("_")
if len(ds)==5:
temp = float(ds[0])
humid = int(ds[1])
CO2 = int(ds[2])
TVOC = int(ds[3])
device = ds[4]
box.append([device, temp, humid, CO2, TVOC, datetime.fromisoformat(date+ " "+time)])
data = pd.DataFrame(box, columns=['device', 'temperature', 'humidity', 'CO2', 'TVOC', 'time'])
data2 = data[(data['time']>datetime.fromisoformat("2024-07-27 22:00:00"))&\
(data['time']<datetime.fromisoformat("2024-07-28 06:00:00"))
]
for metric in ['temperatur', 'humidity', 'CO2', 'TVOC']:
plt.figure(figsize=(7,3))
sns.lineplot(x='time', y=metric, data=data2)
plt.show()
周期性振荡分析
这个数据大体的趋势还算正常,但是存在明显的周期性信号振荡。我比较好奇这个振荡信号的频率,刚好可以复习下傅里叶分析。得到的频谱如下:
提取频谱代码如下:
dt = data2['time'].iloc[-1].timestamp() - data2['time'].iloc[0].timestamp()
# 假设我们有一个时序信号y和对应的时间序列t
signal = data2['temperature'].values
t = np.linspace(0, dt, len(data2))
# 计算FFT,得到复数频谱
fft_signal = np.fft.fft(signal)
# 获取频率轴(单位为Hz)
sample_rate = len(t) / t[-1] # 样本率等于采样点除以时间长度
freqs = np.fft.fftfreq(len(t), 1.0/sample_rate)
# 只保留正频率部分,因为对于实信号,频谱是共轭对称的
positive_freqs = freqs[:len(freqs)//2]
fft_positive = fft_signal[:len(fft_signal)//2]
# 绘制幅度谱(即FFT结果的绝对值)
plt.figure(figsize=(10,5))
plt.plot(positive_freqs, np.abs(fft_positive))
# 注意调整显示区间
plt.xlim(0,0.01)
plt.ylim(0, 100)
plt.xlabel('Frequency (Hz)')
plt.ylabel('|Amplitude|')
plt.title('Power Spectrum')
plt.show()
根据原始信号的特征,大致可以判断这个周期不是传感器本身的测量噪声,因为它的频率比较低。但是频谱上,低频的部分有好几个峰,所以接下来可以根据振幅筛选一下,看看是哪几个频率贡献了这个变化。
筛选特征频率并根据频率重建信号的代码如下:
# 按照振幅来筛选
inds = np.where((np.abs(fft_positive)<20)&(np.abs(fft_positive)>18))
x = t
y = signal
# 进行傅立叶变换
yf = np.fft.fft(y)
# 设定你想要保留的频率范围(例如,只保留前10个频率)
desired_freq_indices = inds[0]
yf[desired_freq_indices] = yf[desired_freq_indices]
yf[:desired_freq_indices[0]] = 0
yf[desired_freq_indices[-1]+1:] = 0
# 反向傅立叶变换以得到重构信号
new_y = np.fft.ifft(yf)
# 绘制原始信号和重构信号
plt.figure()
# plt.plot(x, y)
plt.plot(x, new_y.real, linewidth=3.0)
plt.show()
在变量 inds 中能找到该特征包含的多个频率,拆分作图展示如下:
其中有一个明显的低频信号,其频率索引是11,根据这个索引查找到频率:
一通分析下来,知道了准确的数据,但是还是不知道啥原因,这个时候咨询了下 qwen2,给出的答复如下:
Q: 使用 esp32 开发板,DHT22温湿度传感模块采集的温度和湿度数据,采样率为 0.2 Hz,发现存在显著的大周期性波动,其波动频率为 0.000467 Hz,这是什么原因造成的。
A:观测到的周期性波动现象可能由以下几个因素造成:
1. **环境因素**:DHT22传感器对外界温度和湿度变化敏感。如果周围环境中存在缓慢且有规律的变化(例如,由于日光、空调系统或自然天气循环),则会检测到类似的波动频率。0.000467 Hz的周期性波动对应大约2140秒或者35.6分钟一个周期,这可能与一些环境因素的日变化趋势有关。
2. **电子噪声**:数字电路和模拟信号转换过程中可能会引入噪声。如果ESP32或DHT22模块附近的电源不稳定、电磁干扰(EMI)或其他电子设备产生影响,也可能导致测量值出现规律性波动。
3. **采样时间与环境响应时间不匹配**:DHT22传感器需要一定的时间来稳定其读数以反映当前的环境条件。如果0.2 Hz的采样率过快,而环境变化本身相对较慢(比如大气湿度的变化),那么连续采集的数据可能无法准确反映出实际的渐变过程,而是呈现出“振荡”或波动。
4. **传感器特性**:DHT22温湿度传感器内部有温度和湿度补偿算法。在某些极端环境下,这些算法可能导致输出数据呈现特定模式或周期性变化,尤其是当环境条件接近传感器工作极限时。
5. **软件处理问题**:如果在读取和处理数据的代码中存在缺陷(比如定时器溢出、变量类型不当导致的数据损失等),也可能造成看似规律性的波动现象。
要确定具体原因并解决此问题,可以尝试以下方法:
- 检查并减少环境干扰因素。
- 使用更稳定的电源或添加适当的滤波电路以降低噪声。
- 调整采样率,使其与预期的环境变化周期相匹配,或者使用更平滑的数据处理算法(如移动平均)来过滤短期波动。
- 校准传感器,确保其在合理的测量范围内工作,并检查是否有固件或硬件更新可提高准确性。
最后,如果波动仍然存在,可能需要更详细的现场测试和数据分析以确定根本原因。
我认为主要有两个猜测:
- 移动电源不太稳定,传感器和移动电源都是塞到一个笔筒里边的,所以我有点怀疑是移动电源和传感器靠得太近所致的。
- 空调维持温度存在一定的节能工作机制
根据猜测1,我把传感器从笔筒中扯出来,又对卧室内环境进行了过夜测量,还是存在这样的波动。根据猜测2,我进行了外出测试。
外出测试
出门到附近的万达吃饭然后逛超市,随身带上了环境监测仪,并且开启了手机热点收集数据。
根据数据显示,得出:
- 保持与猜测1相同的方式,外出测试没有周期性波动,说明那个低频的波动是空调的作用。
- DHT22温湿度传感器测量环境切换,如从商场空调区域切换到室外,从26.5到31度,大概需要3分钟, 1.5 度/分钟。
- 商场内的 CO2 和 TVOC 都处于临界水平,家里开空调还是不错的,室外空气质量最好,但是湿度比较大。
此处评论已关闭