DIY-ESP8266移动PM2.5传感器-带屏幕-APP

news2024/12/21 15:28:47

本教程将指导您制作一台专业级的空气质量检测仪。这个项目使用经济实惠的ESP8266和PMS5003传感器,配合OLED显示屏,不仅能实时显示PM2.5数值,还能通过手机APP随时查看数据。总成本70元,相比几百的用的便宜,用的心理踏实。

功能特点

  • 实时检测PM2.5和PM10浓度
  • OLED屏幕直观显示数据
  • WiFi无线连接,支持远程数据查看
  • 手机APP实时监控
  • 支持数据历史记录和趋势分析
  • 24小时持续监测

成品
在这里插入图片描述

网页
在这里插入图片描述

APP
在这里插入图片描述

硬件需求

[upl-image-preview url=https://forum.thingspanel.cn/assets/files/2024-12-20/1734656701-50998-image.png]

ESP8266开发板(如NodeMCU、Wemos D1 mini等)10元
PMS5003/PMS7003系列颗粒物传感器 50元
0.96寸OLED显示屏(I2C接口,128x64分辨率)9元
若干杜邦线

接线说明

PMS传感器接线(5V供电):

VCC → ESP8266的VIN(5V)
GND → ESP8266的GND
TX → D6(GPIO12)
RX → D5(GPIO14)

OLED显示屏接线:

VCC → 3.3V
GND → GND
SDA → D2(GPIO4)
SCL → D1(GPIO5)

准备工作

Arduino IDE配置:

安装ESP8266开发板支持
安装必要的库:

WiFiManager(用于WiFi配置)
PubSubClient(MQTT客户端)
Adafruit GFX和Adafruit SSD1306(OLED驱动)
ArduinoJson(JSON数据处理)

使用说明

首次使用:

给设备上电后,会创建一个名为"AP"的WiFi热点
用手机连接该热点
在弹出的配置页面中设置WiFi信息
设备会自动连接配置好的WiFi

正常使用:

设备每3秒更新一次显示屏数据
每10秒向MQTT服务器发送一次数据
OLED屏幕显示实时PM2.5数值

代码

#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#include <SoftwareSerial.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>

// OLED显示屏设置
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// PMS传感器串口设置
#define PMS_RX 12  // D6
#define PMS_TX 14  // D5
SoftwareSerial pmsSensor(PMS_RX, PMS_TX);

// MQTT设置
const char* mqtt_server = "47.115.210.16";
const int mqtt_port = 1883;
const char* mqtt_username = "cc1fb73f-ec43-3b37-522";
const char* mqtt_password = "b5c6d62";
const char* mqtt_client_id = "mqtt_d59ea842-b79";
const char* mqtt_topic = "devices/telemetry";

// 时间设置
unsigned long lastMQTTPublish = 0;
const long MQTT_PUBLISH_INTERVAL = 10000;  // MQTT发布间隔10秒
const long DISPLAY_UPDATE_INTERVAL = 3000;  // 显示更新间隔3秒

WiFiClient espClient;
PubSubClient client(espClient);

// PMS数据结构
struct PMS_data {
    uint16_t pm1_0;
    uint16_t pm2_5;
    uint16_t pm10_0;
    uint16_t pm1_0_std;   // 添加标准颗粒物浓度
    uint16_t pm2_5_std;
    uint16_t pm10_0_std;
    bool valid;
} pms_data;

void setup() {
    Serial.begin(115200);
    Serial.println("\n启动中...");
    
    // 初始化I2C
    Wire.begin(4, 5);  // SDA = GPIO4 (D2), SCL = GPIO5 (D1)
    
    // 初始化OLED
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println("OLED初始化失败");
    } else {
        Serial.println("OLED初始化成功");
        display.clearDisplay();
        display.setTextColor(SSD1306_WHITE);
        showInitScreen();
    }
    
    // 初始化PMS传感器
    Serial.println("初始化PMS传感器...");
    pmsSensor.begin(9600);
    pms_data.valid = false;
    
    // 配置WiFi
    WiFiManager wifiManager;
    if(!wifiManager.autoConnect("AP")) {
        Serial.println("配置失败,重启");
        ESP.restart();
    }
    
    // 配置MQTT
    client.setServer(mqtt_server, mqtt_port);
}

void loop() {
    static unsigned long lastDisplayUpdate = 0;
    unsigned long currentMillis = millis();
    
    if (!client.connected()) {
        reconnectMQTT();
    }
    client.loop();
    
    if (currentMillis - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL) {
        if (readPMSdata()) {
            updateDisplay();
        }
        lastDisplayUpdate = currentMillis;
    }
    
    if (pms_data.valid && currentMillis - lastMQTTPublish >= MQTT_PUBLISH_INTERVAL) {
        publishData();
        lastMQTTPublish = currentMillis;
    }
}

bool readPMSdata() {
    uint8_t buffer[32];
    uint16_t sum = 0;
    
    // 清空接收缓冲区
    while(pmsSensor.available()) {
        pmsSensor.read();
    }
    
    // 等待数据
    delay(1000);
    
    if (pmsSensor.available() < 32) {
        return false;
    }
    
    // 检查帧头
    if (pmsSensor.read() != 0x42 || pmsSensor.read() != 0x4D) {
        return false;
    }
    
    // 读取剩余数据
    pmsSensor.readBytes(buffer, 30);
    
    // 计算校验和
    sum = 0x42 + 0x4D;
    for (int i = 0; i < 28; i++) {
        sum += buffer[i];
    }
    
    // 验证校验和
    if (sum != ((buffer[28] << 8) | buffer[29])) {
        Serial.println("校验和错误");
        return false;
    }
    
    // 打印原始数据以供调试
    Serial.println("Raw data:");
    for(int i = 0; i < 30; i++) {
        Serial.printf("%02X ", buffer[i]);
        if((i + 1) % 8 == 0) Serial.println();
    }
    Serial.println();

    // 标准颗粒物浓度
    pms_data.pm1_0_std = (buffer[4] << 8) | buffer[5];    // 数据1:PM1.0标准
    pms_data.pm2_5_std = (buffer[6] << 8) | buffer[7];    // 数据2:PM2.5标准
    pms_data.pm10_0_std = (buffer[8] << 8) | buffer[9];   // 数据3:PM10标准

    // 大气环境浓度
    pms_data.pm1_0 = (buffer[10] << 8) | buffer[11];    // 数据4:PM1.0大气环境
    pms_data.pm2_5 = (buffer[12] << 8) | buffer[13];    // 数据5:PM2.5大气环境
    pms_data.pm10_0 = (buffer[14] << 8) | buffer[15];   // 数据6:PM10大气环境



    Serial.println("解析后的数据:");
    Serial.printf("标准颗粒物浓度 - PM1.0: %d, PM2.5: %d, PM10: %d\n", 
                 pms_data.pm1_0_std, pms_data.pm2_5_std, pms_data.pm10_0_std);
    Serial.printf("大气环境浓度 - PM1.0: %d, PM2.5: %d, PM10: %d\n", 
                 pms_data.pm1_0, pms_data.pm2_5, pms_data.pm10_0);

    // 验证数据合理性(暂时注释掉范围检查,看看实际数据)
    // if (pms_data.pm2_5 > 1000 || pms_data.pm1_0 > 1000 || pms_data.pm10_0 > 1000) {
    //     Serial.println("数据超出合理范围");
    //     return false;
    // }
    
    pms_data.valid = true;
    
    // 打印调试信息
    Serial.printf("PM1.0: %d, PM2.5: %d, PM10: %d\n", 
                 pms_data.pm1_0, pms_data.pm2_5, pms_data.pm10_0);
                 
    return true;
}

void updateDisplay() {
    display.clearDisplay();
    
    // 标题 - 确保不会折行
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    const char* title = "AIR QUALITY";
    int16_t titleWidth = strlen(title) * 6;
    display.setCursor((SCREEN_WIDTH - titleWidth) / 2, 2);
    display.print(title);
    
    // 单条分隔线
    display.drawLine(0, 12, SCREEN_WIDTH, 12, SSD1306_WHITE);
    
    // PM2.5值显示
    String pm25Str = String(pms_data.pm2_5);
    int16_t valueX = SCREEN_WIDTH - (pm25Str.length() * 12) - 8;  // 右边距离8像素
    int16_t valueY = 22;
    
    // PM2.5标签
    display.setTextSize(1);
    display.setCursor(8, valueY + 4);
    display.print("PM2.5");
    
    // PM2.5数值(反色显示)
    int16_t boxWidth = pm25Str.length() * 12 + 4;
    int16_t boxHeight = 18;
    display.fillRect(valueX - 2, valueY - 1, boxWidth, boxHeight, SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
    display.setTextSize(2);
    display.setCursor(valueX, valueY);
    display.print(pm25Str);
    
    // PM10显示(保持在一行内)
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1);
    
    // PM10标签和值
    int16_t pm10Y = 48;
    display.setCursor(8, pm10Y);
    display.print("PM10");
    
    String pm10Str = String(pms_data.pm10_0);
    int16_t pm10ValueX = SCREEN_WIDTH - (pm10Str.length() * 6) - 35;  // 预留单位的空间
    display.setCursor(pm10ValueX, pm10Y);
    display.print(pm10Str);
    
    // 单位
    display.setCursor(SCREEN_WIDTH - 30, pm10Y);
    display.print("ug/m3");
    
    display.display();
}

void publishData() {
    StaticJsonDocument<200> doc;
    doc["pm25"] = pms_data.pm2_5;
    doc["pm10"] = pms_data.pm10_0;
    
    char buffer[200];
    serializeJson(doc, buffer);
    
    if (client.publish(mqtt_topic, buffer)) {
        Serial.println("MQTT数据发送成功");
        Serial.println(buffer);
    } else {
        Serial.println("MQTT数据发送失败");
    }
}

void reconnectMQTT() {
    while (!client.connected()) {
        Serial.println("尝试MQTT连接...");
        if (client.connect(mqtt_client_id, mqtt_username, mqtt_password)) {
            Serial.println("MQTT已连接");
        } else {
            Serial.print("MQTT连接失败, rc=");
            Serial.println(client.state());
            delay(2000);
        }
    }
}

void showInitScreen() {
    display.clearDisplay();
    
    // 计算文本位置以实现居中显示
    int16_t startY = (SCREEN_HEIGHT - 32) / 2;  // 32是两行文本的总高度(16px每行)
    
    display.setTextSize(2);
    
    // 显示 "Air PM2.5"
    const char* line1 = "Air PM2.5";
    int16_t line1Width = strlen(line1) * 12;  // 每个字符宽度约12像素
    display.setCursor((SCREEN_WIDTH - line1Width) / 2, startY);
    display.println(line1);
    
    // 显示 "Monitor"
    const char* line2 = "Monitor";
    int16_t line2Width = strlen(line2) * 12;
    display.setCursor((SCREEN_WIDTH - line2Width) / 2, startY + 16);
    display.println(line2);
    
    display.display();
    delay(2000);
}

传感器手册

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263298.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

怎么将pdf中的某一个提取出来?介绍几种提取PDF中页面的方法

怎么将pdf中的某一个提取出来&#xff1f;传统上&#xff0c;我们可能通过手动截取屏幕或使用PDF阅读器的复制功能来提取信息&#xff0c;但这种方法往往不够精确&#xff0c;且无法保留原文档的排版和格式。此外&#xff0c;很多时候我们需要提取的内容可能涉及多个页面、多个…

2024微博用户消费趋势报告:七成城市用户更爱用微博

文 | 魏力 发布 | 大力财经 站在岁末回首这一年&#xff0c;在信息浪潮的汹涌翻涌之下&#xff0c;社交媒体平台犹如社会经济的晴雨表&#xff0c;精准地折射出大众生活与消费的万千景象。近日&#xff0c;大力财经看到一份报告&#xff0c;微博发布了《2024微博用户消费趋势…

#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

【数据安全】如何保证其安全

数据安全风险 数字经济时代&#xff0c;数据已成为重要的生产要素。智慧城市、智慧政务的建设&#xff0c;正以数据为核心&#xff0c;推动城市管理的智能化和公共服务的优化。然而&#xff0c;公共数据开放共享与隐私保护之间的矛盾日益凸显&#xff0c;如何在确保数据安全的…

武汉市电子信息与通信工程职称公示了

2024年武汉市电子信息与通信工程专业职称公示了&#xff0c;本次公示通过人员有109人。 基本这已经是今年武汉市工程相关职称最后公示了&#xff0c;等待出证即可。 为什么有人好奇&#xff0c;一样的资料&#xff0c;都是业绩、论文等&#xff0c;有的人可以过&#xff0c;有的…

勤研低代码平台:高效数据集成助力企业数字化转型

在数字化转型的浪潮中&#xff0c;企业对高效开发工具的需求日益增长。勤研低代码平台强大的开发能力和灵活的数据集成方案&#xff0c;是企业提升效率、降低成本的理想选择。数据集成作为勤研低代码平台的核心功能之一&#xff0c;为企业提供了高效整合和利用数据的能力&#…

【毕业设计】A079-基于Java的影院订票系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看项目链接获取⬇️&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600个选题ex…

大腾智能受邀出席南京工业软件云工程应用创新中心工业软件生态应用推广大会并领奖

12月18日&#xff0c;南京工业软件云工程应用创新中心工业软件生态应用推广大会在南京江北新区圆满召开。本次大会由南京江北新区管委会主办&#xff0c;南京工业软件云工程应用创新中心、南京江北新区智能制造产业发展管理办公室联合承办&#xff0c;华为云计算技术有限公司支…

EasyPlayer.js播放器Web播放H.265要兼顾哪些方面?

在数字化时代&#xff0c;流媒体技术已经成为信息传播和娱乐消费的重要方式。随着互联网技术的飞速发展和移动设备的普及&#xff0c;流媒体服务正在重塑我们的生活和工作方式。从视频点播、在线直播到音乐流媒体&#xff0c;流媒体技术的广泛应用不仅改变了内容的分发和消费模…

fabric.js

目录 一、在canvas上画简单的图形 二、在canvas上用路径(Path)画不规则图形 三、在canvas上插入图片并设置旋转属性(angle) 四、让元素动起来(animate) 五、图像过滤器(filters)让图片多姿多彩 六、颜色模式(Color)和相互转换(toRgb、toHex) 七、对图形的渐变填充(Gradi…

白话AI大模型(LLM)原理

大模型&#xff08;例如 GPT-4或类似的深度学习模型&#xff09;是基于神经网络的系统&#xff0c;用于理解、生成文本、图像或其他数据类型。其工作原理可以分为以下几个核心步骤&#xff0c;我将通过易于理解的例子逐一解释。 1. 神经网络的基本概念 大模型背后有一个非常庞…

基于海思soc的智能产品开发(巧用mcu芯片)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 对于开发车规级嵌入式软件的同学来说&#xff0c;socmcu这样的组合&#xff0c;他们并不陌生。但是传统的工业领域&#xff0c;比如发动机、医疗或…

力扣438-找到字符串中所有字母异位词

力扣438-找到字符串中所有字母异位词 力扣438-找到字符串中所有字母异位词原题地址&#xff1a;https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/ 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找到 s 中所有 p 的 异位词的子串&#x…

C#代码实现把中文录音文件(.mp3 .wav)转为文本文字内容

我们有一个中文录音文件.mp3格式或者是.wav格式&#xff0c;如果我们想要提取录音文件中的文字内容&#xff0c;我们可以采用以下方法&#xff0c;不需要使用Azure Speech API 密钥注册通过离线的方式实现。 1.首先我们先在NuGet中下载两个包 NAudio 2.2.1、Whisper.net 1.7.3…

数据可视化-2. 条形图

目录 1. 条形图适用场景分析 1.1 比较不同类别的数据 1.2 展示数据分布 1.3 强调特定数据点 1.4 展示时间序列数据的对比 1.5 数据可视化教育 1.6 特定领域的应用 2. 条形图局限性 3. 条形图图代码实现 3.1 Python 源代码 3.2 条形图效果&#xff08;网页显示&#…

基于Redis的网关鉴权方案与性能优化

文章目录 前言一、微服务鉴权1.1 前端权限检查1.2 后端权限检查1.3 优缺点 二、网关鉴权2.1 接口权限存储至Redis2.2 网关鉴权做匹配 总结 前言 在微服务架构中&#xff0c;如何通过网关鉴权结合Redis缓存提升权限控制的效率与性能。首先&#xff0c;文章对比了两种常见的权限…

Ansible 批量管理华为 CE 交换机

注&#xff1a;本文为 “Ansible 管理华为 CE 交换机” 相关文章合辑。 使用 CloudEngine - Ansible 批量管理华为 CE 交换机 wsf535 IP 属地&#xff1a;贵州 2018.02.05 15:26:05 总体介绍 Ansible 是一个开源的自动化运维工具&#xff0c;AnsibleWorks 成立于 2012 年&a…

使用自定义分光比对分束器进行建模

分束器将一束光分成两个或多个单独的光束。它常用于各种光学系统&#xff0c;例如显微镜、干涉仪和成像设备。分束器可以由不同的材料制成&#xff0c;通常涂有金属或介电材料的薄层&#xff0c;以达到所需的分束效果。分束器的两种流行形式是立方体和板式。立方体分束器由两个…

C++对象数组对象指针对象指针数组

一、对象数组 对象数组中的每一个元素都是同类的对象&#xff1b; 例1 对象数组成员的初始化 #include<iostream> using namespace std;class Student { public:Student( ){ };Student(int n,string nam,char s):num(n),name(nam),sex(s){};void display(){cout<&l…

SQL进阶技巧:如何计算商品需求与到货队列表进出计划?

目录 0 需求描述 1 数据准备 2 问题分析 3 小结 累计到货数量计算 出货数量计算 剩余数量计算 0 需求描述 假设现有多种商品的订单需求表 DEMO_REQUIREMENT&#xff0c;以及商品的到货队列表 DEMO_ARR_QUEUE&#xff0c;要求按照业务需要&#xff0c;设计一个报表&#…