Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
| ||||||
|
In this project I show how to build a particle detector with data display, data backup on SD card and IoT. Visually a NeoPixel ring display indicates the air quality.
Air quality is an increasingly important concern today. there are systems to measure the dust rate but they are very expensive.
There are low-cost, high-quality particle detectors on the market, as shown by some studies.
https://www.atmos-meas-tech.net/11/4823/2018/amt-11-4823-2018.pdf
I therefore decided to build a device capable of measuring the number of particles by size classes (0.5µm to 10 µm), visually with a simple display of the result (NeoPixel ring), a more detailed display on a TFT screen, and a time-stamped backup on an SD card..
In addition I have added a bluetooth communication module to be able to communicate with an android application and thus publish the results on an IoT server.
The overall cost of the whole does not exceed 60€.
Result:You can see how the system works in the video below.
I hope that you like this project!
I developed a new software with display of the curves in real time for each class of particles whose here is the result.
#include <SoftwareSerial.h>
#include <Wire.h> // Bibliothque pour l'I2C
#include "RTClib.h" // Bibliothque pour le module RTC
RTC_DS1307 RTC;
#include <Adafruit_NeoPixel.h>
// Which pin on the Arduino is connected to the NeoPixels?
#define PIN 6 // On Trinket or Gemma, suggest changing this to 1
// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS 24 // Popular NeoPixel ring size
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
uint32_t vert = pixels.Color(0, 250, 0);
uint32_t orange = pixels.Color(250, 250, 0);
uint32_t rouge = pixels.Color(255, 0, 0);
SoftwareSerial pmsSerial(2, 3);
#define cs 10
#define dc 9
#define rst 8 // you can also connect this to the Arduino reset
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library
#include <SPI.h>
#include<SD.h>
const int cs_sd=4;
int temps; // temps d'acquisition
double tempsInit; // initialisation du timer au dmarrage du loop()
#if defined(__SAM3X8E__)
#undef __FlashStringHelper::F(string_literal)
#define F(string_literal) string_literal
#endif
// Option 1: use any pins but a little slower
//Adafruit_ST7735 tft = Adafruit_ST7735(cs, dc, mosi, sclk, rst);
// Option 2: must use the hardware SPI pins
// (for UNO thats sclk = 13 and sid = 11) and pin 10 must be
// an output. This is much faster - also required if you want
// to use the microSD card (see the image drawing example)
Adafruit_ST7735 tft = Adafruit_ST7735(cs, dc, rst);
float nombre_leds=0;
void setup() {
Serial.begin(9600);
// Initialise la liaison I2C
Wire.begin();
// Initialise le module RTC
RTC.begin();
Serial.print("init SD");
delay(1000);
if(!SD.begin(cs_sd)) //Condition vrifiant si la carte SD est prsente dans l'appareil
{
Serial.print("Defaut SD");
return;
}
Serial.print("Carte SD OK");
File data = SD.open("donnees.txt",FILE_WRITE); // Ouvre le fichier "donnees.txt"
data.println(""); data.println("Dmarrage acquisition"); // Ecrit dans ce fichier
data.close();
tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab
Serial.println("init");
// our debugging output
tft.fillScreen(ST7735_BLACK);
// sensor baud rate is 9600
pmsSerial.begin(9600);
pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
pixels.setBrightness(2);
}
struct pms5003data {
uint16_t framelen;
uint16_t pm10_standard, pm25_standard, pm100_standard;
uint16_t pm10_env, pm25_env, pm100_env;
uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
uint16_t unused;
uint16_t checksum;
};
struct pms5003data data;
void loop()
{
pixels.clear(); // Set all pixel colors to 'off'
DateTime now=RTC.now(); //Rcupre l'heure et le date courante
//affiche_date_heure(now);
temps = ((millis() - tempsInit))/1000 ; // Dmarrage du chrono
if (readPMSdata(&pmsSerial)) {
tft.fillScreen(ST7735_BLACK);
tft.setCursor(10, 5);
tft.setTextColor(ST7735_WHITE);
tft.println(" nbre parts/ 0.1 l");
tft.setCursor(10, 17);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("0.3 um ");tft.print(data.particles_03um);
tft.setCursor(10, 29);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("0.5 um ");tft.print(data.particles_05um);
tft.setCursor(10, 41);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("1.0 um ");tft.print(data.particles_10um);
tft.setCursor(10, 53);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("2.5 um ");tft.print(data.particles_25um);
tft.setCursor(10, 65);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("5.0 um ");tft.print(data.particles_50um);
tft.setCursor(10, 77);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("10 um ");tft.print(data.particles_100um);
tft.setCursor(2, 89);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("PM 1.0 ");tft.setTextColor(ST7735_YELLOW);tft.print(data.pm10_standard);tft.setTextColor(ST7735_GREEN);tft.print(" microg/m3");
tft.setCursor(2, 100);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("PM 2.5 ");tft.setTextColor(ST7735_YELLOW);tft.print(data.pm25_standard);tft.setTextColor(ST7735_GREEN);tft.print(" microg/m3");
tft.setCursor(2, 110);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("PM 10 ");tft.setTextColor(ST7735_YELLOW);tft.print(data.pm100_standard);tft.setTextColor(ST7735_GREEN);tft.print(" microg/m3");
tft.setCursor(10, 5);
tft.setTextColor(ST7735_WHITE);
tft.setTextSize(1);
tft.println(" nbre parts/ 0.1 l");
// Serial.print(temps);
// Serial.print (" ");
Serial.print ("#");
Serial.print ("03m ");
Serial.print(data.particles_03um);
Serial.print (" ");
Serial.print ("05m ");
Serial.print(data.particles_05um);
Serial.print (" ");
Serial.print ("1m ");
Serial.print(data.particles_10um);
Serial.print (" ");
Serial.print ("25m ");
Serial.print(data.particles_25um);
Serial.print (" ");
Serial.print ("50m ");
Serial.print(data.particles_50um);
Serial.print (" ");
Serial.print ("100m ");
Serial.print(data.particles_100um);
Serial.println (" ");
nombre_leds =int (((float (data.particles_03um)/65535)*24));
//nombre_leds =(8);
Serial.println (nombre_leds);
if ((nombre_leds<=8) and (nombre_leds>=1)){
pixels.fill(vert , 0, nombre_leds);
}
else if ((nombre_leds<=16) and (nombre_leds>=8)) {
pixels.fill(vert , 0, 8);
pixels.fill(orange , 8, ((nombre_leds)-8));
}
else if (nombre_leds>16) {
pixels.fill(vert , 0, 8);
pixels.fill(orange , 8, 8);
pixels.fill(rouge , 16, ((nombre_leds)-16));
}
else if (nombre_leds<=1) {
pixels.fill(vert , 0, 1);
}
pixels.show(); // Send the updated pixel colors to the hardware.
// Dfinition donnes
String PM03=String(data.particles_03um);
String PM05=String(data.particles_05um);
String PM10=String(data.particles_10um);
String PM25=String(data.particles_25um);
String PM50=String(data.particles_50um);
String PM100=String(data.particles_100um);
String PMS10=String(data.pm10_standard);
String PMS25=String(data.pm25_standard);
String PMS100=String(data.pm100_standard);
String Temps=String(temps);
//Ecriture des donnes dans le fichier texte
File data=SD.open("donnees.txt",FILE_WRITE);
data.println( Temps + " " + PM03+ " " + PM05 +" " +PM10+" " +PM25+" "+PM50+" " +PM100+" "+PMS10+" "+PMS25+" "+PMS100+" ");
data.close();
}
}
boolean readPMSdata(Stream *s) {
if (! s->available()) {
return false;
}
// Read a byte at a time until we get to the special '0x42' start-byte
if (s->peek() != 0x42) {
s->read();
return false;
}
// Now read all 32 bytes
if (s->available() < 32) {
return false;
}
uint8_t buffer[32];
uint16_t sum = 0;
s->readBytes(buffer, 32);
// get checksum ready
for (uint8_t i=0; i<30; i++) {
sum += buffer[i];
}
/* debugging
for (uint8_t i=2; i<32; i++) {
Serial.print("0x"); Serial.print(buffer[i], HEX); Serial.print(", ");
}
Serial.println();
*/
// The data comes in endian'd, this solves it so it works on all platforms
uint16_t buffer_u16[15];
for (uint8_t i=0; i<15; i++) {
buffer_u16[i] = buffer[2 + i*2 + 1];
buffer_u16[i] += (buffer[2 + i*2] << 8);
}
// put it into a nice struct :)
memcpy((void *)&data, (void *)buffer_u16, 30);
if (sum != data.checksum) {
Serial.println("Checksum failure");
return false;
}
// success!
return true;
}
//Converti le numro de jour en jour /!\ la semaine commence un dimanche
String donne_jour_semaine(uint8_t j){
switch(j){
case 0: return "DIM";
case 1: return "LUN";
case 2: return "MAR";
case 3: return "MER";
case 4: return "JEU";
case 5: return "VEN";
case 6: return "SAM";
default: return " ";
}
}
// affiche la date et l'heure sur l'cran
void affiche_date_heure(DateTime datetime){
// Date
String jour = donne_jour_semaine(datetime.dayOfTheWeek()) + " " +
Vers2Chiffres(datetime.day())+ "/" +
Vers2Chiffres(datetime.month())+ "/" +
String(datetime.year(),DEC);
// heure
String heure = "";
heure = Vers2Chiffres(datetime.hour())+ ":" +
Vers2Chiffres(datetime.minute())+ ":" +
Vers2Chiffres(datetime.second());
// Serial.print(jour);
// Serial.print(" ");
// Serial.print(heure);
//Serial.print(" ");
File data=SD.open("donnees.txt",FILE_WRITE);
data.print(jour + " " + heure+" " );
data.close();
tft.setCursor(2, 120);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.print("date ");tft.setTextColor(ST7735_YELLOW);tft.print(jour);tft.setTextColor(ST7735_GREEN);tft.setCursor(2, 130);tft.print(" heure");tft.setTextColor(ST7735_YELLOW);tft.print(heure);
delay(500);
}
//permet d'afficher les nombres sur deux chiffres
String Vers2Chiffres(byte nombre) {
String resultat = "";
if(nombre < 10)
resultat = "0";
return resultat += String(nombre,DEC);
}
#include <SoftwareSerial.h>
#include <Wire.h> // Bibliothque pour l'I2C
#include "RTClib.h" // Bibliothque pour le module RTC
RTC_DS1307 RTC;
#include <Adafruit_NeoPixel.h>
// Which pin on the Arduino is connected to the NeoPixels?
#define PIN 6 // On Trinket or Gemma, suggest changing this to 1
// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS 24 // Popular NeoPixel ring size
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
uint32_t vert = pixels.Color(0, 250, 0);
uint32_t orange = pixels.Color(250, 250, 0);
uint32_t rouge = pixels.Color(255, 0, 0);
SoftwareSerial pmsSerial(2, 3);
#define cs 10
#define dc 9
#define rst 8 // you can also connect this to the Arduino reset
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library resolution 128X160
#include <SPI.h>
#include<SD.h>
const int cs_sd=4;
int temps; // temps d'acquisition
double tempsInit; // initialisation du timer au dmarrage du loop()
#if defined(__SAM3X8E__)
#undef __FlashStringHelper::F(string_literal)
#define F(string_literal) string_literal
#endif
// Option 1: use any pins but a little slower
//Adafruit_ST7735 tft = Adafruit_ST7735(cs, dc, mosi, sclk, rst);
// Option 2: must use the hardware SPI pins
// (for UNO thats sclk = 13 and sid = 11) and pin 10 must be
// an output. This is much faster - also required if you want
// to use the microSD card (see the image drawing example)
Adafruit_ST7735 tft = Adafruit_ST7735(cs, dc, rst);
float nombre_leds=0;
int x=2;
void setup() {
Serial.begin(9600);
// Initialise la liaison I2C
Wire.begin();
// Initialise le module RTC
RTC.begin();
Serial.print("init SD");
delay(1000);
if(!SD.begin(cs_sd)) //Condition vrifiant si la carte SD est prsente dans l'appareil
{
Serial.print("Defaut SD");
return;
}
Serial.print("Carte SD OK");
File data = SD.open("donnees.txt",FILE_WRITE); // Ouvre le fichier "donnees.txt"
data.println(""); data.println("Dmarrage acquisition"); // Ecrit dans ce fichier
data.close();
tft.initR(INITR_GREENTAB); // initialize a ST7735S chip, black tab
Serial.println("init");
// our debugging output
tft.fillScreen(ST7735_BLACK);
// sensor baud rate is 9600
pmsSerial.begin(9600);
pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
pixels.setBrightness(2);
}
struct pms5003data {
uint16_t framelen;
uint16_t pm10_standard, pm25_standard, pm100_standard;
uint16_t pm10_env, pm25_env, pm100_env;
uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
uint16_t unused;
uint16_t checksum;
};
struct pms5003data data;
void loop()
{
pixels.clear(); // Set all pixel colors to 'off'
DateTime now=RTC.now(); //Rcupre l'heure et le date courante
affiche_date_heure(now);
temps = ((millis() - tempsInit))/1000 ; // Dmarrage du chrono
if (readPMSdata(&pmsSerial)) {
tft.setCursor(10, 17);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.setCursor(10, 29);
tft.print("0.3 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_03um);
courbe();
tft.setCursor(10, 41);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("0.5 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_05um);
tft.setCursor(10, 53);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("1.0 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_10um);
tft.setCursor(10, 65);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("2.5 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_25um);
tft.setCursor(10, 77);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("5.0 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_50um);
tft.setCursor(10, 89);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("10.0 um ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.particles_100um);
tft.setCursor(10, 101);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.print("PM 10 ");tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(data.pm100_standard);tft.setTextColor(ST7735_GREEN,ST7735_BLACK);tft.print(" mic/m3 ");
tft.drawLine(0,113,0,158,ST7735_RED);
tft.drawLine(0,158,128,158,ST7735_RED);
nombre_leds =int (((float (data.particles_03um)/65535)*24));
//nombre_leds =(8);
Serial.println (nombre_leds);
if ((nombre_leds<=8) and (nombre_leds>=1)){
pixels.fill(vert , 0, nombre_leds);
}
else if ((nombre_leds<=16) and (nombre_leds>=8)) {
pixels.fill(vert , 0, 8);
pixels.fill(orange , 8, ((nombre_leds)-8));
}
else if (nombre_leds>16) {
pixels.fill(vert , 0, 8);
pixels.fill(orange , 8, 8);
pixels.fill(rouge , 16, ((nombre_leds)-16));
}
else if (nombre_leds<=1) {
pixels.fill(vert , 0, 1);
}
pixels.show(); // Send the updated pixel colors to the hardware.
// Dfinition donnes
String PM03=String(data.particles_03um);
String PM05=String(data.particles_05um);
String PM10=String(data.particles_10um);
String PM25=String(data.particles_25um);
String PM50=String(data.particles_50um);
String PM100=String(data.particles_100um);
String PMS10=String(data.pm10_standard);
String PMS25=String(data.pm25_standard);
String PMS100=String(data.pm100_standard);
String Temps=String(temps);
//Ecriture des donnes dans le fichier texte
File data=SD.open("donnees.txt",FILE_WRITE);
data.println( Temps + " " + PM03+ " " + PM05 +" " +PM10+" " +PM25+" "+PM50+" " +PM100+" "+PMS10+" "+PMS25+" "+PMS100+" ");
data.close();
}
}
boolean readPMSdata(Stream *s) {
if (! s->available()) {
return false;
}
// Read a byte at a time until we get to the special '0x42' start-byte
if (s->peek() != 0x42) {
s->read();
return false;
}
// Now read all 32 bytes
if (s->available() < 32) {
return false;
}
uint8_t buffer[32];
uint16_t sum = 0;
s->readBytes(buffer, 32);
// get checksum ready
for (uint8_t i=0; i<30; i++) {
sum += buffer[i];
}
// The data comes in endian'd, this solves it so it works on all platforms
uint16_t buffer_u16[15];
for (uint8_t i=0; i<15; i++) {
buffer_u16[i] = buffer[2 + i*2 + 1];
buffer_u16[i] += (buffer[2 + i*2] << 8);
}
// put it into a nice struct :)
memcpy((void *)&data, (void *)buffer_u16, 30);
if (sum != data.checksum) {
Serial.println("Checksum failure");
return false;
}
// success!
return true;
}
//Converti le numro de jour en jour /!\ la semaine commence un dimanche
String donne_jour_semaine(uint8_t j){
switch(j){
case 0: return "DIM";
case 1: return "LUN";
case 2: return "MAR";
case 3: return "MER";
case 4: return "JEU";
case 5: return "VEN";
case 6: return "SAM";
default: return " ";
}
}
// affiche la date et l'heure sur l'cran
void affiche_date_heure(DateTime datetime){
// Date
String jour = donne_jour_semaine(datetime.dayOfTheWeek()) + " " +
Vers2Chiffres(datetime.day())+ "/" +
Vers2Chiffres(datetime.month())+ "/" +
String(datetime.year(),DEC);
// heure
String heure = "";
heure = Vers2Chiffres(datetime.hour())+ ":" +
Vers2Chiffres(datetime.minute())+ ":" +
Vers2Chiffres(datetime.second());
File data=SD.open("donnees.txt",FILE_WRITE);
data.print(jour + " " + heure+" " );
data.close();
tft.setCursor(18, 5);
tft.setTextColor(ST7735_GREEN,ST7735_BLACK);
tft.setTextSize(1);
tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(jour);
tft.setCursor(30, 17);tft.setTextColor(ST7735_CYAN,ST7735_BLACK);tft.print(heure);
}
//permet d'afficher les nombres sur deux chiffres
String Vers2Chiffres(byte nombre) {
String resultat = "";
if(nombre < 10)
resultat = "0";
return resultat += String(nombre,DEC);
}
void courbe() {
int nouvelleValeur03;
int nouvelleValeur05;
int nouvelleValeur10;
int nouvelleValeur25;
int nouvelleValeur50;
nouvelleValeur03 = map((data.particles_03um), 2, 65535, 158, 113);
nouvelleValeur05 = map((data.particles_05um), 2, 65535, 158, 113);
nouvelleValeur10 = map((data.particles_10um), 2, 65535, 158, 113);
nouvelleValeur25 = map((data.particles_25um), 2, 65535, 158, 113);
nouvelleValeur50 = map((data.particles_50um), 2, 65535, 158, 113);
x++;
tft.drawPixel(x,nouvelleValeur03,ST7735_CYAN);
tft.drawPixel(x,nouvelleValeur05,ST7735_YELLOW);
tft.drawPixel(x,nouvelleValeur10,ST7735_RED);
tft.drawPixel(x,nouvelleValeur25,ST7735_WHITE);
tft.drawPixel(x,nouvelleValeur50,ST7735_BLUE);
if (x>123) {
x=2;
tft.fillRect(2,113,126,45,ST7735_BLACK);
}
}
Comments