欢迎光临
我们一直在努力

【毕业设计思路】基于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)
未经允许不得转载:171主机测评 » 【毕业设计思路】基于STM32的室内空气质量监测系统设计:PM2.5+甲醛+CO2+温湿度、WiFi数据上传、PyQt5上位机可视化
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址