【毕业设计思路】基于STM32的室内空气质量监测系统设计:PM2.5+甲醛+CO2+温湿度、WiFi数据上传、PyQt5上位机可视化
·
一、项目概述
1.1 项目背景与目标
随着人们对室内环境健康的关注度不断提升,空气质量监测已成为智能家居和健康管理领域的热门方向。室内空气中的PM2.5颗粒物、甲醛、CO2浓度以及温湿度等参数,直接影响人体健康和生活舒适度。
传统的空气质量检测方式存在以下问题:
- 依赖手持式检测仪,无法实现24小时连续监测
- 数据无法远程查看和存储,缺乏历史趋势分析
- 单一参数检测,无法全面反映室内空气状况
- 缺乏报警机制,无法及时发现空气质量异常
本项目基于STM32F103C8T6微控制器,集成PM2.5、甲醛、CO2、温湿度四类传感器,通过ESP8266 WiFi模块实现数据上传,并配合Python PyQt5开发PC端上位机,实现数据可视化与历史趋势分析。
1.2 技术栈选型
| 技术领域 | 具体选型 | 选择理由 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | 成本低、外设丰富、开发资料完善 |
| PM2.5传感器 | PMS5003 | 激光散射原理,精度高,数字输出 |
| 甲醛传感器 | ZE08-CH2O | 电化学原理,精度高,UART输出 |
| CO2传感器 | MH-Z19B | NDIR红外原理,精度±50ppm,UART输出 |
| 温湿度传感器 | DHT22 | 精度±0.5℃/±2%RH,性价比高 |
| WiFi模块 | ESP8266-01S | 支持AT指令,成熟稳定 |
| 显示模块 | 0.96寸OLED(SSD1306) | I2C接口,功耗低 |
| 上位机 | Python 3.x + PyQt5 | 开发效率高,界面美观 |
1.3 系统功能
- 实时采集PM2.5、甲醛、CO2浓度及温湿度数据
- OLED本地显示当前环境参数
- WiFi无线数据上传至PC上位机
- PyQt5上位机实时数据展示与历史曲线绘制
- 阈值报警功能(蜂鸣器+上位机弹窗)
- SQLite本地数据库存储历史数据
二、系统架构设计
2.1 整体架构
系统采用经典的三层架构设计:

2.2 数据流向

三、硬件设计与连接
3.1 硬件清单
| 组件 | 型号 | 数量 | 接口方式 |
|---|---|---|---|
| 主控芯片 | STM32F103C8T6 | 1 | - |
| PM2.5传感器 | PMS5003 | 1 | UART(USART2) |
| 甲醛传感器 | ZE08-CH2O | 1 | UART(USART3) |
| CO2传感器 | MH-Z19B | 1 | UART(软件串口) |
| 温湿度传感器 | DHT22 | 1 | 单总线(GPIO) |
| WiFi模块 | ESP8266-01S | 1 | UART(USART1) |
| 显示模块 | 0.96寸OLED | 1 | I2C |
| 蜂鸣器 | 有源蜂鸣器 | 1 | GPIO |
| 电源模块 | 5V/3.3V | 1 | - |
3.2 引脚分配
ESP8266: TX → PA10 (USART1_RX)
RX → PA9 (USART1_TX)
VCC → 3.3V
CH_PD → 3.3V
PMS5003: TX → PA3 (USART2_RX)
RX → PA2 (USART2_TX)
VCC → 5V
ZE08-CH2O: TX → PB11 (USART3_RX)
RX → PB10 (USART3_TX)
VCC → 3.3V
MH-Z19B: TX → PB8 (软件串口RX)
RX → PB9 (软件串口TX)
VCC → 5V
DHT22: DATA → PA0 (GPIO)
VCC → 3.3V
OLED: SCL → PB6 (I2C1_SCL)
SDA → PB7 (I2C1_SDA)
蜂鸣器: IN → PA1 (GPIO输出)
⚠️ 重要提醒:
- ESP8266必须使用3.3V供电,接5V会烧毁模块
- PMS5003和MH-Z19B需要5V供电,但数据线为3.3V电平,可直连STM32
- DHT22的DATA引脚需要外接4.7KΩ上拉电阻
四、代码实现详解
4.1 传感器驱动层
PMS5003 PM2.5传感器读取
PMS5003采用UART通信,主动上报模式,数据帧格式固定为32字节:
// pms5003.h
typedef struct {
uint16_t pm1_0; // PM1.0浓度 (μg/m³)
uint16_t pm2_5; // PM2.5浓度 (μg/m³)
uint16_t pm10; // PM10浓度 (μg/m³)
} PMS5003_Data;
HAL_StatusTypeDef PMS5003_ReadData(PMS5003_Data *data);
// pms5003.c
#define PMS_FRAME_LEN 32
#define PMS_START1 0x42
#define PMS_START2 0x4D
static uint8_t pms_rx_buf[PMS_FRAME_LEN];
HAL_StatusTypeDef PMS5003_ReadData(PMS5003_Data *data) {
// 接收一帧数据
if (HAL_UART_Receive(&huart2, pms_rx_buf, PMS_FRAME_LEN, 2000) != HAL_OK) {
return HAL_ERROR;
}
// 校验帧头
if (pms_rx_buf[0] != PMS_START1 || pms_rx_buf[1] != PMS_START2) {
return HAL_ERROR;
}
// 校验和验证
uint16_t checksum = 0;
for (int i = 0; i < PMS_FRAME_LEN - 2; i++) {
checksum += pms_rx_buf[i];
}
uint16_t rx_check = (pms_rx_buf[30] << 8) | pms_rx_buf[31];
if (checksum != rx_check) {
return HAL_ERROR;
}
// 解析数据(大气环境下浓度值)
data->pm1_0 = (pms_rx_buf[10] << 8) | pms_rx_buf[11];
data->pm2_5 = (pms_rx_buf[12] << 8) | pms_rx_buf[13];
data->pm10 = (pms_rx_buf[14] << 8) | pms_rx_buf[15];
return HAL_OK;
}
ZE08-CH2O 甲醛传感器读取
ZE08采用主动上报模式,数据帧为9字节:
// ze08_ch2o.h
float ZE08_ReadCH2O(void);
// ze08_ch2o.c
float ZE08_ReadCH2O(void) {
uint8_t rx_buf[9];
if (HAL_UART_Receive(&huart3, rx_buf, 9, 2000) != HAL_OK) {
return -1.0f;
}
// 校验帧头
if (rx_buf[0] != 0xFF || rx_buf[1] != 0x17 || rx_buf[2] != 0x04) {
return -1.0f;
}
// 校验和
uint8_t checksum = 0;
for (int i = 1; i < 8; i++) {
checksum += rx_buf[i];
}
checksum = (~checksum) + 1;
if (checksum != rx_buf[8]) {
return -1.0f;
}
// 甲醛浓度 = (高字节 * 256 + 低字节) * 0.001 mg/m³
uint16_t raw = (rx_buf[4] << 8) | rx_buf[5];
float ch2o = raw * 0.001f;
return ch2o; // 单位: mg/m³
}
MH-Z19B CO2传感器读取
MH-Z19B通过UART发送查询指令获取CO2浓度:
// mhz19b.h
uint16_t MHZ19B_ReadCO2(void);
// mhz19b.c
// 使用软件串口(定时器模拟)
static uint8_t mhz19_cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
uint16_t MHZ19B_ReadCO2(void) {
uint8_t response[9] = {0};
// 发送读取指令
SoftUART_Transmit(mhz19_cmd, 9);
HAL_Delay(100);
// 接收响应
if (SoftUART_Receive(response, 9, 1000) != 0) {
return 0;
}
// 校验
if (response[0] != 0xFF || response[1] != 0x86) {
return 0;
}
uint8_t checksum = 0;
for (int i = 1; i < 8; i++) {
checksum += response[i];
}
checksum = (~checksum) + 1;
if (checksum != response[8]) {
return 0;
}
// CO2浓度 = 高字节 * 256 + 低字节
uint16_t co2 = (response[2] << 8) | response[3];
return co2; // 单位: ppm
}
DHT22 温湿度传感器读取
// dht22.h
typedef struct {
float temperature; // 温度 (℃)
float humidity; // 湿度 (%RH)
} DHT22_Data;
HAL_StatusTypeDef DHT22_ReadData(DHT22_Data *data);
// dht22.c
static void DHT22_SetPinOutput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT22_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DHT22_PORT, &GPIO_InitStruct);
}
static void DHT22_SetPinInput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT22_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(DHT22_PORT, &GPIO_InitStruct);
}
HAL_StatusTypeDef DHT22_ReadData(DHT22_Data *data) {
uint8_t bits[5] = {0};
uint32_t timeout;
// 主机拉低至少1ms(DHT22要求≥1ms)
DHT22_SetPinOutput();
HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_RESET);
HAL_Delay(2);
HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_SET);
// 切换为输入,等待DHT22响应
DHT22_SetPinInput();
timeout = 0;
while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_SET) {
if (++timeout > 10000) return HAL_ERROR;
}
timeout = 0;
while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_RESET) {
if (++timeout > 10000) return HAL_ERROR;
}
timeout = 0;
while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_SET) {
if (++timeout > 10000) return HAL_ERROR;
}
// 读取40位数据
for (int i = 0; i < 40; i++) {
timeout = 0;
while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_RESET) {
if (++timeout > 10000) return HAL_ERROR;
}
uint32_t start = DWT->CYCCNT;
timeout = 0;
while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_SET) {
if (++timeout > 10000) return HAL_ERROR;
}
uint32_t duration = DWT->CYCCNT - start;
// 高电平持续时间 > 40μs 为'1',否则为'0'
if (duration > (SystemCoreClock / 1000000 * 40)) {
bits[i / 8] |= (1 << (7 - (i % 8)));
}
}
// 校验和
if (bits[4] != ((bits[0] + bits[1] + bits[2] + bits[3]) & 0xFF)) {
return HAL_ERROR;
}
// 解析(DHT22数据含小数位)
int16_t raw_hum = (bits[0] << 8) | bits[1];
int16_t raw_temp = (bits[2] << 8) | bits[3];
data->humidity = raw_hum * 0.1f;
data->temperature = (raw_temp & 0x7FFF) * 0.1f;
if (raw_temp & 0x8000) data->temperature = -data->temperature;
return HAL_OK;
}
4.2 数据采集与WiFi上传
主程序逻辑
// main.c
#include "stm32f1xx_hal.h"
#include "pms5003.h"
#include "ze08_ch2o.h"
#include "mhz19b.h"
#include "dht22.h"
#include "esp8266.h"
#include "oled.h"
#include <stdio.h>
#include <string.h>
// 报警阈值定义
#define PM25_ALARM_THRESHOLD 75 // PM2.5报警阈值 (μg/m³)
#define CH2O_ALARM_THRESHOLD 0.08f // 甲醛报警阈值 (mg/m³)
#define CO2_ALARM_THRESHOLD 1000 // CO2报警阈值 (ppm)
// 全局数据
PMS5003_Data g_pms_data;
DHT22_Data g_dht_data;
float g_ch2o = 0.0f;
uint16_t g_co2 = 0;
// 蜂鸣器控制
void Buzzer_On(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); }
void Buzzer_Off(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); }
// 报警检测
void Check_Alarm(void) {
if (g_pms_data.pm2_5 > PM25_ALARM_THRESHOLD ||
g_ch2o > CH2O_ALARM_THRESHOLD ||
g_co2 > CO2_ALARM_THRESHOLD) {
Buzzer_On();
HAL_Delay(200);
Buzzer_Off();
}
}
// OLED显示
void Display_Data(void) {
char buf[32];
OLED_Clear();
sprintf(buf, "PM2.5: %d ug/m3", g_pms_data.pm2_5);
OLED_ShowString(0, 0, buf);
sprintf(buf, "CH2O: %.3f mg", g_ch2o);
OLED_ShowString(0, 2, buf);
sprintf(buf, "CO2: %d ppm", g_co2);
OLED_ShowString(0, 4, buf);
sprintf(buf, "T:%.1fC H:%.1f%%", g_dht_data.temperature, g_dht_data.humidity);
OLED_ShowString(0, 6, buf);
OLED_Refresh();
}
// 数据打包为JSON并通过WiFi发送
void Send_Data_WiFi(void) {
char json[256];
sprintf(json,
"{\"pm25\":%d,\"pm10\":%d,\"ch2o\":%.3f,\"co2\":%d,\"temp\":%.1f,\"hum\":%.1f}",
g_pms_data.pm2_5, g_pms_data.pm10,
g_ch2o, g_co2,
g_dht_data.temperature, g_dht_data.humidity
);
ESP8266_SendData(json, strlen(json));
}
int main(void) {
HAL_Init();
SystemClock_Config();
// 外设初始化
MX_GPIO_Init();
MX_USART1_UART_Init(); // ESP8266
MX_USART2_UART_Init(); // PMS5003
MX_USART3_UART_Init(); // ZE08-CH2O
MX_I2C1_Init(); // OLED
SoftUART_Init(); // MH-Z19B 软件串口
// OLED初始化
OLED_Init();
OLED_Clear();
OLED_ShowString(0, 3, "System Starting...");
OLED_Refresh();
// ESP8266初始化并连接WiFi
ESP8266_Init();
ESP8266_ConnectWiFi("YourSSID", "YourPassword");
ESP8266_ConnectTCP("192.168.1.100", 8080); // 连接上位机
HAL_Delay(2000);
while (1) {
// 1. 采集传感器数据
PMS5003_ReadData(&g_pms_data);
g_ch2o = ZE08_ReadCH2O();
g_co2 = MHZ19B_ReadCO2();
DHT22_ReadData(&g_dht_data);
// 2. OLED显示
Display_Data();
// 3. 报警检测
Check_Alarm();
// 4. WiFi上传数据
Send_Data_WiFi();
// 5. 采集间隔3秒
HAL_Delay(3000);
}
}
4.3 ESP8266 WiFi模块驱动
// esp8266.h
typedef enum {
ESP_OK = 0,
ESP_ERROR,
ESP_TIMEOUT
} ESP_Status;
ESP_Status ESP8266_Init(void);
ESP_Status ESP8266_ConnectWiFi(const char *ssid, const char *pwd);
ESP_Status ESP8266_ConnectTCP(const char *ip, uint16_t port);
ESP_Status ESP8266_SendData(const char *data, uint16_t len);
// esp8266.c
static char esp_rx_buf[256];
static ESP_Status ESP8266_SendCmd(const char *cmd, const char *expect, uint32_t timeout) {
memset(esp_rx_buf, 0, sizeof(esp_rx_buf));
HAL_UART_Transmit(&huart1, (uint8_t *)cmd, strlen(cmd), 1000);
HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n", 2, 100);
HAL_UART_Receive(&huart1, (uint8_t *)esp_rx_buf, sizeof(esp_rx_buf) - 1, timeout);
if (strstr(esp_rx_buf, expect) != NULL) {
return ESP_OK;
}
return ESP_ERROR;
}
ESP_Status ESP8266_Init(void) {
HAL_Delay(1000);
if (ESP8266_SendCmd("AT", "OK", 1000) != ESP_OK) return ESP_ERROR;
if (ESP8266_SendCmd("AT+CWMODE=1", "OK", 1000) != ESP_OK) return ESP_ERROR;
return ESP_OK;
}
ESP_Status ESP8266_ConnectWiFi(const char *ssid, const char *pwd) {
char cmd[128];
sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"", ssid, pwd);
return ESP8266_SendCmd(cmd, "WIFI GOT IP", 15000);
}
ESP_Status ESP8266_ConnectTCP(const char *ip, uint16_t port) {
char cmd[128];
sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%d", ip, port);
return ESP8266_SendCmd(cmd, "CONNECT", 5000);
}
ESP_Status ESP8266_SendData(const char *data, uint16_t len) {
char cmd[32];
sprintf(cmd, "AT+CIPSEND=%d", len);
if (ESP8266_SendCmd(cmd, ">", 2000) != ESP_OK) return ESP_ERROR;
HAL_UART_Transmit(&huart1, (uint8_t *)data, len, 2000);
HAL_Delay(100);
return ESP_OK;
}
4.4 Python上位机实现
TCP服务端 + 数据存储
# tcp_server.py
import socket
import json
import sqlite3
import threading
from datetime import datetime
from PyQt5.QtCore import QObject, pyqtSignal
class AirQualityServer(QObject):
"""TCP服务端,接收STM32上传的空气质量数据"""
data_received = pyqtSignal(dict)
def __init__(self, host='0.0.0.0', port=8080):
super().__init__()
self.host = host
self.port = port
self.running = False
# 初始化数据库
self.db = sqlite3.connect('air_quality.db', check_same_thread=False)
self.db_lock = threading.Lock()
self._init_db()
def _init_db(self):
cursor = self.db.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sensor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pm25 INTEGER,
pm10 INTEGER,
ch2o REAL,
co2 INTEGER,
temperature REAL,
humidity REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
self.db.commit()
def start(self):
self.running = True
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((self.host, self.port))
self.server.listen(1)
self.server.settimeout(1.0)
threading.Thread(target=self._accept_loop, daemon=True).start()
def _accept_loop(self):
while self.running:
try:
client, addr = self.server.accept()
print(f"设备连接: {addr}")
threading.Thread(target=self._handle_client,
args=(client,), daemon=True).start()
except socket.timeout:
continue
def _handle_client(self, client):
buffer = b''
while self.running:
try:
chunk = client.recv(1024)
if not chunk:
break
buffer += chunk
# 按JSON对象分割(每条数据以}结尾)
while b'}' in buffer:
idx = buffer.index(b'}') + 1
raw = buffer[:idx]
buffer = buffer[idx:]
try:
data = json.loads(raw.decode())
data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self._save_data(data)
self.data_received.emit(data)
except json.JSONDecodeError:
continue
except Exception as e:
print(f"接收异常: {e}")
break
client.close()
def _save_data(self, data):
with self.db_lock:
cursor = self.db.cursor()
cursor.execute('''
INSERT INTO sensor_data (pm25, pm10, ch2o, co2, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?)
''', (data.get('pm25', 0), data.get('pm10', 0),
data.get('ch2o', 0), data.get('co2', 0),
data.get('temp', 0), data.get('hum', 0)))
self.db.commit()
def get_history(self, hours=24):
"""获取最近N小时的历史数据"""
with self.db_lock:
cursor = self.db.cursor()
cursor.execute('''
SELECT pm25, pm10, ch2o, co2, temperature, humidity, timestamp
FROM sensor_data
WHERE timestamp >= datetime('now', ?)
ORDER BY timestamp ASC
''', (f'-{hours} hours',))
return cursor.fetchall()
def stop(self):
self.running = False
self.server.close()
self.db.close()
PyQt5 可视化界面
# main_window.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
from tcp_server import AirQualityServer
class AirQualityMonitor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('室内空气质量监测系统')
self.setGeometry(100, 100, 1280, 800)
self.setStyleSheet("background-color: #1e1e2e; color: #cdd6f4;")
# 历史数据缓存
self.history = {'pm25': [], 'ch2o': [], 'co2': [], 'temp': [], 'hum': [], 'time': []}
self.max_points = 200 # 最多显示200个数据点
self._init_ui()
self._init_server()
def _init_ui(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QVBoxLayout(central)
# ===== 顶部标题栏 =====
title = QLabel('🌿 室内空气质量监测系统')
title.setStyleSheet("font-size: 22px; font-weight: bold; padding: 10px; color: #a6e3a1;")
title.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title)
# ===== 中间区域:数据卡片 + 图表 =====
content_layout = QHBoxLayout()
main_layout.addLayout(content_layout)
# 左侧:实时数据卡片
card_panel = QVBoxLayout()
content_layout.addLayout(card_panel, 1)
self.cards = {}
card_configs = [
('pm25', 'PM2.5', 'μg/m³', '#f38ba8', 75),
('ch2o', '甲醛', 'mg/m³', '#fab387', 0.08),
('co2', 'CO2', 'ppm', '#f9e2af', 1000),
('temp', '温度', '℃', '#89b4fa', None),
('hum', '湿度', '%RH', '#74c7ec', None),
]
for key, name, unit, color, threshold in card_configs:
card = self._create_card(name, unit, color, threshold)
card_panel.addWidget(card['widget'])
self.cards[key] = card
card_panel.addStretch()
# 右侧:实时曲线图
chart_panel = QVBoxLayout()
content_layout.addLayout(chart_panel, 3)
# PM2.5 + CO2 曲线
self.plot1 = pg.PlotWidget(title="PM2.5 & CO2 趋势")
self.plot1.setBackground('#313244')
self.plot1.showGrid(x=True, y=True, alpha=0.3)
self.plot1.addLegend()
self.curve_pm25 = self.plot1.plot(pen=pg.mkPen('#f38ba8', width=2), name='PM2.5')
self.curve_co2 = self.plot1.plot(pen=pg.mkPen('#f9e2af', width=2), name='CO2')
chart_panel.addWidget(self.plot1)
# 温湿度 + 甲醛曲线
self.plot2 = pg.PlotWidget(title="温湿度 & 甲醛趋势")
self.plot2.setBackground('#313244')
self.plot2.showGrid(x=True, y=True, alpha=0.3)
self.plot2.addLegend()
self.curve_temp = self.plot2.plot(pen=pg.mkPen('#89b4fa', width=2), name='温度')
self.curve_hum = self.plot2.plot(pen=pg.mkPen('#74c7ec', width=2), name='湿度')
chart_panel.addWidget(self.plot2)
# ===== 底部状态栏 =====
self.status_bar = QLabel('⏳ 等待设备连接...')
self.status_bar.setStyleSheet("font-size: 13px; padding: 5px; color: #a6adc8;")
main_layout.addWidget(self.status_bar)
def _create_card(self, name, unit, color, threshold):
widget = QFrame()
widget.setStyleSheet(f"""
QFrame {{
background-color: #313244;
border-left: 4px solid {color};
border-radius: 8px;
padding: 12px;
margin: 4px;
}}
""")
layout = QVBoxLayout(widget)
name_label = QLabel(name)
name_label.setStyleSheet(f"font-size: 13px; color: #a6adc8;")
value_label = QLabel('--')
value_label.setStyleSheet(f"font-size: 28px; font-weight: bold; color: {color};")
unit_label = QLabel(unit)
unit_label.setStyleSheet("font-size: 11px; color: #585b70;")
layout.addWidget(name_label)
layout.addWidget(value_label)
layout.addWidget(unit_label)
return {
'widget': widget,
'value_label': value_label,
'threshold': threshold,
'color': color
}
def _init_server(self):
self.server = AirQualityServer(port=8080)
self.server.data_received.connect(self.on_data)
self.server.start()
self.status_bar.setText('✅ 服务已启动,监听端口 8080,等待设备连接...')
@pyqtSlot(dict)
def on_data(self, data):
"""收到新数据时更新界面"""
# 更新数据卡片
mapping = {
'pm25': ('pm25', '{:.0f}'),
'ch2o': ('ch2o', '{:.3f}'),
'co2': ('co2', '{:.0f}'),
'temp': ('temp', '{:.1f}'),
'hum': ('hum', '{:.1f}'),
}
alarm_msgs = []
for key, (data_key, fmt) in mapping.items():
val = data.get(data_key, 0)
card = self.cards[key]
card['value_label'].setText(fmt.format(val))
# 超阈值变红
if card['threshold'] is not None and val > card['threshold']:
card['value_label'].setStyleSheet("font-size: 28px; font-weight: bold; color: #f38ba8;")
alarm_msgs.append(f"{key.upper()} 超标!")
else:
card['value_label'].setStyleSheet(f"font-size: 28px; font-weight: bold; color: {card['color']};")
# 更新历史数据
self.history['pm25'].append(data.get('pm25', 0))
self.history['co2'].append(data.get('co2', 0))
self.history['ch2o'].append(data.get('ch2o', 0))
self.history['temp'].append(data.get('temp', 0))
self.history['hum'].append(data.get('hum', 0))
# 限制数据点数量
for k in self.history:
if len(self.history[k]) > self.max_points:
self.history[k] = self.history[k][-self.max_points:]
# 更新曲线
self.curve_pm25.setData(self.history['pm25'])
self.curve_co2.setData(self.history['co2'])
self.curve_temp.setData(self.history['temp'])
self.curve_hum.setData(self.history['hum'])
# 更新状态栏
ts = data.get('timestamp', '')
if alarm_msgs:
self.status_bar.setText(f'⚠️ {ts} | {" | ".join(alarm_msgs)}')
self.status_bar.setStyleSheet("font-size: 13px; padding: 5px; color: #f38ba8;")
else:
self.status_bar.setText(f'✅ {ts} | 空气质量正常')
self.status_bar.setStyleSheet("font-size: 13px; padding: 5px; color: #a6e3a1;")
def closeEvent(self, event):
self.server.stop()
super().closeEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = AirQualityMonitor()
window.show()
sys.exit(app.exec_())
五、调试与常见问题
5.1 ESP8266连接问题
调试时可通过串口助手发送AT指令测试:
AT → OK
AT+CWMODE=1 → OK
AT+CWLAP → 扫描周围WiFi
AT+CWJAP="SSID","密码" → WIFI GOT IP
AT+CIPSTART="TCP","192.168.1.100",8080 → CONNECT
⚠️ 如果AT无响应,检查:波特率是否匹配(默认115200)、TX/RX是否接反、CH_PD是否拉高
5.2 传感器数据异常
- PMS5003读数为0:检查供电是否为5V,传感器需要预热约30秒
- ZE08甲醛值偏高:新传感器需要48小时老化期,初始读数不准确属正常现象
- MH-Z19B CO2值400不变:传感器需要预热3分钟,且需要校准(通电后在室外放置20分钟,发送校准指令)
- DHT22读取失败:确认DATA引脚是否接了4.7KΩ上拉电阻,读取间隔不要小于2秒
5.3 数据丢包处理
WiFi传输不稳定时,可增加重发机制:
// 带重试的数据发送
void Send_Data_WithRetry(const char *data, uint16_t len, uint8_t max_retry) {
for (uint8_t i = 0; i < max_retry; i++) {
if (ESP8266_SendData(data, len) == ESP_OK) {
return; // 发送成功
}
HAL_Delay(500);
// 重连TCP
if (i == max_retry / 2) {
ESP8266_ConnectTCP("192.168.1.100", 8080);
}
}
}
六、项目总结与扩展
6.1 项目成果
本项目成功实现了一套完整的室内空气质量监测系统,具备以下功能:
- 多参数监测:PM2.5、甲醛、CO2、温湿度四类环境参数实时采集
- 本地显示:OLED屏幕实时显示当前环境数据
- 无线传输:ESP8266 WiFi模块实现数据无线上传
- 可视化界面:PyQt5上位机提供实时数据展示和历史趋势曲线
- 报警功能:阈值超限时蜂鸣器报警 + 上位机界面提示
- 数据存储:SQLite数据库保存历史数据,支持回溯分析
6.2 关键技术点
- STM32多串口管理:USART1/2/3 + 软件串口,同时驱动4个UART设备
- 多传感器协议解析:PMS5003/ZE08/MH-Z19B各有不同的帧格式和校验方式
- ESP8266 AT指令封装:WiFi连接、TCP通信的完整驱动实现
- PyQt5 + pyqtgraph:实时数据可视化与动态曲线绘制
- TCP Socket通信:STM32与PC之间的可靠数据传输
6.3 扩展方向
- 接入MQTT协议,支持阿里云IoT/OneNET等云平台
- 增加手机端小程序或APP远程查看
- 加入空气净化器/新风系统联动控制
- 使用FreeRTOS实现多任务调度,提升系统实时性
- 增加SD卡本地存储,断网时数据不丢失
📌 如果这篇文章对你有帮助,请点赞👍收藏⭐关注🔔,后续会持续更新更多嵌入式物联网项目!
有问题欢迎评论区交流,看到都会回复~
更多推荐
所有评论(0)