Elsmen
Published © MIT

Air Quality Portable Monitoring

A drone-mounted air quality monitor. Real-time data for cleaner skies. Join us in the fight for healthier air. "

IntermediateShowcase (no instructions)Over 1 day59
Air Quality Portable Monitoring

Things used in this project

Hardware components

Photon
Particle Photon
×1
Grove - LoRa Radio 868MHz
Seeed Studio Grove - LoRa Radio 868MHz
×2
Adafruit mini gps
×1
Fermion: Multi-function Environmental Module - CCS811+BME280 (Breakout)
DFRobot Fermion: Multi-function Environmental Module - CCS811+BME280 (Breakout)
×1
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×2
Li-Ion Battery 1000mAh
Li-Ion Battery 1000mAh
×2
Grove - Laser PM2.5 Sensor (HM3301)
Seeed Studio Grove - Laser PM2.5 Sensor (HM3301)
×1

Software apps and online services

3D Solidworks
VS Code
Microsoft VS Code
Video Premier Rush
Adobe illustrator

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Custom parts and enclosures

Enclosure Lid for transmitter

Enclosure for transmitter

Schematics

Wiring schematic for both receiver and transmitter

Schematics

Code

Transmitter code

C/C++
AES password must be generated for these to work
/*
 * Project Portable Air Quality Monitoring devices
 * Description: will monitor air quality, temp, humidity and gps locations while sampling the air quality
 * Author: Elsmen Aragon
 * Date: 24-MAR-2023
 */

// NOTE: The Adafruit_GPS header file has been modified to work with the Particle environment DO NOT INSTALL Adafruit_GPS USE the library from 14_04

#include "Particle.h"
#include "Adafruit_GPS.h"
#include "IoTTimer.h"
#include "credentials.h"
#include "Adafruit_BME280.h"

#define SENSOR_ADDRESS 0x40 // Change this to the address obtained from the I2C scan

Adafruit_BME280 myReading; //Defining bme object mine is called myReading
Adafruit_GPS GPS(&Wire);
IoTTimer sampleTimer;// timer for led onboard
// Define User and Credentials
String password = "------------------"; // AES128 password
String myName = "borrowed";

// Define Constants
const int RADIOADDRESS = 0xA2; // it will be a value between 0xA1 and 0xA9 for my case
const int TIMEZONE = -6;
const unsigned int UPDATEGPS = 5000;
const int RADIONETWORK = 1;    // range of 0-16
const int SENDADDRESS = 0xA1;   // address of radio to be sent to

//Delcare variable for air quality sensor
byte data[29];
//int aqOne;
//int aqOneResult;
const int AQTIMER = 5000;
int aq, aq2, aq10;

// Declare Variables for gps
float lat, lon, alt;
int sat;
unsigned int lastGPS;

//Declare varibles for BME280
const int HEXADDRESS = 0X76;
bool status;
float temperature, humidity;

// Declare Functions
void getBme(float *temperature, float *humidity);
void getAirQuality(int *aq, int *aq2, int *aq10);
//int getAirQuality();
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites);
void sendData(float temp, int aq, float latt, float lonn, float altt, float humidd, int aq2, int aq10);
void reyaxSetup(String password);


SYSTEM_MODE(SEMI_AUTOMATIC);

void setup() {
  Serial.begin(9600);
  waitFor(Serial.isConnected, 5000);
  Serial1.begin(115200);
  reyaxSetup(password);
  //Initialize GPS
  GPS.begin(0x10);  // The I2C address to use is 0x10 from I2C scan
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); 
  GPS.sendCommand(PGCMD_ANTENNA);
  delay(1000);
  GPS.println(PMTK_Q_RELEASE);

  // Initialize I2C communication for air Quality HM3301
  Wire.beginTransmission(SENSOR_ADDRESS);
  if (Wire.endTransmission() == 0) {
    Serial.println("Sensor found at address 0x40");
  } 
  else {
    Serial.println("No sensor found at address 0x40.");
  }
  //BME Check
  status = myReading.begin(HEXADDRESS);
  if ( status == false ) {
    Serial.printf(" BME280 at address 0x%02x X failed to start ", HEXADDRESS );
  }
  //Timer for sample air quality + other data
  sampleTimer.startTimer(AQTIMER);
}

void loop() {
  //Get data from GPS unit (best if you do this continuously) 
  GPS.read();
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA())) {
       return;
    }   
  }
  if (millis() - lastGPS > UPDATEGPS) {
    lastGPS = millis(); // reset the timer
    getGPS(&lat,&lon,&alt,&sat);
    Serial.printf("\n=================================================================\n");
    Serial.printf("Lat: %0.6f, Lon: %0.6f, Alt: %0.6f, Satellites: %i\n",lat, lon, alt, sat);
    Serial.printf("=================================================================\n\n");
  }
  if(sampleTimer.isTimerReady()){
    getAirQuality(&aq, &aq2, &aq10);
    //aqOneResult = getAirQuality();
    Serial.printf("Lat: %0.6f, Lon: %0.6f, Alt: %0.6f, Satellites: %i\n",lat, lon, alt, sat);
    getBme(&temperature, &humidity);
    Serial.printf("Humidity: %.2f%%\nTempF: %.2f\n", humidity, temperature);
    //tempResult = getBme();
    //Serial.printf("TempF = %02f\n", tempResult);
    sampleTimer.startTimer(AQTIMER);
    sendData(temperature, aq, lat, lon, alt, humidity, aq2, aq10);
  }
}

void getAirQuality(int *aq, int *aq2, int *aq10){
  //Serial.printf("im here");
  Wire.requestFrom(SENSOR_ADDRESS, 29); // Request 29 bytes of data
  delay(100); // Allow time for sensor to respond
    // Check if data is available
  if (Wire.available() >= 29) {  //??
  // Read response from sensor
    for (int i = 0; i < 29; i++) {
      data[i] = Wire.read();
    }
    *aq = data[5] | data[6];
    *aq2 = data[7] | data[8];
    *aq10 = data[11] | data[12];
    Serial.printf("sensor# = %i\nPM 1.0 = %i\nPM 2.5 = %i\nPM c10 = %i\n", data [3] | data[4], data[5] | data[6], data[7] | data[8], data[11] | data[12]);
  }     
}
// Get GPS data
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites) {
  int theHour;
  theHour = GPS.hour + TIMEZONE;
  if (theHour < 0) {
    theHour = theHour + 24;
  }

  Serial.printf("Time: %02i:%02i:%02i:%03i\n", theHour, GPS.minute, GPS.seconds, GPS.milliseconds);
  Serial.printf("Dates: %02i-%02i-%02i\n", GPS.month, GPS.day, GPS.year);
  Serial.printf("Fix: %i, Quality: %i", (int)GPS.fix, (int)GPS.fixquality);
  if (GPS.fix) {
    *latitude = GPS.latitudeDegrees;
    *longitude = GPS.longitudeDegrees;
    *altitude = GPS.altitude;
    *satellites = (int)GPS.satellites;
  
  }
}
void getBme(float *temperature, float *humidity){
  float tempC, pressPA, humidRH, tempF, convertedPA;
  tempC = myReading.readTemperature ();
  pressPA = myReading.readPressure ();
  convertedPA = pressPA*0.00029530;
  humidRH = myReading.readHumidity ();
  *humidity = humidRH;
  tempF = (tempC*9/5)+32;
  *temperature = tempF;
}
// Send data to IoT Classroom LoRa basestation in format expected
void sendData(float temp, int aq, float latt, float lonn, float altt, float humidd, int aq2, int aq10) {
  char buffer[60];
  sprintf(buffer, "AT+SEND=%i,60,%0.2f,%i,%0.6f,%0.6f,%0.6f,%0.2f,%i,%i\r\n", SENDADDRESS, temp, aq, latt, lonn, altt, humidd, aq2, aq10);
  Serial1.printf("%s",buffer);
  //Serial.printf("buff: %s", buffer);
  //Serial1.println(buffer); 
  delay(1000);
  if (Serial1.available() > 0)
  {
    Serial.printf("Awaiting Reply from send\n");
    String reply = Serial1.readStringUntil('\n');
    Serial.printf("Send reply: %s\n", reply.c_str());
  }
}

// Configure and Initialize Reyax LoRa module
void reyaxSetup(String password) {
  // following four paramaters have most significant effect on range
  // recommended within 3 km: 10,7,1,7
  // recommended more than 3 km: 12,4,1,7
  const int SPREADINGFACTOR = 10;
  const int BANDWIDTH = 7;
  const int CODINGRATE = 1;
  const int PREAMBLE = 7;
  String reply; // string to store replies from module

  Serial1.printf("AT+ADDRESS=%i\r\n", RADIOADDRESS); // set the radio address
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from address\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply address: %s\n", reply.c_str());
  }

  Serial1.printf("AT+NETWORKID=%i\r\n", RADIONETWORK); // set the radio network
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from networkid\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply network: %s\n", reply.c_str());
  }

  Serial1.printf("AT+CPIN=%s\r\n", password.c_str());
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from password\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply: %s\n", reply.c_str());
  }

  Serial1.printf("AT+PARAMETER=%i,%i,%i,%i\r\n", SPREADINGFACTOR, BANDWIDTH, CODINGRATE, PREAMBLE);
  delay(200);
  if (Serial1.available() > 0) {
    reply = Serial1.readStringUntil('\n');
    Serial.printf("reply: %s\n", reply.c_str());
  }

  Serial1.printf("AT+ADDRESS?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Address: %s\n", reply.c_str());
  }

  Serial1.printf("AT+NETWORKID?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Network: %s\n", reply.c_str());
  }

  Serial1.printf("AT+PARAMETER?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("RadioParameters: %s\n", reply.c_str());
  }

  Serial1.printf("AT+CPIN?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Password: %s\n", reply.c_str());
  }
}

Receiver code

C/C++
/*
 * Project Portable Air Quality Monitoring devices
 * Description: will monitor air quality, temp, humidity and gps locations while sampling the air quality this is for the base station
 * Author: Elsmen Aragon
 * Date: 24-MAR-2023
 */

#include "Particle.h"
#include <Adafruit_MQTT.h>
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include "credentials.h"
#include <JsonParserGeneratorRK.h>
#include "IoTTimer.h"


String password = "----------------------"; // AES128 password
String myName = "Thor2";
const int RADIOADDRESS = 0xA1; // It will be a value between 0xC1 - 0xCF can be anything though up to 255
const int TIMEZONE = -6;
unsigned int last, lastTime;
IoTTimer publishTimer;
const int PUBTIME = 12000;

// Define Constants
const int RADIONETWORK = 1;    // range of 0-16
const int SENDADDRESS = 0xA2;   // address of radio to be sent to if I send back to another LoRa device

void reyaxSetup(String password);
//void sleepULP();

/************ Global State (you don't need to change this!) ***   ***************/ 
TCPClient TheClient; 

// Setup the MQTT client class by passing in the WiFi client and MQTT server and login details. 
Adafruit_MQTT_SPARK mqtt(&TheClient,AIO_SERVER,AIO_SERVERPORT,AIO_USERNAME,AIO_KEY); 
// Setup Feeds to publish or subscribe 
// Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname> 
Adafruit_MQTT_Publish tempDrone = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capTemp");
Adafruit_MQTT_Publish humidityDrone = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capHumidity");
Adafruit_MQTT_Publish airOneDrone = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capAirOne");
Adafruit_MQTT_Publish airTwoDrone= Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capAirTwo");
Adafruit_MQTT_Publish airTenDrone = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capAirTen");
Adafruit_MQTT_Publish gpsDrone = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/capGPS");


/************Declare Functions*************/
void MQTT_connect();
bool MQTT_ping();

//Declare function for json collector
void createEventPayLoad (float lat, float lon, float alt);

// Let Device OS manage the connection to the Particle Cloud comment out or put in manual when connecting to mqtt
//SYSTEM_MODE(AUTOMATIC);

void setup() {

    Wire.begin();
    Serial.begin(9600);
    waitFor(Serial.isConnected, 10000);
    Serial1.begin(115200);
    delay(5000);
    reyaxSetup(password);

    WiFi.on();
    WiFi.connect();
    while(WiFi.connecting()) {
      Serial.printf(".");
    }
    Serial.printf("\n\n");
    publishTimer.startTimer(PUBTIME);
}

void loop() {
  MQTT_connect();
  MQTT_ping();
  
  if (Serial1.available())  { // full incoming buffer: +RCV=203,50,mydata,
    //Serial.printf("here i am ");
    String parse0 = Serial1.readStringUntil('=');  //+RCV
    String parse1 = Serial1.readStringUntil(',');  // address received from device
    String parse2 = Serial1.readStringUntil(',');  // buffer length
    String parse3 = Serial1.readStringUntil(',');  // data received from remote Tempf
    String parse4 = Serial1.readStringUntil(',');  // data received from remote Air Quality PM1.0
    String parse5 = Serial1.readStringUntil(',');  // data received from remote Latitude
    String parse6 = Serial1.readStringUntil(',');  // data received from remote Longitude
    String parse7 = Serial1.readStringUntil(',');  // data received from remote altutude
    String parse8 = Serial1.readStringUntil(','); // data received from humidity
    String parse9 = Serial1.readStringUntil(',');  // data received from remote Air Quality PM2.5
    String parse10 = Serial1.readStringUntil('n');  // data received from remote Air Quality PM10.0

    if(publishTimer.isTimerReady()) {
    if (parse3.length() > 0 && parse4.length() > 0 && parse5.length() > 0) {
        // Print only if all parsed variables contain data
      Serial.printf("TempF: %s\nAir Quality1: %s\nLatitude: %s\nLongitude: %s\nAltutude: %s\nAQ2: %s\nAQten: %s\n", parse3.c_str(), parse4.c_str(), parse5.c_str(), parse6.c_str(), parse7.c_str(), parse8.c_str(), parse9.c_str(), parse10.c_str());
      createEventPayLoad (atof((char *)parse5.c_str()), atof((char *)parse6.c_str()), atof((char *)parse7.c_str())); //json package for GPS
      if(mqtt.Update()) {
        tempDrone.publish(atof((char *)parse3.c_str()));
        airOneDrone.publish(atof((char *)parse4.c_str()));
        airTwoDrone.publish(atof((char *)parse9.c_str()));
        airTenDrone.publish(atof((char *)parse10.c_str()));
        humidityDrone.publish(atof((char *)parse8.c_str()));
        //Serial.printf("Publishing %0.2f \n",atof((char *)parse3.c_str())); 
      }
    }
      publishTimer.startTimer(PUBTIME); 
      lastTime = millis();
    }
  }
  //sleepULP(); apparently this is not supported by the Photon2 on serial comm
}
  
void MQTT_connect() {
  int8_t ret;
 
  // Return if already connected.
  if (mqtt.connected()) {
    return;
  }
 
  Serial.print("Connecting to MQTT... ");
 
  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
       Serial.printf("Error Code %s\n",mqtt.connectErrorString(ret));
       Serial.printf("Retrying MQTT connection in 5 seconds...\n");
       mqtt.disconnect();
       delay(5000);  // wait 5 seconds and try again
  }
  Serial.printf("MQTT Connected!\n");
}

bool MQTT_ping() {
  static unsigned int last;
  bool pingStatus;

  if ((millis()-last)>120000) {
      Serial.printf("Pinging MQTT \n");
      pingStatus = mqtt.ping();
      if(!pingStatus) {
        Serial.printf("Disconnecting \n");
        mqtt.disconnect();
      }
      last = millis();
  }
  return pingStatus;
}
void createEventPayLoad (float lat, float lon, float alt) {
  JsonWriterStatic <256> jw ;{
    JsonWriterAutoObject obj (&jw);
    jw.insertKeyValue ("lat", lat);
    jw.insertKeyValue ("lon", lon);
    jw.insertKeyValue ("alt", alt);
  }
  if (mqtt.Update()) {
    gpsDrone.publish(jw.getBuffer());
  }
}

// Configure and Initialize Reyax LoRa module
void reyaxSetup(String password) {
  // following four paramaters have most significant effect on range
  // recommended within 3 km: 10,7,1,7
  // recommended more than 3 km: 12,4,1,7
  const int SPREADINGFACTOR = 10;
  const int BANDWIDTH = 7;
  const int CODINGRATE = 1;
  const int PREAMBLE = 7;
  String reply; // string to store replies from module

  Serial1.printf("AT+ADDRESS=%i\r\n", RADIOADDRESS); // set the radio address
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from address\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply address: %s\n", reply.c_str());
  }

  Serial1.printf("AT+NETWORKID=%i\r\n", RADIONETWORK); // set the radio network
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from networkid\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply network: %s\n", reply.c_str());
  }

  Serial1.printf("AT+CPIN=%s\r\n", password.c_str());
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply from password\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Reply: %s\n", reply.c_str());
  }

  Serial1.printf("AT+PARAMETER=%i,%i,%i,%i\r\n", SPREADINGFACTOR, BANDWIDTH, CODINGRATE, PREAMBLE);
  delay(200);
  if (Serial1.available() > 0) {
    reply = Serial1.readStringUntil('\n');
    Serial.printf("reply: %s\n", reply.c_str());
  }

  Serial1.printf("AT+ADDRESS?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Address: %s\n", reply.c_str());
  }

  Serial1.printf("AT+NETWORKID?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Network: %s\n", reply.c_str());
  }

  Serial1.printf("AT+PARAMETER?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("RadioParameters: %s\n", reply.c_str());
  }

  Serial1.printf("AT+CPIN?\r\n");
  delay(200);
  if (Serial1.available() > 0) {
    Serial.printf("Awaiting Reply\n");
    reply = Serial1.readStringUntil('\n');
    Serial.printf("Radio Password: %s\n", reply.c_str());
  }
}

Credits

Elsmen

Elsmen

3 projects • 9 followers
I'm a seasoned construction project manager with a passion for innovation in technology. Actively innovating in the particle environment.

Comments